diff options
Diffstat (limited to 'app/assets/javascripts/issue_show/components')
12 files changed, 1021 insertions, 42 deletions
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue index 770a0dcd27e..a6c62ee0f7f 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -1,10 +1,18 @@ <script> +/* global Flash */ import Visibility from 'visibilityjs'; import Poll from '../../lib/utils/poll'; +import eventHub from '../event_hub'; import Service from '../services/index'; import Store from '../stores'; import titleComponent from './title.vue'; import descriptionComponent from './description.vue'; +import editedComponent from './edited.vue'; +<<<<<<< HEAD +import formComponent from './form.vue'; +import '../../lib/utils/url_utility'; +======= +>>>>>>> 07c984d... Port fix-realtime-edited-text-for-issues 9-2-stable fix to master. export default { props: { @@ -12,15 +20,27 @@ export default { required: true, type: String, }, + canMove: { + required: true, + type: Boolean, + }, canUpdate: { required: true, type: Boolean, }, + canDestroy: { + required: true, + type: Boolean, + }, issuableRef: { type: String, required: true, }, - initialTitle: { + initialTitleHtml: { + type: String, + required: true, + }, + initialTitleText: { type: String, required: true, }, @@ -34,30 +54,208 @@ export default { required: false, default: '', }, + updatedAt: { + type: String, + required: false, + default: '', + }, + updatedByName: { + type: String, + required: false, + default: '', + }, + updatedByPath: { + type: String, + required: false, + default: '', + }, +<<<<<<< HEAD + issuableTemplates: { + type: Array, + required: false, + default: () => [], + }, + isConfidential: { + type: Boolean, + required: true, + }, + markdownPreviewUrl: { + type: String, + required: true, + }, + markdownDocs: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: true, + }, + projectNamespace: { + type: String, + required: true, + }, + projectsAutocompleteUrl: { + type: String, + required: true, + }, +======= +>>>>>>> 07c984d... Port fix-realtime-edited-text-for-issues 9-2-stable fix to master. }, data() { const store = new Store({ - titleHtml: this.initialTitle, + titleHtml: this.initialTitleHtml, + titleText: this.initialTitleText, descriptionHtml: this.initialDescriptionHtml, descriptionText: this.initialDescriptionText, + updatedAt: this.updatedAt, + updatedByName: this.updatedByName, + updatedByPath: this.updatedByPath, }); return { store, state: store.state, + showForm: false, }; }, + computed: { + formState() { + return this.store.formState; + }, + hasUpdated() { + return !!this.state.updatedAt; + }, + }, components: { descriptionComponent, titleComponent, + editedComponent, +<<<<<<< HEAD + formComponent, + }, + methods: { + openForm() { + if (!this.showForm) { + this.showForm = true; +<<<<<<< HEAD +<<<<<<< HEAD +======= +>>>>>>> df7c901... fixed issuable shortcuts not working + this.store.setFormState({ + title: this.state.titleText, + confidential: this.isConfidential, + description: this.state.descriptionText, + lockedWarningVisible: false, + move_to_project_id: 0, + updateLoading: false, +<<<<<<< HEAD +======= + this.store.formState = Object.assign(this.store.formState, { + title: this.state.titleText, + confidential: this.isConfidential, + description: this.state.descriptionText, +>>>>>>> 6a14a51... Show warning if realtime data has changed since the form has opened +======= +>>>>>>> 6becf28... use formState to update loading of save button + }); + } + }, + closeForm() { + this.showForm = false; + }, + updateIssuable() { + const canPostUpdate = this.store.formState.move_to_project_id !== 0 ? + confirm('Are you sure you want to move this issue to another project?') : true; // eslint-disable-line no-alert + + if (!canPostUpdate) { +<<<<<<< HEAD +<<<<<<< HEAD +<<<<<<< HEAD + this.store.setFormState({ + updateLoading: false, + }); +======= + eventHub.$emit('enable.submit.btn'); +>>>>>>> 3ce6658... Warn before moving issue in inline edit form +======= + this.store.formState.updateLoading = false; +>>>>>>> 6becf28... use formState to update loading of save button +======= + this.store.setFormState({ + updateLoading: false, + }); +>>>>>>> df7c901... fixed issuable shortcuts not working + return; + } + + this.service.updateIssuable(this.store.formState) + .then(res => res.json()) + .then((data) => { +<<<<<<< HEAD +<<<<<<< HEAD + if (location.pathname !== data.web_url) { + gl.utils.visitUrl(data.web_url); +======= + if (location.pathname !== data.path) { + gl.utils.visitUrl(data.path); +>>>>>>> 1c46358... Removed un-used CSS +======= + if (location.pathname !== data.web_url) { + gl.utils.visitUrl(data.web_url); +>>>>>>> 625d344... renamed path to web_url in endpoint + } else if (data.confidential !== this.isConfidential) { + gl.utils.visitUrl(location.pathname); + } + + return this.service.getData(); + }) + .then(res => res.json()) + .then((data) => { + this.store.updateState(data); + eventHub.$emit('close.form'); + }) + .catch(() => { + eventHub.$emit('close.form'); + return new Flash('Error updating issue'); + }); + }, + deleteIssuable() { + this.service.deleteIssuable() + .then(res => res.json()) + .then((data) => { + // Stop the poll so we don't get 404's with the issue not existing + this.poll.stop(); + + gl.utils.visitUrl(data.web_url); + }) + .catch(() => { + eventHub.$emit('close.form'); + return new Flash('Error deleting issue'); + }); + }, +======= +>>>>>>> 07c984d... Port fix-realtime-edited-text-for-issues 9-2-stable fix to master. + }, + computed: { + hasUpdated() { + return !!this.state.updatedAt; + }, }, created() { - const resource = new Service(this.endpoint); - const poll = new Poll({ - resource, + this.service = new Service(this.endpoint); + this.poll = new Poll({ + resource: this.service, method: 'getData', successCallback: (res) => { - this.store.updateState(res.json()); + const data = res.json(); + const shouldUpdate = this.store.stateShouldUpdate(data); + + this.store.updateState(data); + + if (this.showForm && (shouldUpdate.title || shouldUpdate.description)) { + this.store.formState.lockedWarningVisible = true; + } }, errorCallback(err) { throw new Error(err); @@ -65,22 +263,65 @@ export default { }); if (!Visibility.hidden()) { - poll.makeRequest(); + this.poll.makeRequest(); } Visibility.change(() => { if (!Visibility.hidden()) { - poll.restart(); + this.poll.restart(); } else { - poll.stop(); + this.poll.stop(); } }); + + eventHub.$on('delete.issuable', this.deleteIssuable); + eventHub.$on('update.issuable', this.updateIssuable); + eventHub.$on('close.form', this.closeForm); + eventHub.$on('open.form', this.openForm); + }, + beforeDestroy() { + eventHub.$off('delete.issuable', this.deleteIssuable); + eventHub.$off('update.issuable', this.updateIssuable); + eventHub.$off('close.form', this.closeForm); + eventHub.$off('open.form', this.openForm); }, }; </script> <template> <div> +<<<<<<< HEAD + <form-component + v-if="canUpdate && showForm" + :form-state="formState" + :can-move="canMove" + :can-destroy="canDestroy" + :issuable-templates="issuableTemplates" + :markdown-docs="markdownDocs" + :markdown-preview-url="markdownPreviewUrl" + :project-path="projectPath" + :project-namespace="projectNamespace" + :projects-autocomplete-url="projectsAutocompleteUrl" + /> + <div v-else> + <title-component + :issuable-ref="issuableRef" + :title-html="state.titleHtml" + :title-text="state.titleText" /> + <description-component + v-if="state.descriptionHtml" + :can-update="canUpdate" + :description-html="state.descriptionHtml" + :description-text="state.descriptionText" + :updated-at="state.updatedAt" + :task-status="state.taskStatus" /> + <edited-component + v-if="hasUpdated" + :updated-at="state.updatedAt" + :updated-by-name="state.updatedByName" + :updated-by-path="state.updatedByPath" /> + </div> +======= <title-component :issuable-ref="issuableRef" :title-html="state.titleHtml" @@ -90,7 +331,13 @@ export default { :can-update="canUpdate" :description-html="state.descriptionHtml" :description-text="state.descriptionText" - :updated-at="state.updatedAt" :task-status="state.taskStatus" /> + <edited-component + v-if="hasUpdated" + :updated-at="state.updatedAt" + :updated-by-name="state.updatedByName" + :updated-by-path="state.updatedByPath" + /> +>>>>>>> 07c984d... Port fix-realtime-edited-text-for-issues 9-2-stable fix to master. </div> </template> diff --git a/app/assets/javascripts/issue_show/components/description.vue b/app/assets/javascripts/issue_show/components/description.vue index 4ad3eb7dfd7..1fe3df28872 100644 --- a/app/assets/javascripts/issue_show/components/description.vue +++ b/app/assets/javascripts/issue_show/components/description.vue @@ -1,5 +1,6 @@ <script> import animateMixin from '../mixins/animate'; + import descriptionField from './fields/description.vue'; export default { mixins: [animateMixin], @@ -8,41 +9,53 @@ type: Boolean, required: true, }, - descriptionHtml: { - type: String, + store: { + type: Object, required: true, }, - descriptionText: { - type: String, + showForm: { + type: Boolean, required: true, }, - updatedAt: { +<<<<<<< HEAD + markdownPreviewUrl: { type: String, required: true, }, + markdownDocs: { +======= taskStatus: { +>>>>>>> 07c984d... Port fix-realtime-edited-text-for-issues 9-2-stable fix to master. type: String, required: true, }, }, data() { return { + state: this.store.state, preAnimation: false, pulseAnimation: false, - timeAgoEl: $('.js-issue-edited-ago'), }; }, + computed: { + descriptionHtml() { + return this.state.descriptionHtml; + }, + descriptionText() { + return this.state.descriptionText; + }, + updatedAt() { + return this.state.updated_at; + }, + taskStatus() { + return this.state.taskStatus; + }, + }, watch: { descriptionHtml() { this.animateChange(); this.$nextTick(() => { - const toolTipTime = gl.utils.formatDate(this.updatedAt); - - this.timeAgoEl.attr('datetime', this.updatedAt) - .attr('title', toolTipTime) - .tooltip('fixTitle'); - this.renderGFM(); }); }, @@ -75,6 +88,9 @@ } }, }, + components: { + descriptionField, + }, mounted() { this.renderGFM(); }, @@ -82,24 +98,41 @@ </script> <template> +<<<<<<< HEAD + <div :class="{ 'common-note-form': showForm }"> + <description-field + v-if="showForm" + :store="store" + :markdown-preview-url="markdownPreviewUrl" + :markdown-docs="markdownDocs" /> +======= <div + v-if="descriptionHtml" class="description" :class="{ 'js-task-list-container': canUpdate }"> +>>>>>>> f53d703... Fixed else-if in description <div - class="wiki" + v-else-if="descriptionHtml" + class="description" :class="{ - 'issue-realtime-pre-pulse': preAnimation, - 'issue-realtime-trigger-pulse': pulseAnimation - }" - v-html="descriptionHtml" - ref="gfm-content"> + 'js-task-list-container': canUpdate + }"> + <div + class="wiki" + :class="{ + 'issue-realtime-pre-pulse': preAnimation, + 'issue-realtime-trigger-pulse': pulseAnimation + }" + v-html="descriptionHtml" + ref="gfm-content"> + </div> + <textarea + class="hidden js-task-list-field" + v-if="descriptionText" + v-model="descriptionText"> + </textarea> </div> - <textarea - class="hidden js-task-list-field" - v-if="descriptionText" - v-model="descriptionText"> - </textarea> </div> </template> diff --git a/app/assets/javascripts/issue_show/components/edit_actions.vue b/app/assets/javascripts/issue_show/components/edit_actions.vue new file mode 100644 index 00000000000..e810fba6308 --- /dev/null +++ b/app/assets/javascripts/issue_show/components/edit_actions.vue @@ -0,0 +1,87 @@ +<script> + import updateMixin from '../mixins/update'; + import eventHub from '../event_hub'; + + export default { + mixins: [updateMixin], + props: { + canDestroy: { + type: Boolean, + required: true, + }, + formState: { + type: Object, + required: true, + }, + }, + data() { + return { + deleteLoading: false, + }; + }, + computed: { + isSubmitEnabled() { + return this.formState.title.trim() !== ''; + }, + }, + methods: { + closeForm() { + eventHub.$emit('close.form'); + }, + deleteIssuable() { + // eslint-disable-next-line no-alert + if (confirm('Issue will be removed! Are you sure?')) { + this.deleteLoading = true; + + eventHub.$emit('delete.issuable'); + } + }, + }, + }; +</script> + +<template> + <div class="prepend-top-default append-bottom-default clearfix"> + <button + class="btn btn-save pull-left" + :class="{ disabled: formState.updateLoading || !isSubmitEnabled }" + type="submit" +<<<<<<< HEAD +<<<<<<< HEAD + :disabled="formState.updateLoading || !isSubmitEnabled" +======= + :disabled="updateLoading || !isSubmitEnabled" +>>>>>>> 04178d7... fixed move issue specs +======= + :disabled="formState.updateLoading || !isSubmitEnabled" +>>>>>>> 5227ea5... fixed up weird karma specs with tooltip + @click.prevent="updateIssuable"> + Save changes + <i + class="fa fa-spinner fa-spin" + aria-hidden="true" + v-if="formState.updateLoading"> + </i> + </button> + <button + class="btn btn-default pull-right" + type="button" + @click="closeForm"> + Cancel + </button> + <button + v-if="canDestroy" + class="btn btn-danger pull-right append-right-default" + :class="{ disabled: deleteLoading }" + type="button" + :disabled="deleteLoading" + @click="deleteIssuable"> + Delete + <i + class="fa fa-spinner fa-spin" + aria-hidden="true" + v-if="deleteLoading"> + </i> + </button> + </div> +</template> diff --git a/app/assets/javascripts/issue_show/components/edited.vue b/app/assets/javascripts/issue_show/components/edited.vue new file mode 100644 index 00000000000..d59e6d11032 --- /dev/null +++ b/app/assets/javascripts/issue_show/components/edited.vue @@ -0,0 +1,56 @@ +<script> +import timeAgoTooltip from '../../vue_shared/components/time_ago_tooltip.vue'; + +export default { + props: { + updatedAt: { + type: String, + required: false, + default: '', + }, + updatedByName: { + type: String, + required: false, + default: '', + }, + updatedByPath: { + type: String, + required: false, + default: '', + }, + }, + components: { + timeAgoTooltip, + }, + computed: { + hasUpdatedBy() { + return this.updatedByName && this.updatedByPath; + }, + }, +}; +</script> + +<template> + <small + class="edited-text" + > + Edited + <time-ago-tooltip + v-if="updatedAt" + placement="bottom" + :time="updatedAt" + /> + <span + v-if="hasUpdatedBy" + > + by + <a + class="author_link" + :href="updatedByPath" + > + <span>{{updatedByName}}</span> + </a> + </span> + </small> +</template> + diff --git a/app/assets/javascripts/issue_show/components/fields/confidential_checkbox.vue b/app/assets/javascripts/issue_show/components/fields/confidential_checkbox.vue new file mode 100644 index 00000000000..a0ff08e9111 --- /dev/null +++ b/app/assets/javascripts/issue_show/components/fields/confidential_checkbox.vue @@ -0,0 +1,23 @@ +<script> + export default { + props: { + formState: { + type: Object, + required: true, + }, + }, + }; +</script> + +<template> + <fieldset class="checkbox"> + <label for="issue-confidential"> + <input + type="checkbox" + value="1" + id="issue-confidential" + v-model="formState.confidential" /> + This issue is confidential and should only be visible to team members with at least Reporter access. + </label> + </fieldset> +</template> diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue new file mode 100644 index 00000000000..0d4a39cc4ac --- /dev/null +++ b/app/assets/javascripts/issue_show/components/fields/description.vue @@ -0,0 +1,72 @@ +<script> + /* global Flash */ + import updateMixin from '../../mixins/update'; + import markdownField from '../../../vue_shared/components/markdown/field.vue'; + + export default { + mixins: [updateMixin], + props: { + store: { + type: Object, + required: true, + }, + markdownPreviewUrl: { + type: String, + required: true, + }, + markdownDocs: { + type: String, + required: true, + }, + }, + data() { + return { + state: this.store.formState, + }; + }, + components: { + markdownField, + }, + mounted() { + this.$refs.textarea.focus(); + }, + }; +</script> + +<template> + <div> + <label + class="sr-only" + for="issue-description"> + Description + </label> + <markdown-field + :markdown-preview-url="markdownPreviewUrl" + :markdown-docs="markdownDocs"> + <textarea + id="issue-description" + class="note-textarea js-gfm-input js-autosize markdown-area" + data-supports-slash-commands="false" + aria-label="Description" +<<<<<<< HEAD + v-model="state.description" + ref="textatea" +======= + v-model="formState.description" + ref="textarea" +<<<<<<< HEAD +>>>>>>> 2927802... Focus the description field in the inline form when mounted + slot="textarea"> +======= + slot="textarea" +<<<<<<< HEAD + placeholder="Write a comment or drag your files here..."> +>>>>>>> e20654f... fixed GFM references not working +======= + placeholder="Write a comment or drag your files here..." + @keydown.meta.enter="updateIssuable"> +>>>>>>> 29fd130... enable meta+enter shortcut to quick submit + </textarea> + </markdown-field> + </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 new file mode 100644 index 00000000000..c679616cca6 --- /dev/null +++ b/app/assets/javascripts/issue_show/components/fields/description_template.vue @@ -0,0 +1,111 @@ +<script> + export default { + props: { + formState: { + type: Object, + required: true, + }, + issuableTemplates: { + type: Array, + required: false, + default: () => [], + }, + projectPath: { + type: String, + required: true, + }, + projectNamespace: { + type: String, + required: true, + }, + }, + computed: { + issuableTemplatesJson() { + return JSON.stringify(this.issuableTemplates); + }, + }, + mounted() { + // Create the editor for the template + const editor = document.querySelector('.detail-page-description .note-textarea'); + editor.setValue = (val) => { + this.formState.description = val; + }; + editor.getValue = () => this.formState.description; + + this.issuableTemplate = new gl.IssuableTemplateSelectors({ + $dropdowns: $(this.$refs.toggle), + editor, + }); + }, + }; +</script> + +<template> + <div + class="dropdown js-issuable-selector-wrap" + data-issuable-type="issue"> + <button + class="dropdown-menu-toggle js-issuable-selector" + type="button" + ref="toggle" + data-field-name="issuable_template" + data-selected="null" + data-toggle="dropdown" + :data-namespace-path="projectNamespace" + :data-project-path="projectPath" + :data-data="issuableTemplatesJson"> + <span class="dropdown-toggle-text"> + Choose a template + </span> + <i + aria-hidden="true" + class="fa fa-chevron-down"> + </i> + </button> + <div class="dropdown-menu dropdown-select"> + <div class="dropdown-title"> + Choose a template + <button + class="dropdown-title-button dropdown-menu-close" + aria-label="Close" + type="button"> + <i + aria-hidden="true" + class="fa fa-times dropdown-menu-close-icon"> + </i> + </button> + </div> + <div class="dropdown-input"> + <input + type="search" + class="dropdown-input-field" + placeholder="Filter" + autocomplete="off" /> + <i + aria-hidden="true" + class="fa fa-search dropdown-input-search"> + </i> + <i + role="button" + aria-label="Clear templates search input" + class="fa fa-times dropdown-input-clear js-dropdown-input-clear"> + </i> + </div> + <div class="dropdown-content"></div> + <div class="dropdown-footer"> + <ul class="dropdown-footer-list"> + <li> + <a class="no-template"> + No template + </a> + </li> + <li> + <a class="reset-template"> + Reset template + </a> + </li> + </ul> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/issue_show/components/fields/project_move.vue b/app/assets/javascripts/issue_show/components/fields/project_move.vue new file mode 100644 index 00000000000..f811fb0de24 --- /dev/null +++ b/app/assets/javascripts/issue_show/components/fields/project_move.vue @@ -0,0 +1,83 @@ +<script> + import tooltipMixin from '../../../vue_shared/mixins/tooltip'; + + export default { + mixins: [ + tooltipMixin, + ], + props: { + formState: { + type: Object, + required: true, + }, + projectsAutocompleteUrl: { + type: String, + required: true, + }, + }, + mounted() { + const $moveDropdown = $(this.$refs['move-dropdown']); + + $moveDropdown.select2({ + ajax: { + url: this.projectsAutocompleteUrl, + quietMillis: 125, + data(term, page, context) { + return { + search: term, + offset_id: context, + }; + }, + results(data) { + const more = data.length >= 50; + const context = data[data.length - 1] ? data[data.length - 1].id : null; + + return { + results: data, + more, + context, + }; + }, + }, + formatResult(project) { + return project.name_with_namespace; + }, + formatSelection(project) { + return project.name_with_namespace; + }, + }) + .on('change', (e) => { + this.formState.move_to_project_id = parseInt(e.target.value, 10); + }); + }, + beforeDestroy() { + $(this.$refs['move-dropdown']).select2('destroy'); + }, + }; +</script> + +<template> + <fieldset> + <label + for="issuable-move" + class="sr-only"> + Move + </label> + <div class="issuable-form-select-holder append-right-5"> + <input + ref="move-dropdown" + type="hidden" + id="issuable-move" + data-placeholder="Move to a different project" /> + </div> + <span + data-placement="auto top" + title="Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location." + ref="tooltip"> + <i + class="fa fa-question-circle" + aria-hidden="true"> + </i> + </span> + </fieldset> +</template> diff --git a/app/assets/javascripts/issue_show/components/fields/title.vue b/app/assets/javascripts/issue_show/components/fields/title.vue new file mode 100644 index 00000000000..b0d8a6628dd --- /dev/null +++ b/app/assets/javascripts/issue_show/components/fields/title.vue @@ -0,0 +1,94 @@ +<script> +<<<<<<< HEAD +<<<<<<< HEAD + import descriptionTemplate from './template.vue'; +======= + import updateMixin from '../../mixins/update'; +>>>>>>> 29fd130... enable meta+enter shortcut to quick submit + + export default { + mixins: [updateMixin], + props: { + formState: { + type: Object, + required: true, + }, + issuableTemplates: { + type: Array, + required: false, + default: () => [], + }, + }, + components: { + descriptionTemplate, + }, + computed: { + hasIssuableTemplates() { + return this.issuableTemplates.length !== 0; + }, +======= + export default { + props: { + store: { + type: Object, + required: true, + }, + }, + data() { + return { + state: this.store.formState, + }; +>>>>>>> b1affe0... Issue inline edit title field + }, + }; +</script> + +<template> +<<<<<<< HEAD + <fieldset class="row"> + <div + class="col-sm-4 col-lg-3" + v-if="hasIssuableTemplates"> + <description-template + :issuable-templates="issuableTemplates" /> + </div> + <div + :class="{ + 'col-sm-8 col-lg-9': hasIssuableTemplates, + 'col-xs-12': !hasIssuableTemplates, + }"> + <label + class="sr-only" + for="issue-title"> + Title + </label> + <input + id="issue-title" + class="form-control" + type="text" + placeholder="Issue title" + aria-label="Issue title" + v-model="formState.title" /> + </div> +======= + <fieldset> + <label + class="sr-only" + for="issue-title"> + Title + </label> + <input + id="issue-title" + class="form-control" + type="text" + placeholder="Issue title" + aria-label="Issue title" +<<<<<<< HEAD + v-model="state.title" /> +>>>>>>> b1affe0... Issue inline edit title field +======= + v-model="formState.title" + @keydown.meta.enter="updateIssuable" /> +>>>>>>> 29fd130... enable meta+enter shortcut to quick submit + </fieldset> +</template> diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue new file mode 100644 index 00000000000..d7e812b7f7f --- /dev/null +++ b/app/assets/javascripts/issue_show/components/form.vue @@ -0,0 +1,135 @@ +<script> +<<<<<<< HEAD +<<<<<<< HEAD +======= + import eventHub from '../event_hub'; +>>>>>>> 6a14a51... Show warning if realtime data has changed since the form has opened +======= +>>>>>>> 982ab87... Added specs for testing when warning is visible + import lockedWarning from './locked_warning.vue'; + import titleField from './fields/title.vue'; + import descriptionField from './fields/description.vue'; + import editActions from './edit_actions.vue'; + import descriptionTemplate from './fields/description_template.vue'; + import projectMove from './fields/project_move.vue'; + import confidentialCheckbox from './fields/confidential_checkbox.vue'; + + export default { + props: { + canMove: { + type: Boolean, + required: true, + }, + canDestroy: { + type: Boolean, + required: true, + }, + formState: { + type: Object, + required: true, + }, + issuableTemplates: { + type: Array, + required: false, + default: () => [], + }, + markdownPreviewUrl: { + type: String, + required: true, + }, + markdownDocs: { + type: String, + required: true, + }, + projectPath: { + type: String, + required: true, + }, + projectNamespace: { + type: String, + required: true, + }, + projectsAutocompleteUrl: { + type: String, + required: true, + }, + }, + components: { + lockedWarning, + titleField, + descriptionField, + descriptionTemplate, + editActions, + projectMove, + confidentialCheckbox, + }, +<<<<<<< HEAD +<<<<<<< HEAD + computed: { + hasIssuableTemplates() { + return this.issuableTemplates.length; +======= + methods: { + closeForm() { + eventHub.$emit('close.form'); + this.formState.lockedWarningVisible = false; +>>>>>>> 6a14a51... Show warning if realtime data has changed since the form has opened + }, + }, +======= +>>>>>>> 982ab87... Added specs for testing when warning is visible + }; +</script> + +<template> + <form> +<<<<<<< HEAD +<<<<<<< HEAD + <locked-warning v-if="formState.lockedWarningVisible" /> + <div class="row"> + <div + class="col-sm-4 col-lg-3" + v-if="hasIssuableTemplates"> + <description-template + :form-state="formState" + :issuable-templates="issuableTemplates" + :project-path="projectPath" + :project-namespace="projectNamespace" /> + </div> + <div + :class="{ + 'col-sm-8 col-lg-9': hasIssuableTemplates, + 'col-xs-12': !hasIssuableTemplates, + }"> + <title-field + :form-state="formState" + :issuable-templates="issuableTemplates" /> + </div> + </div> +======= + <locked-warning + v-if="formState.lockedWarningVisible" + @closeForm="closeForm" /> +======= + <locked-warning v-if="formState.lockedWarningVisible" /> +>>>>>>> 982ab87... Added specs for testing when warning is visible + <title-field + :form-state="formState" /> + <confidential-checkbox + :form-state="formState" /> +>>>>>>> 6a14a51... Show warning if realtime data has changed since the form has opened + <description-field + :form-state="formState" + :markdown-preview-url="markdownPreviewUrl" + :markdown-docs="markdownDocs" /> + <confidential-checkbox + :form-state="formState" /> + <project-move + v-if="canMove" + :form-state="formState" + :projects-autocomplete-url="projectsAutocompleteUrl" /> + <edit-actions + :form-state="formState" + :can-destroy="canDestroy" /> + </form> +</template> diff --git a/app/assets/javascripts/issue_show/components/locked_warning.vue b/app/assets/javascripts/issue_show/components/locked_warning.vue new file mode 100644 index 00000000000..1c2789f154a --- /dev/null +++ b/app/assets/javascripts/issue_show/components/locked_warning.vue @@ -0,0 +1,20 @@ +<script> + export default { + computed: { + currentPath() { + return location.pathname; + }, + }, + }; +</script> + +<template> + <div class="alert alert-danger"> + Someone edited the issue at the same time you did. Please check out + <a + :href="currentPath" + target="_blank" + rel="nofollow">the issue</a> + and make sure your changes will not unintentionally remove theirs. + </div> +</template> diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue index a9dabd4cff1..a61ce414891 100644 --- a/app/assets/javascripts/issue_show/components/title.vue +++ b/app/assets/javascripts/issue_show/components/title.vue @@ -1,8 +1,12 @@ <script> import animateMixin from '../mixins/animate'; + import titleField from './fields/title.vue'; export default { mixins: [animateMixin], + components: { + titleField, + }, data() { return { preAnimation: false, @@ -23,6 +27,14 @@ type: String, required: true, }, + store: { + type: Object, + required: true, + }, + showForm: { + type: Boolean, + required: true, + }, }, watch: { titleHtml() { @@ -41,13 +53,19 @@ </script> <template> - <h2 - class="title" - :class="{ - 'issue-realtime-pre-pulse': preAnimation, - 'issue-realtime-trigger-pulse': pulseAnimation - }" - v-html="titleHtml" - > - </h2> + <div> + <title-field + v-if="showForm" + :store="store" /> + <h2 + v-else + class="title" + :class="{ + 'issue-realtime-pre-pulse': preAnimation, + 'issue-realtime-trigger-pulse': pulseAnimation + }" + v-html="titleHtml" + > + </h2> + </div> </template> |