diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-19 15:44:42 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-19 15:44:42 +0000 |
commit | 4555e1b21c365ed8303ffb7a3325d773c9b8bf31 (patch) | |
tree | 5423a1c7516cffe36384133ade12572cf709398d /app/assets/javascripts/boards/components | |
parent | e570267f2f6b326480d284e0164a6464ba4081bc (diff) | |
download | gitlab-ce-4555e1b21c365ed8303ffb7a3325d773c9b8bf31.tar.gz |
Add latest changes from gitlab-org/gitlab@13-12-stable-eev13.12.0-rc42
Diffstat (limited to 'app/assets/javascripts/boards/components')
8 files changed, 241 insertions, 30 deletions
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')" |