diff options
Diffstat (limited to 'app/assets/javascripts/issue_show')
13 files changed, 324 insertions, 97 deletions
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index 01b4e81a11a..b7e24a8b17e 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -1,12 +1,20 @@ <script> import { GlIcon, GlIntersectionObserver } from '@gitlab/ui'; import Visibility from 'visibilityjs'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import Poll from '~/lib/utils/poll'; import { visitUrl } from '~/lib/utils/url_utility'; import { __, s__, sprintf } from '~/locale'; -import { IssuableStatus, IssuableStatusText, IssuableType } from '../constants'; +import { + IssuableStatus, + IssuableStatusText, + IssuableType, + IssueTypePath, + IncidentTypePath, + IncidentType, +} from '../constants'; import eventHub from '../event_hub'; +import getIssueStateQuery from '../queries/get_issue_state.query.graphql'; import Service from '../services/index'; import Store from '../stores'; import descriptionComponent from './description.vue'; @@ -195,8 +203,14 @@ export default { showForm: false, templatesRequested: false, isStickyHeaderShowing: false, + issueState: {}, }; }, + apollo: { + issueState: { + query: getIssueStateQuery, + }, + }, computed: { issuableTemplates() { return this.store.formState.issuableTemplates; @@ -288,7 +302,7 @@ export default { methods: { handleBeforeUnloadEvent(e) { const event = e; - if (this.showForm && this.issueChanged) { + if (this.showForm && this.issueChanged && !this.issueState.isDirty) { event.returnValue = __('Are you sure you want to lose your issue information?'); } return undefined; @@ -302,7 +316,9 @@ export default { this.store.updateState(data); }) .catch(() => { - createFlash(this.defaultErrorMessage); + createFlash({ + message: this.defaultErrorMessage, + }); }); }, @@ -327,7 +343,9 @@ export default { this.updateAndShowForm(res.data); }) .catch(() => { - createFlash(this.defaultErrorMessage); + createFlash({ + message: this.defaultErrorMessage, + }); this.updateAndShowForm(); }); }, @@ -346,14 +364,32 @@ export default { }, updateIssuable() { + const { + store: { formState }, + issueState, + } = this; + const issuablePayload = issueState.isDirty + ? { ...formState, issue_type: issueState.issueType } + : formState; this.clearFlash(); return this.service - .updateIssuable(this.store.formState) + .updateIssuable(issuablePayload) .then((res) => res.data) .then((data) => { - if (!window.location.pathname.includes(data.web_url)) { + if ( + !window.location.pathname.includes(data.web_url) && + issueState.issueType !== IncidentType + ) { visitUrl(data.web_url); } + + if (issueState.isDirty) { + const URI = + issueState.issueType === IncidentType + ? data.web_url.replace(IssueTypePath, IncidentTypePath) + : data.web_url; + visitUrl(URI); + } }) .then(this.updateStoreState) .then(() => { @@ -374,7 +410,9 @@ export default { errMsg += `. ${message}`; } - this.flashContainer = createFlash(errMsg); + this.flashContainer = createFlash({ + message: errMsg, + }); }); }, @@ -389,9 +427,11 @@ export default { visitUrl(data.web_url); }) .catch(() => { - createFlash( - sprintf(s__('Error deleting %{issuableType}'), { issuableType: this.issuableType }), - ); + createFlash({ + message: sprintf(s__('Error deleting %{issuableType}'), { + issuableType: this.issuableType, + }), + }); }); }, diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue index 68bc6fe4c0e..0812392f804 100644 --- a/app/assets/javascripts/issue_show/components/description.vue +++ b/app/assets/javascripts/issue_show/components/description.vue @@ -1,7 +1,7 @@ <script> import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import $ from 'jquery'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import { s__, sprintf } from '~/locale'; import TaskList from '../../task_list'; import animateMixin from '../mixins/animate'; @@ -92,8 +92,8 @@ export default { }, taskListUpdateError() { - createFlash( - sprintf( + createFlash({ + message: sprintf( s__( 'Someone edited this %{issueType} at the same time you did. The description has been updated and you will need to make your changes again.', ), @@ -101,7 +101,7 @@ export default { issueType: this.issuableType, }, ), - ); + }); this.$emit('taskListUpdateFailed'); }, diff --git a/app/assets/javascripts/issue_show/components/edit_actions.vue b/app/assets/javascripts/issue_show/components/edit_actions.vue index 7733e366c4f..5b7d232fde7 100644 --- a/app/assets/javascripts/issue_show/components/edit_actions.vue +++ b/app/assets/javascripts/issue_show/components/edit_actions.vue @@ -1,17 +1,24 @@ <script> -import { GlButton } from '@gitlab/ui'; +import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui'; +import { uniqueId } from 'lodash'; import { __, sprintf } from '~/locale'; import eventHub from '../event_hub'; import updateMixin from '../mixins/update'; +import getIssueStateQuery from '../queries/get_issue_state.query.graphql'; const issuableTypes = { issue: __('Issue'), epic: __('Epic'), + incident: __('Incident'), }; export default { components: { GlButton, + GlModal, + }, + directives: { + GlModal: GlModalDirective, }, mixins: [updateMixin], props: { @@ -36,19 +43,56 @@ export default { data() { return { deleteLoading: false, + skipApollo: false, + issueState: {}, + modalId: uniqueId('delete-issuable-modal-'), }; }, + apollo: { + issueState: { + query: getIssueStateQuery, + skip() { + return this.skipApollo; + }, + result() { + this.skipApollo = true; + }, + }, + }, computed: { + deleteIssuableButtonText() { + return sprintf(__('Delete %{issuableType}'), { + issuableType: this.typeToShow.toLowerCase(), + }); + }, + deleteIssuableModalText() { + return this.issuableType === 'epic' + ? __('Delete this epic and all descendants?') + : sprintf(__('%{issuableType} will be removed! Are you sure?'), { + issuableType: this.typeToShow, + }); + }, isSubmitEnabled() { return this.formState.title.trim() !== ''; }, + modalActionProps() { + return { + primary: { + text: this.deleteIssuableButtonText, + attributes: [{ variant: 'danger' }, { loading: this.deleteLoading }], + }, + cancel: { + text: __('Cancel'), + }, + }; + }, shouldShowDeleteButton() { return this.canDestroy && this.showDeleteButton; }, - deleteIssuableButtonText() { - return sprintf(__('Delete %{issuableType}'), { - issuableType: issuableTypes[this.issuableType].toLowerCase(), - }); + typeToShow() { + const { issueState, issuableType } = this; + const type = issueState.issueType ?? issuableType; + return issuableTypes[type]; }, }, methods: { @@ -56,49 +100,57 @@ export default { eventHub.$emit('close.form'); }, deleteIssuable() { - const confirmMessage = - this.issuableType === 'epic' - ? __('Delete this epic and all descendants?') - : sprintf(__('%{issuableType} will be removed! Are you sure?'), { - issuableType: issuableTypes[this.issuableType], - }); - // eslint-disable-next-line no-alert - if (window.confirm(confirmMessage)) { - this.deleteLoading = true; - - eventHub.$emit('delete.issuable', { destroy_confirm: true }); - } + this.deleteLoading = true; + eventHub.$emit('delete.issuable', { destroy_confirm: true }); }, }, }; </script> <template> - <div class="gl-mt-3 gl-mb-3 clearfix"> - <gl-button - :loading="formState.updateLoading" - :disabled="formState.updateLoading || !isSubmitEnabled" - category="primary" - variant="confirm" - class="float-left qa-save-button gl-mr-3" - type="submit" - @click.prevent="updateIssuable" - > - {{ __('Save changes') }} - </gl-button> - <gl-button @click="closeForm"> - {{ __('Cancel') }} - </gl-button> - <gl-button - v-if="shouldShowDeleteButton" - :loading="deleteLoading" - :disabled="deleteLoading" - category="secondary" - variant="danger" - class="float-right qa-delete-button" - @click="deleteIssuable" - > - {{ deleteIssuableButtonText }} - </gl-button> + <div class="gl-mt-3 gl-mb-3 gl-display-flex gl-justify-content-space-between"> + <div> + <gl-button + :loading="formState.updateLoading" + :disabled="formState.updateLoading || !isSubmitEnabled" + category="primary" + variant="confirm" + class="qa-save-button gl-mr-3" + data-testid="issuable-save-button" + type="submit" + @click.prevent="updateIssuable" + > + {{ __('Save changes') }} + </gl-button> + <gl-button data-testid="issuable-cancel-button" @click="closeForm"> + {{ __('Cancel') }} + </gl-button> + </div> + <div v-if="shouldShowDeleteButton"> + <gl-button + v-gl-modal="modalId" + :loading="deleteLoading" + :disabled="deleteLoading" + category="secondary" + variant="danger" + class="qa-delete-button" + data-testid="issuable-delete-button" + > + {{ deleteIssuableButtonText }} + </gl-button> + <gl-modal + ref="removeModal" + :modal-id="modalId" + size="sm" + :action-primary="modalActionProps.primary" + :action-cancel="modalActionProps.cancel" + @primary="deleteIssuable" + > + <template #modal-title>{{ deleteIssuableButtonText }}</template> + <div> + <p class="gl-mb-1">{{ deleteIssuableModalText }}</p> + </div> + </gl-modal> + </div> </div> </template> diff --git a/app/assets/javascripts/issue_show/components/fields/description_template.vue b/app/assets/javascripts/issue_show/components/fields/description_template.vue index 14df87e486b..9bfdbb41e23 100644 --- a/app/assets/javascripts/issue_show/components/fields/description_template.vue +++ b/app/assets/javascripts/issue_show/components/fields/description_template.vue @@ -54,14 +54,14 @@ export default { <template> <!-- eslint-disable @gitlab/vue-no-data-toggle --> - <div class="dropdown js-issuable-selector-wrap" data-issuable-type="issues"> + <div class="dropdown js-issuable-selector-wrap gl-mb-0" data-issuable-type="issues"> <button ref="toggle" :data-namespace-path="projectNamespace" :data-project-path="projectPath" :data-project-id="projectId" :data-data="issuableTemplatesJson" - class="dropdown-menu-toggle js-issuable-selector" + class="dropdown-menu-toggle js-issuable-selector gl-button" type="button" data-field-name="issuable_template" data-selected="null" diff --git a/app/assets/javascripts/issue_show/components/fields/title.vue b/app/assets/javascripts/issue_show/components/fields/title.vue index d331fb47077..a73926575d0 100644 --- a/app/assets/javascripts/issue_show/components/fields/title.vue +++ b/app/assets/javascripts/issue_show/components/fields/title.vue @@ -20,7 +20,7 @@ export default { id="issuable-title" ref="input" v-model="formState.title" - class="form-control qa-title-input" + class="form-control qa-title-input gl-border-gray-200" dir="auto" type="text" :placeholder="__('Title')" diff --git a/app/assets/javascripts/issue_show/components/fields/type.vue b/app/assets/javascripts/issue_show/components/fields/type.vue new file mode 100644 index 00000000000..1ed222531f4 --- /dev/null +++ b/app/assets/javascripts/issue_show/components/fields/type.vue @@ -0,0 +1,79 @@ +<script> +import { GlFormGroup, GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { capitalize } from 'lodash'; +import { __ } from '~/locale'; +import { IssuableTypes } from '../../constants'; +import getIssueStateQuery from '../../queries/get_issue_state.query.graphql'; +import updateIssueStateMutation from '../../queries/update_issue_state.mutation.graphql'; + +export const i18n = { + label: __('Issue Type'), +}; + +export default { + i18n, + IssuableTypes, + components: { + GlFormGroup, + GlDropdown, + GlDropdownItem, + }, + data() { + return { + issueState: {}, + }; + }, + apollo: { + issueState: { + query: getIssueStateQuery, + }, + }, + computed: { + dropdownText() { + const { + issueState: { issueType }, + } = this; + return capitalize(issueType); + }, + }, + methods: { + updateIssueType(issueType) { + this.$apollo.mutate({ + mutation: updateIssueStateMutation, + variables: { + issueType, + isDirty: true, + }, + }); + }, + }, +}; +</script> + +<template> + <gl-form-group + :label="$options.i18n.label" + label-class="sr-only" + label-for="issuable-type" + class="mb-2 mb-md-0" + > + <gl-dropdown + id="issuable-type" + :aria-labelledby="$options.i18n.label" + :text="dropdownText" + :header-text="$options.i18n.label" + class="gl-w-full" + toggle-class="dropdown-menu-toggle" + > + <gl-dropdown-item + v-for="type in $options.IssuableTypes" + :key="type.value" + :is-checked="issueState.issueType === type.value" + is-check-item + @click="updateIssueType(type.value)" + > + {{ type.text }} + </gl-dropdown-item> + </gl-dropdown> + </gl-form-group> +</template> diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue index b37a911a669..bdaa8a4dd6b 100644 --- a/app/assets/javascripts/issue_show/components/form.vue +++ b/app/assets/javascripts/issue_show/components/form.vue @@ -2,21 +2,24 @@ import { GlAlert } from '@gitlab/ui'; import $ from 'jquery'; import Autosave from '~/autosave'; +import { IssuableType } from '~/issue_show/constants'; import eventHub from '../event_hub'; -import editActions from './edit_actions.vue'; -import descriptionField from './fields/description.vue'; -import descriptionTemplate from './fields/description_template.vue'; -import titleField from './fields/title.vue'; -import lockedWarning from './locked_warning.vue'; +import EditActions from './edit_actions.vue'; +import DescriptionField from './fields/description.vue'; +import DescriptionTemplateField from './fields/description_template.vue'; +import IssuableTitleField from './fields/title.vue'; +import IssuableTypeField from './fields/type.vue'; +import LockedWarning from './locked_warning.vue'; export default { components: { - lockedWarning, - titleField, - descriptionField, - descriptionTemplate, - editActions, + DescriptionField, + DescriptionTemplateField, + EditActions, GlAlert, + IssuableTitleField, + IssuableTypeField, + LockedWarning, }, props: { canDestroy: { @@ -89,6 +92,9 @@ export default { showLockedWarning() { return this.formState.lockedWarningVisible && !this.formState.updateLoading; }, + isIssueType() { + return this.issuableType === IssuableType.Issue; + }, }, created() { eventHub.$on('delete.issuable', this.resetAutosave); @@ -162,7 +168,7 @@ export default { </script> <template> - <form> + <form data-testid="issuable-form"> <locked-warning v-if="showLockedWarning" /> <gl-alert v-if="showOutdatedDescriptionWarning" @@ -179,9 +185,17 @@ export default { ) }}</gl-alert > + <div class="row gl-mb-3"> + <div class="col-12"> + <issuable-title-field ref="title" :form-state="formState" /> + </div> + </div> <div class="row"> - <div v-if="hasIssuableTemplates" class="col-sm-4 col-lg-3"> - <description-template + <div v-if="isIssueType" class="col-12 col-md-4 pr-md-0"> + <issuable-type-field ref="issue-type" /> + </div> + <div v-if="hasIssuableTemplates" class="col-12 col-md-4 pl-md-2"> + <description-template-field :form-state="formState" :issuable-templates="issuableTemplates" :project-path="projectPath" @@ -189,14 +203,6 @@ export default { :project-namespace="projectNamespace" /> </div> - <div - :class="{ - 'col-sm-8 col-lg-9': hasIssuableTemplates, - 'col-12': !hasIssuableTemplates, - }" - > - <title-field ref="title" :form-state="formState" :issuable-templates="issuableTemplates" /> - </div> </div> <description-field ref="description" diff --git a/app/assets/javascripts/issue_show/constants.js b/app/assets/javascripts/issue_show/constants.js index a5ca91dffd4..d93f38c2ee1 100644 --- a/app/assets/javascripts/issue_show/constants.js +++ b/app/assets/javascripts/issue_show/constants.js @@ -16,6 +16,7 @@ export const IssuableType = { Issue: 'issue', Epic: 'epic', MergeRequest: 'merge_request', + Alert: 'alert', }; export const IssueStateEvent = { @@ -25,3 +26,14 @@ export const IssueStateEvent = { export const STATUS_PAGE_PUBLISHED = __('Published on status page'); export const JOIN_ZOOM_MEETING = __('Join Zoom meeting'); + +export const IssuableTypes = [ + { value: 'issue', text: __('Issue') }, + { value: 'incident', text: __('Incident') }, +]; + +export const IssueTypePath = 'issues'; +export const IncidentTypePath = 'issues/incident'; +export const IncidentType = 'incident'; + +export const issueState = { issueType: undefined, isDirty: false }; diff --git a/app/assets/javascripts/issue_show/graphql.js b/app/assets/javascripts/issue_show/graphql.js new file mode 100644 index 00000000000..5b8630f7d63 --- /dev/null +++ b/app/assets/javascripts/issue_show/graphql.js @@ -0,0 +1,9 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import { defaultClient } from '~/sidebar/graphql'; + +Vue.use(VueApollo); + +export default new VueApollo({ + defaultClient, +}); diff --git a/app/assets/javascripts/issue_show/incident.js b/app/assets/javascripts/issue_show/incident.js index 0c81ecdc843..df986195656 100644 --- a/app/assets/javascripts/issue_show/incident.js +++ b/app/assets/javascripts/issue_show/incident.js @@ -1,15 +1,23 @@ import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import createDefaultClient from '~/lib/graphql'; import { parseBoolean } from '~/lib/utils/common_utils'; import issuableApp from './components/app.vue'; import incidentTabs from './components/incidents/incident_tabs.vue'; - -Vue.use(VueApollo); +import { issueState } from './constants'; +import apolloProvider from './graphql'; +import getIssueStateQuery from './queries/get_issue_state.query.graphql'; export default function initIssuableApp(issuableData = {}) { - const apolloProvider = new VueApollo({ - defaultClient: createDefaultClient(), + const el = document.getElementById('js-issuable-app'); + + if (!el) { + return undefined; + } + + apolloProvider.clients.defaultClient.cache.writeQuery({ + query: getIssueStateQuery, + data: { + issueState: { ...issueState, issueType: el.dataset.issueType }, + }, }); const { @@ -25,7 +33,7 @@ export default function initIssuableApp(issuableData = {}) { const fullPath = `${projectNamespace}/${projectPath}`; return new Vue({ - el: document.getElementById('js-issuable-app'), + el, apolloProvider, components: { issuableApp, diff --git a/app/assets/javascripts/issue_show/issue.js b/app/assets/javascripts/issue_show/issue.js index a93abbf64df..4374dba6eb7 100644 --- a/app/assets/javascripts/issue_show/issue.js +++ b/app/assets/javascripts/issue_show/issue.js @@ -1,14 +1,33 @@ import Vue from 'vue'; -import VueApollo from 'vue-apollo'; import { mapGetters } from 'vuex'; -import createDefaultClient from '~/lib/graphql'; import { parseBoolean } from '~/lib/utils/common_utils'; import IssuableApp from './components/app.vue'; import HeaderActions from './components/header_actions.vue'; +import { issueState } from './constants'; +import apolloProvider from './graphql'; +import getIssueStateQuery from './queries/get_issue_state.query.graphql'; + +const bootstrapApollo = (state = {}) => { + return apolloProvider.clients.defaultClient.cache.writeQuery({ + query: getIssueStateQuery, + data: { + issueState: state, + }, + }); +}; export function initIssuableApp(issuableData, store) { + const el = document.getElementById('js-issuable-app'); + + if (!el) { + return undefined; + } + + bootstrapApollo({ ...issueState, issueType: el.dataset.issueType }); + return new Vue({ - el: document.getElementById('js-issuable-app'), + el, + apolloProvider, store, computed: { ...mapGetters(['getNoteableData']), @@ -33,11 +52,7 @@ export function initIssueHeaderActions(store) { return undefined; } - Vue.use(VueApollo); - - const apolloProvider = new VueApollo({ - defaultClient: createDefaultClient(), - }); + bootstrapApollo({ ...issueState, issueType: el.dataset.issueType }); return new Vue({ el, diff --git a/app/assets/javascripts/issue_show/queries/get_issue_state.query.graphql b/app/assets/javascripts/issue_show/queries/get_issue_state.query.graphql new file mode 100644 index 00000000000..33b737d2315 --- /dev/null +++ b/app/assets/javascripts/issue_show/queries/get_issue_state.query.graphql @@ -0,0 +1,3 @@ +query issueState { + issueState @client +} diff --git a/app/assets/javascripts/issue_show/queries/update_issue_state.mutation.graphql b/app/assets/javascripts/issue_show/queries/update_issue_state.mutation.graphql new file mode 100644 index 00000000000..d91ca746066 --- /dev/null +++ b/app/assets/javascripts/issue_show/queries/update_issue_state.mutation.graphql @@ -0,0 +1,3 @@ +mutation updateIssueState($issueType: String, $isDirty: Boolean) { + updateIssueState(issueType: $issueType, isDirty: $isDirty) @client +} |