diff options
Diffstat (limited to 'app/assets/javascripts/issues_list/components')
6 files changed, 0 insertions, 2032 deletions
diff --git a/app/assets/javascripts/issues_list/components/issuable.vue b/app/assets/javascripts/issues_list/components/issuable.vue deleted file mode 100644 index 6476d5be38c..00000000000 --- a/app/assets/javascripts/issues_list/components/issuable.vue +++ /dev/null @@ -1,441 +0,0 @@ -<script> -/* - * 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 jiraLogo from '@gitlab/svgs/dist/illustrations/logos/jira.svg'; -import { - GlLink, - GlTooltipDirective as GlTooltip, - GlSprintf, - GlLabel, - GlIcon, - GlSafeHtmlDirective as SafeHtml, -} from '@gitlab/ui'; -import { escape, isNumber } from 'lodash'; -import { isScopedLabel } from '~/lib/utils/common_utils'; -import { - dateInWords, - formatDate, - getDayDifference, - getTimeago, - timeFor, - newDateAsLocaleTime, -} from '~/lib/utils/datetime_utility'; -import { convertToCamelCase } from '~/lib/utils/text_utility'; -import { mergeUrlParams, setUrlFragment, isExternal } from '~/lib/utils/url_utility'; -import { sprintf, __ } from '~/locale'; -import initUserPopovers from '~/user_popovers'; -import IssueAssignees from '~/issuable/components/issue_assignees.vue'; - -export default { - i18n: { - openedAgo: __('created %{timeAgoString} by %{user}'), - openedAgoJira: __('created %{timeAgoString} by %{user} in Jira'), - openedAgoServiceDesk: __('created %{timeAgoString} by %{email} via %{user}'), - }, - components: { - IssueAssignees, - GlLink, - GlLabel, - GlIcon, - GlSprintf, - IssueHealthStatus: () => - import('ee_component/related_items_tree/components/issue_health_status.vue'), - }, - directives: { - GlTooltip, - SafeHtml, - }, - inject: ['scopedLabelsAvailable'], - props: { - issuable: { - type: Object, - required: true, - }, - isBulkEditing: { - type: Boolean, - required: false, - default: false, - }, - selected: { - type: Boolean, - required: false, - default: false, - }, - baseUrl: { - type: String, - required: false, - default() { - return window.location.href; - }, - }, - }, - data() { - return { - jiraLogo, - }; - }, - computed: { - milestoneLink() { - const { title } = this.issuable.milestone; - - return this.issuableLink({ milestone_title: title }); - }, - hasWeight() { - return isNumber(this.issuable.weight); - }, - dueDate() { - return this.issuable.due_date ? newDateAsLocaleTime(this.issuable.due_date) : undefined; - }, - dueDateWords() { - return this.dueDate ? dateInWords(this.dueDate, true) : undefined; - }, - isOverdue() { - return this.dueDate ? this.dueDate < new Date() : false; - }, - isClosed() { - return this.issuable.state === 'closed'; - }, - isJiraIssue() { - return this.issuable.external_tracker === 'jira'; - }, - webUrl() { - return this.issuable.gitlab_web_url || this.issuable.web_url; - }, - isIssuableUrlExternal() { - return isExternal(this.webUrl); - }, - linkTarget() { - return this.isIssuableUrlExternal ? '_blank' : null; - }, - issueCreatedToday() { - return getDayDifference(new Date(this.issuable.created_at), new Date()) < 1; - }, - labelIdsString() { - return JSON.stringify(this.issuable.labels.map((l) => l.id)); - }, - milestoneDueDate() { - const { due_date: dueDate } = this.issuable.milestone || {}; - - return dueDate ? newDateAsLocaleTime(dueDate) : undefined; - }, - milestoneTooltipText() { - if (this.milestoneDueDate) { - return sprintf(__('%{primary} (%{secondary})'), { - primary: formatDate(this.milestoneDueDate, 'mmm d, yyyy'), - secondary: timeFor(this.milestoneDueDate), - }); - } - return __('Milestone'); - }, - issuableAuthor() { - return this.issuable.author; - }, - issuableCreatedAt() { - return getTimeago().format(this.issuable.created_at); - }, - popoverDataAttrs() { - const { id, username, name, avatar_url } = this.issuableAuthor; - - return { - 'data-user-id': id, - 'data-username': username, - 'data-name': name, - 'data-avatar-url': avatar_url, - }; - }, - referencePath() { - return this.issuable.references.relative; - }, - updatedDateString() { - return formatDate(new Date(this.issuable.updated_at), 'mmm d, yyyy h:MMtt'); - }, - updatedDateAgo() { - // snake_case because it's the same i18n string as the HAML view - return sprintf(__('updated %{time_ago}'), { - time_ago: escape(getTimeago().format(this.issuable.updated_at)), - }); - }, - issuableMeta() { - return [ - { - key: 'merge-requests', - visible: this.issuable.merge_requests_count > 0, - value: this.issuable.merge_requests_count, - title: __('Related merge requests'), - dataTestId: 'merge-requests', - class: 'js-merge-requests', - icon: 'merge-request', - }, - { - key: 'upvotes', - visible: this.issuable.upvotes > 0, - value: this.issuable.upvotes, - title: __('Upvotes'), - dataTestId: 'upvotes', - class: 'js-upvotes issuable-upvotes', - icon: 'thumb-up', - }, - { - key: 'downvotes', - visible: this.issuable.downvotes > 0, - value: this.issuable.downvotes, - title: __('Downvotes'), - dataTestId: 'downvotes', - class: 'js-downvotes issuable-downvotes', - icon: 'thumb-down', - }, - { - key: 'blocking-issues', - visible: this.issuable.blocking_issues_count > 0, - value: this.issuable.blocking_issues_count, - title: __('Blocking issues'), - dataTestId: 'blocking-issues', - href: setUrlFragment(this.webUrl, 'related-issues'), - icon: 'issue-block', - }, - { - key: 'comments-count', - visible: !this.isJiraIssue, - value: this.issuable.user_notes_count, - title: __('Comments'), - dataTestId: 'notes-count', - href: setUrlFragment(this.webUrl, 'notes'), - class: { 'no-comments': !this.issuable.user_notes_count, 'issuable-comments': true }, - icon: 'comments', - }, - ]; - }, - healthStatus() { - return convertToCamelCase(this.issuable.health_status); - }, - openedMessage() { - if (this.isJiraIssue) return this.$options.i18n.openedAgoJira; - if (this.issuable.service_desk_reply_to) return this.$options.i18n.openedAgoServiceDesk; - return this.$options.i18n.openedAgo; - }, - }, - mounted() { - // TODO: Refactor user popover to use its own component instead of - // spawning event listeners on Vue-rendered elements. - initUserPopovers([this.$refs.openedAgoByContainer.$el]); - }, - methods: { - 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) { - this.$emit('select', { - issuable: this.issuable, - selected: ev.target.checked, - }); - }, - issuableMetaComponent(href) { - return href ? 'gl-link' : 'span'; - }, - }, - - confidentialTooltipText: __('Confidential'), -}; -</script> -<template> - <li - :id="`issue_${issuable.id}`" - class="issue" - :class="{ today: issueCreatedToday, closed: isClosed }" - :data-id="issuable.id" - :data-labels="labelIdsString" - :data-url="webUrl" - data-qa-selector="issue_container" - :data-qa-issue-title="issuable.title" - > - <div class="gl-display-flex"> - <!-- Bulk edit checkbox --> - <div v-if="isBulkEditing" class="gl-mr-3"> - <input - :id="`selected_issue_${issuable.id}`" - :checked="selected" - class="selected-issuable" - type="checkbox" - :data-id="issuable.id" - @input="onSelect" - /> - </div> - - <!-- Issuable info container --> - <!-- Issuable main info --> - <div class="gl-flex-grow-1"> - <div class="title"> - <span class="issue-title-text"> - <gl-icon - v-if="issuable.confidential" - v-gl-tooltip - name="eye-slash" - class="gl-vertical-align-text-bottom" - :size="16" - :title="$options.confidentialTooltipText" - :aria-label="$options.confidentialTooltipText" - /> - <gl-link - :href="webUrl" - :target="linkTarget" - data-testid="issuable-title" - data-qa-selector="issue_link" - > - {{ issuable.title }} - <gl-icon - v-if="isIssuableUrlExternal" - name="external-link" - class="gl-vertical-align-text-bottom gl-ml-2" - /> - </gl-link> - </span> - <span - v-if="issuable.has_tasks" - class="gl-ml-2 task-status gl-display-none d-sm-inline-block" - >{{ issuable.task_status }}</span - > - </div> - - <div class="issuable-info"> - <span class="js-ref-path gl-mr-4 mr-sm-0"> - <span - v-if="isJiraIssue" - v-safe-html="jiraLogo" - class="svg-container logo-container" - data-testid="jira-logo" - ></span> - {{ referencePath }} - </span> - - <span data-testid="openedByMessage" class="gl-display-none d-sm-inline-block gl-mr-4"> - · - <gl-sprintf :message="openedMessage"> - <template #timeAgoString> - <span>{{ issuableCreatedAt }}</span> - </template> - <template #user> - <gl-link - ref="openedAgoByContainer" - v-bind="popoverDataAttrs" - :href="issuableAuthor.web_url" - :target="linkTarget" - >{{ issuableAuthor.name }}</gl-link - > - </template> - <template #email> - <span>{{ issuable.service_desk_reply_to }}</span> - </template> - </gl-sprintf> - </span> - - <gl-link - v-if="issuable.milestone" - v-gl-tooltip - class="gl-display-none d-sm-inline-block gl-mr-4 js-milestone milestone" - :href="milestoneLink" - :title="milestoneTooltipText" - > - <gl-icon name="clock" class="s16 gl-vertical-align-text-bottom" /> - {{ issuable.milestone.title }} - </gl-link> - - <span - v-if="dueDate" - v-gl-tooltip - class="gl-display-none d-sm-inline-block gl-mr-4 js-due-date" - :class="{ cred: isOverdue }" - :title="__('Due date')" - > - <gl-icon name="calendar" /> - {{ dueDateWords }} - </span> - - <span - v-if="hasWeight" - v-gl-tooltip - :title="__('Weight')" - class="gl-display-none d-sm-inline-block gl-mr-4" - data-testid="weight" - data-qa-selector="issuable_weight_content" - > - <gl-icon name="weight" class="align-text-bottom" /> - {{ issuable.weight }} - </span> - - <issue-health-status - v-if="issuable.health_status" - :health-status="healthStatus" - class="gl-mr-4 issuable-tag-valign" - /> - - <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="gl-mr-2 issuable-tag-valign" - >{{ label.name }}</gl-label - > - </div> - </div> - - <!-- Issuable meta --> - <div - class="gl-flex-shrink-0 gl-display-flex gl-flex-direction-column align-items-end gl-justify-content-center" - > - <div class="controls gl-display-flex"> - <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" - class="gl-align-items-center gl-display-flex gl-ml-3" - :icon-size="16" - img-css-classes="gl-mr-2!" - :max-visible="4" - /> - - <template v-for="meta in issuableMeta"> - <span - v-if="meta.visible" - :key="meta.key" - v-gl-tooltip - class="gl-display-none gl-sm-display-flex gl-align-items-center gl-ml-3" - :class="meta.class" - :data-testid="meta.dataTestId" - :title="meta.title" - > - <component :is="issuableMetaComponent(meta.href)" :href="meta.href"> - <gl-icon v-if="meta.icon" :name="meta.icon" /> - {{ meta.value }} - </component> - </span> - </template> - </div> - <div v-gl-tooltip class="issuable-updated-at" :title="updatedDateString"> - {{ updatedDateAgo }} - </div> - </div> - </div> - </li> -</template> diff --git a/app/assets/javascripts/issues_list/components/issuables_list_app.vue b/app/assets/javascripts/issues_list/components/issuables_list_app.vue deleted file mode 100644 index 71136bf0159..00000000000 --- a/app/assets/javascripts/issues_list/components/issuables_list_app.vue +++ /dev/null @@ -1,426 +0,0 @@ -<script> -import { - GlEmptyState, - GlPagination, - GlDeprecatedSkeletonLoading as GlSkeletonLoading, - GlSafeHtmlDirective as SafeHtml, -} from '@gitlab/ui'; -import { toNumber, omit } from 'lodash'; -import createFlash from '~/flash'; -import axios from '~/lib/utils/axios_utils'; -import { scrollToElement, historyPushState } from '~/lib/utils/common_utils'; -import { setUrlParams, queryToObject, getParameterByName } from '~/lib/utils/url_utility'; -import { __ } from '~/locale'; -import initManualOrdering from '~/issues/manual_ordering'; -import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; -import { - sortOrderMap, - availableSortOptionsJira, - RELATIVE_POSITION, - PAGE_SIZE, - PAGE_SIZE_MANUAL, - LOADING_LIST_ITEMS_LENGTH, -} from '../constants'; -import issuableEventHub from '../eventhub'; -import { emptyStateHelper } from '../service_desk_helper'; -import Issuable from './issuable.vue'; - -/** - * @deprecated Use app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue instead - */ -export default { - LOADING_LIST_ITEMS_LENGTH, - directives: { - SafeHtml, - }, - components: { - GlEmptyState, - GlPagination, - GlSkeletonLoading, - Issuable, - FilteredSearchBar, - }, - props: { - canBulkEdit: { - type: Boolean, - required: false, - default: false, - }, - emptyStateMeta: { - type: Object, - required: true, - }, - endpoint: { - 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: [], - loading: false, - page: getParameterByName('page') !== null ? toNumber(getParameterByName('page')) : 1, - selection: {}, - totalItems: 0, - }; - }, - computed: { - allIssuablesSelected() { - // WARNING: Because we are only keeping track of selected values - // this works, we will need to rethink this if we start tracking - // [id]: false for not selected values. - return this.issuables.length === Object.keys(this.selection).length; - }, - emptyState() { - if (this.issuables.length) { - return {}; // Empty state shouldn't be shown here - } - - if (this.isServiceDesk) { - return emptyStateHelper(this.emptyStateMeta); - } - - if (this.hasFilters) { - return { - title: __('Sorry, your filter produced no results'), - svgPath: this.emptyStateMeta.svgPath, - description: __('To widen your search, change or remove filters above'), - primaryLink: this.emptyStateMeta.createIssuePath, - primaryText: __('New issue'), - }; - } - - if (this.filters.state === 'opened') { - return { - title: __('There are no open issues'), - svgPath: this.emptyStateMeta.svgPath, - description: __('To keep this project going, create a new issue'), - primaryLink: this.emptyStateMeta.createIssuePath, - primaryText: __('New issue'), - }; - } else if (this.filters.state === 'closed') { - return { - title: __('There are no closed issues'), - svgPath: this.emptyStateMeta.svgPath, - }; - } - - return { - title: __('There are no issues to show'), - svgPath: this.emptyStateMeta.svgPath, - description: __( - 'The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project.', - ), - }; - }, - hasFilters() { - const ignored = ['utf8', 'state', 'scope', 'order_by', 'sort']; - return Object.keys(omit(this.filters, ignored)).length > 0; - }, - isManualOrdering() { - return this.sortKey === RELATIVE_POSITION; - }, - itemsPerPage() { - return this.isManualOrdering ? PAGE_SIZE_MANUAL : PAGE_SIZE; - }, - 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, - }; - }, - isServiceDesk() { - return this.type === 'service_desk'; - }, - 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() { - // We need to call nextTick here to wait for all of the boxes to be checked and rendered - // before we query the dom in issuable_bulk_update_actions.js. - this.$nextTick(() => { - issuableEventHub.$emit('issuables:updateBulkEdit'); - }); - }, - issuables() { - this.$nextTick(() => { - initManualOrdering(); - }); - }, - }, - mounted() { - if (this.canBulkEdit) { - this.unsubscribeToggleBulkEdit = issuableEventHub.$on('issuables:toggleBulkEdit', (val) => { - this.isBulkEditing = val; - }); - } - this.fetchIssuables(); - }, - beforeDestroy() { - // eslint-disable-next-line @gitlab/no-global-event-off - issuableEventHub.$off('issuables:toggleBulkEdit'); - }, - methods: { - isSelected(issuableId) { - return Boolean(this.selection[issuableId]); - }, - setSelection(ids) { - ids.forEach((id) => { - this.select(id, true); - }); - }, - clearSelection() { - this.selection = {}; - }, - select(id, isSelect = true) { - if (isSelect) { - this.$set(this.selection, id, true); - } else { - this.$delete(this.selection, id); - } - }, - fetchIssuables(pageToFetch) { - this.loading = true; - - this.clearSelection(); - - this.setFilters(); - - return axios - .get(this.endpoint, { - params: { - ...this.filters, - - with_labels_details: true, - page: pageToFetch || this.page, - per_page: this.itemsPerPage, - }, - }) - .then((response) => { - this.loading = false; - this.issuables = response.data; - this.totalItems = Number(response.headers['x-total']); - this.page = Number(response.headers['x-page']); - }) - .catch(() => { - this.loading = false; - return createFlash({ - message: __('An error occurred while loading issues'), - }); - }); - }, - getQueryObject() { - return queryToObject(window.location.search, { gatherArrays: true }); - }, - onPaginate(newPage) { - if (newPage === this.page) return; - - scrollToElement('#content-body'); - - // NOTE: This allows for the params to be updated on pagination - historyPushState( - setUrlParams({ ...this.filters, page: newPage }, window.location.href, true), - ); - - this.fetchIssuables(newPage); - }, - onSelectAll() { - if (this.allIssuablesSelected) { - this.selection = {}; - } else { - this.setSelection(this.issuables.map(({ id }) => id)); - } - }, - onSelectIssuable({ issuable, selected }) { - if (!this.canBulkEdit) return; - - this.select(issuable.id, selected); - }, - setFilters() { - 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; - } - if (Array.isArray(labels)) { - filters.labels = labels.join(','); - } - if (!filters.state) { - 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) { - const searchTokens = []; - - filters.forEach((filter) => { - if (filter.type === 'filtered-search-term') { - if (filter.value.data) { - searchTokens.push(filter.value.data); - } - } - }); - - if (searchTokens.length) { - this.filters.search = searchTokens.join(' '); - } - this.page = 1; - - this.refetchIssuables(); - }, - handleSort(sort) { - this.filters.sort = sort; - this.page = 1; - - this.refetchIssuables(); - }, - }, -}; -</script> - -<template> - <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 v-else-if="issuables.length"> - <div v-if="isBulkEditing" class="issue px-3 py-3 border-bottom border-light"> - <input - id="check-all-issues" - 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" - :svg-path="emptyState.svgPath" - :primary-button-link="emptyState.primaryLink" - :primary-button-text="emptyState.primaryText" - > - <template #description> - <div v-safe-html="emptyState.description"></div> - </template> - </gl-empty-state> - </div> -</template> diff --git a/app/assets/javascripts/issues_list/components/issue_card_time_info.vue b/app/assets/javascripts/issues_list/components/issue_card_time_info.vue deleted file mode 100644 index aece7372182..00000000000 --- a/app/assets/javascripts/issues_list/components/issue_card_time_info.vue +++ /dev/null @@ -1,104 +0,0 @@ -<script> -import { GlLink, GlIcon, GlTooltipDirective } from '@gitlab/ui'; -import { - dateInWords, - getTimeRemainingInWords, - isInFuture, - isInPast, - isToday, -} from '~/lib/utils/datetime_utility'; -import { __ } from '~/locale'; - -export default { - components: { - GlLink, - GlIcon, - }, - directives: { - GlTooltip: GlTooltipDirective, - }, - props: { - issue: { - type: Object, - required: true, - }, - }, - computed: { - milestoneDate() { - if (this.issue.milestone?.dueDate) { - const { dueDate, startDate } = this.issue.milestone; - const date = dateInWords(new Date(dueDate), true); - const remainingTime = this.milestoneRemainingTime(dueDate, startDate); - return `${date} (${remainingTime})`; - } - return __('Milestone'); - }, - milestoneLink() { - return this.issue.milestone.webPath || this.issue.milestone.webUrl; - }, - dueDate() { - return this.issue.dueDate && dateInWords(new Date(this.issue.dueDate), true); - }, - showDueDateInRed() { - return isInPast(new Date(this.issue.dueDate)) && !this.issue.closedAt; - }, - timeEstimate() { - return this.issue.humanTimeEstimate || this.issue.timeStats?.humanTimeEstimate; - }, - }, - methods: { - milestoneRemainingTime(dueDate, startDate) { - const due = new Date(dueDate); - const start = new Date(startDate); - - if (dueDate && isInPast(due)) { - return __('Past due'); - } else if (dueDate && isToday(due)) { - return __('Today'); - } else if (startDate && isInFuture(start)) { - return __('Upcoming'); - } else if (dueDate) { - return getTimeRemainingInWords(due); - } - return ''; - }, - }, -}; -</script> - -<template> - <span> - <span - v-if="issue.milestone" - class="issuable-milestone gl-mr-3" - data-testid="issuable-milestone" - > - <gl-link v-gl-tooltip :href="milestoneLink" :title="milestoneDate"> - <gl-icon name="clock" /> - {{ issue.milestone.title }} - </gl-link> - </span> - <span - v-if="issue.dueDate" - v-gl-tooltip - class="issuable-due-date gl-mr-3" - :class="{ 'gl-text-red-500': showDueDateInRed }" - :title="__('Due date')" - data-testid="issuable-due-date" - > - <gl-icon name="calendar" /> - {{ dueDate }} - </span> - <span - v-if="timeEstimate" - v-gl-tooltip - class="gl-mr-3" - :title="__('Estimate')" - data-testid="time-estimate" - > - <gl-icon name="timer" /> - {{ timeEstimate }} - </span> - <slot></slot> - </span> -</template> diff --git a/app/assets/javascripts/issues_list/components/issues_list_app.vue b/app/assets/javascripts/issues_list/components/issues_list_app.vue deleted file mode 100644 index 6ced1080b71..00000000000 --- a/app/assets/javascripts/issues_list/components/issues_list_app.vue +++ /dev/null @@ -1,822 +0,0 @@ -<script> -import { - GlButton, - GlEmptyState, - GlFilteredSearchToken, - GlIcon, - GlLink, - GlSprintf, - GlTooltipDirective, -} from '@gitlab/ui'; -import * as Sentry from '@sentry/browser'; -import fuzzaldrinPlus from 'fuzzaldrin-plus'; -import { orderBy } from 'lodash'; -import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql'; -import getIssuesCountsQuery from 'ee_else_ce/issues_list/queries/get_issues_counts.query.graphql'; -import IssueCardTimeInfo from 'ee_else_ce/issues_list/components/issue_card_time_info.vue'; -import createFlash, { FLASH_TYPES } from '~/flash'; -import { TYPE_USER } from '~/graphql_shared/constants'; -import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { ITEM_TYPE } from '~/groups/constants'; -import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue'; -import IssuableByEmail from '~/issuable/components/issuable_by_email.vue'; -import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue'; -import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants'; -import { - CREATED_DESC, - i18n, - MAX_LIST_SIZE, - PAGE_SIZE, - PARAM_DUE_DATE, - PARAM_SORT, - PARAM_STATE, - RELATIVE_POSITION_ASC, - TOKEN_TYPE_ASSIGNEE, - TOKEN_TYPE_AUTHOR, - TOKEN_TYPE_CONFIDENTIAL, - TOKEN_TYPE_LABEL, - TOKEN_TYPE_MILESTONE, - TOKEN_TYPE_MY_REACTION, - TOKEN_TYPE_RELEASE, - TOKEN_TYPE_TYPE, - UPDATED_DESC, - urlSortParams, -} from '~/issues_list/constants'; -import { - convertToApiParams, - convertToSearchQuery, - convertToUrlParams, - getDueDateValue, - getFilterTokens, - getInitialPageParams, - getSortKey, - getSortOptions, -} from '~/issues_list/utils'; -import axios from '~/lib/utils/axios_utils'; -import { scrollUp } from '~/lib/utils/scroll_utils'; -import { getParameterByName, joinPaths } from '~/lib/utils/url_utility'; -import { - DEFAULT_NONE_ANY, - OPERATOR_IS_ONLY, - TOKEN_TITLE_ASSIGNEE, - TOKEN_TITLE_AUTHOR, - TOKEN_TITLE_CONFIDENTIAL, - TOKEN_TITLE_LABEL, - TOKEN_TITLE_MILESTONE, - TOKEN_TITLE_MY_REACTION, - TOKEN_TITLE_RELEASE, - TOKEN_TITLE_TYPE, -} from '~/vue_shared/components/filtered_search_bar/constants'; -import eventHub from '../eventhub'; -import reorderIssuesMutation from '../queries/reorder_issues.mutation.graphql'; -import searchLabelsQuery from '../queries/search_labels.query.graphql'; -import searchMilestonesQuery from '../queries/search_milestones.query.graphql'; -import searchUsersQuery from '../queries/search_users.query.graphql'; -import NewIssueDropdown from './new_issue_dropdown.vue'; - -const AuthorToken = () => - import('~/vue_shared/components/filtered_search_bar/tokens/author_token.vue'); -const EmojiToken = () => - import('~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue'); -const LabelToken = () => - import('~/vue_shared/components/filtered_search_bar/tokens/label_token.vue'); -const MilestoneToken = () => - import('~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'); -const ReleaseToken = () => - import('~/vue_shared/components/filtered_search_bar/tokens/release_token.vue'); - -export default { - i18n, - IssuableListTabs, - components: { - CsvImportExportButtons, - GlButton, - GlEmptyState, - GlIcon, - GlLink, - GlSprintf, - IssuableByEmail, - IssuableList, - IssueCardTimeInfo, - NewIssueDropdown, - }, - directives: { - GlTooltip: GlTooltipDirective, - }, - inject: { - autocompleteAwardEmojisPath: { - default: '', - }, - calendarPath: { - default: '', - }, - canBulkUpdate: { - default: false, - }, - emptyStateSvgPath: { - default: '', - }, - exportCsvPath: { - default: '', - }, - fullPath: { - default: '', - }, - hasAnyIssues: { - default: false, - }, - hasAnyProjects: { - default: false, - }, - hasBlockedIssuesFeature: { - default: false, - }, - hasIssueWeightsFeature: { - default: false, - }, - hasMultipleIssueAssigneesFeature: { - default: false, - }, - initialEmail: { - default: '', - }, - isAnonymousSearchDisabled: { - default: false, - }, - isIssueRepositioningDisabled: { - default: false, - }, - isProject: { - default: false, - }, - isSignedIn: { - default: false, - }, - jiraIntegrationPath: { - default: '', - }, - newIssuePath: { - default: '', - }, - releasesPath: { - default: '', - }, - rssPath: { - default: '', - }, - showNewIssueLink: { - default: false, - }, - signInPath: { - default: '', - }, - }, - props: { - eeSearchTokens: { - type: Array, - required: false, - default: () => [], - }, - }, - data() { - const state = getParameterByName(PARAM_STATE); - const defaultSortKey = state === IssuableStates.Closed ? UPDATED_DESC : CREATED_DESC; - let sortKey = getSortKey(getParameterByName(PARAM_SORT)) || defaultSortKey; - - if (this.isIssueRepositioningDisabled && sortKey === RELATIVE_POSITION_ASC) { - this.showIssueRepositioningMessage(); - sortKey = defaultSortKey; - } - - const isSearchDisabled = - this.isAnonymousSearchDisabled && - !this.isSignedIn && - window.location.search.includes('search='); - - if (isSearchDisabled) { - this.showAnonymousSearchingMessage(); - } - - return { - dueDateFilter: getDueDateValue(getParameterByName(PARAM_DUE_DATE)), - exportCsvPathWithQuery: this.getExportCsvPathWithQuery(), - filterTokens: isSearchDisabled ? [] : getFilterTokens(window.location.search), - issues: [], - issuesCounts: {}, - issuesError: null, - pageInfo: {}, - pageParams: getInitialPageParams(sortKey), - showBulkEditSidebar: false, - sortKey, - state: state || IssuableStates.Opened, - }; - }, - apollo: { - issues: { - query: getIssuesQuery, - variables() { - return this.queryVariables; - }, - update(data) { - return data[this.namespace]?.issues.nodes ?? []; - }, - result({ data }) { - this.pageInfo = data[this.namespace]?.issues.pageInfo ?? {}; - this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery(); - }, - error(error) { - this.issuesError = this.$options.i18n.errorFetchingIssues; - Sentry.captureException(error); - }, - skip() { - return !this.hasAnyIssues; - }, - debounce: 200, - }, - issuesCounts: { - query: getIssuesCountsQuery, - variables() { - return this.queryVariables; - }, - update(data) { - return data[this.namespace] ?? {}; - }, - error(error) { - this.issuesError = this.$options.i18n.errorFetchingCounts; - Sentry.captureException(error); - }, - skip() { - return !this.hasAnyIssues; - }, - debounce: 200, - context: { - isSingleRequest: true, - }, - }, - }, - computed: { - queryVariables() { - return { - fullPath: this.fullPath, - isProject: this.isProject, - isSignedIn: this.isSignedIn, - search: this.searchQuery, - sort: this.sortKey, - state: this.state, - ...this.pageParams, - ...this.apiFilterParams, - }; - }, - namespace() { - return this.isProject ? ITEM_TYPE.PROJECT : ITEM_TYPE.GROUP; - }, - hasSearch() { - return this.searchQuery || Object.keys(this.urlFilterParams).length; - }, - isBulkEditButtonDisabled() { - return this.showBulkEditSidebar || !this.issues.length; - }, - isManualOrdering() { - return this.sortKey === RELATIVE_POSITION_ASC; - }, - isOpenTab() { - return this.state === IssuableStates.Opened; - }, - showCsvButtons() { - return this.isProject && this.isSignedIn; - }, - showNewIssueDropdown() { - return !this.isProject && this.hasAnyProjects; - }, - apiFilterParams() { - return convertToApiParams(this.filterTokens); - }, - urlFilterParams() { - return convertToUrlParams(this.filterTokens); - }, - searchQuery() { - return convertToSearchQuery(this.filterTokens) || undefined; - }, - searchTokens() { - const preloadedAuthors = []; - - if (gon.current_user_id) { - preloadedAuthors.push({ - id: convertToGraphQLId(TYPE_USER, gon.current_user_id), - name: gon.current_user_fullname, - username: gon.current_username, - avatar_url: gon.current_user_avatar_url, - }); - } - - const tokens = [ - { - type: TOKEN_TYPE_AUTHOR, - title: TOKEN_TITLE_AUTHOR, - icon: 'pencil', - token: AuthorToken, - dataType: 'user', - unique: true, - defaultAuthors: [], - fetchAuthors: this.fetchUsers, - recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-author`, - preloadedAuthors, - }, - { - type: TOKEN_TYPE_ASSIGNEE, - title: TOKEN_TITLE_ASSIGNEE, - icon: 'user', - token: AuthorToken, - dataType: 'user', - unique: !this.hasMultipleIssueAssigneesFeature, - defaultAuthors: DEFAULT_NONE_ANY, - fetchAuthors: this.fetchUsers, - recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-assignee`, - preloadedAuthors, - }, - { - type: TOKEN_TYPE_MILESTONE, - title: TOKEN_TITLE_MILESTONE, - icon: 'clock', - token: MilestoneToken, - fetchMilestones: this.fetchMilestones, - recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-milestone`, - }, - { - type: TOKEN_TYPE_LABEL, - title: TOKEN_TITLE_LABEL, - icon: 'labels', - token: LabelToken, - defaultLabels: DEFAULT_NONE_ANY, - fetchLabels: this.fetchLabels, - recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-label`, - }, - { - type: TOKEN_TYPE_TYPE, - title: TOKEN_TITLE_TYPE, - icon: 'issues', - token: GlFilteredSearchToken, - options: [ - { icon: 'issue-type-issue', title: 'issue', value: 'issue' }, - { icon: 'issue-type-incident', title: 'incident', value: 'incident' }, - { icon: 'issue-type-test-case', title: 'test_case', value: 'test_case' }, - ], - }, - ]; - - if (this.isProject) { - tokens.push({ - type: TOKEN_TYPE_RELEASE, - title: TOKEN_TITLE_RELEASE, - icon: 'rocket', - token: ReleaseToken, - fetchReleases: this.fetchReleases, - recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-release`, - }); - } - - if (this.isSignedIn) { - tokens.push({ - type: TOKEN_TYPE_MY_REACTION, - title: TOKEN_TITLE_MY_REACTION, - icon: 'thumb-up', - token: EmojiToken, - unique: true, - fetchEmojis: this.fetchEmojis, - recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-my_reaction`, - }); - - tokens.push({ - type: TOKEN_TYPE_CONFIDENTIAL, - title: TOKEN_TITLE_CONFIDENTIAL, - icon: 'eye-slash', - token: GlFilteredSearchToken, - unique: true, - operators: OPERATOR_IS_ONLY, - options: [ - { icon: 'eye-slash', value: 'yes', title: this.$options.i18n.confidentialYes }, - { icon: 'eye', value: 'no', title: this.$options.i18n.confidentialNo }, - ], - }); - } - - if (this.eeSearchTokens.length) { - tokens.push(...this.eeSearchTokens); - } - - tokens.sort((a, b) => a.title.localeCompare(b.title)); - - return orderBy(tokens, ['title']); - }, - showPaginationControls() { - return this.issues.length > 0 && (this.pageInfo.hasNextPage || this.pageInfo.hasPreviousPage); - }, - sortOptions() { - return getSortOptions(this.hasIssueWeightsFeature, this.hasBlockedIssuesFeature); - }, - tabCounts() { - const { openedIssues, closedIssues, allIssues } = this.issuesCounts; - return { - [IssuableStates.Opened]: openedIssues?.count, - [IssuableStates.Closed]: closedIssues?.count, - [IssuableStates.All]: allIssues?.count, - }; - }, - currentTabCount() { - return this.tabCounts[this.state] ?? 0; - }, - urlParams() { - return { - due_date: this.dueDateFilter, - search: this.searchQuery, - sort: urlSortParams[this.sortKey], - state: this.state, - ...this.urlFilterParams, - }; - }, - }, - created() { - this.cache = {}; - }, - mounted() { - eventHub.$on('issuables:toggleBulkEdit', this.toggleBulkEditSidebar); - }, - beforeDestroy() { - eventHub.$off('issuables:toggleBulkEdit', this.toggleBulkEditSidebar); - }, - methods: { - fetchWithCache(path, cacheName, searchKey, search, wrapData = false) { - if (this.cache[cacheName]) { - const data = search - ? fuzzaldrinPlus.filter(this.cache[cacheName], search, { key: searchKey }) - : this.cache[cacheName].slice(0, MAX_LIST_SIZE); - return wrapData ? Promise.resolve({ data }) : Promise.resolve(data); - } - - return axios.get(path).then(({ data }) => { - this.cache[cacheName] = data; - const result = data.slice(0, MAX_LIST_SIZE); - return wrapData ? { data: result } : result; - }); - }, - fetchEmojis(search) { - return this.fetchWithCache(this.autocompleteAwardEmojisPath, 'emojis', 'name', search); - }, - fetchReleases(search) { - return this.fetchWithCache(this.releasesPath, 'releases', 'tag', search); - }, - fetchLabels(search) { - return this.$apollo - .query({ - query: searchLabelsQuery, - variables: { fullPath: this.fullPath, search, isProject: this.isProject }, - }) - .then(({ data }) => data[this.namespace]?.labels.nodes) - .then((labels) => - // TODO remove once we can search by title-only on the backend - // https://gitlab.com/gitlab-org/gitlab/-/issues/346353 - labels.filter((label) => label.title.toLowerCase().includes(search.toLowerCase())), - ); - }, - fetchMilestones(search) { - return this.$apollo - .query({ - query: searchMilestonesQuery, - variables: { fullPath: this.fullPath, search, isProject: this.isProject }, - }) - .then(({ data }) => data[this.namespace]?.milestones.nodes); - }, - fetchUsers(search) { - return this.$apollo - .query({ - query: searchUsersQuery, - variables: { fullPath: this.fullPath, search, isProject: this.isProject }, - }) - .then(({ data }) => - data[this.namespace]?.[`${this.namespace}Members`].nodes.map((member) => member.user), - ); - }, - getExportCsvPathWithQuery() { - return `${this.exportCsvPath}${window.location.search}`; - }, - getStatus(issue) { - if (issue.closedAt && issue.moved) { - return this.$options.i18n.closedMoved; - } - if (issue.closedAt) { - return this.$options.i18n.closed; - } - return undefined; - }, - handleUpdateLegacyBulkEdit() { - // If "select all" checkbox was checked, wait for all checkboxes - // to be checked before updating IssuableBulkUpdateSidebar class - this.$nextTick(() => { - eventHub.$emit('issuables:updateBulkEdit'); - }); - }, - async handleBulkUpdateClick() { - if (!this.hasInitBulkEdit) { - const initBulkUpdateSidebar = await import( - '~/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar' - ); - initBulkUpdateSidebar.default.init('issuable_'); - - const usersSelect = await import('~/users_select'); - const UsersSelect = usersSelect.default; - new UsersSelect(); // eslint-disable-line no-new - - this.hasInitBulkEdit = true; - } - - eventHub.$emit('issuables:enableBulkEdit'); - }, - handleClickTab(state) { - if (this.state !== state) { - this.pageParams = getInitialPageParams(this.sortKey); - } - this.state = state; - }, - handleDismissAlert() { - this.issuesError = null; - }, - handleFilter(filter) { - if (this.isAnonymousSearchDisabled && !this.isSignedIn) { - this.showAnonymousSearchingMessage(); - return; - } - this.pageParams = getInitialPageParams(this.sortKey); - this.filterTokens = filter; - }, - handleNextPage() { - this.pageParams = { - afterCursor: this.pageInfo.endCursor, - firstPageSize: PAGE_SIZE, - }; - scrollUp(); - }, - handlePreviousPage() { - this.pageParams = { - beforeCursor: this.pageInfo.startCursor, - lastPageSize: PAGE_SIZE, - }; - scrollUp(); - }, - handleReorder({ newIndex, oldIndex }) { - const issueToMove = this.issues[oldIndex]; - const isDragDropDownwards = newIndex > oldIndex; - const isMovingToBeginning = newIndex === 0; - const isMovingToEnd = newIndex === this.issues.length - 1; - - let moveBeforeId; - let moveAfterId; - - if (isDragDropDownwards) { - const afterIndex = isMovingToEnd ? newIndex : newIndex + 1; - moveBeforeId = this.issues[newIndex].id; - moveAfterId = this.issues[afterIndex].id; - } else { - const beforeIndex = isMovingToBeginning ? newIndex : newIndex - 1; - moveBeforeId = this.issues[beforeIndex].id; - moveAfterId = this.issues[newIndex].id; - } - - return axios - .put(joinPaths(issueToMove.webPath, 'reorder'), { - move_before_id: isMovingToBeginning ? null : getIdFromGraphQLId(moveBeforeId), - move_after_id: isMovingToEnd ? null : getIdFromGraphQLId(moveAfterId), - group_full_path: this.isProject ? undefined : this.fullPath, - }) - .then(() => { - const serializedVariables = JSON.stringify(this.queryVariables); - return this.$apollo.mutate({ - mutation: reorderIssuesMutation, - variables: { oldIndex, newIndex, namespace: this.namespace, serializedVariables }, - }); - }) - .catch((error) => { - this.issuesError = this.$options.i18n.reorderError; - Sentry.captureException(error); - }); - }, - handleSort(sortKey) { - if (this.isIssueRepositioningDisabled && sortKey === RELATIVE_POSITION_ASC) { - this.showIssueRepositioningMessage(); - return; - } - - if (this.sortKey !== sortKey) { - this.pageParams = getInitialPageParams(sortKey); - } - this.sortKey = sortKey; - }, - showAnonymousSearchingMessage() { - createFlash({ - message: this.$options.i18n.anonymousSearchingMessage, - type: FLASH_TYPES.NOTICE, - }); - }, - showIssueRepositioningMessage() { - createFlash({ - message: this.$options.i18n.issueRepositioningMessage, - type: FLASH_TYPES.NOTICE, - }); - }, - toggleBulkEditSidebar(showBulkEditSidebar) { - this.showBulkEditSidebar = showBulkEditSidebar; - }, - }, -}; -</script> - -<template> - <div v-if="hasAnyIssues"> - <issuable-list - :namespace="fullPath" - recent-searches-storage-key="issues" - :search-input-placeholder="$options.i18n.searchPlaceholder" - :search-tokens="searchTokens" - :initial-filter-value="filterTokens" - :sort-options="sortOptions" - :initial-sort-by="sortKey" - :issuables="issues" - :error="issuesError" - label-filter-param="label_name" - :tabs="$options.IssuableListTabs" - :current-tab="state" - :tab-counts="tabCounts" - :issuables-loading="$apollo.queries.issues.loading" - :is-manual-ordering="isManualOrdering" - :show-bulk-edit-sidebar="showBulkEditSidebar" - :show-pagination-controls="showPaginationControls" - :use-keyset-pagination="true" - :has-next-page="pageInfo.hasNextPage" - :has-previous-page="pageInfo.hasPreviousPage" - :url-params="urlParams" - @click-tab="handleClickTab" - @dismiss-alert="handleDismissAlert" - @filter="handleFilter" - @next-page="handleNextPage" - @previous-page="handlePreviousPage" - @reorder="handleReorder" - @sort="handleSort" - @update-legacy-bulk-edit="handleUpdateLegacyBulkEdit" - > - <template #nav-actions> - <gl-button - v-gl-tooltip - :href="rssPath" - icon="rss" - :title="$options.i18n.rssLabel" - :aria-label="$options.i18n.rssLabel" - /> - <gl-button - v-gl-tooltip - :href="calendarPath" - icon="calendar" - :title="$options.i18n.calendarLabel" - :aria-label="$options.i18n.calendarLabel" - /> - <csv-import-export-buttons - v-if="showCsvButtons" - class="gl-md-mr-3" - :export-csv-path="exportCsvPathWithQuery" - :issuable-count="currentTabCount" - /> - <gl-button - v-if="canBulkUpdate" - :disabled="isBulkEditButtonDisabled" - @click="handleBulkUpdateClick" - > - {{ $options.i18n.editIssues }} - </gl-button> - <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm"> - {{ $options.i18n.newIssueLabel }} - </gl-button> - <new-issue-dropdown v-if="showNewIssueDropdown" /> - </template> - - <template #timeframe="{ issuable = {} }"> - <issue-card-time-info :issue="issuable" /> - </template> - - <template #status="{ issuable = {} }"> - {{ getStatus(issuable) }} - </template> - - <template #statistics="{ issuable = {} }"> - <li - v-if="issuable.mergeRequestsCount" - v-gl-tooltip - class="gl-display-none gl-sm-display-block" - :title="$options.i18n.relatedMergeRequests" - data-testid="merge-requests" - > - <gl-icon name="merge-request" /> - {{ issuable.mergeRequestsCount }} - </li> - <li - v-if="issuable.upvotes" - v-gl-tooltip - class="issuable-upvotes gl-display-none gl-sm-display-block" - :title="$options.i18n.upvotes" - data-testid="issuable-upvotes" - > - <gl-icon name="thumb-up" /> - {{ issuable.upvotes }} - </li> - <li - v-if="issuable.downvotes" - v-gl-tooltip - class="issuable-downvotes gl-display-none gl-sm-display-block" - :title="$options.i18n.downvotes" - data-testid="issuable-downvotes" - > - <gl-icon name="thumb-down" /> - {{ issuable.downvotes }} - </li> - <slot :issuable="issuable"></slot> - </template> - - <template #empty-state> - <gl-empty-state - v-if="hasSearch" - :description="$options.i18n.noSearchResultsDescription" - :title="$options.i18n.noSearchResultsTitle" - :svg-path="emptyStateSvgPath" - > - <template #actions> - <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm"> - {{ $options.i18n.newIssueLabel }} - </gl-button> - </template> - </gl-empty-state> - - <gl-empty-state - v-else-if="isOpenTab" - :description="$options.i18n.noOpenIssuesDescription" - :title="$options.i18n.noOpenIssuesTitle" - :svg-path="emptyStateSvgPath" - > - <template #actions> - <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm"> - {{ $options.i18n.newIssueLabel }} - </gl-button> - </template> - </gl-empty-state> - - <gl-empty-state - v-else - :title="$options.i18n.noClosedIssuesTitle" - :svg-path="emptyStateSvgPath" - /> - </template> - </issuable-list> - - <issuable-by-email v-if="initialEmail" class="gl-text-center gl-pt-5 gl-pb-7" /> - </div> - - <div v-else-if="isSignedIn"> - <gl-empty-state - :description="$options.i18n.noIssuesSignedInDescription" - :title="$options.i18n.noIssuesSignedInTitle" - :svg-path="emptyStateSvgPath" - > - <template #actions> - <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm"> - {{ $options.i18n.newIssueLabel }} - </gl-button> - <csv-import-export-buttons - v-if="showCsvButtons" - class="gl-mr-3" - :export-csv-path="exportCsvPathWithQuery" - :issuable-count="currentTabCount" - /> - <new-issue-dropdown v-if="showNewIssueDropdown" /> - </template> - </gl-empty-state> - <hr /> - <p class="gl-text-center gl-font-weight-bold gl-mb-0"> - {{ $options.i18n.jiraIntegrationTitle }} - </p> - <p class="gl-text-center gl-mb-0"> - <gl-sprintf :message="$options.i18n.jiraIntegrationMessage"> - <template #jiraDocsLink="{ content }"> - <gl-link :href="jiraIntegrationPath">{{ content }}</gl-link> - </template> - </gl-sprintf> - </p> - <p class="gl-text-center gl-text-gray-500"> - {{ $options.i18n.jiraIntegrationSecondaryMessage }} - </p> - </div> - - <gl-empty-state - v-else - :description="$options.i18n.noIssuesSignedOutDescription" - :title="$options.i18n.noIssuesSignedOutTitle" - :svg-path="emptyStateSvgPath" - :primary-button-text="$options.i18n.noIssuesSignedOutButtonText" - :primary-button-link="signInPath" - /> -</template> diff --git a/app/assets/javascripts/issues_list/components/jira_issues_import_status_app.vue b/app/assets/javascripts/issues_list/components/jira_issues_import_status_app.vue deleted file mode 100644 index fb1dbef666c..00000000000 --- a/app/assets/javascripts/issues_list/components/jira_issues_import_status_app.vue +++ /dev/null @@ -1,112 +0,0 @@ -<script> -import { GlAlert, GlLabel } from '@gitlab/ui'; -import { last } from 'lodash'; -import { - calculateJiraImportLabel, - isInProgress, - setFinishedAlertHideMap, - shouldShowFinishedAlert, -} from '~/jira_import/utils/jira_import_utils'; -import { n__ } from '~/locale'; -import getIssuesListDetailsQuery from '../queries/get_issues_list_details.query.graphql'; - -export default { - name: 'JiraIssuesImportStatus', - components: { - GlAlert, - GlLabel, - }, - props: { - canEdit: { - type: Boolean, - required: true, - }, - isJiraConfigured: { - type: Boolean, - required: true, - }, - issuesPath: { - type: String, - required: true, - }, - projectPath: { - type: String, - required: true, - }, - }, - data() { - return { - jiraImport: {}, - }; - }, - apollo: { - jiraImport: { - query: getIssuesListDetailsQuery, - variables() { - return { - fullPath: this.projectPath, - }; - }, - 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), - }; - }, - }, - }, - 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)}`; - }, - shouldRender() { - return this.jiraImport.shouldShowInProgressAlert || this.jiraImport.shouldShowFinishedAlert; - }, - }, - methods: { - hideFinishedAlert() { - setFinishedAlertHideMap(this.jiraImport.label.title); - this.jiraImport.shouldShowFinishedAlert = false; - }, - hideInProgressAlert() { - this.jiraImport.shouldShowInProgressAlert = false; - }, - }, -}; -</script> - -<template> - <div v-if="shouldRender" class="gl-my-5"> - <gl-alert v-if="jiraImport.shouldShowInProgressAlert" @dismiss="hideInProgressAlert"> - {{ __('Import in progress. Refresh page to see newly added issues.') }} - </gl-alert> - - <gl-alert - v-else-if="jiraImport.shouldShowFinishedAlert" - variant="success" - @dismiss="hideFinishedAlert" - > - {{ finishedMessage }} - <gl-label - :background-color="jiraImport.label.color" - scoped - size="sm" - :target="labelTarget" - :title="jiraImport.label.title" - /> - </gl-alert> - </div> -</template> diff --git a/app/assets/javascripts/issues_list/components/new_issue_dropdown.vue b/app/assets/javascripts/issues_list/components/new_issue_dropdown.vue deleted file mode 100644 index e749579af80..00000000000 --- a/app/assets/javascripts/issues_list/components/new_issue_dropdown.vue +++ /dev/null @@ -1,127 +0,0 @@ -<script> -import { - GlDropdown, - GlDropdownItem, - GlDropdownText, - GlLoadingIcon, - GlSearchBoxByType, -} from '@gitlab/ui'; -import createFlash from '~/flash'; -import searchProjectsQuery from '~/issues_list/queries/search_projects.query.graphql'; -import { DASH_SCOPE, joinPaths } from '~/lib/utils/url_utility'; -import { __, sprintf } from '~/locale'; -import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants'; - -export default { - i18n: { - defaultDropdownText: __('Select project to create issue'), - noMatchesFound: __('No matches found'), - toggleButtonLabel: __('Toggle project select'), - }, - components: { - GlDropdown, - GlDropdownItem, - GlDropdownText, - GlLoadingIcon, - GlSearchBoxByType, - }, - inject: ['fullPath'], - data() { - return { - projects: [], - search: '', - selectedProject: {}, - shouldSkipQuery: true, - }; - }, - apollo: { - projects: { - query: searchProjectsQuery, - variables() { - return { - fullPath: this.fullPath, - search: this.search, - }; - }, - update: ({ group }) => group.projects.nodes ?? [], - error(error) { - createFlash({ - message: __('An error occurred while loading projects.'), - captureError: true, - error, - }); - }, - skip() { - return this.shouldSkipQuery; - }, - debounce: DEBOUNCE_DELAY, - }, - }, - computed: { - dropdownHref() { - return this.hasSelectedProject - ? joinPaths(this.selectedProject.webUrl, DASH_SCOPE, 'issues/new') - : undefined; - }, - dropdownText() { - return this.hasSelectedProject - ? sprintf(__('New issue in %{project}'), { project: this.selectedProject.name }) - : this.$options.i18n.defaultDropdownText; - }, - hasSelectedProject() { - return this.selectedProject.id; - }, - projectsWithIssuesEnabled() { - return this.projects.filter((project) => project.issuesEnabled); - }, - showNoSearchResultsText() { - return !this.projectsWithIssuesEnabled.length && this.search; - }, - }, - methods: { - handleDropdownClick() { - if (!this.dropdownHref) { - this.$refs.dropdown.show(); - } - }, - handleDropdownShown() { - if (this.shouldSkipQuery) { - this.shouldSkipQuery = false; - } - this.$refs.search.focusInput(); - }, - selectProject(project) { - this.selectedProject = project; - }, - }, -}; -</script> - -<template> - <gl-dropdown - ref="dropdown" - right - split - :split-href="dropdownHref" - :text="dropdownText" - :toggle-text="$options.i18n.toggleButtonLabel" - variant="confirm" - @click="handleDropdownClick" - @shown="handleDropdownShown" - > - <gl-search-box-by-type ref="search" v-model.trim="search" /> - <gl-loading-icon v-if="$apollo.queries.projects.loading" /> - <template v-else> - <gl-dropdown-item - v-for="project of projectsWithIssuesEnabled" - :key="project.id" - @click="selectProject(project)" - > - {{ project.nameWithNamespace }} - </gl-dropdown-item> - <gl-dropdown-text v-if="showNoSearchResultsText"> - {{ $options.i18n.noMatchesFound }} - </gl-dropdown-text> - </template> - </gl-dropdown> -</template> |