diff options
Diffstat (limited to 'app/assets/javascripts/boards/components')
9 files changed, 332 insertions, 175 deletions
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue index 1e780f9ef84..563bed6a6b8 100644 --- a/app/assets/javascripts/boards/components/board_card.vue +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -83,7 +83,7 @@ export default { :data-item-path="item.referencePath" data-testid="board_card" class="board-card gl-p-5 gl-rounded-base" - @mouseup="toggleIssue($event)" + @click="toggleIssue($event)" > <board-card-inner :list="list" :item="item" :update-filters="true" /> </li> diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue index 9bbb8a1a1b2..54668c9e88e 100644 --- a/app/assets/javascripts/boards/components/board_content_sidebar.vue +++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue @@ -15,6 +15,7 @@ import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue'; import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue'; import SidebarLabelsWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue'; +import { LabelType } from '~/vue_shared/components/sidebar/labels_select_widget/constants'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { @@ -53,6 +54,9 @@ export default { allowLabelEdit: { default: false, }, + labelsFilterBasePath: { + default: '', + }, }, inheritAttrs: false, computed: { @@ -63,7 +67,7 @@ export default { 'groupPathForActiveIssue', 'projectPathForActiveIssue', ]), - ...mapState(['sidebarType', 'issuableType', 'isSettingLabels']), + ...mapState(['sidebarType', 'issuableType']), isIssuableSidebar() { return this.sidebarType === ISSUABLE; }, @@ -84,7 +88,15 @@ export default { }); }, attrWorkspacePath() { - return this.isGroupBoard ? this.groupPathForActiveIssue : undefined; + return this.isGroupBoard ? this.groupPathForActiveIssue : this.projectPathForActiveIssue; + }, + labelType() { + return this.isGroupBoard ? LabelType.group : LabelType.project; + }, + labelsFilterPath() { + return this.isGroupBoard + ? this.labelsFilterBasePath.replace(':project_path', this.projectPathForActiveIssue) + : this.labelsFilterBasePath; }, }, methods: { @@ -98,21 +110,19 @@ export default { handleClose() { this.toggleBoardItem({ boardItem: this.activeBoardItem, sidebarType: this.sidebarType }); }, - handleUpdateSelectedLabels(input) { + handleUpdateSelectedLabels({ labels, id }) { this.setActiveBoardItemLabels({ - iid: this.activeBoardItem.iid, + id, projectPath: this.projectPathForActiveIssue, - addLabelIds: input.map((label) => getIdFromGraphQLId(label.id)), - removeLabelIds: this.activeBoardItem.labels - .filter((label) => !input.find((selected) => selected.id === label.id)) - .map((label) => label.id), + labelIds: labels.map((label) => getIdFromGraphQLId(label.id)), + labels, }); }, - handleLabelRemove(input) { + handleLabelRemove(removeLabelId) { this.setActiveBoardItemLabels({ iid: this.activeBoardItem.iid, projectPath: this.projectPathForActiveIssue, - removeLabelIds: [input], + removeLabelIds: [removeLabelId], }); }, }, @@ -207,14 +217,14 @@ export default { :full-path="projectPathForActiveIssue" :allow-label-remove="allowLabelEdit" :allow-multiselect="true" - :selected-labels="activeBoardItem.labels" - :labels-select-in-progress="isSettingLabels" :footer-create-label-title="createLabelTitle" :footer-manage-label-title="manageLabelTitle" :labels-create-title="createLabelTitle" - :labels-filter-base-path="projectPathForActiveIssue" + :labels-filter-base-path="labelsFilterPath" :attr-workspace-path="attrWorkspacePath" + workspace-type="project" :issuable-type="issuableType" + :label-create-type="labelType" @onLabelRemove="handleLabelRemove" @updateSelectedLabels="handleUpdateSelectedLabels" > diff --git a/app/assets/javascripts/boards/components/board_filtered_search.vue b/app/assets/javascripts/boards/components/board_filtered_search.vue index 7f242dea644..6e6ada2d109 100644 --- a/app/assets/javascripts/boards/components/board_filtered_search.vue +++ b/app/assets/javascripts/boards/components/board_filtered_search.vue @@ -1,6 +1,7 @@ <script> -import { pickBy } from 'lodash'; +import { pickBy, isEmpty } from 'lodash'; import { mapActions } from 'vuex'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { updateHistory, setUrlParams } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants'; @@ -19,6 +20,11 @@ export default { type: Array, required: true, }, + eeFilters: { + required: false, + type: Object, + default: () => ({}), + }, }, data() { return { @@ -26,57 +32,6 @@ export default { }; }, computed: { - urlParams() { - const { - authorUsername, - labelName, - assigneeUsername, - search, - milestoneTitle, - types, - weight, - } = this.filterParams; - let notParams = {}; - - if (Object.prototype.hasOwnProperty.call(this.filterParams, 'not')) { - notParams = pickBy( - { - 'not[label_name][]': this.filterParams.not.labelName, - 'not[author_username]': this.filterParams.not.authorUsername, - 'not[assignee_username]': this.filterParams.not.assigneeUsername, - 'not[types]': this.filterParams.not.types, - 'not[milestone_title]': this.filterParams.not.milestoneTitle, - 'not[weight]': this.filterParams.not.weight, - }, - undefined, - ); - } - - return { - ...notParams, - author_username: authorUsername, - 'label_name[]': labelName, - assignee_username: assigneeUsername, - milestone_title: milestoneTitle, - search, - types, - weight, - }; - }, - }, - methods: { - ...mapActions(['performSearch']), - handleFilter(filters) { - this.filterParams = this.getFilterParams(filters); - - updateHistory({ - url: setUrlParams(this.urlParams, window.location.href, true, false, true), - title: document.title, - replace: true, - }); - - this.performSearch(); - }, getFilteredSearchValue() { const { authorUsername, @@ -86,6 +41,8 @@ export default { milestoneTitle, types, weight, + epicId, + myReactionEmoji, } = this.filterParams; const filteredSearchValue = []; @@ -133,6 +90,20 @@ export default { }); } + if (myReactionEmoji) { + filteredSearchValue.push({ + type: 'my_reaction_emoji', + value: { data: myReactionEmoji, operator: '=' }, + }); + } + + if (epicId) { + filteredSearchValue.push({ + type: 'epic_id', + value: { data: epicId, operator: '=' }, + }); + } + if (this.filterParams['not[authorUsername]']) { filteredSearchValue.push({ type: 'author_username', @@ -177,12 +148,89 @@ export default { }); } + if (this.filterParams['not[epicId]']) { + filteredSearchValue.push({ + type: 'epic_id', + value: { data: this.filterParams['not[epicId]'], operator: '!=' }, + }); + } + + if (this.filterParams['not[myReactionEmoji]']) { + filteredSearchValue.push({ + type: 'my_reaction_emoji', + value: { data: this.filterParams['not[myReactionEmoji]'], operator: '!=' }, + }); + } + if (search) { filteredSearchValue.push(search); } return filteredSearchValue; }, + urlParams() { + const { + authorUsername, + labelName, + assigneeUsername, + search, + milestoneTitle, + types, + weight, + epicId, + myReactionEmoji, + } = this.filterParams; + + let notParams = {}; + + if (Object.prototype.hasOwnProperty.call(this.filterParams, 'not')) { + notParams = pickBy( + { + 'not[label_name][]': this.filterParams.not.labelName, + 'not[author_username]': this.filterParams.not.authorUsername, + 'not[assignee_username]': this.filterParams.not.assigneeUsername, + 'not[types]': this.filterParams.not.types, + 'not[milestone_title]': this.filterParams.not.milestoneTitle, + 'not[weight]': this.filterParams.not.weight, + 'not[epic_id]': this.filterParams.not.epicId, + 'not[my_reaction_emoji]': this.filterParams.not.myReactionEmoji, + }, + undefined, + ); + } + + return { + ...notParams, + author_username: authorUsername, + 'label_name[]': labelName, + assignee_username: assigneeUsername, + milestone_title: milestoneTitle, + search, + types, + weight, + epic_id: getIdFromGraphQLId(epicId), + my_reaction_emoji: myReactionEmoji, + }; + }, + }, + created() { + if (!isEmpty(this.eeFilters)) { + this.filterParams = this.eeFilters; + } + }, + methods: { + ...mapActions(['performSearch']), + handleFilter(filters) { + this.filterParams = this.getFilterParams(filters); + + updateHistory({ + url: setUrlParams(this.urlParams, window.location.href, true, false, true), + title: document.title, + replace: true, + }); + + this.performSearch(); + }, getFilterParams(filters = []) { const notFilters = filters.filter((item) => item.value.operator === '!='); const equalsFilters = filters.filter( @@ -216,6 +264,12 @@ export default { case 'weight': filterParams.weight = filter.value.data; break; + case 'epic_id': + filterParams.epicId = filter.value.data; + break; + case 'my_reaction_emoji': + filterParams.myReactionEmoji = filter.value.data; + break; case 'filtered-search-term': if (filter.value.data) plainText.push(filter.value.data); break; @@ -243,7 +297,7 @@ export default { namespace="" :tokens="tokens" :search-input-placeholder="$options.i18n.search" - :initial-filter-value="getFilteredSearchValue()" + :initial-filter-value="getFilteredSearchValue" @onFilter="handleFilter" /> </template> diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue index e939f0c0ebe..6ad57fd8985 100644 --- a/app/assets/javascripts/boards/components/board_form.vue +++ b/app/assets/javascripts/boards/components/board_form.vue @@ -2,10 +2,10 @@ import { GlModal, GlAlert } from '@gitlab/ui'; import { mapGetters, mapActions, mapState } from 'vuex'; import { TYPE_USER, TYPE_ITERATION, TYPE_MILESTONE } from '~/graphql_shared/constants'; -import { convertToGraphQLId } from '~/graphql_shared/utils'; +import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getParameterByName, visitUrl } from '~/lib/utils/url_utility'; import { __, s__ } from '~/locale'; -import { fullLabelId, fullBoardId } from '../boards_util'; +import { fullLabelId } from '../boards_util'; import { formType } from '../constants'; import createBoardMutation from '../graphql/board_create.mutation.graphql'; @@ -18,11 +18,11 @@ const boardDefaults = { name: '', labels: [], milestone: {}, - iteration_id: undefined, + iteration: {}, assignee: {}, weight: null, - hide_backlog_list: false, - hide_closed_list: false, + hideBacklogList: false, + hideClosedList: false, }; export default { @@ -57,39 +57,16 @@ export default { type: Boolean, required: true, }, - labelsPath: { - type: String, - required: true, - }, - labelsWebUrl: { - type: String, - required: true, - }, scopedIssueBoardFeatureEnabled: { type: Boolean, required: false, default: false, }, - projectId: { - type: Number, - required: false, - default: 0, - }, - groupId: { - type: Number, - required: false, - default: 0, - }, weights: { type: Array, required: false, default: () => [], }, - enableScopedLabels: { - type: Boolean, - required: false, - default: false, - }, currentBoard: { type: Object, required: true, @@ -167,17 +144,16 @@ export default { return destroyBoardMutation; }, baseMutationVariables() { - const { board } = this; - const variables = { - name: board.name, - hideBacklogList: board.hide_backlog_list, - hideClosedList: board.hide_closed_list, - }; + const { + board: { name, hideBacklogList, hideClosedList, id }, + } = this; - return board.id + const variables = { name, hideBacklogList, hideClosedList }; + + return id ? { ...variables, - id: fullBoardId(board.id), + id, } : { ...variables, @@ -191,11 +167,13 @@ export default { assigneeId: this.board.assignee?.id ? convertToGraphQLId(TYPE_USER, this.board.assignee.id) : null, + // Temporarily converting to milestone ID due to https://gitlab.com/gitlab-org/gitlab/-/issues/344779 milestoneId: this.board.milestone?.id - ? convertToGraphQLId(TYPE_MILESTONE, this.board.milestone.id) + ? convertToGraphQLId(TYPE_MILESTONE, getIdFromGraphQLId(this.board.milestone.id)) : null, - iterationId: this.board.iteration_id - ? convertToGraphQLId(TYPE_ITERATION, this.board.iteration_id) + // Temporarily converting to iteration ID due to https://gitlab.com/gitlab-org/gitlab/-/issues/344779 + iterationId: this.board.iteration?.id + ? convertToGraphQLId(TYPE_ITERATION, getIdFromGraphQLId(this.board.iteration.id)) : null, }; }, @@ -249,7 +227,7 @@ export default { await this.$apollo.mutate({ mutation: this.deleteMutation, variables: { - id: fullBoardId(this.board.id), + id: this.board.id, }, }); }, @@ -285,19 +263,12 @@ export default { } }, setIteration(iterationId) { - this.board.iteration_id = iterationId; + this.$set(this.board, 'iteration', { + id: iterationId, + }); }, setBoardLabels(labels) { - labels.forEach((label) => { - if (label.set && !this.board.labels.find((l) => l.id === label.id)) { - 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); - } - }); + this.board.labels = labels; }, setAssignee(assigneeId) { this.$set(this.board, 'assignee', { @@ -361,8 +332,8 @@ export default { </div> <board-configuration-options - :hide-backlog-list.sync="board.hide_backlog_list" - :hide-closed-list.sync="board.hide_closed_list" + :hide-backlog-list.sync="board.hideBacklogList" + :hide-closed-list.sync="board.hideClosedList" :readonly="readonly" /> @@ -371,11 +342,6 @@ export default { :collapse-scope="isNewForm" :board="board" :can-admin-board="canAdminBoard" - :labels-path="labelsPath" - :labels-web-url="labelsWebUrl" - :enable-scoped-labels="enableScopedLabels" - :project-id="projectId" - :group-id="groupId" :weights="weights" @set-iteration="setIteration" @set-board-labels="setBoardLabels" diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue index a8d71ab7a35..e985a368e64 100644 --- a/app/assets/javascripts/boards/components/board_list_header.vue +++ b/app/assets/javascripts/boards/components/board_list_header.vue @@ -15,6 +15,8 @@ import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants'; import { n__, s__, __ } from '~/locale'; import sidebarEventHub from '~/sidebar/event_hub'; import Tracking from '~/tracking'; +import { formatDate } from '~/lib/utils/datetime_utility'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import AccessorUtilities from '../../lib/utils/accessor'; import { inactiveId, LIST, ListType, toggleFormEventPrefix } from '../constants'; import eventHub from '../eventhub'; @@ -40,7 +42,7 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, - mixins: [Tracking.mixin()], + mixins: [Tracking.mixin(), glFeatureFlagMixin()], inject: { boardId: { default: '', @@ -86,6 +88,13 @@ export default { listTitle() { return this.list?.label?.description || this.list?.assignee?.name || this.list.title || ''; }, + listIterationPeriod() { + const iteration = this.list?.iteration; + return iteration ? this.getIterationPeriod(iteration) : ''; + }, + isIterationList() { + return this.listType === ListType.iteration; + }, showListHeaderButton() { return !this.disabled && this.listType !== ListType.closed; }, @@ -96,7 +105,10 @@ export default { return this.listType === ListType.assignee && this.showListDetails; }, showIterationListDetails() { - return this.listType === ListType.iteration && this.showListDetails; + return this.isIterationList && this.showListDetails; + }, + iterationCadencesAvailable() { + return this.isIterationList && this.glFeatures.iterationCadences; }, showListDetails() { return !this.list.collapsed || !this.isSwimlanesHeader; @@ -208,6 +220,16 @@ export default { updateListFunction() { this.updateList({ listId: this.list.id, collapsed: this.list.collapsed }); }, + /** + * TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/344619 + * This method also exists as a utility function in ee/../iterations/utils.js + * Remove the duplication when the EE code is separated from this compoment. + */ + getIterationPeriod({ startDate, dueDate }) { + const start = formatDate(startDate, 'mmm d, yyyy', true); + const due = formatDate(dueDate, 'mmm d, yyyy', true); + return `${start} - ${due}`; + }, }, }; </script> @@ -307,6 +329,13 @@ export default { class="board-title-main-text gl-text-truncate" > {{ listTitle }} + <span + v-if="iterationCadencesAvailable" + class="gl-display-inline-block gl-text-gray-400" + data-testid="board-list-iteration-period" + > + {{ listIterationPeriod }}</span + > </span> <span v-if="listType === 'assignee'" diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue index 98027917221..71facba1378 100644 --- a/app/assets/javascripts/boards/components/boards_selector.vue +++ b/app/assets/javascripts/boards/components/boards_selector.vue @@ -9,17 +9,20 @@ import { GlModalDirective, } from '@gitlab/ui'; import { throttle } from 'lodash'; -import { mapGetters, mapState } from 'vuex'; +import { mapActions, mapGetters, mapState } from 'vuex'; import BoardForm from 'ee_else_ce/boards/components/board_form.vue'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import axios from '~/lib/utils/axios_utils'; import httpStatusCodes from '~/lib/utils/http_status'; +import { s__ } from '~/locale'; import eventHub from '../eventhub'; -import groupQuery from '../graphql/group_boards.query.graphql'; -import projectQuery from '../graphql/project_boards.query.graphql'; +import groupBoardsQuery from '../graphql/group_boards.query.graphql'; +import projectBoardsQuery from '../graphql/project_boards.query.graphql'; +import groupBoardQuery from '../graphql/group_board.query.graphql'; +import projectBoardQuery from '../graphql/project_board.query.graphql'; const MIN_BOARDS_TO_VIEW_RECENT = 10; @@ -39,10 +42,6 @@ export default { }, inject: ['fullPath', 'recentBoardsEndpoint'], props: { - currentBoard: { - type: Object, - required: true, - }, throttleDuration: { type: Number, default: 200, @@ -64,22 +63,6 @@ export default { 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, @@ -88,11 +71,6 @@ export default { type: Array, required: true, }, - enabledScopedLabels: { - type: Boolean, - required: false, - default: false, - }, }, data() { return { @@ -107,14 +85,47 @@ export default { maxPosition: 0, filterTerm: '', currentPage: '', + board: {}, }; }, + apollo: { + board: { + query() { + return this.currentBoardQuery; + }, + variables() { + return { + fullPath: this.fullPath, + boardId: this.fullBoardId, + }; + }, + update(data) { + const board = data.workspace?.board; + return { + ...board, + labels: board?.labels?.nodes, + }; + }, + error() { + this.setError({ message: this.$options.i18n.errorFetchingBoard }); + }, + }, + }, computed: { - ...mapState(['boardType']), - ...mapGetters(['isGroupBoard']), + ...mapState(['boardType', 'fullBoardId']), + ...mapGetters(['isGroupBoard', 'isProjectBoard']), parentType() { return this.boardType; }, + currentBoardQueryCE() { + return this.isGroupBoard ? groupBoardQuery : projectBoardQuery; + }, + currentBoardQuery() { + return this.currentBoardQueryCE; + }, + isBoardLoading() { + return this.$apollo.queries.board.loading; + }, loading() { return this.loadingRecentBoards || Boolean(this.loadingBoards); }, @@ -123,9 +134,6 @@ export default { board.name.toLowerCase().includes(this.filterTerm.toLowerCase()), ); }, - board() { - return this.currentBoard; - }, showCreate() { return this.multipleIssueBoardsAvailable; }, @@ -158,6 +166,7 @@ export default { eventHub.$off('showBoardModal', this.showPage); }, methods: { + ...mapActions(['setError']), showPage(page) { this.currentPage = page; }, @@ -174,7 +183,7 @@ export default { })); }, boardQuery() { - return this.isGroupBoard ? groupQuery : projectQuery; + return this.isGroupBoard ? groupBoardsQuery : projectBoardsQuery; }, loadBoards(toggleDropdown = true) { if (toggleDropdown && this.boards.length > 0) { @@ -250,6 +259,9 @@ export default { this.hasScrollFade = this.isScrolledUp(); }, }, + i18n: { + errorFetchingBoard: s__('Board|An error occurred while fetching the board, please try again.'), + }, }; </script> @@ -260,6 +272,7 @@ export default { data-qa-selector="boards_dropdown" toggle-class="dropdown-menu-toggle js-dropdown-toggle" menu-class="flex-column dropdown-extended-height" + :loading="isBoardLoading" :text="board.name" @show="loadBoards" > @@ -354,15 +367,10 @@ export default { <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-board="board" :current-page="currentPage" @cancel="cancel" /> diff --git a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue index b6c5ef955c6..bdb9c2be836 100644 --- a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue +++ b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue @@ -1,13 +1,20 @@ <script> import { GlFilteredSearchToken } from '@gitlab/ui'; +import fuzzaldrinPlus from 'fuzzaldrin-plus'; import { mapActions } from 'vuex'; -import BoardFilteredSearch from '~/boards/components/board_filtered_search.vue'; +import BoardFilteredSearch from 'ee_else_ce/boards/components/board_filtered_search.vue'; +import { BoardType } from '~/boards/constants'; +import axios from '~/lib/utils/axios_utils'; import issueBoardFilters from '~/boards/issue_board_filters'; import { TYPE_USER } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; import { __ } from '~/locale'; -import { DEFAULT_MILESTONES_GRAPHQL } from '~/vue_shared/components/filtered_search_bar/constants'; +import { + DEFAULT_MILESTONES_GRAPHQL, + TOKEN_TITLE_MY_REACTION, +} from '~/vue_shared/components/filtered_search_bar/constants'; import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'; +import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue'; import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue'; import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue'; @@ -19,6 +26,7 @@ export default { }, i18n: { search: __('Search'), + epic: __('Epic'), label: __('Label'), author: __('Author'), assignee: __('Assignee'), @@ -31,6 +39,7 @@ export default { isNot: __('is not'), }, components: { BoardFilteredSearch }, + inject: ['isSignedIn'], props: { fullPath: { type: String, @@ -42,7 +51,15 @@ export default { }, }, computed: { - tokens() { + isGroupBoard() { + return this.boardType === BoardType.group; + }, + epicsGroupPath() { + return this.isGroupBoard + ? this.fullPath + : this.fullPath.slice(0, this.fullPath.lastIndexOf('/')); + }, + tokensCE() { const { label, is, @@ -103,6 +120,32 @@ export default { symbol: '~', fetchLabels, }, + ...(this.isSignedIn + ? [ + { + type: 'my_reaction_emoji', + title: TOKEN_TITLE_MY_REACTION, + icon: 'thumb-up', + token: EmojiToken, + unique: true, + fetchEmojis: (search = '') => { + // TODO: Switch to GraphQL query when backend is ready: https://gitlab.com/gitlab-org/gitlab/-/issues/339694 + return axios + .get(`${gon.relative_url_root || ''}/-/autocomplete/award_emojis`) + .then(({ data }) => { + if (search) { + return { + data: fuzzaldrinPlus.filter(data, search, { + key: ['name'], + }), + }; + } + return { data }; + }); + }, + }, + ] + : []), { type: 'milestone_title', title: milestone, @@ -117,7 +160,6 @@ export default { icon: 'issues', title: type, type: 'types', - operators: [{ value: '=', description: is }], token: GlFilteredSearchToken, unique: true, options: [ @@ -134,6 +176,9 @@ export default { }, ]; }, + tokens() { + return this.tokensCE; + }, }, methods: { ...mapActions(['fetchMilestones']), diff --git a/app/assets/javascripts/boards/components/new_board_button.vue b/app/assets/javascripts/boards/components/new_board_button.vue new file mode 100644 index 00000000000..f7914c636cc --- /dev/null +++ b/app/assets/javascripts/boards/components/new_board_button.vue @@ -0,0 +1,47 @@ +<script> +import { GlButton, GlModalDirective } from '@gitlab/ui'; +import { formType } from '~/boards/constants'; +import eventHub from '~/boards/eventhub'; +import { s__ } from '~/locale'; +import Tracking from '~/tracking'; +import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue'; + +export default { + components: { + GlButton, + GitlabExperiment, + }, + directives: { + GlModalDirective, + }, + mixins: [Tracking.mixin()], + inject: ['multipleIssueBoardsAvailable', 'canAdminBoard'], + computed: { + canShowCreateButton() { + return this.canAdminBoard && this.multipleIssueBoardsAvailable; + }, + createButtonText() { + return s__('Boards|New board'); + }, + }, + methods: { + showDialog() { + this.track('click_button', { label: 'create_board' }); + eventHub.$emit('showBoardModal', formType.new); + }, + }, +}; +</script> + +<template> + <gitlab-experiment name="prominent_create_board_btn"> + <template #control> </template> + <template #candidate> + <div v-if="canShowCreateButton" class="gl-ml-1 gl-mr-3 gl-display-flex gl-align-items-center"> + <gl-button data-qa-selector="new_board_button" @click.prevent="showDialog"> + {{ createButtonText }} + </gl-button> + </div> + </template> + </gitlab-experiment> +</template> diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue index e74463825c5..ec53947fd5f 100644 --- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue +++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_labels_select.vue @@ -91,9 +91,7 @@ export default { try { const addLabelIds = payload.filter((label) => label.set).map((label) => label.id); - const removeLabelIds = this.selectedLabels - .filter((label) => !payload.find((selected) => selected.id === label.id)) - .map((label) => label.id); + const removeLabelIds = payload.filter((label) => !label.set).map((label) => label.id); const input = { addLabelIds, @@ -164,7 +162,7 @@ export default { :labels-list-title="__('Select label')" :dropdown-button-text="__('Choose labels')" :is-editing="edit" - variant="embedded" + variant="sidebar" class="gl-display-block labels gl-w-full" @updateSelectedLabels="setLabels" > |