diff options
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 <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 |