summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/issue_show
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/issue_show')
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue62
-rw-r--r--app/assets/javascripts/issue_show/components/description.vue8
-rw-r--r--app/assets/javascripts/issue_show/components/edit_actions.vue138
-rw-r--r--app/assets/javascripts/issue_show/components/fields/description_template.vue4
-rw-r--r--app/assets/javascripts/issue_show/components/fields/title.vue2
-rw-r--r--app/assets/javascripts/issue_show/components/fields/type.vue79
-rw-r--r--app/assets/javascripts/issue_show/components/form.vue48
-rw-r--r--app/assets/javascripts/issue_show/constants.js12
-rw-r--r--app/assets/javascripts/issue_show/graphql.js9
-rw-r--r--app/assets/javascripts/issue_show/incident.js22
-rw-r--r--app/assets/javascripts/issue_show/issue.js31
-rw-r--r--app/assets/javascripts/issue_show/queries/get_issue_state.query.graphql3
-rw-r--r--app/assets/javascripts/issue_show/queries/update_issue_state.mutation.graphql3
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
+}