summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue5
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue13
-rw-r--r--app/assets/javascripts/boards/components/boards_selector_deprecated.vue360
-rw-r--r--app/assets/javascripts/boards/ee_functions.js4
-rw-r--r--app/assets/javascripts/boards/index.js139
-rw-r--r--app/assets/javascripts/boards/models/assignee.js13
-rw-r--r--app/assets/javascripts/boards/models/issue.js99
-rw-r--r--app/assets/javascripts/boards/models/iteration.js9
-rw-r--r--app/assets/javascripts/boards/models/label.js11
-rw-r--r--app/assets/javascripts/boards/models/list.js182
-rw-r--r--app/assets/javascripts/boards/models/milestone.js15
-rw-r--r--app/assets/javascripts/boards/models/project.js7
-rw-r--r--app/assets/javascripts/boards/stores/actions.js3
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js872
-rw-r--r--app/assets/javascripts/boards/stores/boards_store_ee.js5
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor.vue6
-rw-r--r--app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue6
-rw-r--r--app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue10
-rw-r--r--app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue5
-rw-r--r--app/models/user.rb2
-rw-r--r--app/views/shared/boards/_show.html.haml2
-rw-r--r--app/workers/all_queues.yml2
-rw-r--r--app/workers/post_receive.rb4
-rw-r--r--config/feature_flags/development/linear_groups_template_finder_extended_group_search.yml8
-rw-r--r--doc/administration/pages/index.md8
-rw-r--r--doc/user/profile/personal_access_tokens.md10
-rw-r--r--doc/user/project/settings/project_access_tokens.md29
-rw-r--r--locale/gitlab.pot37
-rwxr-xr-xscripts/review_apps/review-apps.sh7
-rw-r--r--spec/features/profiles/user_edit_profile_spec.rb15
-rw-r--r--spec/frontend/boards/boards_store_spec.js1013
-rw-r--r--spec/frontend/boards/components/board_content_spec.js6
-rw-r--r--spec/frontend/boards/issue_spec.js162
-rw-r--r--spec/frontend/boards/list_spec.js230
-rw-r--r--spec/frontend/boards/mock_data.js23
-rw-r--r--spec/frontend/content_editor/components/content_editor_spec.js10
-rw-r--r--spec/frontend/pipeline_new/components/pipeline_new_form_spec.js17
-rw-r--r--spec/lib/gitlab/ci/pipeline/metrics_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/templates/Terraform/base_latest_gitlab_ci_yaml_spec.rb2
-rw-r--r--spec/support/shared_examples/features/content_editor_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb8
-rw-r--r--spec/workers/post_receive_spec.rb10
42 files changed, 162 insertions, 3229 deletions
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index 50f444f2788..27ea2e7a608 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -21,11 +21,6 @@ export default {
},
inject: ['canAdminList'],
props: {
- lists: {
- type: Array,
- required: false,
- default: () => [],
- },
disabled: {
type: Boolean,
required: true,
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index a89f71504a9..88bd1b0eaf0 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -1,7 +1,6 @@
<script>
import { GlModal, GlAlert } from '@gitlab/ui';
import { mapGetters, mapActions, mapState } from 'vuex';
-import ListLabel from '~/boards/models/label';
import { TYPE_ITERATION, TYPE_MILESTONE } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { getParameterByName, visitUrl } from '~/lib/utils/url_utility';
@@ -289,14 +288,10 @@ export default {
setBoardLabels(labels) {
labels.forEach((label) => {
if (label.set && !this.board.labels.find((l) => l.id === label.id)) {
- this.board.labels.push(
- new ListLabel({
- id: label.id,
- title: label.title,
- color: label.color,
- textColor: label.text_color,
- }),
- );
+ this.board.labels.push({
+ ...label,
+ textColor: label.text_color,
+ });
} else if (!label.set) {
this.board.labels = this.board.labels.filter((selected) => selected.id !== label.id);
}
diff --git a/app/assets/javascripts/boards/components/boards_selector_deprecated.vue b/app/assets/javascripts/boards/components/boards_selector_deprecated.vue
deleted file mode 100644
index c1536dff2c6..00000000000
--- a/app/assets/javascripts/boards/components/boards_selector_deprecated.vue
+++ /dev/null
@@ -1,360 +0,0 @@
-<script>
-import {
- GlLoadingIcon,
- GlSearchBoxByType,
- GlDropdown,
- GlDropdownDivider,
- GlDropdownSectionHeader,
- GlDropdownItem,
- GlModalDirective,
-} from '@gitlab/ui';
-import { throttle } from 'lodash';
-import { mapGetters, mapState } from 'vuex';
-
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import httpStatusCodes from '~/lib/utils/http_status';
-
-import groupQuery from '../graphql/group_boards.query.graphql';
-import projectQuery from '../graphql/project_boards.query.graphql';
-
-import boardsStore from '../stores/boards_store';
-import BoardForm from './board_form.vue';
-
-const MIN_BOARDS_TO_VIEW_RECENT = 10;
-
-export default {
- name: 'BoardsSelector',
- components: {
- BoardForm,
- GlLoadingIcon,
- GlSearchBoxByType,
- GlDropdown,
- GlDropdownDivider,
- GlDropdownSectionHeader,
- GlDropdownItem,
- },
- directives: {
- GlModalDirective,
- },
- props: {
- currentBoard: {
- type: Object,
- required: true,
- },
- throttleDuration: {
- type: Number,
- default: 200,
- required: false,
- },
- boardBaseUrl: {
- type: String,
- required: true,
- },
- hasMissingBoards: {
- type: Boolean,
- required: true,
- },
- canAdminBoard: {
- type: Boolean,
- required: true,
- },
- multipleIssueBoardsAvailable: {
- type: Boolean,
- required: true,
- },
- labelsPath: {
- type: String,
- required: true,
- },
- labelsWebUrl: {
- type: String,
- required: true,
- },
- projectId: {
- type: Number,
- required: true,
- },
- groupId: {
- type: Number,
- required: true,
- },
- scopedIssueBoardFeatureEnabled: {
- type: Boolean,
- required: true,
- },
- weights: {
- type: Array,
- required: true,
- },
- enabledScopedLabels: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- data() {
- return {
- hasScrollFade: false,
- loadingBoards: 0,
- loadingRecentBoards: false,
- scrollFadeInitialized: false,
- boards: [],
- recentBoards: [],
- state: boardsStore.state,
- throttledSetScrollFade: throttle(this.setScrollFade, this.throttleDuration),
- contentClientHeight: 0,
- maxPosition: 0,
- store: boardsStore,
- filterTerm: '',
- };
- },
- computed: {
- ...mapState(['boardType']),
- ...mapGetters(['isGroupBoard']),
- parentType() {
- return this.boardType;
- },
- loading() {
- return this.loadingRecentBoards || Boolean(this.loadingBoards);
- },
- currentPage() {
- return this.state.currentPage;
- },
- filteredBoards() {
- return this.boards.filter((board) =>
- board.name.toLowerCase().includes(this.filterTerm.toLowerCase()),
- );
- },
- board() {
- return this.state.currentBoard;
- },
- showDelete() {
- return this.boards.length > 1;
- },
- scrollFadeClass() {
- return {
- 'fade-out': !this.hasScrollFade,
- };
- },
- showRecentSection() {
- return (
- this.recentBoards.length &&
- this.boards.length > MIN_BOARDS_TO_VIEW_RECENT &&
- !this.filterTerm.length
- );
- },
- },
- watch: {
- filteredBoards() {
- this.scrollFadeInitialized = false;
- this.$nextTick(this.setScrollFade);
- },
- },
- created() {
- boardsStore.setCurrentBoard(this.currentBoard);
- },
- methods: {
- showPage(page) {
- boardsStore.showPage(page);
- },
- cancel() {
- this.showPage('');
- },
- loadBoards(toggleDropdown = true) {
- if (toggleDropdown && this.boards.length > 0) {
- return;
- }
-
- this.$apollo.addSmartQuery('boards', {
- variables() {
- return { fullPath: this.state.endpoints.fullPath };
- },
- query() {
- return this.isGroupBoard ? groupQuery : projectQuery;
- },
- loadingKey: 'loadingBoards',
- update(data) {
- if (!data?.[this.parentType]) {
- return [];
- }
- return data[this.parentType].boards.edges.map(({ node }) => ({
- id: getIdFromGraphQLId(node.id),
- name: node.name,
- }));
- },
- });
-
- this.loadingRecentBoards = true;
- boardsStore
- .recentBoards()
- .then((res) => {
- this.recentBoards = res.data;
- })
- .catch((err) => {
- /**
- * If user is unauthorized we'd still want to resolve the
- * request to display all boards.
- */
- if (err?.response?.status === httpStatusCodes.UNAUTHORIZED) {
- this.recentBoards = []; // recent boards are empty
- return;
- }
- throw err;
- })
- .then(() => this.$nextTick()) // Wait for boards list in DOM
- .then(() => {
- this.setScrollFade();
- })
- .catch(() => {})
- .finally(() => {
- this.loadingRecentBoards = false;
- });
- },
- isScrolledUp() {
- const { content } = this.$refs;
-
- if (!content) {
- return false;
- }
-
- const currentPosition = this.contentClientHeight + content.scrollTop;
-
- return currentPosition < this.maxPosition;
- },
- initScrollFade() {
- const { content } = this.$refs;
-
- if (!content) {
- return;
- }
-
- this.scrollFadeInitialized = true;
-
- this.contentClientHeight = content.clientHeight;
- this.maxPosition = content.scrollHeight;
- },
- setScrollFade() {
- if (!this.scrollFadeInitialized) this.initScrollFade();
-
- this.hasScrollFade = this.isScrolledUp();
- },
- },
-};
-</script>
-
-<template>
- <div class="boards-switcher js-boards-selector gl-mr-3">
- <span class="boards-selector-wrapper js-boards-selector-wrapper">
- <gl-dropdown
- data-qa-selector="boards_dropdown"
- toggle-class="dropdown-menu-toggle js-dropdown-toggle"
- menu-class="flex-column dropdown-extended-height"
- :text="board.name"
- @show="loadBoards"
- >
- <p class="gl-new-dropdown-header-top" @mousedown.prevent>
- {{ s__('IssueBoards|Switch board') }}
- </p>
- <gl-search-box-by-type ref="searchBox" v-model="filterTerm" class="m-2" />
-
- <div
- v-if="!loading"
- ref="content"
- data-qa-selector="boards_dropdown_content"
- class="dropdown-content flex-fill"
- @scroll.passive="throttledSetScrollFade"
- >
- <gl-dropdown-item
- v-show="filteredBoards.length === 0"
- class="gl-pointer-events-none text-secondary"
- >
- {{ s__('IssueBoards|No matching boards found') }}
- </gl-dropdown-item>
-
- <gl-dropdown-section-header v-if="showRecentSection">
- {{ __('Recent') }}
- </gl-dropdown-section-header>
-
- <template v-if="showRecentSection">
- <gl-dropdown-item
- v-for="recentBoard in recentBoards"
- :key="`recent-${recentBoard.id}`"
- class="js-dropdown-item"
- :href="`${boardBaseUrl}/${recentBoard.id}`"
- >
- {{ recentBoard.name }}
- </gl-dropdown-item>
- </template>
-
- <gl-dropdown-divider v-if="showRecentSection" />
-
- <gl-dropdown-section-header v-if="showRecentSection">
- {{ __('All') }}
- </gl-dropdown-section-header>
-
- <gl-dropdown-item
- v-for="otherBoard in filteredBoards"
- :key="otherBoard.id"
- class="js-dropdown-item"
- :href="`${boardBaseUrl}/${otherBoard.id}`"
- >
- {{ otherBoard.name }}
- </gl-dropdown-item>
-
- <gl-dropdown-item v-if="hasMissingBoards" class="no-pointer-events">
- {{
- s__(
- 'IssueBoards|Some of your boards are hidden, activate a license to see them again.',
- )
- }}
- </gl-dropdown-item>
- </div>
-
- <div
- v-show="filteredBoards.length > 0"
- class="dropdown-content-faded-mask"
- :class="scrollFadeClass"
- ></div>
-
- <gl-loading-icon v-if="loading" size="sm" />
-
- <div v-if="canAdminBoard">
- <gl-dropdown-divider />
-
- <gl-dropdown-item
- v-if="multipleIssueBoardsAvailable"
- v-gl-modal-directive="'board-config-modal'"
- data-qa-selector="create_new_board_button"
- @click.prevent="showPage('new')"
- >
- {{ s__('IssueBoards|Create new board') }}
- </gl-dropdown-item>
-
- <gl-dropdown-item
- v-if="showDelete"
- v-gl-modal-directive="'board-config-modal'"
- class="text-danger js-delete-board"
- @click.prevent="showPage('delete')"
- >
- {{ s__('IssueBoards|Delete board') }}
- </gl-dropdown-item>
- </div>
- </gl-dropdown>
-
- <board-form
- v-if="currentPage"
- :labels-path="labelsPath"
- :labels-web-url="labelsWebUrl"
- :project-id="projectId"
- :group-id="groupId"
- :can-admin-board="canAdminBoard"
- :scoped-issue-board-feature-enabled="scopedIssueBoardFeatureEnabled"
- :weights="weights"
- :enable-scoped-labels="enabledScopedLabels"
- :current-board="currentBoard"
- :current-page="state.currentPage"
- @cancel="cancel"
- />
- </span>
- </div>
-</template>
diff --git a/app/assets/javascripts/boards/ee_functions.js b/app/assets/javascripts/boards/ee_functions.js
deleted file mode 100644
index 62a0d930ec0..00000000000
--- a/app/assets/javascripts/boards/ee_functions.js
+++ /dev/null
@@ -1,4 +0,0 @@
-export const setWeightFetchingState = () => {};
-export const setEpicFetchingState = () => {};
-
-export const getMilestoneTitle = () => ({});
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 304fea53a34..66f99b47732 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -4,33 +4,19 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { mapActions } from 'vuex';
-import 'ee_else_ce/boards/models/issue';
-import 'ee_else_ce/boards/models/list';
-import { setWeightFetchingState, setEpicFetchingState } from 'ee_else_ce/boards/ee_functions';
import toggleEpicsSwimlanes from 'ee_else_ce/boards/toggle_epics_swimlanes';
import toggleLabels from 'ee_else_ce/boards/toggle_labels';
import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
import BoardContent from '~/boards/components/board_content.vue';
-import './models/label';
-import './models/assignee';
-import '~/boards/models/milestone';
-import '~/boards/models/project';
import '~/boards/filters/due_date_filters';
import { issuableTypes } from '~/boards/constants';
import eventHub from '~/boards/eventhub';
import FilteredSearchBoards from '~/boards/filtered_search_boards';
import initBoardsFilteredSearch from '~/boards/mount_filtered_search_issue_boards';
import store from '~/boards/stores';
-import boardsStore from '~/boards/stores/boards_store';
import toggleFocusMode from '~/boards/toggle_focus';
import createDefaultClient from '~/lib/graphql';
-import {
- NavigationType,
- convertObjectPropsToCamelCase,
- parseBoolean,
-} from '~/lib/utils/common_utils';
-import { __ } from '~/locale';
-import sidebarEventHub from '~/sidebar/event_hub';
+import { NavigationType, parseBoolean } from '~/lib/utils/common_utils';
import introspectionQueryResultData from '~/sidebar/fragmentTypes.json';
import { fullBoardId } from './boards_util';
import boardConfigToggle from './config_toggle';
@@ -77,8 +63,6 @@ export default () => {
initBoardsFilteredSearch(apolloProvider);
}
- boardsStore.create();
-
// eslint-disable-next-line @gitlab/no-runtime-template-compiler
issueBoardsApp = new Vue({
el: $boardApp,
@@ -116,22 +100,13 @@ export default () => {
apolloProvider,
data() {
return {
- state: boardsStore.state,
loading: 0,
- boardsEndpoint: $boardApp.dataset.boardsEndpoint,
recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
- listsEndpoint: $boardApp.dataset.listsEndpoint,
disabled: parseBoolean($boardApp.dataset.disabled),
- bulkUpdatePath: $boardApp.dataset.bulkUpdatePath,
- detailIssue: boardsStore.detail,
parent: $boardApp.dataset.parent,
+ detailIssueVisible: false,
};
},
- computed: {
- detailIssueVisible() {
- return Object.keys(this.detailIssue.issue).length;
- },
- },
created() {
this.setInitialBoardData({
boardId: $boardApp.dataset.boardId,
@@ -154,129 +129,29 @@ export default () => {
: null,
},
});
- boardsStore.setEndpoints({
- boardsEndpoint: this.boardsEndpoint,
- recentBoardsEndpoint: this.recentBoardsEndpoint,
- listsEndpoint: this.listsEndpoint,
- bulkUpdatePath: this.bulkUpdatePath,
- boardId: $boardApp.dataset.boardId,
- fullPath: $boardApp.dataset.fullPath,
- });
- boardsStore.rootPath = this.boardsEndpoint;
eventHub.$on('updateTokens', this.updateTokens);
- eventHub.$on('newDetailIssue', this.updateDetailIssue);
- eventHub.$on('clearDetailIssue', this.clearDetailIssue);
- sidebarEventHub.$on('toggleSubscription', this.toggleSubscription);
+ eventHub.$on('toggleDetailIssue', this.toggleDetailIssue);
},
beforeDestroy() {
eventHub.$off('updateTokens', this.updateTokens);
- eventHub.$off('newDetailIssue', this.updateDetailIssue);
- eventHub.$off('clearDetailIssue', this.clearDetailIssue);
- sidebarEventHub.$off('toggleSubscription', this.toggleSubscription);
+ eventHub.$off('toggleDetailIssue', this.toggleDetailIssue);
},
mounted() {
if (!gon?.features?.issueBoardsFilteredSearch) {
- this.filterManager = new FilteredSearchBoards(
- boardsStore.filter,
- true,
- boardsStore.cantEdit,
- );
+ this.filterManager = new FilteredSearchBoards({ path: '' }, true, []);
this.filterManager.setup();
}
this.performSearch();
-
- boardsStore.disabled = this.disabled;
},
methods: {
...mapActions(['setInitialBoardData', 'performSearch', 'setError']),
updateTokens() {
this.filterManager.updateTokens();
},
- updateDetailIssue(newIssue, multiSelect = false) {
- const { sidebarInfoEndpoint } = newIssue;
- if (sidebarInfoEndpoint && newIssue.subscribed === undefined) {
- newIssue.setFetchingState('subscriptions', true);
- setWeightFetchingState(newIssue, true);
- setEpicFetchingState(newIssue, true);
- boardsStore
- .getIssueInfo(sidebarInfoEndpoint)
- .then((res) => res.data)
- .then((data) => {
- const {
- subscribed,
- totalTimeSpent,
- timeEstimate,
- humanTimeEstimate,
- humanTotalTimeSpent,
- weight,
- epic,
- assignees,
- } = convertObjectPropsToCamelCase(data);
-
- newIssue.setFetchingState('subscriptions', false);
- setWeightFetchingState(newIssue, false);
- setEpicFetchingState(newIssue, false);
- newIssue.updateData({
- humanTimeSpent: humanTotalTimeSpent,
- timeSpent: totalTimeSpent,
- humanTimeEstimate,
- timeEstimate,
- subscribed,
- weight,
- epic,
- assignees,
- });
- })
- .catch(() => {
- newIssue.setFetchingState('subscriptions', false);
- setWeightFetchingState(newIssue, false);
- this.setError({ message: __('An error occurred while fetching sidebar data') });
- });
- }
-
- if (multiSelect) {
- boardsStore.toggleMultiSelect(newIssue);
-
- if (boardsStore.detail.issue) {
- boardsStore.clearDetailIssue();
- return;
- }
-
- return;
- }
-
- boardsStore.setIssueDetail(newIssue);
- },
- clearDetailIssue(multiSelect = false) {
- if (multiSelect) {
- boardsStore.clearMultiSelect();
- }
- boardsStore.clearDetailIssue();
- },
- toggleSubscription(id) {
- const { issue } = boardsStore.detail;
- if (issue.id === id && issue.toggleSubscriptionEndpoint) {
- issue.setFetchingState('subscriptions', true);
- boardsStore
- .toggleIssueSubscription(issue.toggleSubscriptionEndpoint)
- .then(() => {
- issue.setFetchingState('subscriptions', false);
- issue.updateData({
- subscribed: !issue.subscribed,
- });
- })
- .catch(() => {
- issue.setFetchingState('subscriptions', false);
- this.setError({
- message: __('An error occurred when toggling the notification subscription'),
- });
- });
- }
- },
- getNodes(data) {
- return data[this.parent]?.board?.lists.nodes;
+ toggleDetailIssue(hasSidebar) {
+ this.detailIssueVisible = hasSidebar;
},
},
});
diff --git a/app/assets/javascripts/boards/models/assignee.js b/app/assets/javascripts/boards/models/assignee.js
deleted file mode 100644
index 1e822d06bfd..00000000000
--- a/app/assets/javascripts/boards/models/assignee.js
+++ /dev/null
@@ -1,13 +0,0 @@
-export default class ListAssignee {
- constructor(obj) {
- this.id = obj.id;
- this.name = obj.name;
- this.username = obj.username;
- this.avatar = obj.avatarUrl || obj.avatar_url || obj.avatar || gon.default_avatar_url;
- this.path = obj.path;
- this.state = obj.state;
- this.webUrl = obj.web_url || obj.webUrl;
- }
-}
-
-window.ListAssignee = ListAssignee;
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
deleted file mode 100644
index 46d1239457d..00000000000
--- a/app/assets/javascripts/boards/models/issue.js
+++ /dev/null
@@ -1,99 +0,0 @@
-/* eslint-disable no-unused-vars */
-/* global ListLabel */
-/* global ListMilestone */
-/* global ListAssignee */
-
-import axios from '~/lib/utils/axios_utils';
-import './label';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import boardsStore from '../stores/boards_store';
-import IssueProject from './project';
-
-class ListIssue {
- constructor(obj) {
- this.subscribed = obj.subscribed;
- this.labels = [];
- this.assignees = [];
- this.selected = false;
- this.position = obj.position || obj.relative_position || obj.relativePosition || Infinity;
- this.isFetching = {
- subscriptions: true,
- };
- this.closed = obj.closed;
- this.isLoading = {};
-
- this.refreshData(obj);
- }
-
- refreshData(obj) {
- boardsStore.refreshIssueData(this, obj);
- }
-
- addLabel(label) {
- boardsStore.addIssueLabel(this, label);
- }
-
- findLabel(findLabel) {
- return boardsStore.findIssueLabel(this, findLabel);
- }
-
- removeLabel(removeLabel) {
- boardsStore.removeIssueLabel(this, removeLabel);
- }
-
- removeLabels(labels) {
- boardsStore.removeIssueLabels(this, labels);
- }
-
- addAssignee(assignee) {
- boardsStore.addIssueAssignee(this, assignee);
- }
-
- findAssignee(findAssignee) {
- return boardsStore.findIssueAssignee(this, findAssignee);
- }
-
- setAssignees(assignees) {
- boardsStore.setIssueAssignees(this, assignees);
- }
-
- removeAssignee(removeAssignee) {
- boardsStore.removeIssueAssignee(this, removeAssignee);
- }
-
- removeAllAssignees() {
- boardsStore.removeAllIssueAssignees(this);
- }
-
- addMilestone(milestone) {
- boardsStore.addIssueMilestone(this, milestone);
- }
-
- removeMilestone(removeMilestone) {
- boardsStore.removeIssueMilestone(this, removeMilestone);
- }
-
- getLists() {
- return boardsStore.state.lists.filter((list) => list.findIssue(this.id));
- }
-
- updateData(newData) {
- boardsStore.updateIssueData(this, newData);
- }
-
- setFetchingState(key, value) {
- boardsStore.setIssueFetchingState(this, key, value);
- }
-
- setLoadingState(key, value) {
- boardsStore.setIssueLoadingState(this, key, value);
- }
-
- update() {
- return boardsStore.updateIssue(this);
- }
-}
-
-window.ListIssue = ListIssue;
-
-export default ListIssue;
diff --git a/app/assets/javascripts/boards/models/iteration.js b/app/assets/javascripts/boards/models/iteration.js
deleted file mode 100644
index b7bdc204f7c..00000000000
--- a/app/assets/javascripts/boards/models/iteration.js
+++ /dev/null
@@ -1,9 +0,0 @@
-export default class ListIteration {
- constructor(obj) {
- this.id = obj.id;
- this.title = obj.title;
- this.state = obj.state;
- this.webUrl = obj.web_url || obj.webUrl;
- this.description = obj.description;
- }
-}
diff --git a/app/assets/javascripts/boards/models/label.js b/app/assets/javascripts/boards/models/label.js
deleted file mode 100644
index cd2a2c0137f..00000000000
--- a/app/assets/javascripts/boards/models/label.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-
-export default class ListLabel {
- constructor(obj) {
- Object.assign(this, convertObjectPropsToCamelCase(obj, { dropKeys: ['priority'] }), {
- priority: obj.priority !== null ? obj.priority : Infinity,
- });
- }
-}
-
-window.ListLabel = ListLabel;
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
deleted file mode 100644
index ab24532d87f..00000000000
--- a/app/assets/javascripts/boards/models/list.js
+++ /dev/null
@@ -1,182 +0,0 @@
-/* eslint-disable class-methods-use-this */
-import createFlash from '~/flash';
-import { __ } from '~/locale';
-import boardsStore from '../stores/boards_store';
-import ListAssignee from './assignee';
-import ListIteration from './iteration';
-import ListLabel from './label';
-import ListMilestone from './milestone';
-import 'ee_else_ce/boards/models/issue';
-
-const TYPES = {
- backlog: {
- isPreset: true,
- isExpandable: true,
- isBlank: false,
- },
- closed: {
- isPreset: true,
- isExpandable: true,
- isBlank: false,
- },
- blank: {
- isPreset: true,
- isExpandable: false,
- isBlank: true,
- },
- default: {
- // includes label, assignee, and milestone lists
- isPreset: false,
- isExpandable: true,
- isBlank: false,
- },
-};
-
-class List {
- constructor(obj) {
- this.id = obj.id;
- this.position = obj.position;
- this.title = obj.title;
- this.type = obj.list_type || obj.listType;
-
- const typeInfo = this.getTypeInfo(this.type);
- this.preset = Boolean(typeInfo.isPreset);
- this.isExpandable = Boolean(typeInfo.isExpandable);
- this.isExpanded = !obj.collapsed;
- this.page = 1;
- this.highlighted = obj.highlighted;
- this.loading = true;
- this.loadingMore = false;
- this.issues = obj.issues || [];
- this.issuesSize = obj.issuesSize || obj.issuesCount || 0;
- this.maxIssueCount = obj.maxIssueCount || obj.max_issue_count || 0;
-
- if (obj.label) {
- this.label = new ListLabel(obj.label);
- } else if (obj.user || obj.assignee) {
- this.assignee = new ListAssignee(obj.user || obj.assignee);
- this.title = this.assignee.name;
- } else if (IS_EE && obj.milestone) {
- this.milestone = new ListMilestone(obj.milestone);
- this.title = this.milestone.title;
- } else if (IS_EE && obj.iteration) {
- this.iteration = new ListIteration(obj.iteration);
- this.title = this.iteration.title;
- }
-
- // doNotFetchIssues is a temporary workaround until issues are fetched using GraphQL on issue boards
- // Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/229416
- if (!typeInfo.isBlank && this.id && !obj.doNotFetchIssues) {
- this.getIssues().catch(() => {
- // TODO: handle request error
- });
- }
- }
-
- guid() {
- const s4 = () =>
- Math.floor((1 + Math.random()) * 0x10000)
- .toString(16)
- .substring(1);
- return `${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`;
- }
-
- save() {
- return boardsStore.saveList(this);
- }
-
- destroy() {
- boardsStore.destroy(this);
- }
-
- update() {
- return boardsStore.updateListFunc(this);
- }
-
- nextPage() {
- return boardsStore.goToNextPage(this);
- }
-
- getIssues(emptyIssues = true) {
- return boardsStore.getListIssues(this, emptyIssues);
- }
-
- newIssue(issue) {
- return boardsStore.newListIssue(this, issue);
- }
-
- addMultipleIssues(issues, listFrom, newIndex) {
- boardsStore.addMultipleListIssues(this, issues, listFrom, newIndex);
- }
-
- addIssue(issue, listFrom, newIndex) {
- boardsStore.addListIssue(this, issue, listFrom, newIndex);
- }
-
- moveIssue(issue, oldIndex, newIndex, moveBeforeId, moveAfterId) {
- boardsStore.moveListIssues(this, issue, oldIndex, newIndex, moveBeforeId, moveAfterId);
- }
-
- moveMultipleIssues({ issues, oldIndicies, newIndex, moveBeforeId, moveAfterId }) {
- boardsStore
- .moveListMultipleIssues({
- list: this,
- issues,
- oldIndicies,
- newIndex,
- moveBeforeId,
- moveAfterId,
- })
- .catch(() =>
- createFlash({
- message: __('Something went wrong while moving issues.'),
- }),
- );
- }
-
- updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId) {
- boardsStore.moveIssue(issue.id, listFrom.id, this.id, moveBeforeId, moveAfterId).catch(() => {
- // TODO: handle request error
- });
- }
-
- updateMultipleIssues(issues, listFrom, moveBeforeId, moveAfterId) {
- boardsStore
- .moveMultipleIssues({
- ids: issues.map((issue) => issue.id),
- fromListId: listFrom.id,
- toListId: this.id,
- moveBeforeId,
- moveAfterId,
- })
- .catch(() =>
- createFlash({
- message: __('Something went wrong while moving issues.'),
- }),
- );
- }
-
- findIssue(id) {
- return boardsStore.findListIssue(this, id);
- }
-
- removeMultipleIssues(removeIssues) {
- return boardsStore.removeListMultipleIssues(this, removeIssues);
- }
-
- removeIssue(removeIssue) {
- return boardsStore.removeListIssues(this, removeIssue);
- }
-
- getTypeInfo(type) {
- return TYPES[type] || TYPES.default;
- }
-
- onNewIssueResponse(issue, data) {
- boardsStore.onNewListIssueResponse(this, issue, data);
- }
-}
-
-window.List = List;
-
-export default List;
diff --git a/app/assets/javascripts/boards/models/milestone.js b/app/assets/javascripts/boards/models/milestone.js
deleted file mode 100644
index 7201b6e91f5..00000000000
--- a/app/assets/javascripts/boards/models/milestone.js
+++ /dev/null
@@ -1,15 +0,0 @@
-export default class ListMilestone {
- constructor(obj) {
- this.id = obj.id;
- this.title = obj.title;
-
- if (IS_EE) {
- this.path = obj.path;
- this.state = obj.state;
- this.webUrl = obj.web_url || obj.webUrl;
- this.description = obj.description;
- }
- }
-}
-
-window.ListMilestone = ListMilestone;
diff --git a/app/assets/javascripts/boards/models/project.js b/app/assets/javascripts/boards/models/project.js
deleted file mode 100644
index 9468a02856e..00000000000
--- a/app/assets/javascripts/boards/models/project.js
+++ /dev/null
@@ -1,7 +0,0 @@
-export default class IssueProject {
- constructor(obj) {
- this.id = obj.id;
- this.path = obj.path;
- this.fullPath = obj.path_with_namespace;
- }
-}
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index 402205334c8..476cf2e4c73 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -18,6 +18,7 @@ import {
} from 'ee_else_ce/boards/constants';
import createBoardListMutation from 'ee_else_ce/boards/graphql/board_list_create.mutation.graphql';
import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql';
+import eventHub from '~/boards/eventhub';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
@@ -61,10 +62,12 @@ export default {
setActiveId({ commit }, { id, sidebarType }) {
commit(types.SET_ACTIVE_ID, { id, sidebarType });
+ eventHub.$emit('toggleDetailIssue', true);
},
unsetActiveId({ dispatch }) {
dispatch('setActiveId', { id: inactiveId, sidebarType: '' });
+ eventHub.$emit('toggleDetailIssue', false);
},
setFilters: ({ commit, state: { issuableType } }, filters) => {
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
deleted file mode 100644
index f31f6d5fc5b..00000000000
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ /dev/null
@@ -1,872 +0,0 @@
-/* eslint-disable no-shadow, no-param-reassign, consistent-return */
-/* global List */
-/* global ListIssue */
-
-import { sortBy } from 'lodash';
-import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import createDefaultClient from '~/lib/graphql';
-import axios from '~/lib/utils/axios_utils';
-import { parseBoolean, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import { mergeUrlParams, queryToObject, getUrlParamsArray } from '~/lib/utils/url_utility';
-import { ListType, flashAnimationDuration } from '../constants';
-import eventHub from '../eventhub';
-import ListAssignee from '../models/assignee';
-import ListLabel from '../models/label';
-import ListMilestone from '../models/milestone';
-import IssueProject from '../models/project';
-
-const PER_PAGE = 20;
-export const gqlClient = createDefaultClient();
-
-const boardsStore = {
- disabled: false,
- timeTracking: {
- limitToHours: false,
- },
- scopedLabels: {
- enabled: false,
- },
- filter: {
- path: '',
- },
- state: {
- currentBoard: {
- labels: [],
- },
- currentPage: '',
- endpoints: {},
- },
- detail: {
- issue: {},
- list: {},
- },
- moving: {
- issue: {},
- list: {},
- },
- multiSelect: { list: [] },
-
- setEndpoints({
- boardsEndpoint,
- listsEndpoint,
- bulkUpdatePath,
- boardId,
- recentBoardsEndpoint,
- fullPath,
- }) {
- const listsEndpointGenerate = `${listsEndpoint}/generate.json`;
- this.state.endpoints = {
- boardsEndpoint,
- boardId,
- listsEndpoint,
- listsEndpointGenerate,
- bulkUpdatePath,
- fullPath,
- recentBoardsEndpoint: `${recentBoardsEndpoint}.json`,
- };
- },
- create() {
- this.state.lists = [];
- this.filter.path = getUrlParamsArray().join('&');
- this.detail = {
- issue: {},
- list: {},
- };
- },
- showPage(page) {
- this.state.currentPage = page;
- },
- updateListPosition(listObj) {
- const listType = listObj.listType || listObj.list_type;
- let { position } = listObj;
- if (listType === ListType.closed) {
- position = Infinity;
- } else if (listType === ListType.backlog) {
- position = -1;
- }
-
- const list = new List({ ...listObj, position });
- return list;
- },
- addList(listObj) {
- const list = this.updateListPosition(listObj);
- this.state.lists = sortBy([...this.state.lists, list], 'position');
- return list;
- },
- new(listObj) {
- const list = this.addList(listObj);
- const backlogList = this.findList('type', 'backlog');
-
- list
- .save()
- .then(() => {
- list.highlighted = true;
- setTimeout(() => {
- list.highlighted = false;
- }, flashAnimationDuration);
-
- // Remove any new issues from the backlog
- // as they will be visible in the new list
- list.issues.forEach(backlogList.removeIssue.bind(backlogList));
- this.state.lists = sortBy(this.state.lists, 'position');
- })
- .catch(() => {
- // https://gitlab.com/gitlab-org/gitlab-foss/issues/30821
- });
- },
-
- updateNewListDropdown(listId) {
- document
- .querySelector(`.js-board-list-${getIdFromGraphQLId(listId)}`)
- ?.classList.remove('is-active');
- },
-
- findIssueLabel(issue, findLabel) {
- return issue.labels.find((label) => label.id === findLabel.id);
- },
-
- goToNextPage(list) {
- if (list.issuesSize > list.issues.length) {
- if (list.issues.length / PER_PAGE >= 1) {
- list.page += 1;
- }
-
- return list.getIssues(false);
- }
- },
-
- addListIssue(list, issue, listFrom, newIndex) {
- let moveBeforeId = null;
- let moveAfterId = null;
-
- if (!list.findIssue(issue.id)) {
- if (newIndex !== undefined) {
- list.issues.splice(newIndex, 0, issue);
-
- if (list.issues[newIndex - 1]) {
- moveBeforeId = list.issues[newIndex - 1].id;
- }
-
- if (list.issues[newIndex + 1]) {
- moveAfterId = list.issues[newIndex + 1].id;
- }
- } else {
- list.issues.push(issue);
- }
-
- if (list.label) {
- issue.addLabel(list.label);
- }
-
- if (list.assignee) {
- if (listFrom && listFrom.type === 'assignee') {
- issue.removeAssignee(listFrom.assignee);
- }
- issue.addAssignee(list.assignee);
- }
-
- if (IS_EE && list.milestone) {
- if (listFrom && listFrom.type === 'milestone') {
- issue.removeMilestone(listFrom.milestone);
- }
- issue.addMilestone(list.milestone);
- }
-
- if (listFrom) {
- list.issuesSize += 1;
-
- list.updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId);
- }
- }
- },
- findListIssue(list, id) {
- return list.issues.find((issue) => issue.id === id);
- },
-
- removeList(id) {
- const list = this.findList('id', id);
-
- if (!list) return;
-
- this.state.lists = this.state.lists.filter((list) => list.id !== id);
- },
- moveList(listFrom, orderLists) {
- orderLists.forEach((id, i) => {
- const list = this.findList('id', parseInt(id, 10));
-
- list.position = i;
- });
- listFrom.update();
- },
-
- addMultipleListIssues(list, issues, listFrom, newIndex) {
- let moveBeforeId = null;
- let moveAfterId = null;
-
- const listHasIssues = issues.every((issue) => list.findIssue(issue.id));
-
- if (!listHasIssues) {
- if (newIndex !== undefined) {
- if (list.issues[newIndex - 1]) {
- moveBeforeId = list.issues[newIndex - 1].id;
- }
-
- if (list.issues[newIndex]) {
- moveAfterId = list.issues[newIndex].id;
- }
-
- list.issues.splice(newIndex, 0, ...issues);
- } else {
- list.issues.push(...issues);
- }
-
- if (list.label) {
- issues.forEach((issue) => issue.addLabel(list.label));
- }
-
- if (list.assignee) {
- if (listFrom && listFrom.type === 'assignee') {
- issues.forEach((issue) => issue.removeAssignee(listFrom.assignee));
- }
- issues.forEach((issue) => issue.addAssignee(list.assignee));
- }
-
- if (IS_EE && list.milestone) {
- if (listFrom && listFrom.type === 'milestone') {
- issues.forEach((issue) => issue.removeMilestone(listFrom.milestone));
- }
- issues.forEach((issue) => issue.addMilestone(list.milestone));
- }
-
- if (listFrom) {
- list.issuesSize += issues.length;
-
- list.updateMultipleIssues(issues, listFrom, moveBeforeId, moveAfterId);
- }
- }
- },
-
- removeListIssues(list, removeIssue) {
- list.issues = list.issues.filter((issue) => {
- const matchesRemove = removeIssue.id === issue.id;
-
- if (matchesRemove) {
- list.issuesSize -= 1;
- issue.removeLabel(list.label);
- }
-
- return !matchesRemove;
- });
- },
- removeListMultipleIssues(list, removeIssues) {
- const ids = removeIssues.map((issue) => issue.id);
-
- list.issues = list.issues.filter((issue) => {
- const matchesRemove = ids.includes(issue.id);
-
- if (matchesRemove) {
- list.issuesSize -= 1;
- issue.removeLabel(list.label);
- }
-
- return !matchesRemove;
- });
- },
-
- startMoving(list, issue) {
- Object.assign(this.moving, { list, issue });
- },
-
- onNewListIssueResponse(list, issue, data) {
- issue.refreshData(data);
-
- if (list.issues.length > 1) {
- const moveBeforeId = list.issues[1].id;
- this.moveIssue(issue.id, null, null, null, moveBeforeId);
- }
- },
-
- moveMultipleIssuesToList({ listFrom, listTo, issues, newIndex }) {
- const issueTo = issues.map((issue) => listTo.findIssue(issue.id));
- const issueLists = issues.map((issue) => issue.getLists()).flat();
- const listLabels = issueLists.map((list) => list.label);
- const hasMoveableIssues = issueTo.filter(Boolean).length > 0;
-
- if (!hasMoveableIssues) {
- // Check if target list assignee is already present in this issue
- if (
- listTo.type === ListType.assignee &&
- listFrom.type === ListType.assignee &&
- issues.some((issue) => issue.findAssignee(listTo.assignee))
- ) {
- const targetIssues = issues.map((issue) => listTo.findIssue(issue.id));
- targetIssues.forEach((targetIssue) => targetIssue.removeAssignee(listFrom.assignee));
- } else if (listTo.type === 'milestone') {
- const currentMilestones = issues.map((issue) => issue.milestone);
- const currentLists = this.state.lists
- .filter((list) => list.type === 'milestone' && list.id !== listTo.id)
- .filter((list) =>
- list.issues.some((listIssue) => issues.some((issue) => listIssue.id === issue.id)),
- );
-
- issues.forEach((issue) => {
- currentMilestones.forEach((milestone) => {
- issue.removeMilestone(milestone);
- });
- });
-
- issues.forEach((issue) => {
- issue.addMilestone(listTo.milestone);
- });
-
- currentLists.forEach((currentList) => {
- issues.forEach((issue) => {
- currentList.removeIssue(issue);
- });
- });
-
- listTo.addMultipleIssues(issues, listFrom, newIndex);
- } else {
- // Add to new lists issues if it doesn't already exist
- listTo.addMultipleIssues(issues, listFrom, newIndex);
- }
- } else {
- listTo.updateMultipleIssues(issues, listFrom);
- issues.forEach((issue) => {
- issue.removeLabel(listFrom.label);
- });
- }
-
- if (listTo.type === ListType.closed && listFrom.type !== ListType.backlog) {
- issueLists.forEach((list) => {
- issues.forEach((issue) => {
- list.removeIssue(issue);
- });
- });
-
- issues.forEach((issue) => {
- issue.removeLabels(listLabels);
- });
- } else if (listTo.type === ListType.backlog && listFrom.type === ListType.assignee) {
- issues.forEach((issue) => {
- issue.removeAssignee(listFrom.assignee);
- });
- issueLists.forEach((list) => {
- issues.forEach((issue) => {
- list.removeIssue(issue);
- });
- });
- } else if (listTo.type === ListType.backlog && listFrom.type === ListType.milestone) {
- issues.forEach((issue) => {
- issue.removeMilestone(listFrom.milestone);
- });
- issueLists.forEach((list) => {
- issues.forEach((issue) => {
- list.removeIssue(issue);
- });
- });
- } else if (
- this.shouldRemoveIssue(listFrom, listTo) &&
- this.issuesAreContiguous(listFrom, issues)
- ) {
- listFrom.removeMultipleIssues(issues);
- }
- },
-
- issuesAreContiguous(list, issues) {
- // When there's only 1 issue selected, we can return early.
- if (issues.length === 1) return true;
-
- // Create list of ids for issues involved.
- const listIssueIds = list.issues.map((issue) => issue.id);
- const movedIssueIds = issues.map((issue) => issue.id);
-
- // Check if moved issue IDs is sub-array
- // of source list issue IDs (i.e. contiguous selection).
- return listIssueIds.join('|').includes(movedIssueIds.join('|'));
- },
-
- moveIssueToList(listFrom, listTo, issue, newIndex) {
- const issueTo = listTo.findIssue(issue.id);
- const issueLists = issue.getLists();
- const listLabels = issueLists.map((listIssue) => listIssue.label);
-
- if (!issueTo) {
- // Check if target list assignee is already present in this issue
- if (
- listTo.type === 'assignee' &&
- listFrom.type === 'assignee' &&
- issue.findAssignee(listTo.assignee)
- ) {
- const targetIssue = listTo.findIssue(issue.id);
- targetIssue.removeAssignee(listFrom.assignee);
- } else if (listTo.type === 'milestone') {
- const currentMilestone = issue.milestone;
- const currentLists = this.state.lists
- .filter((list) => list.type === 'milestone' && list.id !== listTo.id)
- .filter((list) => list.issues.some((listIssue) => issue.id === listIssue.id));
-
- issue.removeMilestone(currentMilestone);
- issue.addMilestone(listTo.milestone);
- currentLists.forEach((currentList) => currentList.removeIssue(issue));
- listTo.addIssue(issue, listFrom, newIndex);
- } else {
- // Add to new lists issues if it doesn't already exist
- listTo.addIssue(issue, listFrom, newIndex);
- }
- } else {
- listTo.updateIssueLabel(issue, listFrom);
- issueTo.removeLabel(listFrom.label);
- }
-
- if (listTo.type === 'closed' && listFrom.type !== 'backlog') {
- issueLists.forEach((list) => {
- list.removeIssue(issue);
- });
- issue.removeLabels(listLabels);
- } else if (listTo.type === 'backlog' && listFrom.type === 'assignee') {
- issue.removeAssignee(listFrom.assignee);
- listFrom.removeIssue(issue);
- } else if (listTo.type === 'backlog' && listFrom.type === 'milestone') {
- issue.removeMilestone(listFrom.milestone);
- listFrom.removeIssue(issue);
- } else if (this.shouldRemoveIssue(listFrom, listTo)) {
- listFrom.removeIssue(issue);
- }
- },
- shouldRemoveIssue(listFrom, listTo) {
- return (
- (listTo.type !== 'label' && listFrom.type === 'assignee') ||
- (listTo.type !== 'assignee' && listFrom.type === 'label') ||
- listFrom.type === 'backlog' ||
- listFrom.type === 'closed'
- );
- },
- moveIssueInList(list, issue, oldIndex, newIndex, idArray) {
- const beforeId = parseInt(idArray[newIndex - 1], 10) || null;
- const afterId = parseInt(idArray[newIndex + 1], 10) || null;
-
- list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId);
- },
- moveMultipleIssuesInList({ list, issues, oldIndicies, newIndex, idArray }) {
- const beforeId = parseInt(idArray[newIndex - 1], 10) || null;
- const afterId = parseInt(idArray[newIndex + issues.length], 10) || null;
- list.moveMultipleIssues({
- issues,
- oldIndicies,
- newIndex,
- moveBeforeId: beforeId,
- moveAfterId: afterId,
- });
- },
- findList(key, val) {
- return this.state.lists.find((list) => list[key] === val);
- },
- findListByLabelId(id) {
- return this.state.lists.find((list) => list.type === 'label' && list.label.id === id);
- },
-
- toggleFilter(filter) {
- const filterPath = this.filter.path.split('&');
- const filterIndex = filterPath.indexOf(filter);
-
- if (filterIndex === -1) {
- filterPath.push(filter);
- } else {
- filterPath.splice(filterIndex, 1);
- }
-
- this.filter.path = filterPath.join('&');
-
- this.updateFiltersUrl();
-
- eventHub.$emit('updateTokens');
- },
-
- setListDetail(newList) {
- this.detail.list = newList;
- },
-
- updateFiltersUrl() {
- window.history.pushState(null, null, `?${this.filter.path}`);
- },
-
- clearDetailIssue() {
- this.setIssueDetail({});
- },
-
- setIssueDetail(issueDetail) {
- this.detail.issue = issueDetail;
- },
-
- setTimeTrackingLimitToHours(limitToHours) {
- this.timeTracking.limitToHours = parseBoolean(limitToHours);
- },
-
- generateBoardGid(boardId) {
- return `gid://gitlab/Board/${boardId}`;
- },
-
- generateBoardsPath(id) {
- return `${this.state.endpoints.boardsEndpoint}${id ? `/${id}` : ''}.json`;
- },
-
- generateIssuesPath(id) {
- return `${this.state.endpoints.listsEndpoint}${id ? `/${id}` : ''}/issues`;
- },
-
- generateIssuePath(boardId, id) {
- return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${
- id ? `/${id}` : ''
- }`;
- },
-
- generateMultiDragPath(boardId) {
- return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues/bulk_move`;
- },
-
- all() {
- return axios.get(this.state.endpoints.listsEndpoint);
- },
-
- createList(entityId, entityType) {
- const list = {
- [entityType]: entityId,
- };
-
- return axios.post(this.state.endpoints.listsEndpoint, {
- list,
- });
- },
-
- updateList(id, position, collapsed) {
- return axios.put(`${this.state.endpoints.listsEndpoint}/${id}`, {
- list: {
- position,
- collapsed,
- },
- });
- },
-
- updateListFunc(list) {
- const collapsed = !list.isExpanded;
- return this.updateList(list.id, list.position, collapsed).catch(() => {
- // TODO: handle request error
- });
- },
-
- destroyList(id) {
- return axios.delete(`${this.state.endpoints.listsEndpoint}/${id}`);
- },
- destroy(list) {
- const index = this.state.lists.indexOf(list);
- this.state.lists.splice(index, 1);
- this.updateNewListDropdown(list.id);
-
- this.destroyList(list.id).catch(() => {
- // TODO: handle request error
- });
- },
-
- saveList(list) {
- const entity = list.label || list.assignee || list.milestone || list.iteration;
- let entityType = '';
- if (list.label) {
- entityType = 'label_id';
- } else if (list.assignee) {
- entityType = 'assignee_id';
- } else if (IS_EE && list.milestone) {
- entityType = 'milestone_id';
- } else if (IS_EE && list.iteration) {
- entityType = 'iteration_id';
- }
-
- return this.createList(entity.id, entityType)
- .then((res) => res.data)
- .then((data) => {
- list.id = data.id;
- list.type = data.list_type;
- list.position = data.position;
- list.label = data.label;
-
- return list.getIssues();
- });
- },
-
- getListIssues(list, emptyIssues = true) {
- const data = {
- ...queryToObject(this.filter.path, { gatherArrays: true }),
- page: list.page,
- };
-
- if (list.label && data.label_name) {
- data.label_name = data.label_name.filter((label) => label !== list.label.title);
- }
-
- if (emptyIssues) {
- list.loading = true;
- }
-
- return this.getIssuesForList(list.id, data)
- .then((res) => res.data)
- .then((data) => {
- list.loading = false;
- list.issuesSize = data.size;
-
- if (emptyIssues) {
- list.issues = [];
- }
-
- data.issues.forEach((issueObj) => {
- list.addIssue(new ListIssue(issueObj));
- });
-
- return data;
- });
- },
-
- getIssuesForList(id, filter = {}) {
- const data = { id };
- Object.keys(filter).forEach((key) => {
- data[key] = filter[key];
- });
-
- return axios.get(mergeUrlParams(data, this.generateIssuesPath(id)));
- },
-
- moveIssue(id, fromListId = null, toListId = null, moveBeforeId = null, moveAfterId = null) {
- return axios.put(this.generateIssuePath(this.state.endpoints.boardId, id), {
- from_list_id: fromListId,
- to_list_id: toListId,
- move_before_id: moveBeforeId,
- move_after_id: moveAfterId,
- });
- },
-
- moveListIssues(list, issue, oldIndex, newIndex, moveBeforeId, moveAfterId) {
- list.issues.splice(oldIndex, 1);
- list.issues.splice(newIndex, 0, issue);
-
- this.moveIssue(issue.id, null, null, moveBeforeId, moveAfterId).catch(() => {
- // TODO: handle request error
- });
- },
-
- moveMultipleIssues({ ids, fromListId, toListId, moveBeforeId, moveAfterId }) {
- return axios.put(this.generateMultiDragPath(this.state.endpoints.boardId), {
- from_list_id: fromListId,
- to_list_id: toListId,
- move_before_id: moveBeforeId,
- move_after_id: moveAfterId,
- ids,
- });
- },
-
- moveListMultipleIssues({ list, issues, oldIndicies, newIndex, moveBeforeId, moveAfterId }) {
- oldIndicies.reverse().forEach((index) => {
- list.issues.splice(index, 1);
- });
- list.issues.splice(newIndex, 0, ...issues);
-
- return this.moveMultipleIssues({
- ids: issues.map((issue) => issue.id),
- fromListId: null,
- toListId: null,
- moveBeforeId,
- moveAfterId,
- });
- },
-
- newIssue(id, issue) {
- if (typeof id === 'string') {
- id = getIdFromGraphQLId(id);
- }
-
- return axios.post(this.generateIssuesPath(id), {
- issue,
- });
- },
-
- newListIssue(list, issue) {
- list.addIssue(issue, null, 0);
- list.issuesSize += 1;
- let listId = list.id;
- if (typeof listId === 'string') {
- listId = getIdFromGraphQLId(listId);
- }
-
- return this.newIssue(list.id, issue)
- .then((res) => res.data)
- .then((data) => list.onNewIssueResponse(issue, data));
- },
-
- getBacklog(data) {
- return axios.get(
- mergeUrlParams(
- data,
- `${gon.relative_url_root}/-/boards/${this.state.endpoints.boardId}/issues.json`,
- ),
- );
- },
- removeIssueLabel(issue, removeLabel) {
- if (removeLabel) {
- issue.labels = issue.labels.filter((label) => removeLabel.id !== label.id);
- }
- },
-
- addIssueAssignee(issue, assignee) {
- if (!issue.findAssignee(assignee)) {
- issue.assignees.push(new ListAssignee(assignee));
- }
- },
-
- setIssueAssignees(issue, assignees) {
- issue.assignees = [...assignees];
- },
-
- removeIssueLabels(issue, labels) {
- labels.forEach(issue.removeLabel.bind(issue));
- },
-
- bulkUpdate(issueIds, extraData = {}) {
- const data = {
- update: Object.assign(extraData, {
- issuable_ids: issueIds.join(','),
- }),
- };
-
- return axios.post(this.state.endpoints.bulkUpdatePath, data);
- },
-
- getIssueInfo(endpoint) {
- return axios.get(endpoint);
- },
-
- toggleIssueSubscription(endpoint) {
- return axios.post(endpoint);
- },
-
- recentBoards() {
- return axios.get(this.state.endpoints.recentBoardsEndpoint);
- },
-
- setCurrentBoard(board) {
- this.state.currentBoard = board;
- },
-
- toggleMultiSelect(issue) {
- const selectedIssueIds = this.multiSelect.list.map((issue) => issue.id);
- const index = selectedIssueIds.indexOf(issue.id);
-
- if (index === -1) {
- this.multiSelect.list.push(issue);
- return;
- }
-
- this.multiSelect.list = [
- ...this.multiSelect.list.slice(0, index),
- ...this.multiSelect.list.slice(index + 1),
- ];
- },
- removeIssueAssignee(issue, removeAssignee) {
- if (removeAssignee) {
- issue.assignees = issue.assignees.filter((assignee) => assignee.id !== removeAssignee.id);
- }
- },
-
- findIssueAssignee(issue, findAssignee) {
- return issue.assignees.find((assignee) => assignee.id === findAssignee.id);
- },
-
- clearMultiSelect() {
- this.multiSelect.list = [];
- },
-
- removeAllIssueAssignees(issue) {
- issue.assignees = [];
- },
-
- addIssueMilestone(issue, milestone) {
- const miletoneId = issue.milestone ? issue.milestone.id : null;
- if (IS_EE && milestone.id !== miletoneId) {
- issue.milestone = new ListMilestone(milestone);
- }
- },
-
- setIssueLoadingState(issue, key, value) {
- issue.isLoading[key] = value;
- },
-
- updateIssueData(issue, newData) {
- Object.assign(issue, newData);
- },
-
- setIssueFetchingState(issue, key, value) {
- issue.isFetching[key] = value;
- },
-
- removeIssueMilestone(issue, removeMilestone) {
- if (IS_EE && removeMilestone && removeMilestone.id === issue.milestone.id) {
- issue.milestone = {};
- }
- },
-
- refreshIssueData(issue, obj) {
- const convertedObj = convertObjectPropsToCamelCase(obj, {
- dropKeys: ['issue_sidebar_endpoint', 'real_path', 'webUrl'],
- });
- convertedObj.sidebarInfoEndpoint = obj.issue_sidebar_endpoint;
- issue.path = obj.real_path || obj.webUrl;
- issue.project_id = obj.project_id;
- Object.assign(issue, convertedObj);
-
- if (obj.project) {
- issue.project = new IssueProject(obj.project);
- }
-
- if (obj.milestone) {
- issue.milestone = new ListMilestone(obj.milestone);
- issue.milestone_id = obj.milestone.id;
- }
-
- if (obj.labels) {
- issue.labels = obj.labels.map((label) => new ListLabel(label));
- }
-
- if (obj.assignees) {
- issue.assignees = obj.assignees.map((a) => new ListAssignee(a));
- }
- },
- addIssueLabel(issue, label) {
- if (!issue.findLabel(label)) {
- issue.labels.push(new ListLabel(label));
- }
- },
- updateIssue(issue) {
- const data = {
- issue: {
- milestone_id: issue.milestone ? issue.milestone.id : null,
- due_date: issue.dueDate,
- assignee_ids: issue.assignees.length > 0 ? issue.assignees.map(({ id }) => id) : [0],
- label_ids: issue.labels.length > 0 ? issue.labels.map(({ id }) => id) : [''],
- },
- };
-
- return axios.patch(`${issue.path}.json`, data).then(({ data: body = {} } = {}) => {
- /**
- * Since post implementation of Scoped labels, server can reject
- * same key-ed labels. To keep the UI and server Model consistent,
- * we're just assigning labels that server echo's back to us when we
- * PATCH the said object.
- */
- if (body) {
- issue.labels = convertObjectPropsToCamelCase(body.labels, { deep: true });
- }
- });
- },
-};
-
-BoardsStoreEE.initEESpecific(boardsStore);
-
-export default boardsStore;
diff --git a/app/assets/javascripts/boards/stores/boards_store_ee.js b/app/assets/javascripts/boards/stores/boards_store_ee.js
deleted file mode 100644
index 2a289ce5d0a..00000000000
--- a/app/assets/javascripts/boards/stores/boards_store_ee.js
+++ /dev/null
@@ -1,5 +0,0 @@
-// this is just to make ee_else_ce happy and will be cleaned up in https://gitlab.com/gitlab-org/gitlab-foss/issues/59807
-
-export default {
- initEESpecific() {},
-};
diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue
index a372233e543..02ab34447ca 100644
--- a/app/assets/javascripts/content_editor/components/content_editor.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor.vue
@@ -100,11 +100,13 @@ export default {
:class="{ 'is-focused': focused }"
>
<top-toolbar ref="toolbar" class="gl-mb-4" />
- <formatting-bubble-menu />
<div v-if="isLoadingContent" class="gl-w-full gl-display-flex gl-justify-content-center">
<gl-loading-icon size="sm" />
</div>
- <tiptap-editor-content v-else class="md" :editor="contentEditor.tiptapEditor" />
+ <template v-else>
+ <formatting-bubble-menu />
+ <tiptap-editor-content class="md" :editor="contentEditor.tiptapEditor" />
+ </template>
</div>
</div>
</content-editor-provider>
diff --git a/app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue b/app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue
index 6c00480b87e..14a553ff30b 100644
--- a/app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue
+++ b/app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue
@@ -20,7 +20,11 @@ export default {
};
</script>
<template>
- <bubble-menu class="gl-shadow gl-rounded-base" :editor="tiptapEditor">
+ <bubble-menu
+ data-testid="formatting-bubble-menu"
+ class="gl-shadow gl-rounded-base"
+ :editor="tiptapEditor"
+ >
<gl-button-group>
<toolbar-button
data-testid="bold"
diff --git a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
index 5472e51445a..d74b6e8edf6 100644
--- a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
+++ b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
@@ -123,6 +123,7 @@ export default {
isWarningDismissed: false,
isLoading: false,
submitted: false,
+ ccAlertDismissed: false,
};
},
computed: {
@@ -151,7 +152,7 @@ export default {
return this.form[this.refFullName]?.descriptions ?? {};
},
ccRequiredError() {
- return this.error === CC_VALIDATION_REQUIRED_ERROR;
+ return this.error === CC_VALIDATION_REQUIRED_ERROR && !this.ccAlertDismissed;
},
},
watch: {
@@ -292,6 +293,7 @@ export default {
},
createPipeline() {
this.submitted = true;
+ this.ccAlertDismissed = false;
return axios
.post(this.pipelinesPath, {
@@ -333,13 +335,17 @@ export default {
this.warnings = warnings;
this.totalWarnings = totalWarnings;
},
+ dismissError() {
+ this.ccAlertDismissed = true;
+ this.error = null;
+ },
},
};
</script>
<template>
<gl-form @submit.prevent="createPipeline">
- <cc-validation-required-alert v-if="ccRequiredError" class="gl-pb-5" />
+ <cc-validation-required-alert v-if="ccRequiredError" class="gl-pb-5" @dismiss="dismissError" />
<gl-alert
v-else-if="error"
:title="errorTitle"
diff --git a/app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue b/app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue
index e4edb950a1e..91d8fca0487 100644
--- a/app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue
+++ b/app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue
@@ -43,6 +43,7 @@ export default {
isSharedRunnerEnabled: this.isEnabled,
errorMessage: null,
successfulValidation: false,
+ ccAlertDismissed: false,
};
},
computed: {
@@ -50,7 +51,8 @@ export default {
return (
this.isCreditCardValidationRequired &&
!this.isSharedRunnerEnabled &&
- !this.successfulValidation
+ !this.successfulValidation &&
+ !this.ccAlertDismissed
);
},
},
@@ -89,6 +91,7 @@ export default {
class="gl-pb-5"
:custom-message="$options.i18n.REQUIRES_VALIDATION_TEXT"
@verifiedCreditCard="creditCardValidated"
+ @dismiss="ccAlertDismissed = true"
/>
<gl-toggle
diff --git a/app/models/user.rb b/app/models/user.rb
index e714c114857..e6f3ce38ba8 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -253,8 +253,6 @@ class User < ApplicationRecord
validates :color_scheme_id, allow_nil: true, inclusion: { in: Gitlab::ColorSchemes.valid_ids,
message: _("%{placeholder} is not a valid color scheme") % { placeholder: '%{value}' } }
- validates :website_url, allow_blank: true, url: true
-
before_validation :sanitize_attrs
before_validation :reset_secondary_emails, if: :email_changed?
before_save :default_private_profile_to_false
diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml
index a3f817cdd72..524f6dc820e 100644
--- a/app/views/shared/boards/_show.html.haml
+++ b/app/views/shared/boards/_show.html.haml
@@ -18,5 +18,5 @@
= render 'shared/issuable/search_bar', type: :boards, board: board
#board-app.boards-app.position-relative{ "v-cloak" => "true", data: board_data, ":class" => "{ 'is-compact': detailIssueVisible }" }
- %board-content{ ":lists" => "state.lists", ":disabled" => "disabled" }
+ %board-content{ ":disabled" => "disabled" }
%board-settings-sidebar
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index d4588ee78a4..d6838255916 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -2445,7 +2445,7 @@
:urgency: :high
:resource_boundary: :cpu
:weight: 5
- :idempotent:
+ :idempotent: true
:tags: []
- :name: process_commit
:worker_name: ProcessCommitWorker
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 4a49e18eb9b..7d0322361b8 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -1,8 +1,10 @@
# frozen_string_literal: true
-class PostReceive # rubocop:disable Scalability/IdempotentWorker
+class PostReceive
include ApplicationWorker
+ idempotent!
+ deduplicate :none
data_consistency :always
sidekiq_options retry: 3
diff --git a/config/feature_flags/development/linear_groups_template_finder_extended_group_search.yml b/config/feature_flags/development/linear_groups_template_finder_extended_group_search.yml
new file mode 100644
index 00000000000..98505f561b0
--- /dev/null
+++ b/config/feature_flags/development/linear_groups_template_finder_extended_group_search.yml
@@ -0,0 +1,8 @@
+---
+name: linear_groups_template_finder_extended_group_search
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68936
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339439
+milestone: '14.3'
+type: development
+group: group::access
+default_enabled: false
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index 3cfcfacf6ae..54fb13c566a 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -1263,6 +1263,14 @@ For example, you can adapt the `rsync` strategy from the
[moving repositories documentation](../operations/moving_repositories.md).
Alternatively, run the CI pipelines of those projects that contain a `pages` job again.
+## 404 or 500 error when accessing GitLab Pages in a Geo setup
+
+Pages sites are only available on the primary Geo site, while the codebase of the project is available on all sites.
+
+If you try to access a Pages page on a secondary site, you will get a 404 or 500 HTTP code depending on the access control.
+
+Read more which [features don't support Geo replication/verification](../geo/replication/datatypes.md#limitations-on-replicationverification).
+
### Failed to connect to the internal GitLab API
If you see the following error:
diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md
index c534a630480..8acb8d34214 100644
--- a/doc/user/profile/personal_access_tokens.md
+++ b/doc/user/profile/personal_access_tokens.md
@@ -12,11 +12,17 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - Introduced in GitLab 13.3: [Additional notifications for expiring tokens](https://gitlab.com/gitlab-org/gitlab/-/issues/214721).
> - Introduced in GitLab 14.1: [Prefill token name and scopes](https://gitlab.com/gitlab-org/gitlab/-/issues/334664).
-If you're unable to use [OAuth2](../../api/oauth2.md), you can use a personal access token to authenticate with the [GitLab API](../../api/index.md#personalproject-access-tokens). You can also use a personal access token with Git to authenticate over HTTP.
+Personal access tokens can be an alternative to [OAuth2](../../api/oauth2.md) and used to:
+
+- Authenticate with the [GitLab API](../../api/index.md#personalproject-access-tokens).
+- Authenticate with Git using HTTP Basic Authentication.
In both cases, you authenticate with a personal access token in place of your password.
-Personal access tokens are required when [Two-Factor Authentication (2FA)](account/two_factor_authentication.md) is enabled.
+Personal access tokens are:
+
+- Required when [two-factor authentication (2FA)](account/two_factor_authentication.md) is enabled.
+- Similar to [project access tokens](../project/settings/project_access_tokens.md), but are attached to a user rather than a project.
For examples of how you can use a personal access token to authenticate with the API, see the [API documentation](../../api/index.md#personalproject-access-tokens).
diff --git a/doc/user/project/settings/project_access_tokens.md b/doc/user/project/settings/project_access_tokens.md
index b5a9537d827..8b2b22bd521 100644
--- a/doc/user/project/settings/project_access_tokens.md
+++ b/doc/user/project/settings/project_access_tokens.md
@@ -7,25 +7,30 @@ type: reference, howto
# Project access tokens
-NOTE:
-Project access tokens are supported for self-managed instances on Free and above. They are also supported on GitLab SaaS Premium and above (excluding [trial licenses](https://about.gitlab.com/free-trial/)). Self-managed Free instances should review their security and compliance policies with regards to [user self-enrollment](../../admin_area/settings/sign_up_restrictions.md#disable-new-sign-ups) and consider [disabling project access tokens](#enable-or-disable-project-access-token-creation) to lower potential abuse.
-
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/210181) in GitLab 13.0.
> - [Became available on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/235765) in GitLab 13.5 for paid groups only.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/235765) in GitLab 13.5.
-WARNING:
-This feature might not be available to you. Check the **version history** note above for details.
+Project access tokens are similar to [personal access tokens](../../profile/personal_access_tokens.md)
+except they are attached to a project rather than a user. They can be used to:
+
+- Authenticate with the [GitLab API](../../../api/index.md#personalproject-access-tokens).
+- Authenticate with Git using HTTP Basic Authentication. If you are asked for a username when
+ authenticating, you can use any non-empty value because only the token is needed.
-Project access tokens are scoped to a project and can be used to authenticate with the
-[GitLab API](../../../api/index.md#personalproject-access-tokens). You can also use
-project access tokens with Git to authenticate over HTTPS. If you are asked for a
-username when authenticating over HTTPS, you can use any non-empty value because only
-the token is needed.
+Project access tokens:
-Project access tokens expire on the date you define, at midnight UTC.
+- Expire on the date you define, at midnight UTC.
+- Are supported for self-managed instances on Free tier and above. Free self-managed instances
+ should:
+ - Review their security and compliance policies with regards to
+ [user self-enrollment](../../admin_area/settings/sign_up_restrictions.md#disable-new-sign-ups).
+ - Consider [disabling project access tokens](#enable-or-disable-project-access-token-creation) to
+ lower potential abuse.
+- Are also supported on GitLab SaaS Premium and above (excluding [trial licenses](https://about.gitlab.com/free-trial/).)
-For examples of how you can use a project access token to authenticate with the API, see the following section from our [API Docs](../../../api/index.md#personalproject-access-tokens).
+For examples of how you can use a project access token to authenticate with the API, see the
+[relevant section from our API Docs](../../../api/index.md#personalproject-access-tokens).
## Creating a project access token
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index a50f3fda7d3..4fc2f939c97 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1570,7 +1570,7 @@ msgstr ""
msgid "APIFuzzing|Configure HTTP basic authentication values. Other authentication methods are supported. %{linkStart}Learn more%{linkEnd}."
msgstr ""
-msgid "APIFuzzing|Customize common API fuzzing settings to suit your requirements. For details of more advanced configuration options, see the %{docsLinkStart}GitLab API Fuzzing documentation%{docsLinkEnd}."
+msgid "APIFuzzing|Customize your project's API fuzzing configuration options and copy the code snippet to your .gitlab-ci.yml file to apply any changes. Note that this tool does not reflect or update your .gitlab-ci.yml file automatically. For details of more advanced configuration options, see the %{docsLinkStart}GitLab API Fuzzing documentation%{docsLinkEnd}."
msgstr ""
msgid "APIFuzzing|Enable authentication"
@@ -1633,9 +1633,6 @@ msgstr ""
msgid "APIFuzzing|To prevent a security leak, authentication info must be added as a %{ciVariablesLinkStart}CI variable%{ciVariablesLinkEnd}. As a user with maintainer access rights, you can manage CI variables in the %{ciSettingsLinkStart}Settings%{ciSettingsLinkEnd} area."
msgstr ""
-msgid "APIFuzzing|Use this tool to generate API fuzzing configuration YAML to copy into your .gitlab-ci.yml file. This tool does not reflect or update your .gitlab-ci.yml file automatically."
-msgstr ""
-
msgid "APIFuzzing|Username for basic authentication"
msgstr ""
@@ -3517,12 +3514,6 @@ msgstr ""
msgid "An error occurred when removing the label."
msgstr ""
-msgid "An error occurred when toggling the notification subscription"
-msgstr ""
-
-msgid "An error occurred when updating the issue weight"
-msgstr ""
-
msgid "An error occurred when updating the title"
msgstr ""
@@ -3619,9 +3610,6 @@ msgstr ""
msgid "An error occurred while fetching reference"
msgstr ""
-msgid "An error occurred while fetching sidebar data"
-msgstr ""
-
msgid "An error occurred while fetching tags. Retry the search."
msgstr ""
@@ -8490,6 +8478,9 @@ msgstr ""
msgid "Configure a %{codeStart}.gitlab-webide.yml%{codeEnd} file in the %{codeStart}.gitlab%{codeEnd} directory to start using the Web Terminal. %{helpStart}Learn more.%{helpEnd}"
msgstr ""
+msgid "Configure approvals by authors and committers on all projects."
+msgstr ""
+
msgid "Configure existing installation"
msgstr ""
@@ -10725,10 +10716,10 @@ msgstr ""
msgid "Define a custom pattern with cron syntax"
msgstr ""
-msgid "Define approval settings."
+msgid "Define approval rules."
msgstr ""
-msgid "Define approval settings. %{linkStart}Learn more.%{linkEnd}"
+msgid "Define approval rules. %{linkStart}Learn more.%{linkEnd}"
msgstr ""
msgid "Define custom rules for what constitutes spam, independent of Akismet"
@@ -10737,7 +10728,7 @@ msgstr ""
msgid "Define environments in the deploy stage(s) in %{code_open}.gitlab-ci.yml%{code_close} to track deployments here."
msgstr ""
-msgid "Define how approval rules are applied as a merge request moves toward completion."
+msgid "Define how approval rules are applied to merge requests."
msgstr ""
msgid "Definition"
@@ -21041,9 +21032,6 @@ msgstr ""
msgid "Merge request %{mr_link} was reviewed by %{mr_author}"
msgstr ""
-msgid "Merge request (MR) approvals"
-msgstr ""
-
msgid "Merge request analytics"
msgstr ""
@@ -27682,9 +27670,6 @@ msgstr ""
msgid "Registry setup"
msgstr ""
-msgid "Regulate approvals by authors/committers. Affects all projects."
-msgstr ""
-
msgid "Reindexing Status: %{status} (Slice multiplier: %{multiplier}, Maximum running slices: %{max_slices})"
msgstr ""
@@ -29558,7 +29543,7 @@ msgstr ""
msgid "SecurityApprovals|Requires approval for Denied licenses. %{linkStart}More information%{linkEnd}"
msgstr ""
-msgid "SecurityApprovals|Requires approval for decreases in test coverage. %{linkStart}More information%{linkEnd}"
+msgid "SecurityApprovals|Requires approval for decreases in test coverage. %{linkStart}Learn more.%{linkEnd}"
msgstr ""
msgid "SecurityApprovals|Requires approval for vulnerabilities. %{linkStart}Learn more.%{linkEnd}"
@@ -31254,9 +31239,6 @@ msgstr ""
msgid "Something went wrong while exporting requirements"
msgstr ""
-msgid "Something went wrong while fetching %{listType} list"
-msgstr ""
-
msgid "Something went wrong while fetching branches"
msgstr ""
@@ -31311,9 +31293,6 @@ msgstr ""
msgid "Something went wrong while merging this merge request. Please try again."
msgstr ""
-msgid "Something went wrong while moving issues."
-msgstr ""
-
msgid "Something went wrong while obtaining the Let's Encrypt certificate."
msgstr ""
diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh
index 641bf6a5d10..8ec26e7ba89 100755
--- a/scripts/review_apps/review-apps.sh
+++ b/scripts/review_apps/review-apps.sh
@@ -244,10 +244,9 @@ function deploy() {
echoinfo "Deploying ${release} to ${CI_ENVIRONMENT_URL} ..." true
IMAGE_REPOSITORY="registry.gitlab.com/gitlab-org/build/cng-mirror"
- gitlab_migrations_image_repository="${IMAGE_REPOSITORY}/gitlab-rails-ee"
+ gitlab_toolbox_image_repository="${IMAGE_REPOSITORY}/gitlab-toolbox-ee"
gitlab_sidekiq_image_repository="${IMAGE_REPOSITORY}/gitlab-sidekiq-ee"
gitlab_webservice_image_repository="${IMAGE_REPOSITORY}/gitlab-webservice-ee"
- gitlab_task_runner_image_repository="${IMAGE_REPOSITORY}/gitlab-toolbox-ee"
gitlab_gitaly_image_repository="${IMAGE_REPOSITORY}/gitaly"
gitaly_image_tag=$(parse_gitaly_image_tag)
gitlab_shell_image_repository="${IMAGE_REPOSITORY}/gitlab-shell"
@@ -272,7 +271,7 @@ HELM_CMD=$(cat << EOF
--set releaseOverride="${release}" \
--set global.hosts.hostSuffix="${HOST_SUFFIX}" \
--set global.hosts.domain="${REVIEW_APPS_DOMAIN}" \
- --set gitlab.migrations.image.repository="${gitlab_migrations_image_repository}" \
+ --set gitlab.migrations.image.repository="${gitlab_toolbox_image_repository}" \
--set gitlab.migrations.image.tag="${CI_COMMIT_REF_SLUG}" \
--set gitlab.gitaly.image.repository="${gitlab_gitaly_image_repository}" \
--set gitlab.gitaly.image.tag="${gitaly_image_tag}" \
@@ -286,7 +285,7 @@ HELM_CMD=$(cat << EOF
--set gitlab.webservice.image.tag="${CI_COMMIT_REF_SLUG}" \
--set gitlab.webservice.workhorse.image="${gitlab_workhorse_image_repository}" \
--set gitlab.webservice.workhorse.tag="${CI_COMMIT_REF_SLUG}" \
- --set gitlab.task-runner.image.repository="${gitlab_task_runner_image_repository}" \
+ --set gitlab.task-runner.image.repository="${gitlab_toolbox_image_repository}" \
--set gitlab.task-runner.image.tag="${CI_COMMIT_REF_SLUG}"
EOF
)
diff --git a/spec/features/profiles/user_edit_profile_spec.rb b/spec/features/profiles/user_edit_profile_spec.rb
index 48686239297..d941988d12f 100644
--- a/spec/features/profiles/user_edit_profile_spec.rb
+++ b/spec/features/profiles/user_edit_profile_spec.rb
@@ -32,7 +32,7 @@ RSpec.describe 'User edit profile' do
fill_in 'user_skype', with: 'testskype'
fill_in 'user_linkedin', with: 'testlinkedin'
fill_in 'user_twitter', with: 'testtwitter'
- fill_in 'user_website_url', with: 'http://testurl.com'
+ fill_in 'user_website_url', with: 'testurl'
fill_in 'user_location', with: 'Ukraine'
fill_in 'user_bio', with: 'I <3 GitLab :tada:'
fill_in 'user_job_title', with: 'Frontend Engineer'
@@ -43,7 +43,7 @@ RSpec.describe 'User edit profile' do
skype: 'testskype',
linkedin: 'testlinkedin',
twitter: 'testtwitter',
- website_url: 'http://testurl.com',
+ website_url: 'testurl',
bio: 'I <3 GitLab :tada:',
bio_html: '<p data-sourcepos="1:1-1:18" dir="auto">I &lt;3 GitLab <gl-emoji title="party popper" data-name="tada" data-unicode-version="6.0">🎉</gl-emoji></p>',
job_title: 'Frontend Engineer',
@@ -65,17 +65,6 @@ RSpec.describe 'User edit profile' do
end
end
- it 'shows an error if the website url is not valid' do
- fill_in 'user_website_url', with: 'admin@gitlab.com'
- submit_settings
-
- expect(user.reload).to have_attributes(
- website_url: ''
- )
-
- expect(page).to have_content('Website url is not a valid URL')
- end
-
describe 'when I change my email' do
before do
user.send_reset_password_instructions
diff --git a/spec/frontend/boards/boards_store_spec.js b/spec/frontend/boards/boards_store_spec.js
deleted file mode 100644
index 02881333273..00000000000
--- a/spec/frontend/boards/boards_store_spec.js
+++ /dev/null
@@ -1,1013 +0,0 @@
-import AxiosMockAdapter from 'axios-mock-adapter';
-import { TEST_HOST } from 'helpers/test_constants';
-import eventHub from '~/boards/eventhub';
-
-import ListIssue from '~/boards/models/issue';
-import List from '~/boards/models/list';
-import boardsStore from '~/boards/stores/boards_store';
-import axios from '~/lib/utils/axios_utils';
-import { listObj, listObjDuplicate } from './mock_data';
-
-jest.mock('js-cookie');
-
-const createTestIssue = () => ({
- title: 'Testing',
- id: 1,
- iid: 1,
- confidential: false,
- labels: [],
- assignees: [],
-});
-
-describe('boardsStore', () => {
- const dummyResponse = "without type checking this doesn't matter";
- const boardId = 'dummy-board-id';
- const endpoints = {
- boardsEndpoint: `${TEST_HOST}/boards`,
- listsEndpoint: `${TEST_HOST}/lists`,
- bulkUpdatePath: `${TEST_HOST}/bulk/update`,
- recentBoardsEndpoint: `${TEST_HOST}/recent/boards`,
- };
-
- let axiosMock;
-
- beforeEach(() => {
- axiosMock = new AxiosMockAdapter(axios);
- boardsStore.setEndpoints({
- ...endpoints,
- boardId,
- });
- });
-
- afterEach(() => {
- axiosMock.restore();
- });
-
- const setupDefaultResponses = () => {
- axiosMock
- .onGet(`${endpoints.listsEndpoint}/${listObj.id}/issues?id=${listObj.id}&page=1`)
- .reply(200, { issues: [createTestIssue()] });
- axiosMock.onPost(endpoints.listsEndpoint).reply(200, listObj);
- axiosMock.onPut();
- };
-
- describe('all', () => {
- it('makes a request to fetch lists', () => {
- axiosMock.onGet(endpoints.listsEndpoint).replyOnce(200, dummyResponse);
- const expectedResponse = expect.objectContaining({ data: dummyResponse });
-
- return expect(boardsStore.all()).resolves.toEqual(expectedResponse);
- });
-
- it('fails for error response', () => {
- axiosMock.onGet(endpoints.listsEndpoint).replyOnce(500);
-
- return expect(boardsStore.all()).rejects.toThrow();
- });
- });
-
- describe('createList', () => {
- const entityType = 'moorhen';
- const entityId = 'quack';
- const expectedRequest = expect.objectContaining({
- data: JSON.stringify({ list: { [entityType]: entityId } }),
- });
-
- let requestSpy;
-
- beforeEach(() => {
- requestSpy = jest.fn();
- axiosMock.onPost(endpoints.listsEndpoint).replyOnce((config) => requestSpy(config));
- });
-
- it('makes a request to create a list', () => {
- requestSpy.mockReturnValue([200, dummyResponse]);
- const expectedResponse = expect.objectContaining({ data: dummyResponse });
-
- return expect(boardsStore.createList(entityId, entityType))
- .resolves.toEqual(expectedResponse)
- .then(() => {
- expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
- });
- });
-
- it('fails for error response', () => {
- requestSpy.mockReturnValue([500]);
-
- return expect(boardsStore.createList(entityId, entityType))
- .rejects.toThrow()
- .then(() => {
- expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
- });
- });
- });
-
- describe('updateList', () => {
- const id = 'David Webb';
- const position = 'unknown';
- const collapsed = false;
- const expectedRequest = expect.objectContaining({
- data: JSON.stringify({ list: { position, collapsed } }),
- });
-
- let requestSpy;
-
- beforeEach(() => {
- requestSpy = jest.fn();
- axiosMock.onPut(`${endpoints.listsEndpoint}/${id}`).replyOnce((config) => requestSpy(config));
- });
-
- it('makes a request to update a list position', () => {
- requestSpy.mockReturnValue([200, dummyResponse]);
- const expectedResponse = expect.objectContaining({ data: dummyResponse });
-
- return expect(boardsStore.updateList(id, position, collapsed))
- .resolves.toEqual(expectedResponse)
- .then(() => {
- expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
- });
- });
-
- it('fails for error response', () => {
- requestSpy.mockReturnValue([500]);
-
- return expect(boardsStore.updateList(id, position, collapsed))
- .rejects.toThrow()
- .then(() => {
- expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
- });
- });
- });
-
- describe('destroyList', () => {
- const id = '-42';
-
- let requestSpy;
-
- beforeEach(() => {
- requestSpy = jest.fn();
- axiosMock
- .onDelete(`${endpoints.listsEndpoint}/${id}`)
- .replyOnce((config) => requestSpy(config));
- });
-
- it('makes a request to delete a list', () => {
- requestSpy.mockReturnValue([200, dummyResponse]);
- const expectedResponse = expect.objectContaining({ data: dummyResponse });
-
- return expect(boardsStore.destroyList(id))
- .resolves.toEqual(expectedResponse)
- .then(() => {
- expect(requestSpy).toHaveBeenCalled();
- });
- });
-
- it('fails for error response', () => {
- requestSpy.mockReturnValue([500]);
-
- return expect(boardsStore.destroyList(id))
- .rejects.toThrow()
- .then(() => {
- expect(requestSpy).toHaveBeenCalled();
- });
- });
- });
-
- describe('saveList', () => {
- let list;
-
- beforeEach(() => {
- list = new List(listObj);
- setupDefaultResponses();
- });
-
- it('makes a request to save a list', () => {
- const expectedResponse = expect.objectContaining({ issues: [createTestIssue()] });
- const expectedListValue = {
- id: listObj.id,
- position: listObj.position,
- type: listObj.list_type,
- label: listObj.label,
- };
- expect(list.id).toBe(listObj.id);
- expect(list.position).toBe(listObj.position);
- expect(list).toMatchObject(expectedListValue);
-
- return expect(boardsStore.saveList(list)).resolves.toEqual(expectedResponse);
- });
- });
-
- describe('getListIssues', () => {
- let list;
-
- beforeEach(() => {
- list = new List(listObj);
- setupDefaultResponses();
- });
-
- it('makes a request to get issues', () => {
- const expectedResponse = expect.objectContaining({ issues: [createTestIssue()] });
- expect(list.issues).toEqual([]);
-
- return expect(boardsStore.getListIssues(list, true)).resolves.toEqual(expectedResponse);
- });
- });
-
- describe('getIssuesForList', () => {
- const id = 'TOO-MUCH';
- const url = `${endpoints.listsEndpoint}/${id}/issues?id=${id}`;
-
- it('makes a request to fetch list issues', () => {
- axiosMock.onGet(url).replyOnce(200, dummyResponse);
- const expectedResponse = expect.objectContaining({ data: dummyResponse });
-
- return expect(boardsStore.getIssuesForList(id)).resolves.toEqual(expectedResponse);
- });
-
- it('makes a request to fetch list issues with filter', () => {
- const filter = { algal: 'scrubber' };
- axiosMock.onGet(`${url}&algal=scrubber`).replyOnce(200, dummyResponse);
- const expectedResponse = expect.objectContaining({ data: dummyResponse });
-
- return expect(boardsStore.getIssuesForList(id, filter)).resolves.toEqual(expectedResponse);
- });
-
- it('fails for error response', () => {
- axiosMock.onGet(url).replyOnce(500);
-
- return expect(boardsStore.getIssuesForList(id)).rejects.toThrow();
- });
- });
-
- describe('moveIssue', () => {
- const urlRoot = 'potato';
- const id = 'over 9000';
- const fromListId = 'left';
- const toListId = 'right';
- const moveBeforeId = 'up';
- const moveAfterId = 'down';
- const expectedRequest = expect.objectContaining({
- data: JSON.stringify({
- from_list_id: fromListId,
- to_list_id: toListId,
- move_before_id: moveBeforeId,
- move_after_id: moveAfterId,
- }),
- });
-
- let requestSpy;
-
- beforeAll(() => {
- global.gon.relative_url_root = urlRoot;
- });
-
- afterAll(() => {
- delete global.gon.relative_url_root;
- });
-
- beforeEach(() => {
- requestSpy = jest.fn();
- axiosMock
- .onPut(`${urlRoot}/-/boards/${boardId}/issues/${id}`)
- .replyOnce((config) => requestSpy(config));
- });
-
- it('makes a request to move an issue between lists', () => {
- requestSpy.mockReturnValue([200, dummyResponse]);
- const expectedResponse = expect.objectContaining({ data: dummyResponse });
-
- return expect(boardsStore.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId))
- .resolves.toEqual(expectedResponse)
- .then(() => {
- expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
- });
- });
-
- it('fails for error response', () => {
- requestSpy.mockReturnValue([500]);
-
- return expect(boardsStore.moveIssue(id, fromListId, toListId, moveBeforeId, moveAfterId))
- .rejects.toThrow()
- .then(() => {
- expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
- });
- });
- });
-
- describe('newIssue', () => {
- const id = 1;
- const issue = { some: 'issue data' };
- const url = `${endpoints.listsEndpoint}/${id}/issues`;
- const expectedRequest = expect.objectContaining({
- data: JSON.stringify({
- issue,
- }),
- });
-
- let requestSpy;
-
- beforeEach(() => {
- requestSpy = jest.fn();
- axiosMock.onPost(url).replyOnce((config) => requestSpy(config));
- });
-
- it('makes a request to create a new issue', () => {
- requestSpy.mockReturnValue([200, dummyResponse]);
- const expectedResponse = expect.objectContaining({ data: dummyResponse });
-
- return expect(boardsStore.newIssue(id, issue))
- .resolves.toEqual(expectedResponse)
- .then(() => {
- expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
- });
- });
-
- it('fails for error response', () => {
- requestSpy.mockReturnValue([500]);
-
- return expect(boardsStore.newIssue(id, issue))
- .rejects.toThrow()
- .then(() => {
- expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
- });
- });
- });
-
- describe('getBacklog', () => {
- const urlRoot = 'deep';
- const url = `${urlRoot}/-/boards/${boardId}/issues.json?not=relevant`;
- const requestParams = {
- not: 'relevant',
- };
-
- beforeAll(() => {
- global.gon.relative_url_root = urlRoot;
- });
-
- afterAll(() => {
- delete global.gon.relative_url_root;
- });
-
- it('makes a request to fetch backlog', () => {
- axiosMock.onGet(url).replyOnce(200, dummyResponse);
- const expectedResponse = expect.objectContaining({ data: dummyResponse });
-
- return expect(boardsStore.getBacklog(requestParams)).resolves.toEqual(expectedResponse);
- });
-
- it('fails for error response', () => {
- axiosMock.onGet(url).replyOnce(500);
-
- return expect(boardsStore.getBacklog(requestParams)).rejects.toThrow();
- });
- });
-
- describe('bulkUpdate', () => {
- const issueIds = [1, 2, 3];
- const extraData = { moar: 'data' };
- const expectedRequest = expect.objectContaining({
- data: JSON.stringify({
- update: {
- ...extraData,
- issuable_ids: '1,2,3',
- },
- }),
- });
-
- let requestSpy;
-
- beforeEach(() => {
- requestSpy = jest.fn();
- axiosMock.onPost(endpoints.bulkUpdatePath).replyOnce((config) => requestSpy(config));
- });
-
- it('makes a request to create a list', () => {
- requestSpy.mockReturnValue([200, dummyResponse]);
- const expectedResponse = expect.objectContaining({ data: dummyResponse });
-
- return expect(boardsStore.bulkUpdate(issueIds, extraData))
- .resolves.toEqual(expectedResponse)
- .then(() => {
- expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
- });
- });
-
- it('fails for error response', () => {
- requestSpy.mockReturnValue([500]);
-
- return expect(boardsStore.bulkUpdate(issueIds, extraData))
- .rejects.toThrow()
- .then(() => {
- expect(requestSpy).toHaveBeenCalledWith(expectedRequest);
- });
- });
- });
-
- describe('getIssueInfo', () => {
- const dummyEndpoint = `${TEST_HOST}/some/where`;
-
- it('makes a request to the given endpoint', () => {
- axiosMock.onGet(dummyEndpoint).replyOnce(200, dummyResponse);
- const expectedResponse = expect.objectContaining({ data: dummyResponse });
-
- return expect(boardsStore.getIssueInfo(dummyEndpoint)).resolves.toEqual(expectedResponse);
- });
-
- it('fails for error response', () => {
- axiosMock.onGet(dummyEndpoint).replyOnce(500);
-
- return expect(boardsStore.getIssueInfo(dummyEndpoint)).rejects.toThrow();
- });
- });
-
- describe('toggleIssueSubscription', () => {
- const dummyEndpoint = `${TEST_HOST}/some/where`;
-
- it('makes a request to the given endpoint', () => {
- axiosMock.onPost(dummyEndpoint).replyOnce(200, dummyResponse);
- const expectedResponse = expect.objectContaining({ data: dummyResponse });
-
- return expect(boardsStore.toggleIssueSubscription(dummyEndpoint)).resolves.toEqual(
- expectedResponse,
- );
- });
-
- it('fails for error response', () => {
- axiosMock.onPost(dummyEndpoint).replyOnce(500);
-
- return expect(boardsStore.toggleIssueSubscription(dummyEndpoint)).rejects.toThrow();
- });
- });
-
- describe('recentBoards', () => {
- const url = `${endpoints.recentBoardsEndpoint}.json`;
-
- it('makes a request to fetch all boards', () => {
- axiosMock.onGet(url).replyOnce(200, dummyResponse);
- const expectedResponse = expect.objectContaining({ data: dummyResponse });
-
- return expect(boardsStore.recentBoards()).resolves.toEqual(expectedResponse);
- });
-
- it('fails for error response', () => {
- axiosMock.onGet(url).replyOnce(500);
-
- return expect(boardsStore.recentBoards()).rejects.toThrow();
- });
- });
-
- describe('when created', () => {
- beforeEach(() => {
- setupDefaultResponses();
-
- jest.spyOn(boardsStore, 'moveIssue').mockReturnValue(Promise.resolve());
- jest.spyOn(boardsStore, 'moveMultipleIssues').mockReturnValue(Promise.resolve());
-
- boardsStore.create();
- });
-
- it('starts with a blank state', () => {
- expect(boardsStore.state.lists.length).toBe(0);
- });
-
- describe('addList', () => {
- it('sorts by position', () => {
- boardsStore.addList({ position: 2 });
- boardsStore.addList({ position: 1 });
-
- expect(boardsStore.state.lists.map(({ position }) => position)).toEqual([1, 2]);
- });
- });
-
- describe('toggleFilter', () => {
- const dummyFilter = 'x=42';
- let updateTokensSpy;
-
- beforeEach(() => {
- updateTokensSpy = jest.fn();
- eventHub.$once('updateTokens', updateTokensSpy);
-
- // prevent using window.history
- jest.spyOn(boardsStore, 'updateFiltersUrl').mockReturnValue();
- });
-
- it('adds the filter if it is not present', () => {
- boardsStore.filter.path = 'something';
-
- boardsStore.toggleFilter(dummyFilter);
-
- expect(boardsStore.filter.path).toEqual(`something&${dummyFilter}`);
- expect(updateTokensSpy).toHaveBeenCalled();
- expect(boardsStore.updateFiltersUrl).toHaveBeenCalled();
- });
-
- it('removes the filter if it is present', () => {
- boardsStore.filter.path = `something&${dummyFilter}`;
-
- boardsStore.toggleFilter(dummyFilter);
-
- expect(boardsStore.filter.path).toEqual('something');
- expect(updateTokensSpy).toHaveBeenCalled();
- expect(boardsStore.updateFiltersUrl).toHaveBeenCalled();
- });
- });
-
- describe('lists', () => {
- it('creates new list without persisting to DB', () => {
- expect(boardsStore.state.lists.length).toBe(0);
-
- boardsStore.addList(listObj);
-
- expect(boardsStore.state.lists.length).toBe(1);
- });
-
- it('finds list by ID', () => {
- boardsStore.addList(listObj);
- const list = boardsStore.findList('id', listObj.id);
-
- expect(list.id).toBe(listObj.id);
- });
-
- it('finds list by type', () => {
- boardsStore.addList(listObj);
- const list = boardsStore.findList('type', 'label');
-
- expect(list).toBeDefined();
- });
-
- it('finds list by label ID', () => {
- boardsStore.addList(listObj);
- const list = boardsStore.findListByLabelId(listObj.label.id);
-
- expect(list.id).toBe(listObj.id);
- });
-
- it('gets issue when new list added', () => {
- boardsStore.addList(listObj);
- const list = boardsStore.findList('id', listObj.id);
-
- expect(boardsStore.state.lists.length).toBe(1);
-
- return axios.waitForAll().then(() => {
- expect(list.issues.length).toBe(1);
- expect(list.issues[0].id).toBe(1);
- });
- });
-
- it('persists new list', () => {
- boardsStore.new({
- title: 'Test',
- list_type: 'label',
- label: {
- id: 1,
- title: 'Testing',
- color: 'red',
- description: 'testing;',
- },
- });
-
- expect(boardsStore.state.lists.length).toBe(1);
-
- return axios.waitForAll().then(() => {
- const list = boardsStore.findList('id', listObj.id);
-
- expect(list).toEqual(
- expect.objectContaining({
- id: listObj.id,
- position: 0,
- }),
- );
- });
- });
-
- it('removes list from state', () => {
- boardsStore.addList(listObj);
-
- expect(boardsStore.state.lists.length).toBe(1);
-
- boardsStore.removeList(listObj.id);
-
- expect(boardsStore.state.lists.length).toBe(0);
- });
-
- it('moves the position of lists', () => {
- const listOne = boardsStore.addList(listObj);
- boardsStore.addList(listObjDuplicate);
-
- expect(boardsStore.state.lists.length).toBe(2);
-
- boardsStore.moveList(listOne, [listObjDuplicate.id, listObj.id]);
-
- expect(listOne.position).toBe(1);
- });
-
- it('moves an issue from one list to another', () => {
- const listOne = boardsStore.addList(listObj);
- const listTwo = boardsStore.addList(listObjDuplicate);
-
- expect(boardsStore.state.lists.length).toBe(2);
-
- return axios.waitForAll().then(() => {
- expect(listOne.issues.length).toBe(1);
- expect(listTwo.issues.length).toBe(1);
-
- boardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(1));
-
- expect(listOne.issues.length).toBe(0);
- expect(listTwo.issues.length).toBe(1);
- });
- });
-
- it('moves an issue from backlog to a list', () => {
- const backlog = boardsStore.addList({
- ...listObj,
- list_type: 'backlog',
- });
- const listTwo = boardsStore.addList(listObjDuplicate);
-
- expect(boardsStore.state.lists.length).toBe(2);
-
- return axios.waitForAll().then(() => {
- expect(backlog.issues.length).toBe(1);
- expect(listTwo.issues.length).toBe(1);
-
- boardsStore.moveIssueToList(backlog, listTwo, backlog.findIssue(1));
-
- expect(backlog.issues.length).toBe(0);
- expect(listTwo.issues.length).toBe(1);
- });
- });
-
- it('moves issue to top of another list', () => {
- const listOne = boardsStore.addList(listObj);
- const listTwo = boardsStore.addList(listObjDuplicate);
-
- expect(boardsStore.state.lists.length).toBe(2);
-
- return axios.waitForAll().then(() => {
- listOne.issues[0].id = 2;
-
- expect(listOne.issues.length).toBe(1);
- expect(listTwo.issues.length).toBe(1);
-
- boardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(2), 0);
-
- expect(listOne.issues.length).toBe(0);
- expect(listTwo.issues.length).toBe(2);
- expect(listTwo.issues[0].id).toBe(2);
- expect(boardsStore.moveIssue).toHaveBeenCalledWith(2, listOne.id, listTwo.id, null, 1);
- });
- });
-
- it('moves issue to bottom of another list', () => {
- const listOne = boardsStore.addList(listObj);
- const listTwo = boardsStore.addList(listObjDuplicate);
-
- expect(boardsStore.state.lists.length).toBe(2);
-
- return axios.waitForAll().then(() => {
- listOne.issues[0].id = 2;
-
- expect(listOne.issues.length).toBe(1);
- expect(listTwo.issues.length).toBe(1);
-
- boardsStore.moveIssueToList(listOne, listTwo, listOne.findIssue(2), 1);
-
- expect(listOne.issues.length).toBe(0);
- expect(listTwo.issues.length).toBe(2);
- expect(listTwo.issues[1].id).toBe(2);
- expect(boardsStore.moveIssue).toHaveBeenCalledWith(2, listOne.id, listTwo.id, 1, null);
- });
- });
-
- it('moves issue in list', () => {
- const issue = new ListIssue({
- title: 'Testing',
- id: 2,
- iid: 2,
- confidential: false,
- labels: [],
- assignees: [],
- });
- const list = boardsStore.addList(listObj);
-
- return axios.waitForAll().then(() => {
- list.addIssue(issue);
-
- expect(list.issues.length).toBe(2);
-
- boardsStore.moveIssueInList(list, issue, 0, 1, [1, 2]);
-
- expect(list.issues[0].id).toBe(2);
- expect(boardsStore.moveIssue).toHaveBeenCalledWith(2, null, null, 1, null);
- });
- });
- });
-
- describe('setListDetail', () => {
- it('sets the list detail', () => {
- boardsStore.detail.list = 'not a list';
-
- const dummyValue = 'new list';
- boardsStore.setListDetail(dummyValue);
-
- expect(boardsStore.detail.list).toEqual(dummyValue);
- });
- });
-
- describe('clearDetailIssue', () => {
- it('resets issue details', () => {
- boardsStore.detail.issue = 'something';
-
- boardsStore.clearDetailIssue();
-
- expect(boardsStore.detail.issue).toEqual({});
- });
- });
-
- describe('setIssueDetail', () => {
- it('sets issue details', () => {
- boardsStore.detail.issue = 'some details';
-
- const dummyValue = 'new details';
- boardsStore.setIssueDetail(dummyValue);
-
- expect(boardsStore.detail.issue).toEqual(dummyValue);
- });
- });
-
- describe('startMoving', () => {
- it('stores list and issue', () => {
- const dummyIssue = 'some issue';
- const dummyList = 'some list';
-
- boardsStore.startMoving(dummyList, dummyIssue);
-
- expect(boardsStore.moving.issue).toEqual(dummyIssue);
- expect(boardsStore.moving.list).toEqual(dummyList);
- });
- });
-
- describe('setTimeTrackingLimitToHours', () => {
- it('sets the timeTracking.LimitToHours option', () => {
- boardsStore.timeTracking.limitToHours = false;
-
- boardsStore.setTimeTrackingLimitToHours('true');
-
- expect(boardsStore.timeTracking.limitToHours).toEqual(true);
- });
- });
-
- describe('setCurrentBoard', () => {
- const dummyBoard = 'hoverboard';
-
- it('sets the current board', () => {
- const { state } = boardsStore;
- state.currentBoard = null;
-
- boardsStore.setCurrentBoard(dummyBoard);
-
- expect(state.currentBoard).toEqual(dummyBoard);
- });
- });
-
- describe('toggleMultiSelect', () => {
- let basicIssueObj;
-
- beforeAll(() => {
- basicIssueObj = { id: 987654 };
- });
-
- afterEach(() => {
- boardsStore.clearMultiSelect();
- });
-
- it('adds issue when not present', () => {
- boardsStore.toggleMultiSelect(basicIssueObj);
-
- const selectedIds = boardsStore.multiSelect.list.map(({ id }) => id);
-
- expect(selectedIds.includes(basicIssueObj.id)).toEqual(true);
- });
-
- it('removes issue when issue is present', () => {
- boardsStore.toggleMultiSelect(basicIssueObj);
- let selectedIds = boardsStore.multiSelect.list.map(({ id }) => id);
-
- expect(selectedIds.includes(basicIssueObj.id)).toEqual(true);
-
- boardsStore.toggleMultiSelect(basicIssueObj);
- selectedIds = boardsStore.multiSelect.list.map(({ id }) => id);
-
- expect(selectedIds.includes(basicIssueObj.id)).toEqual(false);
- });
- });
-
- describe('clearMultiSelect', () => {
- it('clears all the multi selected issues', () => {
- const issue1 = { id: 12345 };
- const issue2 = { id: 12346 };
-
- boardsStore.toggleMultiSelect(issue1);
- boardsStore.toggleMultiSelect(issue2);
-
- expect(boardsStore.multiSelect.list.length).toEqual(2);
-
- boardsStore.clearMultiSelect();
-
- expect(boardsStore.multiSelect.list.length).toEqual(0);
- });
- });
-
- describe('moveMultipleIssuesToList', () => {
- it('move issues on the new index', () => {
- const listOne = boardsStore.addList(listObj);
- const listTwo = boardsStore.addList(listObjDuplicate);
-
- expect(boardsStore.state.lists.length).toBe(2);
-
- return axios.waitForAll().then(() => {
- expect(listOne.issues.length).toBe(1);
- expect(listTwo.issues.length).toBe(1);
-
- boardsStore.moveMultipleIssuesToList({
- listFrom: listOne,
- listTo: listTwo,
- issues: listOne.issues,
- newIndex: 0,
- });
-
- expect(listTwo.issues.length).toBe(1);
- });
- });
- });
-
- describe('moveMultipleIssuesInList', () => {
- it('moves multiple issues in list', () => {
- const issueObj = {
- title: 'Issue #1',
- id: 12345,
- iid: 2,
- confidential: false,
- labels: [],
- assignees: [],
- };
- const issue1 = new ListIssue(issueObj);
- const issue2 = new ListIssue({
- ...issueObj,
- title: 'Issue #2',
- id: 12346,
- });
-
- const list = boardsStore.addList(listObj);
-
- return axios.waitForAll().then(() => {
- list.addIssue(issue1);
- list.addIssue(issue2);
-
- expect(list.issues.length).toBe(3);
- expect(list.issues[0].id).not.toBe(issue2.id);
-
- boardsStore.moveMultipleIssuesInList({
- list,
- issues: [issue1, issue2],
- oldIndicies: [0],
- newIndex: 1,
- idArray: [1, 12345, 12346],
- });
-
- expect(list.issues[0].id).toBe(issue1.id);
-
- expect(boardsStore.moveMultipleIssues).toHaveBeenCalledWith({
- ids: [issue1.id, issue2.id],
- fromListId: null,
- toListId: null,
- moveBeforeId: 1,
- moveAfterId: null,
- });
- });
- });
- });
-
- describe('addListIssue', () => {
- let list;
- const issue1 = new ListIssue({
- title: 'Testing',
- id: 2,
- iid: 2,
- confidential: false,
- labels: [
- {
- color: '#ff0000',
- description: 'testing;',
- id: 5000,
- priority: undefined,
- textColor: 'white',
- title: 'Test',
- },
- ],
- assignees: [],
- });
- const issue2 = new ListIssue({
- title: 'Testing',
- id: 1,
- iid: 1,
- confidential: false,
- labels: [
- {
- id: 1,
- title: 'test',
- color: 'red',
- description: 'testing',
- },
- ],
- assignees: [
- {
- id: 1,
- name: 'name',
- username: 'username',
- avatar_url: 'http://avatar_url',
- },
- ],
- real_path: 'path/to/issue',
- });
-
- beforeEach(() => {
- list = new List(listObj);
- list.addIssue(issue1);
- setupDefaultResponses();
- });
-
- it('adds issues that are not already on the list', () => {
- expect(list.findIssue(issue2.id)).toBe(undefined);
- expect(list.issues).toEqual([issue1]);
-
- boardsStore.addListIssue(list, issue2);
- expect(list.findIssue(issue2.id)).toBe(issue2);
- expect(list.issues.length).toBe(2);
- expect(list.issues).toEqual([issue1, issue2]);
- });
- });
-
- describe('updateIssue', () => {
- let issue;
- let patchSpy;
-
- beforeEach(() => {
- issue = new ListIssue({
- title: 'Testing',
- id: 1,
- iid: 1,
- confidential: false,
- labels: [
- {
- id: 1,
- title: 'test',
- color: 'red',
- description: 'testing',
- },
- ],
- assignees: [
- {
- id: 1,
- name: 'name',
- username: 'username',
- avatar_url: 'http://avatar_url',
- },
- ],
- real_path: 'path/to/issue',
- });
-
- patchSpy = jest.fn().mockReturnValue([200, { labels: [] }]);
- axiosMock.onPatch(`path/to/issue.json`).reply(({ data }) => patchSpy(JSON.parse(data)));
- });
-
- it('passes assignee ids when there are assignees', () => {
- boardsStore.updateIssue(issue);
- return boardsStore.updateIssue(issue).then(() => {
- expect(patchSpy).toHaveBeenCalledWith({
- issue: {
- milestone_id: null,
- assignee_ids: [1],
- label_ids: [1],
- },
- });
- });
- });
-
- it('passes assignee ids of [0] when there are no assignees', () => {
- issue.removeAllAssignees();
-
- return boardsStore.updateIssue(issue).then(() => {
- expect(patchSpy).toHaveBeenCalledWith({
- issue: {
- milestone_id: null,
- assignee_ids: [0],
- label_ids: [1],
- },
- });
- });
- });
- });
- });
-});
diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js
index a4cb2de71f4..f535679b8a0 100644
--- a/spec/frontend/boards/components/board_content_spec.js
+++ b/spec/frontend/boards/components/board_content_spec.js
@@ -8,7 +8,7 @@ import getters from 'ee_else_ce/boards/stores/getters';
import BoardColumn from '~/boards/components/board_column.vue';
import BoardContent from '~/boards/components/board_content.vue';
import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
-import { mockLists, mockListsWithModel } from '../mock_data';
+import { mockLists } from '../mock_data';
Vue.use(Vuex);
@@ -42,7 +42,7 @@ describe('BoardContent', () => {
});
wrapper = shallowMount(BoardContent, {
propsData: {
- lists: mockListsWithModel,
+ lists: mockLists,
disabled: false,
...props,
},
@@ -63,7 +63,7 @@ describe('BoardContent', () => {
});
it('renders a BoardColumn component per list', () => {
- expect(wrapper.findAllComponents(BoardColumn)).toHaveLength(mockListsWithModel.length);
+ expect(wrapper.findAllComponents(BoardColumn)).toHaveLength(mockLists.length);
});
it('renders BoardContentSidebar', () => {
diff --git a/spec/frontend/boards/issue_spec.js b/spec/frontend/boards/issue_spec.js
deleted file mode 100644
index 1f354fb04db..00000000000
--- a/spec/frontend/boards/issue_spec.js
+++ /dev/null
@@ -1,162 +0,0 @@
-/* global ListIssue */
-
-import '~/boards/models/label';
-import '~/boards/models/assignee';
-import '~/boards/models/issue';
-import '~/boards/models/list';
-import boardsStore from '~/boards/stores/boards_store';
-import { setMockEndpoints, mockIssue } from './mock_data';
-
-describe('Issue model', () => {
- let issue;
-
- beforeEach(() => {
- setMockEndpoints();
- boardsStore.create();
-
- issue = new ListIssue(mockIssue);
- });
-
- it('has label', () => {
- expect(issue.labels.length).toBe(1);
- });
-
- it('add new label', () => {
- issue.addLabel({
- id: 2,
- title: 'bug',
- color: 'blue',
- description: 'bugs!',
- });
-
- expect(issue.labels.length).toBe(2);
- });
-
- it('does not add label if label id exists', () => {
- issue.addLabel({
- id: 1,
- title: 'test 2',
- color: 'blue',
- description: 'testing',
- });
-
- expect(issue.labels.length).toBe(1);
- expect(issue.labels[0].color).toBe('#F0AD4E');
- });
-
- it('adds other label with same title', () => {
- issue.addLabel({
- id: 2,
- title: 'test',
- color: 'blue',
- description: 'other test',
- });
-
- expect(issue.labels.length).toBe(2);
- });
-
- it('finds label', () => {
- const label = issue.findLabel(issue.labels[0]);
-
- expect(label).toBeDefined();
- });
-
- it('removes label', () => {
- const label = issue.findLabel(issue.labels[0]);
- issue.removeLabel(label);
-
- expect(issue.labels.length).toBe(0);
- });
-
- it('removes multiple labels', () => {
- issue.addLabel({
- id: 2,
- title: 'bug',
- color: 'blue',
- description: 'bugs!',
- });
-
- expect(issue.labels.length).toBe(2);
-
- issue.removeLabels([issue.labels[0], issue.labels[1]]);
-
- expect(issue.labels.length).toBe(0);
- });
-
- it('adds assignee', () => {
- issue.addAssignee({
- id: 2,
- name: 'Bruce Wayne',
- username: 'batman',
- avatar_url: 'http://batman',
- });
-
- expect(issue.assignees.length).toBe(2);
- });
-
- it('finds assignee', () => {
- const assignee = issue.findAssignee(issue.assignees[0]);
-
- expect(assignee).toBeDefined();
- });
-
- it('removes assignee', () => {
- const assignee = issue.findAssignee(issue.assignees[0]);
- issue.removeAssignee(assignee);
-
- expect(issue.assignees.length).toBe(0);
- });
-
- it('removes all assignees', () => {
- issue.removeAllAssignees();
-
- expect(issue.assignees.length).toBe(0);
- });
-
- it('sets position to infinity if no position is stored', () => {
- expect(issue.position).toBe(Infinity);
- });
-
- it('sets position', () => {
- const relativePositionIssue = new ListIssue({
- title: 'Testing',
- iid: 1,
- confidential: false,
- relative_position: 1,
- labels: [],
- assignees: [],
- });
-
- expect(relativePositionIssue.position).toBe(1);
- });
-
- it('updates data', () => {
- issue.updateData({ subscribed: true });
-
- expect(issue.subscribed).toBe(true);
- });
-
- it('sets fetching state', () => {
- expect(issue.isFetching.subscriptions).toBe(true);
-
- issue.setFetchingState('subscriptions', false);
-
- expect(issue.isFetching.subscriptions).toBe(false);
- });
-
- it('sets loading state', () => {
- issue.setLoadingState('foo', true);
-
- expect(issue.isLoading.foo).toBe(true);
- });
-
- describe('update', () => {
- it('passes update to boardsStore', () => {
- jest.spyOn(boardsStore, 'updateIssue').mockImplementation();
-
- issue.update();
-
- expect(boardsStore.updateIssue).toHaveBeenCalledWith(issue);
- });
- });
-});
diff --git a/spec/frontend/boards/list_spec.js b/spec/frontend/boards/list_spec.js
deleted file mode 100644
index 4d6a82bdff0..00000000000
--- a/spec/frontend/boards/list_spec.js
+++ /dev/null
@@ -1,230 +0,0 @@
-/* global List */
-/* global ListAssignee */
-/* global ListIssue */
-/* global ListLabel */
-import MockAdapter from 'axios-mock-adapter';
-import waitForPromises from 'helpers/wait_for_promises';
-import '~/boards/models/label';
-import '~/boards/models/assignee';
-import '~/boards/models/issue';
-import '~/boards/models/list';
-import { ListType } from '~/boards/constants';
-import boardsStore from '~/boards/stores/boards_store';
-import axios from '~/lib/utils/axios_utils';
-import { listObj, listObjDuplicate, boardsMockInterceptor } from './mock_data';
-
-describe('List model', () => {
- let list;
- let mock;
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- mock.onAny().reply(boardsMockInterceptor);
- boardsStore.create();
- boardsStore.setEndpoints({
- listsEndpoint: '/test/-/boards/1/lists',
- });
-
- list = new List(listObj);
- return waitForPromises();
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- describe('list type', () => {
- const notExpandableList = ['blank'];
-
- const table = Object.keys(ListType).map((k) => {
- const value = ListType[k];
- return [value, !notExpandableList.includes(value)];
- });
- it.each(table)(`when list_type is %s boards isExpandable is %p`, (type, result) => {
- expect(new List({ id: 1, list_type: type }).isExpandable).toBe(result);
- });
- });
-
- it('gets issues when created', () => {
- expect(list.issues.length).toBe(1);
- });
-
- it('saves list and returns ID', () => {
- list = new List({
- title: 'test',
- label: {
- id: 1,
- title: 'test',
- color: '#ff0000',
- text_color: 'white',
- },
- });
- return list.save().then(() => {
- expect(list.id).toBe(listObj.id);
- expect(list.type).toBe('label');
- expect(list.position).toBe(0);
- expect(list.label).toEqual(listObj.label);
- });
- });
-
- it('destroys the list', () => {
- boardsStore.addList(listObj);
- list = boardsStore.findList('id', listObj.id);
-
- expect(boardsStore.state.lists.length).toBe(1);
- list.destroy();
-
- return waitForPromises().then(() => {
- expect(boardsStore.state.lists.length).toBe(0);
- });
- });
-
- it('gets issue from list', () => {
- const issue = list.findIssue(1);
-
- expect(issue).toBeDefined();
- });
-
- it('removes issue', () => {
- const issue = list.findIssue(1);
-
- expect(list.issues.length).toBe(1);
- list.removeIssue(issue);
-
- expect(list.issues.length).toBe(0);
- });
-
- it('sends service request to update issue label', () => {
- const listDup = new List(listObjDuplicate);
- const issue = new ListIssue({
- title: 'Testing',
- id: 1,
- iid: 1,
- confidential: false,
- labels: [list.label, listDup.label],
- assignees: [],
- });
-
- list.issues.push(issue);
- listDup.issues.push(issue);
-
- jest.spyOn(boardsStore, 'moveIssue');
-
- listDup.updateIssueLabel(issue, list);
-
- expect(boardsStore.moveIssue).toHaveBeenCalledWith(
- issue.id,
- list.id,
- listDup.id,
- undefined,
- undefined,
- );
- });
-
- describe('page number', () => {
- beforeEach(() => {
- jest.spyOn(list, 'getIssues').mockImplementation(() => {});
- list.issues = [];
- });
-
- it('increase page number if current issue count is more than the page size', () => {
- for (let i = 0; i < 30; i += 1) {
- list.issues.push(
- new ListIssue({
- title: 'Testing',
- id: i,
- iid: i,
- confidential: false,
- labels: [list.label],
- assignees: [],
- }),
- );
- }
- list.issuesSize = 50;
-
- expect(list.issues.length).toBe(30);
-
- list.nextPage();
-
- expect(list.page).toBe(2);
- expect(list.getIssues).toHaveBeenCalled();
- });
-
- it('does not increase page number if issue count is less than the page size', () => {
- list.issues.push(
- new ListIssue({
- title: 'Testing',
- id: 1,
- confidential: false,
- labels: [list.label],
- assignees: [],
- }),
- );
- list.issuesSize = 2;
-
- list.nextPage();
-
- expect(list.page).toBe(1);
- expect(list.getIssues).toHaveBeenCalled();
- });
- });
-
- describe('newIssue', () => {
- beforeEach(() => {
- jest.spyOn(boardsStore, 'newIssue').mockReturnValue(
- Promise.resolve({
- data: {
- id: 42,
- subscribed: false,
- assignable_labels_endpoint: '/issue/42/labels',
- toggle_subscription_endpoint: '/issue/42/subscriptions',
- issue_sidebar_endpoint: '/issue/42/sidebar_info',
- },
- }),
- );
- list.issues = [];
- });
-
- it('adds new issue to top of list', (done) => {
- const user = new ListAssignee({
- id: 1,
- name: 'testing 123',
- username: 'test',
- avatar: 'test_image',
- });
-
- list.issues.push(
- new ListIssue({
- title: 'Testing',
- id: 1,
- confidential: false,
- labels: [new ListLabel(list.label)],
- assignees: [],
- }),
- );
- const dummyIssue = new ListIssue({
- title: 'new issue',
- id: 2,
- confidential: false,
- labels: [new ListLabel(list.label)],
- assignees: [user],
- subscribed: false,
- });
-
- list
- .newIssue(dummyIssue)
- .then(() => {
- expect(list.issues.length).toBe(2);
- expect(list.issues[0]).toBe(dummyIssue);
- expect(list.issues[0].subscribed).toBe(false);
- expect(list.issues[0].assignableLabelsEndpoint).toBe('/issue/42/labels');
- expect(list.issues[0].toggleSubscriptionEndpoint).toBe('/issue/42/subscriptions');
- expect(list.issues[0].sidebarInfoEndpoint).toBe('/issue/42/sidebar_info');
- expect(list.issues[0].labels).toBe(dummyIssue.labels);
- expect(list.issues[0].assignees).toBe(dummyIssue.assignees);
- })
- .then(done)
- .catch(done.fail);
- });
- });
-});
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index 97e8b689819..1bc61c6a112 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -1,11 +1,6 @@
-/* global List */
-
import { GlFilteredSearchToken } from '@gitlab/ui';
import { keyBy } from 'lodash';
-import Vue from 'vue';
-import '~/boards/models/list';
import { ListType } from '~/boards/constants';
-import boardsStore from '~/boards/stores/boards_store';
import { __ } from '~/locale';
import { DEFAULT_MILESTONES_GRAPHQL } from '~/vue_shared/components/filtered_search_bar/constants';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
@@ -290,20 +285,6 @@ export const boardsMockInterceptor = (config) => {
return [200, body];
};
-export const setMockEndpoints = (opts = {}) => {
- const boardsEndpoint = opts.boardsEndpoint || '/test/issue-boards/-/boards.json';
- const listsEndpoint = opts.listsEndpoint || '/test/-/boards/1/lists';
- const bulkUpdatePath = opts.bulkUpdatePath || '';
- const boardId = opts.boardId || '1';
-
- boardsStore.setEndpoints({
- boardsEndpoint,
- listsEndpoint,
- bulkUpdatePath,
- boardId,
- });
-};
-
export const mockList = {
id: 'gid://gitlab/List/1',
title: 'Open',
@@ -356,10 +337,6 @@ export const mockLists = [mockList, mockLabelList];
export const mockListsById = keyBy(mockLists, 'id');
-export const mockListsWithModel = mockLists.map((listMock) =>
- Vue.observable(new List({ ...listMock, doNotFetchIssues: true })),
-);
-
export const mockIssuesByListId = {
'gid://gitlab/List/1': [mockIssue.id, mockIssue3.id, mockIssue4.id],
'gid://gitlab/List/2': mockIssues.map(({ id }) => id),
diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js
index d516baf6f0f..3d1ef03083d 100644
--- a/spec/frontend/content_editor/components/content_editor_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_spec.js
@@ -6,6 +6,7 @@ import ContentEditor from '~/content_editor/components/content_editor.vue';
import ContentEditorError from '~/content_editor/components/content_editor_error.vue';
import ContentEditorProvider from '~/content_editor/components/content_editor_provider.vue';
import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue';
+import FormattingBubbleMenu from '~/content_editor/components/formatting_bubble_menu.vue';
import TopToolbar from '~/content_editor/components/top_toolbar.vue';
import {
LOADING_CONTENT_EVENT,
@@ -25,6 +26,7 @@ describe('ContentEditor', () => {
const findEditorElement = () => wrapper.findByTestId('content-editor');
const findEditorContent = () => wrapper.findComponent(EditorContent);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findBubbleMenu = () => wrapper.findComponent(FormattingBubbleMenu);
const createWrapper = (propsData = {}) => {
renderMarkdown = jest.fn();
@@ -131,6 +133,10 @@ describe('ContentEditor', () => {
it('hides EditorContent component', () => {
expect(findEditorContent().exists()).toBe(false);
});
+
+ it('hides formatting bubble menu', () => {
+ expect(findBubbleMenu().exists()).toBe(false);
+ });
});
describe('when loading content succeeds', () => {
@@ -171,5 +177,9 @@ describe('ContentEditor', () => {
it('displays EditorContent component', () => {
expect(findEditorContent().exists()).toBe(true);
});
+
+ it('displays formatting bubble menu', () => {
+ expect(findBubbleMenu().exists()).toBe(true);
+ });
});
});
diff --git a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
index 2a3f4f56f36..9e2bf1bd367 100644
--- a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
+++ b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
@@ -45,6 +45,7 @@ describe('Pipeline New Form', () => {
const findWarningAlertSummary = () => findWarningAlert().find(GlSprintf);
const findWarnings = () => wrapper.findAll('[data-testid="run-pipeline-warning"]');
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findCCAlert = () => wrapper.findComponent(CreditCardValidationRequiredAlert);
const getFormPostParams = () => JSON.parse(mock.history.post[0].data);
const selectBranch = (branch) => {
@@ -387,7 +388,7 @@ describe('Pipeline New Form', () => {
});
it('does not show the credit card validation required alert', () => {
- expect(wrapper.findComponent(CreditCardValidationRequiredAlert).exists()).toBe(false);
+ expect(findCCAlert().exists()).toBe(false);
});
describe('when the error response is credit card validation required', () => {
@@ -408,7 +409,19 @@ describe('Pipeline New Form', () => {
it('shows credit card validation required alert', () => {
expect(findErrorAlert().exists()).toBe(false);
- expect(wrapper.findComponent(CreditCardValidationRequiredAlert).exists()).toBe(true);
+ expect(findCCAlert().exists()).toBe(true);
+ });
+
+ it('clears error and hides the alert on dismiss', async () => {
+ expect(findCCAlert().exists()).toBe(true);
+ expect(wrapper.vm.$data.error).toBe(mockCreditCardValidationRequiredError.errors[0]);
+
+ findCCAlert().vm.$emit('dismiss');
+
+ await wrapper.vm.$nextTick();
+
+ expect(findCCAlert().exists()).toBe(false);
+ expect(wrapper.vm.$data.error).toBe(null);
});
});
});
diff --git a/spec/lib/gitlab/ci/pipeline/metrics_spec.rb b/spec/lib/gitlab/ci/pipeline/metrics_spec.rb
index 0b0b81ce614..83b969ff3c4 100644
--- a/spec/lib/gitlab/ci/pipeline/metrics_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/metrics_spec.rb
@@ -4,9 +4,15 @@ require 'spec_helper'
RSpec.describe ::Gitlab::Ci::Pipeline::Metrics do
describe '.pipeline_creation_step_duration_histogram' do
- it 'adds the step to the step duration histogram' do
+ around do |example|
+ described_class.clear_memoization(:pipeline_creation_step_histogram)
+
+ example.run
+
described_class.clear_memoization(:pipeline_creation_step_histogram)
+ end
+ it 'adds the step to the step duration histogram' do
expect(::Gitlab::Metrics).to receive(:histogram)
.with(
:gitlab_ci_pipeline_creation_step_duration_seconds,
diff --git a/spec/lib/gitlab/ci/templates/Terraform/base_latest_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Terraform/base_latest_gitlab_ci_yaml_spec.rb
index fbde160c196..e35f2eabe8e 100644
--- a/spec/lib/gitlab/ci/templates/Terraform/base_latest_gitlab_ci_yaml_spec.rb
+++ b/spec/lib/gitlab/ci/templates/Terraform/base_latest_gitlab_ci_yaml_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe 'Terraform/Base.latest.gitlab-ci.yml' do
allow(project).to receive(:default_branch).and_return(default_branch)
end
- it 'does not create any jobs', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/339979' do
+ it 'does not create any jobs' do
expect(build_names).to be_empty
end
end
diff --git a/spec/support/shared_examples/features/content_editor_shared_examples.rb b/spec/support/shared_examples/features/content_editor_shared_examples.rb
new file mode 100644
index 00000000000..2332285540a
--- /dev/null
+++ b/spec/support/shared_examples/features/content_editor_shared_examples.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'edits content using the content editor' do
+ it 'formats text as bold using bubble menu' do
+ content_editor_testid = '[data-testid="content-editor"] [contenteditable]'
+
+ expect(page).to have_css(content_editor_testid)
+
+ find(content_editor_testid).send_keys 'Typing text in the content editor'
+ find(content_editor_testid).send_keys [:shift, :left]
+
+ expect(page).to have_css('[data-testid="formatting-bubble-menu"]')
+ end
+end
diff --git a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb
index 9587da0233e..7ced8508a31 100644
--- a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb
+++ b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb
@@ -136,6 +136,14 @@ RSpec.shared_examples 'User updates wiki page' do
expect(find('textarea#wiki_content').value).to eq('Updated Wiki Content')
end
end
+
+ context 'when using the content editor' do
+ before do
+ click_button 'Use the new editor'
+ end
+
+ it_behaves_like 'edits content using the content editor'
+ end
end
context 'when the page is in a subdir', :js do
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index c111c3164eb..ddd295215a1 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -22,6 +22,8 @@ RSpec.describe PostReceive do
create(:project, :repository, auto_cancel_pending_pipelines: 'disabled')
end
+ let(:job_args) { [gl_repository, key_id, base64_changes] }
+
def perform(changes: base64_changes)
described_class.new.perform(gl_repository, key_id, changes)
end
@@ -282,6 +284,8 @@ RSpec.describe PostReceive do
end
end
end
+
+ it_behaves_like 'an idempotent worker'
end
describe '#process_wiki_changes' do
@@ -352,6 +356,8 @@ RSpec.describe PostReceive do
perform
end
end
+
+ it_behaves_like 'an idempotent worker'
end
context 'webhook' do
@@ -458,6 +464,8 @@ RSpec.describe PostReceive do
end
end
end
+
+ it_behaves_like 'an idempotent worker'
end
context 'with PersonalSnippet' do
@@ -484,5 +492,7 @@ RSpec.describe PostReceive do
described_class.new.perform(gl_repository, key_id, base64_changes)
end
+
+ it_behaves_like 'an idempotent worker'
end
end