diff options
Diffstat (limited to 'app/assets/javascripts/issues')
9 files changed, 109 insertions, 66 deletions
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 0b424d105b9..acb6aa93f0f 100644 --- a/app/assets/javascripts/issues/list/components/issues_list_app.vue +++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue @@ -247,8 +247,8 @@ export default { }, defaultWorkItemTypes() { return this.isWorkItemsEnabled - ? defaultWorkItemTypes.concat(WORK_ITEM_TYPE_ENUM_TASK) - : defaultWorkItemTypes; + ? defaultWorkItemTypes + : defaultWorkItemTypes.filter((type) => type !== WORK_ITEM_TYPE_ENUM_TASK); }, typeTokenOptions() { return this.isWorkItemsEnabled @@ -563,7 +563,8 @@ export default { if (!this.hasInitBulkEdit) { const bulkUpdateSidebar = await import('~/issuable/bulk_update_sidebar'); bulkUpdateSidebar.initBulkUpdateSidebar('issuable_'); - bulkUpdateSidebar.initIssueStatusSelect(); + bulkUpdateSidebar.initStatusDropdown(); + bulkUpdateSidebar.initSubscriptionsDropdown(); const usersSelect = await import('~/users_select'); const UsersSelect = usersSelect.default; diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js index 27738d7a3e6..9fe8899ab39 100644 --- a/app/assets/javascripts/issues/list/constants.js +++ b/app/assets/javascripts/issues/list/constants.js @@ -7,11 +7,13 @@ import { FILTER_UPCOMING, OPERATOR_IS, OPERATOR_IS_NOT, + TOKEN_TYPE_HEALTH, } from '~/vue_shared/components/filtered_search_bar/constants'; import { WORK_ITEM_TYPE_ENUM_INCIDENT, WORK_ITEM_TYPE_ENUM_ISSUE, WORK_ITEM_TYPE_ENUM_TEST_CASE, + WORK_ITEM_TYPE_ENUM_TASK, } from '~/work_items/constants'; export const i18n = { @@ -147,14 +149,16 @@ export const TOKEN_TYPE_EPIC = 'epic_id'; export const TOKEN_TYPE_WEIGHT = 'weight'; export const TOKEN_TYPE_CONTACT = 'crm_contact'; export const TOKEN_TYPE_ORGANIZATION = 'crm_organization'; -export const TOKEN_TYPE_HEALTH = 'health_status'; -export const TYPE_TOKEN_TASK_OPTION = { icon: 'task-done', title: 'task', value: 'task' }; +export const TYPE_TOKEN_TASK_OPTION = { icon: 'issue-type-task', title: 'task', value: 'task' }; +// This should be consistent with Issue::TYPES_FOR_LIST in the backend +// https://gitlab.com/gitlab-org/gitlab/-/blob/1379c2d7bffe2a8d809f23ac5ef9b4114f789c07/app/models/issue.rb#L48 export const defaultWorkItemTypes = [ WORK_ITEM_TYPE_ENUM_ISSUE, WORK_ITEM_TYPE_ENUM_INCIDENT, WORK_ITEM_TYPE_ENUM_TEST_CASE, + WORK_ITEM_TYPE_ENUM_TASK, ]; export const defaultTypeTokenOptions = [ @@ -327,10 +331,12 @@ export const filters = { [TOKEN_TYPE_HEALTH]: { [API_PARAM]: { [NORMAL_FILTER]: 'healthStatus', + [SPECIAL_FILTER]: 'healthStatus', }, [URL_PARAM]: { [OPERATOR_IS]: { [NORMAL_FILTER]: 'health_status', + [SPECIAL_FILTER]: 'health_status', }, }, }, diff --git a/app/assets/javascripts/issues/show/components/edited.vue b/app/assets/javascripts/issues/show/components/edited.vue index 4c5f783cd66..5138a4530e9 100644 --- a/app/assets/javascripts/issues/show/components/edited.vue +++ b/app/assets/javascripts/issues/show/components/edited.vue @@ -1,10 +1,11 @@ <script> -/* eslint-disable @gitlab/vue-require-i18n-strings */ +import { GlSprintf } from '@gitlab/ui'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; export default { components: { TimeAgoTooltip, + GlSprintf, }, props: { updatedAt: { @@ -33,13 +34,27 @@ export default { <template> <small class="edited-text js-issue-widgets"> - Edited - <time-ago-tooltip v-if="updatedAt" :time="updatedAt" tooltip-placement="bottom" /> - <span v-if="hasUpdatedBy"> - by - <a :href="updatedByPath" class="author-link"> - <span>{{ updatedByName }}</span> - </a> - </span> + <gl-sprintf v-if="!hasUpdatedBy" :message="__('Edited %{timeago}')"> + <template #timeago> + <time-ago-tooltip :time="updatedAt" tooltip-placement="bottom" /> + </template> + </gl-sprintf> + <gl-sprintf v-else-if="!updatedAt" :message="__('Edited by %{author}')"> + <template #author> + <a :href="updatedByPath" class="author-link gl-hover-text-decoration-underline"> + <span>{{ updatedByName }}</span> + </a> + </template> + </gl-sprintf> + <gl-sprintf v-else :message="__('Edited %{timeago} by %{author}')"> + <template #timeago> + <time-ago-tooltip :time="updatedAt" tooltip-placement="bottom" /> + </template> + <template #author> + <a :href="updatedByPath" class="author-link gl-hover-text-decoration-underline"> + <span>{{ updatedByName }}</span> + </a> + </template> + </gl-sprintf> </small> </template> diff --git a/app/assets/javascripts/issues/show/components/fields/description.vue b/app/assets/javascripts/issues/show/components/fields/description.vue index c2ab7c4f298..dbe634e7295 100644 --- a/app/assets/javascripts/issues/show/components/fields/description.vue +++ b/app/assets/javascripts/issues/show/components/fields/description.vue @@ -1,13 +1,16 @@ <script> import MarkdownField from '~/vue_shared/components/markdown/field.vue'; import { helpPagePath } from '~/helpers/help_page_helper'; +import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue'; +import glFeaturesFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import updateMixin from '../../mixins/update'; export default { components: { MarkdownField, + MarkdownEditor, }, - mixins: [updateMixin], + mixins: [updateMixin, glFeaturesFlagMixin()], props: { value: { type: String, @@ -38,7 +41,12 @@ export default { }, }, mounted() { - this.$refs.textarea.focus(); + this.focus(); + }, + methods: { + focus() { + this.$refs.textarea?.focus(); + }, }, }; </script> @@ -46,7 +54,26 @@ export default { <template> <div class="common-note-form"> <label class="sr-only" for="issue-description">{{ __('Description') }}</label> + <markdown-editor + v-if="glFeatures.contentEditorOnIssues" + class="gl-mt-3" + :value="value" + :render-markdown-path="markdownPreviewPath" + :markdown-docs-path="markdownDocsPath" + :form-field-aria-label="__('Description')" + :form-field-placeholder="__('Write a comment or drag your files hereā¦')" + form-field-id="issue-description" + form-field-name="issue-description" + :quick-actions-docs-path="quickActionsDocsPath" + :enable-autocomplete="enableAutocomplete" + supports-quick-actions + init-on-autofocus + @input="$emit('input', $event)" + @keydown.meta.enter="updateIssuable" + @keydown.ctrl.enter="updateIssuable" + /> <markdown-field + v-else :markdown-preview-path="markdownPreviewPath" :markdown-docs-path="markdownDocsPath" :quick-actions-docs-path="quickActionsDocsPath" diff --git a/app/assets/javascripts/issues/show/components/form.vue b/app/assets/javascripts/issues/show/components/form.vue index f479c8ae78d..0c6b61fb893 100644 --- a/app/assets/javascripts/issues/show/components/form.vue +++ b/app/assets/javascripts/issues/show/components/form.vue @@ -1,7 +1,6 @@ <script> import { GlAlert } from '@gitlab/ui'; -import $ from 'jquery'; -import Autosave from '~/autosave'; +import { getDraft, updateDraft, getLockVersion, clearDraft } from '~/lib/utils/autosave'; import { IssuableType } from '~/issues/constants'; import eventHub from '../event_hub'; import EditActions from './edit_actions.vue'; @@ -76,10 +75,17 @@ export default { }, }, data() { + const autosaveKey = [document.location.pathname, document.location.search]; + const descriptionAutosaveKey = [...autosaveKey, 'description']; + const titleAutosaveKey = [...autosaveKey, 'title']; + return { + titleAutosaveKey, + descriptionAutosaveKey, + autosaveReset: false, formData: { - title: this.formState.title, - description: this.formState.description, + title: getDraft(titleAutosaveKey) || this.formState.title, + description: getDraft(descriptionAutosaveKey) || this.formState.description, }, showOutdatedDescriptionWarning: false, }; @@ -118,58 +124,40 @@ export default { }, methods: { initAutosave() { - const { - description: { - $refs: { textarea }, - }, - title: { - $refs: { input }, - }, - } = this.$refs; - - this.autosaveDescription = new Autosave( - $(textarea), - [document.location.pathname, document.location.search, 'description'], - null, - this.formState.lock_version, - ); - - const savedLockVersion = this.autosaveDescription.getSavedLockVersion(); + const savedLockVersion = getLockVersion(this.descriptionAutosaveKey); this.showOutdatedDescriptionWarning = savedLockVersion && String(this.formState.lock_version) !== savedLockVersion; - - this.autosaveTitle = new Autosave($(input), [ - document.location.pathname, - document.location.search, - 'title', - ]); }, resetAutosave() { - this.autosaveDescription.reset(); - this.autosaveTitle.reset(); + this.autosaveReset = true; + clearDraft(this.descriptionAutosaveKey); + clearDraft(this.titleAutosaveKey); }, keepAutosave() { - const { - description: { - $refs: { textarea }, - }, - } = this.$refs; - - textarea.focus(); + this.$refs.description.focus(); this.showOutdatedDescriptionWarning = false; }, discardAutosave() { - const { - description: { - $refs: { textarea }, - }, - } = this.$refs; - - textarea.value = this.initialDescriptionText; - textarea.focus(); + this.formData.description = this.initialDescriptionText; + clearDraft(this.descriptionAutosaveKey); + this.$refs.description.focus(); this.showOutdatedDescriptionWarning = false; }, + updateTitleDraft(title) { + updateDraft(this.titleAutosaveKey, title); + }, + updateDescriptionDraft(description) { + /* + * This conditional statement prevents a race-condition + * between clearing the draft and submitting a new draft + * update while the user is typing. It happens when saving + * using the cmd + enter keyboard shortcut. + */ + if (!this.autosaveReset) { + updateDraft(this.descriptionAutosaveKey, description, this.formState.lock_version); + } + }, }, }; </script> @@ -194,7 +182,7 @@ export default { > <div class="row gl-mb-3"> <div class="col-12"> - <issuable-title-field ref="title" v-model="formData.title" /> + <issuable-title-field ref="title" v-model="formData.title" @input="updateTitleDraft" /> </div> </div> <div class="row"> @@ -220,6 +208,7 @@ export default { :markdown-docs-path="markdownDocsPath" :can-attach-file="canAttachFile" :enable-autocomplete="enableAutocomplete" + @input="updateDescriptionDraft" /> <edit-actions :endpoint="endpoint" :form-state="formState" :issuable-type="issuableType" /> diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue index adf449aca7b..74d166f82bb 100644 --- a/app/assets/javascripts/issues/show/components/header_actions.vue +++ b/app/assets/javascripts/issues/show/components/header_actions.vue @@ -229,7 +229,7 @@ export default { </script> <template> - <div class="detail-page-header-actions gl-display-flex"> + <div class="detail-page-header-actions gl-display-flex gl-align-self-start"> <gl-dropdown v-if="hasMobileDropdown" class="gl-sm-display-none! w-100" diff --git a/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue b/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue index dd84a1d7d67..5725d0f8d6a 100644 --- a/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue +++ b/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue @@ -52,9 +52,6 @@ export default { loading() { return this.$apollo.queries.alert.loading; }, - incidentTabEnabled() { - return this.glFeatures.incidentTimeline; - }, }, mounted() { this.trackPageViews(); @@ -112,7 +109,7 @@ export default { > <alert-details-table :alert="alert" :loading="loading" /> </gl-tab> - <timeline-tab v-if="incidentTabEnabled" /> + <timeline-tab /> </gl-tabs> </div> </template> 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 b7ae18372ab..55cd8b5f606 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 @@ -74,6 +74,9 @@ export default { return utcDate.toISOString(); }, + hasTimelineText() { + return this.timelineText.length > 0; + }, }, mounted() { this.focusDate(); @@ -167,6 +170,8 @@ export default { variant="confirm" category="primary" class="gl-mr-3" + data-testid="save-button" + :disabled="!hasTimelineText" :loading="isEventProcessed" @click="handleSave(false)" > @@ -177,6 +182,8 @@ export default { variant="confirm" category="secondary" class="gl-mr-3 gl-ml-n2" + data-testid="save-and-add-button" + :disabled="!hasTimelineText" :loading="isEventProcessed" @click="handleSave(true)" > diff --git a/app/assets/javascripts/issues/show/index.js b/app/assets/javascripts/issues/show/index.js index e5eed9f6b79..3cb5007ab0d 100644 --- a/app/assets/javascripts/issues/show/index.js +++ b/app/assets/javascripts/issues/show/index.js @@ -60,6 +60,7 @@ export function initIncidentApp(issueData = {}) { projectId, slaFeatureAvailable: parseBoolean(slaFeatureAvailable), uploadMetricsFeatureAvailable: parseBoolean(uploadMetricsFeatureAvailable), + contentEditorOnIssues: gon.features.contentEditorOnIssues, }, render(createElement) { return createElement(IssueApp, { |