diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-20 12:26:25 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-20 12:26:25 +0000 |
commit | a09983ae35713f5a2bbb100981116d31ce99826e (patch) | |
tree | 2ee2af7bd104d57086db360a7e6d8c9d5d43667a /app/assets/javascripts/issuables_list | |
parent | 18c5ab32b738c0b6ecb4d0df3994000482f34bd8 (diff) | |
download | gitlab-ce-a09983ae35713f5a2bbb100981116d31ce99826e.tar.gz |
Add latest changes from gitlab-org/gitlab@13-2-stable-ee
Diffstat (limited to 'app/assets/javascripts/issuables_list')
6 files changed, 283 insertions, 107 deletions
diff --git a/app/assets/javascripts/issuables_list/components/issuable.vue b/app/assets/javascripts/issuables_list/components/issuable.vue index 947c7518289..b7f4292a126 100644 --- a/app/assets/javascripts/issuables_list/components/issuable.vue +++ b/app/assets/javascripts/issuables_list/components/issuable.vue @@ -3,8 +3,11 @@ * This is tightly coupled to projects/issues/_issue.html.haml, * any changes done to the haml need to be reflected here. */ + +// TODO: need to move this component to graphql - https://gitlab.com/gitlab-org/gitlab/-/issues/221246 import { escape, isNumber } from 'lodash'; -import { GlLink, GlTooltipDirective as GlTooltip, GlSprintf } from '@gitlab/ui'; +import { GlLink, GlTooltipDirective as GlTooltip, GlSprintf, GlLabel, GlIcon } from '@gitlab/ui'; +import jiraLogo from '@gitlab/svgs/dist/illustrations/logos/jira.svg'; import { dateInWords, formatDate, @@ -16,22 +19,26 @@ import { import { sprintf, __ } from '~/locale'; import initUserPopovers from '~/user_popovers'; import { mergeUrlParams } from '~/lib/utils/url_utility'; -import Icon from '~/vue_shared/components/icon.vue'; import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue'; +import { isScopedLabel } from '~/lib/utils/common_utils'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { i18n: { openedAgo: __('opened %{timeAgoString} by %{user}'), + openedAgoJira: __('opened %{timeAgoString} by %{user} in Jira'), }, components: { - Icon, IssueAssignees, GlLink, + GlLabel, + GlIcon, GlSprintf, }, directives: { GlTooltip, }, + mixins: [glFeatureFlagsMixin()], props: { issuable: { type: Object, @@ -55,14 +62,19 @@ export default { }, }, }, + data() { + return { + jiraLogo, + }; + }, computed: { milestoneLink() { const { title } = this.issuable.milestone; return this.issuableLink({ milestone_title: title }); }, - hasLabels() { - return Boolean(this.issuable.labels && this.issuable.labels.length); + scopedLabelsAvailable() { + return this.glFeatures.scopedLabels; }, hasWeight() { return isNumber(this.issuable.weight); @@ -82,6 +94,12 @@ export default { isClosed() { return this.issuable.state === 'closed'; }, + isJiraIssue() { + return this.issuable.external_tracker === 'jira'; + }, + linkTarget() { + return this.isJiraIssue ? '_blank' : null; + }, issueCreatedToday() { return getDayDifference(new Date(this.issuable.created_at), new Date()) < 1; }, @@ -147,14 +165,14 @@ export default { value: this.issuable.upvotes, title: __('Upvotes'), class: 'js-upvotes', - faicon: 'fa-thumbs-up', + icon: 'thumb-up', }, { key: 'downvotes', value: this.issuable.downvotes, title: __('Downvotes'), class: 'js-downvotes', - faicon: 'fa-thumbs-down', + icon: 'thumb-down', }, ]; }, @@ -165,16 +183,17 @@ export default { initUserPopovers([this.$refs.openedAgoByContainer.$el]); }, methods: { - labelStyle(label) { - return { - backgroundColor: label.color, - color: label.text_color, - }; - }, issuableLink(params) { return mergeUrlParams(params, this.baseUrl); }, + isScoped({ name }) { + return isScopedLabel({ title: name }) && this.scopedLabelsAvailable; + }, labelHref({ name }) { + if (this.isJiraIssue) { + return this.issuableLink({ 'labels[]': name }); + } + return this.issuableLink({ 'label_name[]': name }); }, onSelect(ev) { @@ -214,14 +233,23 @@ export default { <div class="flex-grow-1"> <div class="title"> <span class="issue-title-text"> - <i + <gl-icon v-if="issuable.confidential" v-gl-tooltip - class="fa fa-eye-slash" + name="eye-slash" + class="gl-vertical-align-text-bottom" + :size="16" :title="$options.confidentialTooltipText" :aria-label="$options.confidentialTooltipText" - ></i> - <gl-link :href="issuable.web_url">{{ issuable.title }}</gl-link> + /> + <gl-link :href="issuable.web_url" :target="linkTarget" data-testid="issuable-title"> + {{ issuable.title }} + <gl-icon + v-if="isJiraIssue" + name="external-link" + class="gl-vertical-align-text-bottom" + /> + </gl-link> </span> <span v-if="issuable.has_tasks" class="ml-1 task-status d-none d-sm-inline-block"> {{ issuable.task_status }} @@ -229,11 +257,21 @@ export default { </div> <div class="issuable-info"> - <span class="js-ref-path">{{ referencePath }}</span> + <span class="js-ref-path"> + <span + v-if="isJiraIssue" + class="svg-container jira-logo-container" + data-testid="jira-logo" + v-html="jiraLogo" + ></span> + {{ referencePath }} + </span> <span data-testid="openedByMessage" class="d-none d-sm-inline-block mr-1"> · - <gl-sprintf :message="$options.i18n.openedAgo"> + <gl-sprintf + :message="isJiraIssue ? $options.i18n.openedAgoJira : $options.i18n.openedAgo" + > <template #timeAgoString> <span>{{ issuableCreatedAt }}</span> </template> @@ -242,6 +280,7 @@ export default { ref="openedAgoByContainer" v-bind="popoverDataAttrs" :href="issuableAuthor.web_url" + :target="linkTarget" > {{ issuableAuthor.name }} </gl-link> @@ -271,30 +310,29 @@ export default { {{ dueDateWords }} </span> - <span v-if="hasLabels" class="js-labels"> - <gl-link - v-for="label in issuable.labels" - :key="label.id" - class="label-link mr-1" - :href="labelHref(label)" - > - <span - v-gl-tooltip - class="badge color-label" - :style="labelStyle(label)" - :title="label.description" - >{{ label.name }}</span - > - </gl-link> - </span> + <gl-label + v-for="label in issuable.labels" + :key="label.id" + data-qa-selector="issuable-label" + :target="labelHref(label)" + :background-color="label.color" + :description="label.description" + :color="label.text_color" + :title="label.name" + :scoped="isScoped(label)" + size="sm" + class="mr-1" + >{{ label.name }}</gl-label + > <span v-if="hasWeight" v-gl-tooltip :title="__('Weight')" class="d-none d-sm-inline-block js-weight" + data-testid="weight" > - <icon name="weight" class="align-text-bottom" /> + <gl-icon name="weight" class="align-text-bottom" /> {{ issuable.weight }} </span> </div> @@ -303,7 +341,8 @@ export default { <!-- Issuable meta --> <div class="flex-shrink-0 d-flex flex-column align-items-end justify-content-center"> <div class="controls d-flex"> - <span v-if="isClosed" class="issuable-status">{{ __('CLOSED') }}</span> + <span v-if="isJiraIssue" data-testid="issuable-status">{{ issuable.status }}</span> + <span v-else-if="isClosed" class="issuable-status">{{ __('CLOSED') }}</span> <issue-assignees :assignees="issuable.assignees" @@ -318,23 +357,23 @@ export default { v-if="meta.value" :key="meta.key" v-gl-tooltip - :class="['d-none d-sm-inline-block ml-2', meta.class]" + :class="['d-none d-sm-inline-block ml-2 vertical-align-middle', meta.class]" :title="meta.title" > - <icon v-if="meta.icon" :name="meta.icon" /> - <i v-else :class="['fa', meta.faicon]"></i> + <gl-icon v-if="meta.icon" :name="meta.icon" /> {{ meta.value }} </span> </template> <gl-link + v-if="!isJiraIssue" v-gl-tooltip class="ml-2 js-notes" :href="`${issuable.web_url}#notes`" :title="__('Comments')" :class="{ 'no-comments': hasNoComments }" > - <i class="fa fa-comments"></i> + <gl-icon name="comments" class="gl-vertical-align-text-bottom" /> {{ userNotesCount }} </gl-link> </div> diff --git a/app/assets/javascripts/issuables_list/components/issuable_list_root_app.vue b/app/assets/javascripts/issuables_list/components/issuable_list_root_app.vue index 49a89d15c35..cc90d23eda7 100644 --- a/app/assets/javascripts/issuables_list/components/issuable_list_root_app.vue +++ b/app/assets/javascripts/issuables_list/components/issuable_list_root_app.vue @@ -1,10 +1,13 @@ <script> import { GlAlert, GlLabel } from '@gitlab/ui'; +import { last } from 'lodash'; +import { n__ } from '~/locale'; import getIssuesListDetailsQuery from '../queries/get_issues_list_details.query.graphql'; import { calculateJiraImportLabel, - isFinished, isInProgress, + setFinishedAlertHideMap, + shouldShowFinishedAlert, } from '~/jira_import/utils/jira_import_utils'; export default { @@ -33,8 +36,6 @@ export default { }, data() { return { - isFinishedAlertShowing: true, - isInProgressAlertShowing: true, jiraImport: {}, }; }, @@ -46,36 +47,42 @@ export default { fullPath: this.projectPath, }; }, - update: ({ project }) => ({ - isInProgress: isInProgress(project.jiraImportStatus), - isFinished: isFinished(project.jiraImportStatus), - label: calculateJiraImportLabel( + update: ({ project }) => { + const label = calculateJiraImportLabel( project.jiraImports.nodes, project.issues.nodes.flatMap(({ labels }) => labels.nodes), - ), - }), + ); + return { + importedIssuesCount: last(project.jiraImports.nodes)?.importedIssuesCount, + label, + shouldShowFinishedAlert: shouldShowFinishedAlert(label.title, project.jiraImportStatus), + shouldShowInProgressAlert: isInProgress(project.jiraImportStatus), + }; + }, skip() { return !this.isJiraConfigured || !this.canEdit; }, }, }, computed: { + finishedMessage() { + return n__( + '%d issue successfully imported with the label', + '%d issues successfully imported with the label', + this.jiraImport.importedIssuesCount, + ); + }, labelTarget() { return `${this.issuesPath}?label_name[]=${encodeURIComponent(this.jiraImport.label.title)}`; }, - shouldShowFinishedAlert() { - return this.isFinishedAlertShowing && this.jiraImport.isFinished; - }, - shouldShowInProgressAlert() { - return this.isInProgressAlertShowing && this.jiraImport.isInProgress; - }, }, methods: { hideFinishedAlert() { - this.isFinishedAlertShowing = false; + setFinishedAlertHideMap(this.jiraImport.label.title); + this.jiraImport.shouldShowFinishedAlert = false; }, hideInProgressAlert() { - this.isInProgressAlertShowing = false; + this.jiraImport.shouldShowInProgressAlert = false; }, }, }; @@ -83,11 +90,16 @@ export default { <template> <div class="issuable-list-root"> - <gl-alert v-if="shouldShowInProgressAlert" @dismiss="hideInProgressAlert"> + <gl-alert v-if="jiraImport.shouldShowInProgressAlert" @dismiss="hideInProgressAlert"> {{ __('Import in progress. Refresh page to see newly added issues.') }} </gl-alert> - <gl-alert v-if="shouldShowFinishedAlert" variant="success" @dismiss="hideFinishedAlert"> - {{ __('Issues successfully imported with the label') }} + + <gl-alert + v-if="jiraImport.shouldShowFinishedAlert" + variant="success" + @dismiss="hideFinishedAlert" + > + {{ finishedMessage }} <gl-label :background-color="jiraImport.label.color" scoped diff --git a/app/assets/javascripts/issuables_list/components/issuables_list_app.vue b/app/assets/javascripts/issuables_list/components/issuables_list_app.vue index 1c395fd9795..21aeb2ca143 100644 --- a/app/assets/javascripts/issuables_list/components/issuables_list_app.vue +++ b/app/assets/javascripts/issuables_list/components/issuables_list_app.vue @@ -12,8 +12,10 @@ import { import { __ } from '~/locale'; import initManualOrdering from '~/manual_ordering'; import Issuable from './issuable.vue'; +import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; import { sortOrderMap, + availableSortOptionsJira, RELATIVE_POSITION, PAGE_SIZE, PAGE_SIZE_MANUAL, @@ -29,6 +31,7 @@ export default { GlPagination, GlSkeletonLoading, Issuable, + FilteredSearchBar, }, props: { canBulkEdit: { @@ -50,14 +53,25 @@ export default { type: String, required: true, }, + projectPath: { + type: String, + required: false, + default: '', + }, sortKey: { type: String, required: false, default: '', }, + type: { + type: String, + required: false, + default: '', + }, }, data() { return { + availableSortOptionsJira, filters: {}, isBulkEditing: false, issuables: [], @@ -118,6 +132,45 @@ export default { baseUrl() { return window.location.href.replace(/(\?.*)?(#.*)?$/, ''); }, + paginationNext() { + return this.page + 1; + }, + paginationPrev() { + return this.page - 1; + }, + paginationProps() { + const paginationProps = { value: this.page }; + + if (this.totalItems) { + return { + ...paginationProps, + perPage: this.itemsPerPage, + totalItems: this.totalItems, + }; + } + + return { + ...paginationProps, + prevPage: this.paginationPrev, + nextPage: this.paginationNext, + }; + }, + isJira() { + return this.type === 'jira'; + }, + initialFilterValue() { + const value = []; + const { search } = this.getQueryObject(); + + if (search) { + value.push(search); + } + return value; + }, + initialSortBy() { + const { sort } = this.getQueryObject(); + return sort || 'created_desc'; + }, }, watch: { selection() { @@ -222,9 +275,13 @@ export default { const { label_name: labels, milestone_title: milestoneTitle, + 'not[label_name]': excludedLabels, + 'not[milestone_title]': excludedMilestone, ...filters } = this.getQueryObject(); + // TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/227880 + if (milestoneTitle) { filters.milestone = milestoneTitle; } @@ -235,58 +292,104 @@ export default { filters.state = 'opened'; } + if (excludedLabels) { + filters['not[labels]'] = excludedLabels; + } + + if (excludedMilestone) { + filters['not[milestone]'] = excludedMilestone; + } + Object.assign(filters, sortOrderMap[this.sortKey]); this.filters = filters; }, + refetchIssuables() { + const ignored = ['utf8']; + const params = omit(this.filters, ignored); + + historyPushState(setUrlParams(params, window.location.href, true, true)); + this.fetchIssuables(); + }, + handleFilter(filters) { + let search = null; + + filters.forEach(filter => { + if (typeof filter === 'string') { + search = filter; + } + }); + + this.filters.search = search; + this.page = 1; + + this.refetchIssuables(); + }, + handleSort(sort) { + this.filters.sort = sort; + this.page = 1; + + this.refetchIssuables(); + }, }, }; </script> <template> - <ul v-if="loading" class="content-list"> - <li v-for="n in $options.LOADING_LIST_ITEMS_LENGTH" :key="n" class="issue gl-px-5! gl-py-5!"> - <gl-skeleton-loading /> - </li> - </ul> - <div v-else-if="issuables.length"> - <div v-if="isBulkEditing" class="issue px-3 py-3 border-bottom border-light"> - <input type="checkbox" :checked="allIssuablesSelected" class="mr-2" @click="onSelectAll" /> - <strong>{{ __('Select all') }}</strong> - </div> - <ul - class="content-list issuable-list issues-list" - :class="{ 'manual-ordering': isManualOrdering }" - > - <issuable - v-for="issuable in issuables" - :key="issuable.id" - class="pr-3" - :class="{ 'user-can-drag': isManualOrdering }" - :issuable="issuable" - :is-bulk-editing="isBulkEditing" - :selected="isSelected(issuable.id)" - :base-url="baseUrl" - @select="onSelectIssuable" - /> + <div> + <filtered-search-bar + v-if="isJira" + :namespace="projectPath" + :search-input-placeholder="__('Search Jira issues')" + :tokens="[]" + :sort-options="availableSortOptionsJira" + :initial-filter-value="initialFilterValue" + :initial-sort-by="initialSortBy" + class="row-content-block" + @onFilter="handleFilter" + @onSort="handleSort" + /> + <ul v-if="loading" class="content-list"> + <li v-for="n in $options.LOADING_LIST_ITEMS_LENGTH" :key="n" class="issue gl-px-5! gl-py-5!"> + <gl-skeleton-loading /> + </li> </ul> - <div class="mt-3"> - <gl-pagination - v-if="totalItems" - :value="page" - :per-page="itemsPerPage" - :total-items="totalItems" - class="justify-content-center" - @input="onPaginate" - /> + <div v-else-if="issuables.length"> + <div v-if="isBulkEditing" class="issue px-3 py-3 border-bottom border-light"> + <input type="checkbox" :checked="allIssuablesSelected" class="mr-2" @click="onSelectAll" /> + <strong>{{ __('Select all') }}</strong> + </div> + <ul + class="content-list issuable-list issues-list" + :class="{ 'manual-ordering': isManualOrdering }" + > + <issuable + v-for="issuable in issuables" + :key="issuable.id" + class="pr-3" + :class="{ 'user-can-drag': isManualOrdering }" + :issuable="issuable" + :is-bulk-editing="isBulkEditing" + :selected="isSelected(issuable.id)" + :base-url="baseUrl" + @select="onSelectIssuable" + /> + </ul> + <div class="mt-3"> + <gl-pagination + v-bind="paginationProps" + class="gl-justify-content-center" + @input="onPaginate" + /> + </div> </div> + <gl-empty-state + v-else + :title="emptyState.title" + :description="emptyState.description" + :svg-path="emptySvgPath" + :primary-button-link="emptyState.primaryLink" + :primary-button-text="emptyState.primaryText" + /> </div> - <gl-empty-state - v-else - :title="emptyState.title" - :description="emptyState.description" - :svg-path="emptySvgPath" - :primary-button-link="emptyState.primaryLink" - :primary-button-text="emptyState.primaryText" - /> </template> diff --git a/app/assets/javascripts/issuables_list/constants.js b/app/assets/javascripts/issuables_list/constants.js index 71b9c52c703..f008ba1bf4a 100644 --- a/app/assets/javascripts/issuables_list/constants.js +++ b/app/assets/javascripts/issuables_list/constants.js @@ -1,3 +1,5 @@ +import { __ } from '~/locale'; + // Maps sort order as it appears in the URL query to API `order_by` and `sort` params. const PRIORITY = 'priority'; const ASC = 'asc'; @@ -31,3 +33,24 @@ export const sortOrderMap = { weight_desc: { order_by: WEIGHT, sort: DESC }, weight: { order_by: WEIGHT, sort: ASC }, }; + +export const availableSortOptionsJira = [ + { + id: 1, + title: __('Created date'), + sortDirection: { + descending: 'created_desc', + ascending: 'created_asc', + }, + }, + { + id: 2, + title: __('Last updated'), + sortDirection: { + descending: 'updated_desc', + ascending: 'updated_asc', + }, + }, +]; + +export const JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY = 'jira-import-success-alert-hide-map'; diff --git a/app/assets/javascripts/issuables_list/index.js b/app/assets/javascripts/issuables_list/index.js index 6bfb885a8af..40252c10d5f 100644 --- a/app/assets/javascripts/issuables_list/index.js +++ b/app/assets/javascripts/issuables_list/index.js @@ -36,7 +36,7 @@ function mountIssuableListRootApp() { } function mountIssuablesListApp() { - if (!gon.features?.vueIssuablesList) { + if (!gon.features?.vueIssuablesList && !gon.features?.jiraIssuesIntegration) { return; } diff --git a/app/assets/javascripts/issuables_list/queries/get_issues_list_details.query.graphql b/app/assets/javascripts/issuables_list/queries/get_issues_list_details.query.graphql index b62b9b2af60..8f9b888d19b 100644 --- a/app/assets/javascripts/issuables_list/queries/get_issues_list_details.query.graphql +++ b/app/assets/javascripts/issuables_list/queries/get_issues_list_details.query.graphql @@ -1,5 +1,3 @@ -#import "~/jira_import/queries/jira_import.fragment.graphql" - query($fullPath: ID!) { project(fullPath: $fullPath) { issues { @@ -15,7 +13,8 @@ query($fullPath: ID!) { jiraImportStatus jiraImports { nodes { - ...JiraImport + importedIssuesCount + jiraProjectKey } } } |