diff options
Diffstat (limited to 'app/assets/javascripts/boards')
18 files changed, 385 insertions, 163 deletions
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js index a8b870f9b8e..f53d41dd0f4 100644 --- a/app/assets/javascripts/boards/boards_util.js +++ b/app/assets/javascripts/boards/boards_util.js @@ -1,6 +1,6 @@ import { sortBy, cloneDeep } from 'lodash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { ListType, NOT_FILTER } from './constants'; +import { ListType, NOT_FILTER, AssigneeIdParamValues } from './constants'; export function getMilestone() { return null; @@ -186,6 +186,35 @@ export function transformNotFilters(filters) { }, {}); } +export function getSupportedParams(filters, supportedFilters) { + return supportedFilters.reduce((acc, f) => { + /** + * TODO the API endpoint for the classic boards + * accepts assignee wildcard value as 'assigneeId' param - + * while the GraphQL query accepts the value in 'assigneWildcardId' field. + * Once we deprecate the classics boards, + * we should change the filtered search bar to use 'asssigneeWildcardId' as a token name. + */ + if (f === 'assigneeId' && filters[f]) { + return AssigneeIdParamValues.includes(filters[f]) + ? { + ...acc, + assigneeWildcardId: filters[f].toUpperCase(), + } + : acc; + } + + if (filters[f]) { + return { + ...acc, + [f]: filters[f], + }; + } + + return acc; + }, {}); +} + // EE-specific feature. Find the implementation in the `ee/`-folder export function transformBoardConfig() { return ''; diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue index aacea0b970c..2821b799cef 100644 --- a/app/assets/javascripts/boards/components/board_card.vue +++ b/app/assets/javascripts/boards/components/board_card.vue @@ -1,5 +1,5 @@ <script> -import { mapActions, mapGetters, mapState } from 'vuex'; +import { mapActions, mapState } from 'vuex'; import BoardCardInner from './board_card_inner.vue'; export default { @@ -31,7 +31,6 @@ export default { }, computed: { ...mapState(['selectedBoardItems', 'activeId']), - ...mapGetters(['isSwimlanesOn']), isActive() { return this.item.id === this.activeId; }, @@ -46,7 +45,7 @@ export default { ...mapActions(['toggleBoardItemMultiSelection', 'toggleBoardItem']), toggleIssue(e) { // Don't do anything if this happened on a no trigger element - if (e.target.classList.contains('js-no-trigger')) return; + if (e.target.closest('.js-no-trigger')) return; const isMultiSelect = e.ctrlKey || e.metaKey; if (isMultiSelect) { diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue index 9ff2cdd76d0..0cb2e64042e 100644 --- a/app/assets/javascripts/boards/components/board_card_inner.vue +++ b/app/assets/javascripts/boards/components/board_card_inner.vue @@ -190,6 +190,7 @@ export default { <template v-for="label in orderedLabels"> <gl-label :key="label.id" + class="js-no-trigger" :background-color="label.color" :title="label.title" :description="label.description" diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue index a4b1e6adacf..b8a38d833ad 100644 --- a/app/assets/javascripts/boards/components/board_content.vue +++ b/app/assets/javascripts/boards/components/board_content.vue @@ -4,7 +4,6 @@ import { sortBy } from 'lodash'; import Draggable from 'vuedraggable'; import { mapState, mapGetters, mapActions } from 'vuex'; import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue'; -import { sortableEnd, sortableStart } from '~/boards/mixins/sortable_default_options'; import defaultSortableConfig from '~/sortable/sortable_config'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import BoardColumn from './board_column.vue'; @@ -48,7 +47,7 @@ export default { : this.lists; }, canDragColumns() { - return !this.isEpicBoard && this.glFeatures.graphqlBoardLists && this.canAdminList; + return (this.isEpicBoard || this.glFeatures.graphqlBoardLists) && this.canAdminList; }, boardColumnWrapper() { return this.canDragColumns ? Draggable : 'div'; @@ -73,14 +72,7 @@ export default { const el = this.canDragColumns ? this.$refs.list.$el : this.$refs.list; el.scrollTo({ left: el.scrollWidth, behavior: 'smooth' }); }, - handleDragOnStart() { - sortableStart(); - }, - handleDragOnEnd(params) { - sortableEnd(); - if (this.isEpicBoard) return; - const { item, newIndex, oldIndex, to } = params; const listId = item.dataset.id; @@ -108,7 +100,6 @@ export default { ref="list" v-bind="draggableOptions" class="boards-list gl-w-full gl-py-5 gl-px-3 gl-white-space-nowrap" - @start="handleDragOnStart" @end="handleDragOnEnd" > <board-column diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue index 46359cc2bca..e1f8457c0e2 100644 --- a/app/assets/javascripts/boards/components/board_content_sidebar.vue +++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue @@ -4,13 +4,13 @@ import { mapState, mapActions, mapGetters } from 'vuex'; import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue'; import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue'; import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue'; -import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue'; import BoardSidebarTimeTracker from '~/boards/components/sidebar/board_sidebar_time_tracker.vue'; import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue'; import { ISSUABLE } from '~/boards/constants'; import { contentTop } from '~/lib/utils/common_utils'; import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue'; +import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue'; export default { headerHeight: `${contentTop()}px`, @@ -18,10 +18,11 @@ export default { GlDrawer, BoardSidebarTitle, SidebarAssigneesWidget, + SidebarConfidentialityWidget, BoardSidebarTimeTracker, BoardSidebarLabelsSelect, BoardSidebarDueDate, - BoardSidebarSubscription, + SidebarSubscriptionsWidget, BoardSidebarMilestoneSelect, BoardSidebarEpicSelect: () => import('ee_component/boards/components/sidebar/board_sidebar_epic_select.vue'), @@ -30,7 +31,20 @@ export default { SidebarIterationWidget: () => import('ee_component/sidebar/components/sidebar_iteration_widget.vue'), }, - mixins: [glFeatureFlagsMixin()], + inject: { + multipleAssigneesFeatureAvailable: { + default: false, + }, + epicFeatureAvailable: { + default: false, + }, + iterationFeatureAvailable: { + default: false, + }, + weightFeatureAvailable: { + default: false, + }, + }, computed: { ...mapGetters([ 'isSidebarOpen', @@ -50,7 +64,7 @@ export default { }, }, methods: { - ...mapActions(['toggleBoardItem', 'setAssignees']), + ...mapActions(['toggleBoardItem', 'setAssignees', 'setActiveItemConfidential']), handleClose() { this.toggleBoardItem({ boardItem: this.activeBoardItem, sidebarType: this.sidebarType }); }, @@ -72,13 +86,14 @@ export default { :iid="activeBoardItem.iid" :full-path="fullPath" :initial-assignees="activeBoardItem.assignees" - class="assignee" + :allow-multiple-assignees="multipleAssigneesFeatureAvailable" @assignees-updated="setAssignees" /> - <board-sidebar-epic-select class="epic" /> + <board-sidebar-epic-select v-if="epicFeatureAvailable" class="epic" /> <div> <board-sidebar-milestone-select /> <sidebar-iteration-widget + v-if="iterationFeatureAvailable" :iid="activeBoardItem.iid" :workspace-path="projectPathForActiveIssue" :iterations-workspace-path="groupPathForActiveIssue" @@ -89,8 +104,19 @@ export default { <board-sidebar-time-tracker class="swimlanes-sidebar-time-tracker" /> <board-sidebar-due-date /> <board-sidebar-labels-select class="labels" /> - <board-sidebar-weight-input v-if="glFeatures.issueWeights" class="weight" /> - <board-sidebar-subscription class="subscriptions" /> + <board-sidebar-weight-input v-if="weightFeatureAvailable" class="weight" /> + <sidebar-confidentiality-widget + :iid="activeBoardItem.iid" + :full-path="fullPath" + :issuable-type="issuableType" + @confidentialityUpdated="setActiveItemConfidential($event)" + /> + <sidebar-subscriptions-widget + :iid="activeBoardItem.iid" + :full-path="fullPath" + :issuable-type="issuableType" + data-testid="sidebar-notifications" + /> </template> </gl-drawer> </template> diff --git a/app/assets/javascripts/boards/components/board_filtered_search.vue b/app/assets/javascripts/boards/components/board_filtered_search.vue new file mode 100644 index 00000000000..e564af0c353 --- /dev/null +++ b/app/assets/javascripts/boards/components/board_filtered_search.vue @@ -0,0 +1,154 @@ +<script> +import { pickBy } from 'lodash'; +import { mapActions } from 'vuex'; +import { updateHistory, setUrlParams } from '~/lib/utils/url_utility'; +import { __ } from '~/locale'; +import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; + +export default { + i18n: { + search: __('Search'), + label: __('Label'), + author: __('Author'), + }, + components: { FilteredSearch }, + inject: ['initialFilterParams'], + props: { + tokens: { + type: Array, + required: true, + }, + }, + data() { + return { + filterParams: this.initialFilterParams, + }; + }, + computed: { + urlParams() { + const { authorUsername, labelName, search } = 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, + }, + undefined, + ); + } + + return { + ...notParams, + author_username: authorUsername, + 'label_name[]': labelName, + search, + }; + }, + }, + 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, labelName, search } = this.filterParams; + const filteredSearchValue = []; + + if (authorUsername) { + filteredSearchValue.push({ + type: 'author_username', + value: { data: authorUsername, operator: '=' }, + }); + } + + if (labelName?.length) { + filteredSearchValue.push( + ...labelName.map((label) => ({ + type: 'label_name', + value: { data: label, operator: '=' }, + })), + ); + } + + if (this.filterParams['not[authorUsername]']) { + filteredSearchValue.push({ + type: 'author_username', + value: { data: this.filterParams['not[authorUsername]'], operator: '!=' }, + }); + } + + if (this.filterParams['not[labelName]']) { + filteredSearchValue.push( + ...this.filterParams['not[labelName]'].map((label) => ({ + type: 'label_name', + value: { data: label, operator: '!=' }, + })), + ); + } + + if (search) { + filteredSearchValue.push(search); + } + + return filteredSearchValue; + }, + getFilterParams(filters = []) { + const notFilters = filters.filter((item) => item.value.operator === '!='); + const equalsFilters = filters.filter((item) => item.value.operator === '='); + + return { ...this.generateParams(equalsFilters), not: { ...this.generateParams(notFilters) } }; + }, + generateParams(filters = []) { + const filterParams = {}; + const labels = []; + const plainText = []; + + filters.forEach((filter) => { + switch (filter.type) { + case 'author_username': + filterParams.authorUsername = filter.value.data; + break; + case 'label_name': + labels.push(filter.value.data); + break; + case 'filtered-search-term': + if (filter.value.data) plainText.push(filter.value.data); + break; + default: + break; + } + }); + + if (labels.length) { + filterParams.labelName = labels; + } + + if (plainText.length) { + filterParams.search = plainText.join(' '); + } + return filterParams; + }, + }, +}; +</script> + +<template> + <filtered-search + class="gl-w-full" + namespace="" + :tokens="tokens" + :search-input-placeholder="$options.i18n.search" + :initial-filter-value="getFilteredSearchValue()" + @onFilter="handleFilter" + /> +</template> diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue index ca66ad6934a..f94697172ac 100644 --- a/app/assets/javascripts/boards/components/board_list_header.vue +++ b/app/assets/javascripts/boards/components/board_list_header.vue @@ -161,7 +161,7 @@ export default { const collapsed = !this.list.collapsed; this.toggleListCollapsed({ listId: this.list.id, collapsed }); - if (!this.isLoggedIn || this.isEpicBoard) { + if (!this.isLoggedIn) { this.addToLocalStorage(); } else { this.updateListFunction(); diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue index 997655c346a..3d7f1f38a34 100644 --- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue +++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue @@ -29,17 +29,17 @@ export default { }; }, computed: { - ...mapGetters(['isSidebarOpen', 'shouldUseGraphQL']), + ...mapGetters(['isSidebarOpen', 'shouldUseGraphQL', 'isEpicBoard']), ...mapState(['activeId', 'sidebarType', 'boardLists']), isWipLimitsOn() { - return this.glFeatures.wipLimits; + return this.glFeatures.wipLimits && !this.isEpicBoard; }, activeList() { /* Warning: Though a computed property it is not reactive because we are referencing a List Model class. Reactivity only applies to plain JS objects */ - if (this.shouldUseGraphQL) { + if (this.shouldUseGraphQL || this.isEpicBoard) { return this.boardLists[this.activeId]; } return boardsStore.state.lists.find(({ id }) => id === this.activeId); @@ -71,7 +71,7 @@ export default { deleteBoard() { // eslint-disable-next-line no-alert if (window.confirm(__('Are you sure you want to remove this list?'))) { - if (this.shouldUseGraphQL) { + if (this.shouldUseGraphQL || this.isEpicBoard) { this.removeList(this.activeId); } else { this.activeList.destroy(); 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 f78be83cd82..919ef0d3783 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 @@ -1,10 +1,12 @@ <script> import { GlLabel } from '@gitlab/ui'; import { mapGetters, mapActions } from 'vuex'; +import Api from '~/api'; import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue'; import createFlash from '~/flash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { isScopedLabel } from '~/lib/utils/common_utils'; +import { mergeUrlParams } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue'; @@ -14,7 +16,13 @@ export default { LabelsSelect, GlLabel, }, - inject: ['labelsFetchPath', 'labelsManagePath', 'labelsFilterBasePath'], + inject: { + labelsFetchPath: { + default: null, + }, + labelsManagePath: {}, + labelsFilterBasePath: {}, + }, data() { return { loading: false, @@ -38,6 +46,32 @@ export default { scoped: isScopedLabel(label), })); }, + fetchPath() { + /* + Labels fetched in epic boards are always group-level labels + and the correct path are passed from the backend (injected through labelsFetchPath) + + For issue boards, we should always include project-level labels and use a different endpoint. + (it requires knowing the project path of a selected issue.) + + Note 1. that we will be using GraphQL to fetch labels when we create a labels select widget. + And this component will be removed _wholesale_ https://gitlab.com/gitlab-org/gitlab/-/issues/300653. + + Note 2. Moreover, 'fetchPath' needs to be used as a key for 'labels-select' component to force updates. + 'labels-select' has its own vuex store and initializes the passed props as states + and these states aren't reactively bound to the passed props. + */ + + const projectLabelsFetchPath = mergeUrlParams( + { include_ancestor_groups: true }, + Api.buildUrl(Api.projectLabelsPath).replace( + ':namespace_path/:project_path', + this.projectPathForActiveIssue, + ), + ); + + return this.labelsFetchPath || projectLabelsFetchPath; + }, }, methods: { ...mapActions(['setActiveBoardItemLabels']), @@ -77,7 +111,12 @@ export default { </script> <template> - <board-editable-item ref="sidebarItem" :title="__('Labels')" :loading="loading"> + <board-editable-item + ref="sidebarItem" + :title="__('Labels')" + :loading="loading" + data-testid="sidebar-labels" + > <template #collapsed> <gl-label v-for="label in issueLabels" @@ -95,12 +134,13 @@ export default { <template #default="{ edit }"> <labels-select ref="labelsSelect" + :key="fetchPath" :allow-label-edit="false" :allow-label-create="false" :allow-multiselect="true" :allow-scoped-labels="true" :selected-labels="selectedLabels" - :labels-fetch-path="labelsFetchPath" + :labels-fetch-path="fetchPath" :labels-manage-path="labelsManagePath" :labels-filter-base-path="labelsFilterBasePath" :labels-list-title="__('Select label')" diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js index 4ebd30fe67b..d88774d11c1 100644 --- a/app/assets/javascripts/boards/constants.js +++ b/app/assets/javascripts/boards/constants.js @@ -1,10 +1,28 @@ +import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql'; import { __ } from '~/locale'; import updateEpicSubscriptionMutation from '~/sidebar/queries/update_epic_subscription.mutation.graphql'; import updateEpicTitleMutation from '~/sidebar/queries/update_epic_title.mutation.graphql'; import boardBlockingIssuesQuery from './graphql/board_blocking_issues.query.graphql'; +import destroyBoardListMutation from './graphql/board_list_destroy.mutation.graphql'; +import updateBoardListMutation from './graphql/board_list_update.mutation.graphql'; + import issueSetSubscriptionMutation from './graphql/issue_set_subscription.mutation.graphql'; import issueSetTitleMutation from './graphql/issue_set_title.mutation.graphql'; +export const SupportedFilters = [ + 'assigneeUsername', + 'authorUsername', + 'labelName', + 'milestoneTitle', + 'releaseTag', + 'search', + 'myReactionEmoji', + 'assigneeId', +]; + +/* eslint-disable-next-line @gitlab/require-i18n-strings */ +export const AssigneeIdParamValues = ['Any', 'None']; + export const issuableTypes = { issue: 'issue', epic: 'epic', @@ -46,9 +64,10 @@ export const NOT_FILTER = 'not['; export const flashAnimationDuration = 2000; -export default { - BoardType, - ListType, +export const listsQuery = { + [issuableTypes.issue]: { + query: boardListsQuery, + }, }; export const blockingIssuablesQueries = { @@ -57,6 +76,18 @@ export const blockingIssuablesQueries = { }, }; +export const updateListQueries = { + [issuableTypes.issue]: { + mutation: updateBoardListMutation, + }, +}; + +export const deleteListQueries = { + [issuableTypes.issue]: { + mutation: destroyBoardListMutation, + }, +}; + export const titleQueries = { [issuableTypes.issue]: { mutation: issueSetTitleMutation, @@ -74,3 +105,8 @@ export const subscriptionQueries = { mutation: updateEpicSubscriptionMutation, }, }; + +export default { + BoardType, + ListType, +}; diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js index 66580bdd30f..c6040f1e4aa 100644 --- a/app/assets/javascripts/boards/filtered_search_boards.js +++ b/app/assets/javascripts/boards/filtered_search_boards.js @@ -28,6 +28,10 @@ export default class FilteredSearchBoards extends FilteredSearchManager { if (vuexstore.getters.shouldUseGraphQL && vuexstore.state.boardConfig) { const boardConfigPath = transformBoardConfig(vuexstore.state.boardConfig); + // TODO Refactor: https://gitlab.com/gitlab-org/gitlab/-/issues/329274 + // here we are using "window.location.search" as a temporary store + // only to unpack the params and do another validation inside + // 'performSearch' and 'setFilter' vuex actions. if (boardConfigPath !== '') { const filterPath = window.location.search ? `${window.location.search}&` : '?'; updateHistory({ diff --git a/app/assets/javascripts/boards/graphql/group_projects.query.graphql b/app/assets/javascripts/boards/graphql/group_projects.query.graphql index 80a37c9943d..3218c06357c 100644 --- a/app/assets/javascripts/boards/graphql/group_projects.query.graphql +++ b/app/assets/javascripts/boards/graphql/group_projects.query.graphql @@ -2,7 +2,7 @@ query getGroupProjects($fullPath: ID!, $search: String, $after: String) { group(fullPath: $fullPath) { - projects(search: $search, after: $after, first: 100) { + projects(search: $search, after: $after, first: 100, includeSubgroups: true) { nodes { id name diff --git a/app/assets/javascripts/boards/graphql/issue.fragment.graphql b/app/assets/javascripts/boards/graphql/issue.fragment.graphql index 7ecf9261214..47ecb55c72b 100644 --- a/app/assets/javascripts/boards/graphql/issue.fragment.graphql +++ b/app/assets/javascripts/boards/graphql/issue.fragment.graphql @@ -13,7 +13,6 @@ fragment IssueNode on Issue { emailsDisabled confidential webUrl - subscribed relativePosition milestone { id diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index e3f9d2f24c2..1888645ef78 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -1,3 +1,4 @@ +import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'; import Vue from 'vue'; import VueApollo from 'vue-apollo'; import { mapActions, mapGetters } from 'vuex'; @@ -35,13 +36,27 @@ import { } from '~/lib/utils/common_utils'; import { __ } from '~/locale'; import sidebarEventHub from '~/sidebar/event_hub'; +import introspectionQueryResultData from '~/sidebar/fragmentTypes.json'; +import { fullBoardId } from './boards_util'; import boardConfigToggle from './config_toggle'; import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher'; Vue.use(VueApollo); +const fragmentMatcher = new IntrospectionFragmentMatcher({ + introspectionQueryResultData, +}); + const apolloProvider = new VueApollo({ - defaultClient: createDefaultClient(), + defaultClient: createDefaultClient( + {}, + { + cacheConfig: { + fragmentMatcher, + }, + assumeImmutableResults: true, + }, + ), }); let issueBoardsApp; @@ -82,10 +97,14 @@ export default () => { currentUserId: gon.current_user_id || null, canUpdate: parseBoolean($boardApp.dataset.canUpdate), canAdminList: parseBoolean($boardApp.dataset.canAdminList), - labelsFetchPath: $boardApp.dataset.labelsFetchPath, labelsManagePath: $boardApp.dataset.labelsManagePath, labelsFilterBasePath: $boardApp.dataset.labelsFilterBasePath, timeTrackingLimitToHours: parseBoolean($boardApp.dataset.timeTrackingLimitToHours), + multipleAssigneesFeatureAvailable: parseBoolean( + $boardApp.dataset.multipleAssigneesFeatureAvailable, + ), + epicFeatureAvailable: parseBoolean($boardApp.dataset.epicFeatureAvailable), + iterationFeatureAvailable: parseBoolean($boardApp.dataset.iterationFeatureAvailable), weightFeatureAvailable: parseBoolean($boardApp.dataset.weightFeatureAvailable), boardWeight: $boardApp.dataset.boardWeight ? parseInt($boardApp.dataset.boardWeight, 10) @@ -121,6 +140,7 @@ export default () => { created() { this.setInitialBoardData({ boardId: $boardApp.dataset.boardId, + fullBoardId: fullBoardId($boardApp.dataset.boardId), fullPath: $boardApp.dataset.fullPath, boardType: this.parent, disabled: this.disabled, diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js index 8005414962c..5158e82c320 100644 --- a/app/assets/javascripts/boards/stores/actions.js +++ b/app/assets/javascripts/boards/stores/actions.js @@ -1,8 +1,4 @@ import * as Sentry from '@sentry/browser'; -import { pick } from 'lodash'; -import createBoardListMutation from 'ee_else_ce/boards/graphql/board_list_create.mutation.graphql'; -import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql'; -import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql'; import { BoardType, ListType, @@ -11,7 +7,14 @@ import { ISSUABLE, titleQueries, subscriptionQueries, -} from '~/boards/constants'; + SupportedFilters, + deleteListQueries, + listsQuery, + updateListQueries, + issuableTypes, +} 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 { getIdFromGraphQLId } from '~/graphql_shared/utils'; import createGqClient, { fetchPolicies } from '~/lib/graphql'; import { convertObjectPropsToCamelCase, urlParamsToObject } from '~/lib/utils/common_utils'; @@ -19,7 +22,6 @@ import { s__ } from '~/locale'; import { formatBoardLists, formatListIssues, - fullBoardId, formatListsPageInfo, formatIssue, formatIssueInput, @@ -27,10 +29,9 @@ import { transformNotFilters, moveItemListHelper, getMoveData, + getSupportedParams, } from '../boards_util'; import boardLabelsQuery from '../graphql/board_labels.query.graphql'; -import destroyBoardListMutation from '../graphql/board_list_destroy.mutation.graphql'; -import updateBoardListMutation from '../graphql/board_list_update.mutation.graphql'; import groupProjectsQuery from '../graphql/group_projects.query.graphql'; import issueCreateMutation from '../graphql/issue_create.mutation.graphql'; import issueSetDueDateMutation from '../graphql/issue_set_due_date.mutation.graphql'; @@ -39,11 +40,6 @@ import issueSetMilestoneMutation from '../graphql/issue_set_milestone.mutation.g import listsIssuesQuery from '../graphql/lists_issues.query.graphql'; import * as types from './mutation_types'; -const notImplemented = () => { - /* eslint-disable-next-line @gitlab/require-i18n-strings */ - throw new Error('Not implemented!'); -}; - export const gqlClient = createGqClient( {}, { @@ -65,16 +61,11 @@ export default { }, setFilters: ({ commit }, filters) => { - const filterParams = pick(filters, [ - 'assigneeUsername', - 'authorUsername', - 'labelName', - 'milestoneTitle', - 'releaseTag', - 'search', - 'myReactionEmoji', - ]); - filterParams.not = transformNotFilters(filters); + const filterParams = { + ...getSupportedParams(filters, SupportedFilters), + not: transformNotFilters(filters), + }; + commit(types.SET_FILTERS, filterParams); }, @@ -90,24 +81,22 @@ export default { } }, - fetchLists: ({ dispatch }) => { - dispatch('fetchIssueLists'); - }, - - fetchIssueLists: ({ commit, state, dispatch }) => { - const { boardType, filterParams, fullPath, boardId } = state; + fetchLists: ({ commit, state, dispatch }) => { + const { boardType, filterParams, fullPath, fullBoardId, issuableType } = state; const variables = { fullPath, - boardId: fullBoardId(boardId), + boardId: fullBoardId, filters: filterParams, - isGroup: boardType === BoardType.group, - isProject: boardType === BoardType.project, + ...(issuableType === issuableTypes.issue && { + isGroup: boardType === BoardType.group, + isProject: boardType === BoardType.project, + }), }; return gqlClient .query({ - query: boardListsQuery, + query: listsQuery[issuableType].query, variables, }) .then(({ data }) => { @@ -141,7 +130,7 @@ export default { { state, commit, dispatch, getters }, { backlog, labelId, milestoneId, assigneeId, iterationId }, ) => { - const { boardId } = state; + const { fullBoardId } = state; const existingList = getters.getListByLabelId(labelId); @@ -154,7 +143,7 @@ export default { .mutate({ mutation: createBoardListMutation, variables: { - boardId: fullBoardId(boardId), + boardId: fullBoardId, backlog, labelId, milestoneId, @@ -242,10 +231,13 @@ export default { dispatch('updateList', { listId, position: newPosition, backupList }); }, - updateList: ({ commit }, { listId, position, collapsed, backupList }) => { + updateList: ( + { commit, state: { issuableType } }, + { listId, position, collapsed, backupList }, + ) => { gqlClient .mutate({ - mutation: updateBoardListMutation, + mutation: updateListQueries[issuableType].mutation, variables: { listId, position, @@ -266,14 +258,14 @@ export default { commit(types.TOGGLE_LIST_COLLAPSED, { listId, collapsed }); }, - removeList: ({ state, commit }, listId) => { - const listsBackup = { ...state.boardLists }; + removeList: ({ state: { issuableType, boardLists }, commit }, listId) => { + const listsBackup = { ...boardLists }; commit(types.REMOVE_LIST, listId); return gqlClient .mutate({ - mutation: destroyBoardListMutation, + mutation: deleteListQueries[issuableType].mutation, variables: { listId, }, @@ -297,11 +289,11 @@ export default { fetchItemsForList: ({ state, commit }, { listId, fetchNext = false }) => { commit(types.REQUEST_ITEMS_FOR_LIST, { listId, fetchNext }); - const { fullPath, boardId, boardType, filterParams } = state; + const { fullPath, fullBoardId, boardType, filterParams } = state; const variables = { fullPath, - boardId: fullBoardId(boardId), + boardId: fullBoardId, id: listId, filters: filterParams, isGroup: boardType === BoardType.group, @@ -430,7 +422,7 @@ export default { try { const { itemId, fromListId, toListId, moveBeforeId, moveAfterId } = moveData; const { - boardId, + fullBoardId, boardItems: { [itemId]: { iid, referencePath }, }, @@ -441,7 +433,7 @@ export default { variables: { iid, projectPath: referencePath.split(/[#]/)[0], - boardId: fullBoardId(boardId), + boardId: fullBoardId, fromListId: getIdFromGraphQLId(fromListId), toListId: getIdFromGraphQLId(toListId), moveBeforeId, @@ -653,6 +645,15 @@ export default { }); }, + setActiveItemConfidential: ({ commit, getters }, confidential) => { + const { activeBoardItem } = getters; + commit(types.UPDATE_BOARD_ITEM_BY_ID, { + itemId: activeBoardItem.id, + prop: 'confidential', + value: confidential, + }); + }, + fetchGroupProjects: ({ commit, state }, { search = '', fetchNext = false }) => { commit(types.REQUEST_GROUP_PROJECTS, fetchNext); @@ -731,28 +732,4 @@ export default { unsetError: ({ commit }) => { commit(types.SET_ERROR, undefined); }, - - fetchBacklog: () => { - notImplemented(); - }, - - bulkUpdateIssues: () => { - notImplemented(); - }, - - fetchIssue: () => { - notImplemented(); - }, - - toggleIssueSubscription: () => { - notImplemented(); - }, - - showPage: () => { - notImplemented(); - }, - - toggleEmptyState: () => { - notImplemented(); - }, }; diff --git a/app/assets/javascripts/boards/stores/getters.js b/app/assets/javascripts/boards/stores/getters.js index 0589851c658..b61ecc5ccb6 100644 --- a/app/assets/javascripts/boards/stores/getters.js +++ b/app/assets/javascripts/boards/stores/getters.js @@ -21,7 +21,7 @@ export default { groupPathForActiveIssue: (_, getters) => { const { referencePath = '' } = getters.activeBoardItem; - return referencePath.slice(0, referencePath.indexOf('/')); + return referencePath.slice(0, referencePath.lastIndexOf('/')); }, projectPathForActiveIssue: (_, getters) => { diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js index 22b9905ee62..ccea2917c2c 100644 --- a/app/assets/javascripts/boards/stores/mutation_types.js +++ b/app/assets/javascripts/boards/stores/mutation_types.js @@ -9,9 +9,7 @@ export const GENERATE_DEFAULT_LISTS_FAILURE = 'GENERATE_DEFAULT_LISTS_FAILURE'; export const RECEIVE_BOARD_LISTS_SUCCESS = 'RECEIVE_BOARD_LISTS_SUCCESS'; export const RECEIVE_BOARD_LISTS_FAILURE = 'RECEIVE_BOARD_LISTS_FAILURE'; export const SHOW_PROMOTION_LIST = 'SHOW_PROMOTION_LIST'; -export const REQUEST_ADD_LIST = 'REQUEST_ADD_LIST'; export const RECEIVE_ADD_LIST_SUCCESS = 'RECEIVE_ADD_LIST_SUCCESS'; -export const RECEIVE_ADD_LIST_ERROR = 'RECEIVE_ADD_LIST_ERROR'; export const MOVE_LIST = 'MOVE_LIST'; export const UPDATE_LIST_FAILURE = 'UPDATE_LIST_FAILURE'; export const TOGGLE_LIST_COLLAPSED = 'TOGGLE_LIST_COLLAPSED'; @@ -20,19 +18,11 @@ export const REMOVE_LIST_FAILURE = 'REMOVE_LIST_FAILURE'; export const REQUEST_ITEMS_FOR_LIST = 'REQUEST_ITEMS_FOR_LIST'; export const RECEIVE_ITEMS_FOR_LIST_FAILURE = 'RECEIVE_ITEMS_FOR_LIST_FAILURE'; export const RECEIVE_ITEMS_FOR_LIST_SUCCESS = 'RECEIVE_ITEMS_FOR_LIST_SUCCESS'; -export const REQUEST_ADD_ISSUE = 'REQUEST_ADD_ISSUE'; -export const RECEIVE_ADD_ISSUE_SUCCESS = 'RECEIVE_ADD_ISSUE_SUCCESS'; -export const RECEIVE_ADD_ISSUE_ERROR = 'RECEIVE_ADD_ISSUE_ERROR'; export const UPDATE_BOARD_ITEM = 'UPDATE_BOARD_ITEM'; export const REMOVE_BOARD_ITEM = 'REMOVE_BOARD_ITEM'; -export const REQUEST_UPDATE_ISSUE = 'REQUEST_UPDATE_ISSUE'; export const MUTATE_ISSUE_SUCCESS = 'MUTATE_ISSUE_SUCCESS'; -export const RECEIVE_UPDATE_ISSUE_SUCCESS = 'RECEIVE_UPDATE_ISSUE_SUCCESS'; -export const RECEIVE_UPDATE_ISSUE_ERROR = 'RECEIVE_UPDATE_ISSUE_ERROR'; export const ADD_BOARD_ITEM_TO_LIST = 'ADD_BOARD_ITEM_TO_LIST'; export const REMOVE_BOARD_ITEM_FROM_LIST = 'REMOVE_BOARD_ITEM_FROM_LIST'; -export const SET_CURRENT_PAGE = 'SET_CURRENT_PAGE'; -export const TOGGLE_EMPTY_STATE = 'TOGGLE_EMPTY_STATE'; export const SET_ACTIVE_ID = 'SET_ACTIVE_ID'; export const UPDATE_BOARD_ITEM_BY_ID = 'UPDATE_BOARD_ITEM_BY_ID'; export const SET_ASSIGNEE_LOADING = 'SET_ASSIGNEE_LOADING'; diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js index 561c21b78c1..667628b2998 100644 --- a/app/assets/javascripts/boards/stores/mutations.js +++ b/app/assets/javascripts/boards/stores/mutations.js @@ -6,11 +6,6 @@ import { formatIssue } from '../boards_util'; import { issuableTypes } from '../constants'; import * as mutationTypes from './mutation_types'; -const notImplemented = () => { - /* eslint-disable-next-line @gitlab/require-i18n-strings */ - throw new Error('Not implemented!'); -}; - const updateListItemsCount = ({ state, listId, value }) => { const list = state.boardLists[listId]; if (state.issuableType === issuableTypes.epic) { @@ -40,8 +35,9 @@ export const addItemToList = ({ state, listId, itemId, moveBeforeId, moveAfterId export default { [mutationTypes.SET_INITIAL_BOARD_DATA](state, data) { - const { boardType, disabled, boardId, fullPath, boardConfig, issuableType } = data; + const { boardType, disabled, boardId, fullBoardId, fullPath, boardConfig, issuableType } = data; state.boardId = boardId; + state.fullBoardId = fullBoardId; state.fullPath = fullPath; state.boardType = boardType; state.disabled = disabled; @@ -93,18 +89,10 @@ export default { state.error = s__('Boards|An error occurred while generating lists. Please reload the page.'); }, - [mutationTypes.REQUEST_ADD_LIST]: () => { - notImplemented(); - }, - [mutationTypes.RECEIVE_ADD_LIST_SUCCESS]: (state, list) => { Vue.set(state.boardLists, list.id, list); }, - [mutationTypes.RECEIVE_ADD_LIST_ERROR]: () => { - notImplemented(); - }, - [mutationTypes.MOVE_LIST]: (state, { movedList, listAtNewIndex }) => { const { boardLists } = state; Vue.set(boardLists, movedList.id, movedList); @@ -171,35 +159,11 @@ export default { state.isSettingAssignees = isLoading; }, - [mutationTypes.REQUEST_ADD_ISSUE]: () => { - notImplemented(); - }, - - [mutationTypes.RECEIVE_ADD_ISSUE_SUCCESS]: () => { - notImplemented(); - }, - - [mutationTypes.RECEIVE_ADD_ISSUE_ERROR]: () => { - notImplemented(); - }, - [mutationTypes.MUTATE_ISSUE_SUCCESS]: (state, { issue }) => { const issueId = getIdFromGraphQLId(issue.id); Vue.set(state.boardItems, issueId, formatIssue({ ...issue, id: issueId })); }, - [mutationTypes.REQUEST_UPDATE_ISSUE]: () => { - notImplemented(); - }, - - [mutationTypes.RECEIVE_UPDATE_ISSUE_SUCCESS]: () => { - notImplemented(); - }, - - [mutationTypes.RECEIVE_UPDATE_ISSUE_ERROR]: () => { - notImplemented(); - }, - [mutationTypes.ADD_BOARD_ITEM_TO_LIST]: ( state, { itemId, listId, moveBeforeId, moveAfterId, atIndex }, @@ -219,14 +183,6 @@ export default { Vue.delete(state.boardItems, itemId); }, - [mutationTypes.SET_CURRENT_PAGE]: () => { - notImplemented(); - }, - - [mutationTypes.TOGGLE_EMPTY_STATE]: () => { - notImplemented(); - }, - [mutationTypes.REQUEST_GROUP_PROJECTS]: (state, fetchNext) => { Vue.set(state, 'groupProjectsFlags', { [fetchNext ? 'isLoadingMore' : 'isLoading']: true, |