Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/issues')
-rw-r--r--app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue116
-rw-r--r--app/assets/javascripts/issues/dashboard/index.js12
-rw-r--r--app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql12
-rw-r--r--app/assets/javascripts/issues/dashboard/utils.js23
-rw-r--r--app/assets/javascripts/issues/list/components/issues_list_app.vue1
-rw-r--r--app/assets/javascripts/issues/list/constants.js13
-rw-r--r--app/assets/javascripts/issues/list/utils.js12
-rw-r--r--app/assets/javascripts/issues/show/components/header_actions.vue22
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/constants.js13
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/create_timeline_event.vue1
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue2
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue70
-rw-r--r--app/assets/javascripts/issues/show/index.js6
13 files changed, 280 insertions, 23 deletions
diff --git a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
index b9d876ef72f..8edc9a08c9e 100644
--- a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
+++ b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
@@ -30,20 +30,35 @@ import { __ } from '~/locale';
import {
TOKEN_TITLE_ASSIGNEE,
TOKEN_TITLE_AUTHOR,
+ TOKEN_TITLE_LABEL,
+ TOKEN_TITLE_MILESTONE,
+ TOKEN_TITLE_MY_REACTION,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_MY_REACTION,
} from '~/vue_shared/components/filtered_search_bar/constants';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants';
+import { AutocompleteCache } from '../utils';
const UserToken = () => import('~/vue_shared/components/filtered_search_bar/tokens/user_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');
export default {
i18n: {
calendarButtonText: __('Subscribe to calendar'),
closed: __('CLOSED'),
closedMoved: __('CLOSED (MOVED)'),
- emptyStateTitle: __('Please select at least one filter to see results'),
+ emptyStateWithFilterTitle: __('Sorry, your filter produced no results'),
+ emptyStateWithFilterDescription: __('To widen your search, change or remove filters above'),
+ emptyStateWithoutFilterTitle: __('Please select at least one filter to see results'),
errorFetchingIssues: __('An error occurred while loading issues'),
rssButtonText: __('Subscribe to RSS feed'),
searchInputPlaceholder: __('Search or filter results...'),
@@ -60,8 +75,12 @@ export default {
GlTooltip: GlTooltipDirective,
},
inject: [
+ 'autocompleteAwardEmojisPath',
'calendarPath',
- 'emptyStateSvgPath',
+ 'dashboardLabelsPath',
+ 'dashboardMilestonesPath',
+ 'emptyStateWithFilterSvgPath',
+ 'emptyStateWithoutFilterSvgPath',
'hasBlockedIssuesFeature',
'hasIssuableHealthStatusFeature',
'hasIssueWeightsFeature',
@@ -117,6 +136,9 @@ export default {
this.issuesError = this.$options.i18n.errorFetchingIssues;
Sentry.captureException(error);
},
+ skip() {
+ return !this.hasSearch;
+ },
debounce: 200,
},
},
@@ -124,6 +146,25 @@ export default {
apiFilterParams() {
return convertToApiParams(this.filterTokens);
},
+ emptyStateDescription() {
+ return this.hasSearch ? this.$options.i18n.emptyStateWithFilterDescription : undefined;
+ },
+ emptyStateSvgPath() {
+ return this.hasSearch
+ ? this.emptyStateWithFilterSvgPath
+ : this.emptyStateWithoutFilterSvgPath;
+ },
+ emptyStateTitle() {
+ return this.hasSearch
+ ? this.$options.i18n.emptyStateWithFilterTitle
+ : this.$options.i18n.emptyStateWithoutFilterTitle;
+ },
+ hasSearch() {
+ return Boolean(this.searchQuery || Object.keys(this.urlFilterParams).length);
+ },
+ renderedIssues() {
+ return this.hasSearch ? this.issues : [];
+ },
searchQuery() {
return convertToSearchQuery(this.filterTokens);
},
@@ -159,12 +200,46 @@ export default {
preloadedUsers,
recentSuggestionsStorageKey: 'dashboard-issues-recent-tokens-author',
},
+ {
+ type: TOKEN_TYPE_LABEL,
+ title: TOKEN_TITLE_LABEL,
+ icon: 'labels',
+ token: LabelToken,
+ fetchLabels: this.fetchLabels,
+ recentSuggestionsStorageKey: 'dashboard-issues-recent-tokens-label',
+ },
+ {
+ type: TOKEN_TYPE_MILESTONE,
+ title: TOKEN_TITLE_MILESTONE,
+ icon: 'clock',
+ token: MilestoneToken,
+ fetchMilestones: this.fetchMilestones,
+ recentSuggestionsStorageKey: 'dashboard-issues-recent-tokens-milestone',
+ shouldSkipSort: true,
+ },
];
+ 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: 'dashboard-issues-recent-tokens-my_reaction',
+ });
+ }
+
+ tokens.sort((a, b) => a.title.localeCompare(b.title));
+
return tokens;
},
showPaginationControls() {
- return this.issues.length > 0 && (this.pageInfo.hasNextPage || this.pageInfo.hasPreviousPage);
+ return (
+ this.renderedIssues.length > 0 &&
+ (this.pageInfo.hasNextPage || this.pageInfo.hasPreviousPage)
+ );
},
sortOptions() {
return getSortOptions({
@@ -185,7 +260,34 @@ export default {
};
},
},
+ created() {
+ this.autocompleteCache = new AutocompleteCache();
+ },
methods: {
+ fetchEmojis(search) {
+ return this.autocompleteCache.fetch({
+ url: this.autocompleteAwardEmojisPath,
+ cacheName: 'emojis',
+ searchProperty: 'name',
+ search,
+ });
+ },
+ fetchLabels(search) {
+ return this.autocompleteCache.fetch({
+ url: this.dashboardLabelsPath,
+ cacheName: 'labels',
+ searchProperty: 'title',
+ search,
+ });
+ },
+ fetchMilestones(search) {
+ return this.autocompleteCache.fetch({
+ url: this.dashboardMilestonesPath,
+ cacheName: 'milestones',
+ searchProperty: 'title',
+ search,
+ });
+ },
fetchUsers(search) {
return axios.get('/-/autocomplete/users.json', { params: { active: true, search } });
},
@@ -266,7 +368,7 @@ export default {
:has-scoped-labels-feature="hasScopedLabelsFeature"
:initial-filter-value="filterTokens"
:initial-sort-by="sortKey"
- :issuables="issues"
+ :issuables="renderedIssues"
:issuables-loading="$apollo.queries.issues.loading"
namespace="dashboard"
recent-searches-storage-key="issues"
@@ -307,7 +409,11 @@ export default {
</template>
<template #empty-state>
- <gl-empty-state :svg-path="emptyStateSvgPath" :title="$options.i18n.emptyStateTitle" />
+ <gl-empty-state
+ :description="emptyStateDescription"
+ :svg-path="emptyStateSvgPath"
+ :title="emptyStateTitle"
+ />
</template>
</issuable-list>
</template>
diff --git a/app/assets/javascripts/issues/dashboard/index.js b/app/assets/javascripts/issues/dashboard/index.js
index e3e5cc614cb..005ab5ce3b0 100644
--- a/app/assets/javascripts/issues/dashboard/index.js
+++ b/app/assets/javascripts/issues/dashboard/index.js
@@ -14,8 +14,12 @@ export function mountIssuesDashboardApp() {
Vue.use(VueApollo);
const {
+ autocompleteAwardEmojisPath,
calendarPath,
- emptyStateSvgPath,
+ dashboardLabelsPath,
+ dashboardMilestonesPath,
+ emptyStateWithFilterSvgPath,
+ emptyStateWithoutFilterSvgPath,
hasBlockedIssuesFeature,
hasIssuableHealthStatusFeature,
hasIssueWeightsFeature,
@@ -33,8 +37,12 @@ export function mountIssuesDashboardApp() {
defaultClient: createDefaultClient(),
}),
provide: {
+ autocompleteAwardEmojisPath,
calendarPath,
- emptyStateSvgPath,
+ dashboardLabelsPath,
+ dashboardMilestonesPath,
+ emptyStateWithFilterSvgPath,
+ emptyStateWithoutFilterSvgPath,
hasBlockedIssuesFeature: parseBoolean(hasBlockedIssuesFeature),
hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
diff --git a/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql b/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql
index 8ffcb456755..43b8804108c 100644
--- a/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql
+++ b/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql
@@ -7,8 +7,14 @@ query getDashboardIssues(
$search: String
$sort: IssueSort
$state: IssuableState
+ $assigneeId: String
$assigneeUsernames: [String!]
$authorUsername: String
+ $labelName: [String]
+ $milestoneTitle: [String]
+ $milestoneWildcardId: MilestoneWildcardId
+ $myReactionEmoji: String
+ $not: NegatedIssueFilterInput
$afterCursor: String
$beforeCursor: String
$firstPageSize: Int
@@ -18,8 +24,14 @@ query getDashboardIssues(
search: $search
sort: $sort
state: $state
+ assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
+ labelName: $labelName
+ milestoneTitle: $milestoneTitle
+ milestoneWildcardId: $milestoneWildcardId
+ myReactionEmoji: $myReactionEmoji
+ not: $not
after: $afterCursor
before: $beforeCursor
first: $firstPageSize
diff --git a/app/assets/javascripts/issues/dashboard/utils.js b/app/assets/javascripts/issues/dashboard/utils.js
new file mode 100644
index 00000000000..6fa95b38649
--- /dev/null
+++ b/app/assets/javascripts/issues/dashboard/utils.js
@@ -0,0 +1,23 @@
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import { MAX_LIST_SIZE } from '~/issues/list/constants';
+import axios from '~/lib/utils/axios_utils';
+
+export class AutocompleteCache {
+ constructor() {
+ this.cache = {};
+ }
+
+ fetch({ url, cacheName, searchProperty, search }) {
+ if (this.cache[cacheName]) {
+ const data = search
+ ? fuzzaldrinPlus.filter(this.cache[cacheName], search, { key: searchProperty })
+ : this.cache[cacheName].slice(0, MAX_LIST_SIZE);
+ return Promise.resolve(data);
+ }
+
+ return axios.get(url).then(({ data }) => {
+ this.cache[cacheName] = data;
+ return data.slice(0, MAX_LIST_SIZE);
+ });
+ }
+}
diff --git a/app/assets/javascripts/issues/list/components/issues_list_app.vue b/app/assets/javascripts/issues/list/components/issues_list_app.vue
index 12a83f06453..e4000184f41 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -352,6 +352,7 @@ export default {
title: TOKEN_TITLE_LABEL,
icon: 'labels',
token: LabelToken,
+ operators: this.hasOrFeature ? OPERATORS_IS_NOT_OR : OPERATORS_IS_NOT,
fetchLabels: this.fetchLabels,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-label`,
},
diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js
index 49a953cad43..87184799d5f 100644
--- a/app/assets/javascripts/issues/list/constants.js
+++ b/app/assets/javascripts/issues/list/constants.js
@@ -159,7 +159,7 @@ export const TYPE_TOKEN_OBJECTIVE_OPTION = {
};
export const TYPE_TOKEN_KEY_RESULT_OPTION = {
- icon: 'issue-type-key-result',
+ icon: 'issue-type-keyresult',
title: 'key_result',
value: 'key_result',
};
@@ -247,6 +247,7 @@ export const filters = {
[API_PARAM]: {
[NORMAL_FILTER]: 'labelName',
[SPECIAL_FILTER]: 'labelName',
+ [ALTERNATIVE_FILTER]: 'labelNames',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
@@ -257,6 +258,9 @@ export const filters = {
[OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[label_name][]',
},
+ [OPERATOR_OR]: {
+ [ALTERNATIVE_FILTER]: 'or[label_name][]',
+ },
},
},
[TOKEN_TYPE_TYPE]: {
@@ -360,14 +364,17 @@ export const filters = {
},
[TOKEN_TYPE_HEALTH]: {
[API_PARAM]: {
- [NORMAL_FILTER]: 'healthStatus',
- [SPECIAL_FILTER]: 'healthStatus',
+ [NORMAL_FILTER]: 'healthStatusFilter',
+ [SPECIAL_FILTER]: 'healthStatusFilter',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'health_status',
[SPECIAL_FILTER]: 'health_status',
},
+ [OPERATOR_NOT]: {
+ [NORMAL_FILTER]: 'not[health_status]',
+ },
},
},
[TOKEN_TYPE_CONTACT]: {
diff --git a/app/assets/javascripts/issues/list/utils.js b/app/assets/javascripts/issues/list/utils.js
index b566e08731c..bbd081843ca 100644
--- a/app/assets/javascripts/issues/list/utils.js
+++ b/app/assets/javascripts/issues/list/utils.js
@@ -13,6 +13,8 @@ import {
TOKEN_TYPE_MILESTONE,
TOKEN_TYPE_RELEASE,
TOKEN_TYPE_TYPE,
+ TOKEN_TYPE_HEALTH,
+ TOKEN_TYPE_LABEL,
} from '~/vue_shared/components/filtered_search_bar/constants';
import {
ALTERNATIVE_FILTER,
@@ -252,8 +254,9 @@ const isSpecialFilter = (type, data) => {
const getFilterType = ({ type, value: { data, operator } }) => {
const isUnionedAuthor = type === TOKEN_TYPE_AUTHOR && operator === OPERATOR_OR;
+ const isUnionedLabel = type === TOKEN_TYPE_LABEL && operator === OPERATOR_OR;
- if (isUnionedAuthor) {
+ if (isUnionedAuthor || isUnionedLabel) {
return ALTERNATIVE_FILTER;
}
if (isSpecialFilter(type, data)) {
@@ -267,8 +270,13 @@ const wildcardTokens = [TOKEN_TYPE_ITERATION, TOKEN_TYPE_MILESTONE, TOKEN_TYPE_R
const isWildcardValue = (tokenType, value) =>
wildcardTokens.includes(tokenType) && specialFilterValues.includes(value);
+const isHealthStatusSpecialFilter = (tokenType, value) =>
+ tokenType === TOKEN_TYPE_HEALTH && specialFilterValues.includes(value);
+
const requiresUpperCaseValue = (tokenType, value) =>
- tokenType === TOKEN_TYPE_TYPE || isWildcardValue(tokenType, value);
+ tokenType === TOKEN_TYPE_TYPE ||
+ isWildcardValue(tokenType, value) ||
+ isHealthStatusSpecialFilter(tokenType, value);
const formatData = (token) => {
if (requiresUpperCaseValue(token.type, token.value.data)) {
diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue
index 983e2e6530e..56e360c75e3 100644
--- a/app/assets/javascripts/issues/show/components/header_actions.vue
+++ b/app/assets/javascripts/issues/show/components/header_actions.vue
@@ -19,6 +19,7 @@ import { visitUrl } from '~/lib/utils/url_utility';
import { s__, __, sprintf } from '~/locale';
import eventHub from '~/notes/event_hub';
import Tracking from '~/tracking';
+import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import promoteToEpicMutation from '../queries/promote_to_epic.mutation.graphql';
import updateIssueMutation from '../queries/update_issue.mutation.graphql';
import DeleteIssueModal from './delete_issue_modal.vue';
@@ -50,6 +51,7 @@ export default {
GlDropdownItem,
GlLink,
GlModal,
+ AbuseCategorySelector,
},
directives: {
GlModal: GlModalDirective,
@@ -93,13 +95,15 @@ export default {
projectPath: {
default: '',
},
- reportAbusePath: {
- default: '',
- },
submitAsSpamPath: {
default: '',
},
},
+ data() {
+ return {
+ isReportAbuseDrawerOpen: false,
+ };
+ },
computed: {
...mapState(['isToggleStateButtonLoading']),
...mapGetters(['openState', 'getBlockedByIssues']),
@@ -163,6 +167,9 @@ export default {
this.invokeUpdateIssueMutation();
},
+ toggleReportAbuseDrawer(isOpen) {
+ this.isReportAbuseDrawerOpen = isOpen;
+ },
invokeUpdateIssueMutation() {
this.toggleStateButtonLoading(true);
@@ -255,7 +262,7 @@ export default {
<gl-dropdown-item v-if="canPromoteToEpic" @click="promoteToEpic">
{{ __('Promote to epic') }}
</gl-dropdown-item>
- <gl-dropdown-item v-if="!isIssueAuthor" :href="reportAbusePath">
+ <gl-dropdown-item v-if="!isIssueAuthor" @click="toggleReportAbuseDrawer(true)">
{{ $options.i18n.reportAbuse }}
</gl-dropdown-item>
<gl-dropdown-item
@@ -314,7 +321,7 @@ export default {
>
{{ __('Promote to epic') }}
</gl-dropdown-item>
- <gl-dropdown-item v-if="!isIssueAuthor" :href="reportAbusePath">
+ <gl-dropdown-item v-if="!isIssueAuthor" @click="toggleReportAbuseDrawer(true)">
{{ $options.i18n.reportAbuse }}
</gl-dropdown-item>
<gl-dropdown-item
@@ -360,5 +367,10 @@ export default {
:modal-id="$options.deleteModalId"
:title="deleteButtonText"
/>
+
+ <abuse-category-selector
+ :show-drawer="isReportAbuseDrawerOpen"
+ @close-drawer="toggleReportAbuseDrawer(false)"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/issues/show/components/incidents/constants.js b/app/assets/javascripts/issues/show/components/incidents/constants.js
index 22db19610c1..2fdae538902 100644
--- a/app/assets/javascripts/issues/show/components/incidents/constants.js
+++ b/app/assets/javascripts/issues/show/components/incidents/constants.js
@@ -12,6 +12,9 @@ export const timelineFormI18n = Object.freeze({
'Incident|Something went wrong while creating the incident timeline event.',
),
areaPlaceholder: s__('Incident|Timeline text...'),
+ areaDefaultMessage: s__('Incident|Incident'),
+ selectTags: __('Select tags'),
+ tagsLabel: __('Event tag (optional)'),
save: __('Save'),
cancel: __('Cancel'),
delete: __('Delete'),
@@ -42,4 +45,14 @@ export const timelineItemI18n = Object.freeze({
timeUTC: __('%{time} UTC'),
});
+export const timelineEventTagsI18n = Object.freeze({
+ startTime: __('Start time'),
+ endTime: __('End time'),
+});
+
export const MAX_TEXT_LENGTH = 280;
+
+export const TIMELINE_EVENT_TAGS = Object.values(timelineEventTagsI18n).map((item) => ({
+ text: item,
+ value: item,
+}));
diff --git a/app/assets/javascripts/issues/show/components/incidents/create_timeline_event.vue b/app/assets/javascripts/issues/show/components/incidents/create_timeline_event.vue
index 6bb72e82778..81111d42b39 100644
--- a/app/assets/javascripts/issues/show/components/incidents/create_timeline_event.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/create_timeline_event.vue
@@ -74,6 +74,7 @@ export default {
incidentId: convertToGraphQLId(TYPE_ISSUE, this.issuableId),
note: eventDetails.note,
occurredAt: eventDetails.occurredAt,
+ timelineEventTagNames: eventDetails.timelineEventTags,
},
},
update: this.updateCache,
diff --git a/app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue b/app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue
index 8cdd62ca9ef..4ef9b9c5a99 100644
--- a/app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue
@@ -40,7 +40,7 @@ export default {
:is-event-processed="editTimelineEventActive"
:previous-occurred-at="event.occurredAt"
:previous-note="event.note"
- show-delete
+ is-editing
@save-event="saveEvent"
@cancel="$emit('hide-edit')"
@delete="$emit('delete')"
diff --git a/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue b/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
index f1a3aebc990..6648e20865d 100644
--- a/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
@@ -1,7 +1,9 @@
<script>
-import { GlDatepicker, GlFormInput, GlFormGroup, GlButton } from '@gitlab/ui';
+import { GlDatepicker, GlFormInput, GlFormGroup, GlButton, GlListbox } from '@gitlab/ui';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
-import { MAX_TEXT_LENGTH, timelineFormI18n } from './constants';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { __, sprintf } from '~/locale';
+import { MAX_TEXT_LENGTH, TIMELINE_EVENT_TAGS, timelineFormI18n } from './constants';
import { getUtcShiftedDate } from './utils';
export default {
@@ -23,7 +25,9 @@ export default {
GlFormInput,
GlFormGroup,
GlButton,
+ GlListbox,
},
+ mixins: [glFeatureFlagsMixin()],
i18n: timelineFormI18n,
MAX_TEXT_LENGTH,
props: {
@@ -32,7 +36,7 @@ export default {
required: false,
default: false,
},
- showDelete: {
+ isEditing: {
type: Boolean,
required: false,
default: false,
@@ -51,6 +55,16 @@ export default {
required: false,
default: '',
},
+ previousTags: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ tags: {
+ type: Array,
+ required: false,
+ default: () => TIMELINE_EVENT_TAGS,
+ },
},
data() {
// if occurredAt is null, returns "now" in UTC
@@ -58,10 +72,12 @@ export default {
return {
timelineText: this.previousNote,
+ timelineTextIsDirty: this.isEditing,
placeholderDate,
hourPickerInput: placeholderDate.getHours(),
minutePickerInput: placeholderDate.getMinutes(),
datePickerInput: placeholderDate,
+ selectedTags: [...this.previousTags],
};
},
computed: {
@@ -85,6 +101,20 @@ export default {
timelineTextCount() {
return this.timelineText.length;
},
+ dropdownText() {
+ if (!this.selectedTags.length) {
+ return timelineFormI18n.selectTags;
+ }
+
+ const dropdownText =
+ this.selectedTags.length === 1
+ ? this.selectedTags[0]
+ : sprintf(__('%{numberOfSelectedTags} tags'), {
+ numberOfSelectedTags: this.selectedTags.length,
+ });
+
+ return dropdownText;
+ },
},
mounted() {
this.focusDate();
@@ -96,14 +126,35 @@ export default {
this.hourPickerInput = newPlaceholderDate.getHours();
this.minutePickerInput = newPlaceholderDate.getMinutes();
this.timelineText = '';
+ this.selectedTags = [];
},
focusDate() {
this.$refs.datepicker.$el.querySelector('input')?.focus();
},
+ setTimelineTextDirty() {
+ this.timelineTextIsDirty = true;
+ },
+ onTagsChange(tagValue) {
+ this.selectedTags = [...tagValue];
+
+ if (!this.timelineTextIsDirty) {
+ this.timelineText = this.generateTimelineTextFromTags(this.selectedTags);
+ }
+ },
+ generateTimelineTextFromTags(tags) {
+ if (!tags.length) {
+ return '';
+ }
+
+ const tagsMessage = tags.map((tag) => tag.toLocaleLowerCase()).join(', ');
+
+ return `${timelineFormI18n.areaDefaultMessage} ${tagsMessage}`;
+ },
handleSave(addAnotherEvent) {
const event = {
note: this.timelineText,
occurredAt: this.occurredAtString,
+ timelineEventTags: this.selectedTags,
};
this.$emit('save-event', event, addAnotherEvent);
},
@@ -146,6 +197,16 @@ export default {
<p class="gl-ml-3 gl-align-self-end gl-line-height-32">{{ __('UTC') }}</p>
</div>
</div>
+ <gl-form-group v-if="glFeatures.incidentEventTags" :label="$options.i18n.tagsLabel">
+ <gl-listbox
+ :selected="selectedTags"
+ :toggle-text="dropdownText"
+ :items="tags"
+ :is-check-centered="true"
+ :multiple="true"
+ @select="onTagsChange"
+ />
+ </gl-form-group>
<div class="common-note-form">
<gl-form-group class="gl-mb-3" :label="$options.i18n.areaLabel">
<markdown-field
@@ -169,6 +230,7 @@ export default {
aria-describedby="timeline-form-hint"
:placeholder="$options.i18n.areaPlaceholder"
:maxlength="$options.MAX_TEXT_LENGTH"
+ @input="setTimelineTextDirty"
>
</textarea>
<div id="timeline-form-hint" class="gl-sr-only">{{ $options.i18n.hint }}</div>
@@ -214,7 +276,7 @@ export default {
{{ $options.i18n.cancel }}
</gl-button>
<gl-button
- v-if="showDelete"
+ v-if="isEditing"
class="gl-ml-auto btn-danger"
:disabled="isEventProcessed"
@click="$emit('delete')"
diff --git a/app/assets/javascripts/issues/show/index.js b/app/assets/javascripts/issues/show/index.js
index 3cb5007ab0d..21d877c5fe6 100644
--- a/app/assets/javascripts/issues/show/index.js
+++ b/app/assets/javascripts/issues/show/index.js
@@ -83,7 +83,7 @@ export function initIssueApp(issueData, store) {
return undefined;
}
- const { fullPath } = el.dataset;
+ const { fullPath, registerPath, signInPath } = el.dataset;
scrollToTargetOnResize();
@@ -99,6 +99,8 @@ export function initIssueApp(issueData, store) {
provide: {
canCreateIncident,
fullPath,
+ registerPath,
+ signInPath,
hasIssueWeightsFeature,
},
computed: {
@@ -150,6 +152,8 @@ export function initHeaderActions(store, type = '') {
projectPath: el.dataset.projectPath,
projectId: el.dataset.projectId,
reportAbusePath: el.dataset.reportAbusePath,
+ reportedUserId: el.dataset.reportedUserId,
+ reportedFromUrl: el.dataset.reportedFromUrl,
submitAsSpamPath: el.dataset.submitAsSpamPath,
},
render: (createElement) => createElement(HeaderActions),