summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/issuables_list
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/issuables_list')
-rw-r--r--app/assets/javascripts/issuables_list/components/issuable.vue121
-rw-r--r--app/assets/javascripts/issuables_list/components/issuable_list_root_app.vue52
-rw-r--r--app/assets/javascripts/issuables_list/components/issuables_list_app.vue187
-rw-r--r--app/assets/javascripts/issuables_list/constants.js23
-rw-r--r--app/assets/javascripts/issuables_list/index.js2
-rw-r--r--app/assets/javascripts/issuables_list/queries/get_issues_list_details.query.graphql5
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">
&middot;
- <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
}
}
}