diff options
Diffstat (limited to 'app/assets/javascripts/related_issues/components/related_issues_root.vue')
-rw-r--r-- | app/assets/javascripts/related_issues/components/related_issues_root.vue | 247 |
1 files changed, 247 insertions, 0 deletions
diff --git a/app/assets/javascripts/related_issues/components/related_issues_root.vue b/app/assets/javascripts/related_issues/components/related_issues_root.vue new file mode 100644 index 00000000000..6f68b25b6fb --- /dev/null +++ b/app/assets/javascripts/related_issues/components/related_issues_root.vue @@ -0,0 +1,247 @@ +<script> +/* +`rawReferences` are separated by spaces. +Given `abc 123 zxc`, `rawReferences = ['abc', '123', 'zxc']` + +Consider you are typing `abc 123 zxc` in the input and your caret position is +at position 4 right before the `123` `rawReference`. Then you type `#` and +it becomes a valid reference, `#123`, but we don't want to jump it straight into +`pendingReferences` because you could still want to type. Say you typed `999` +and now we have `#999123`. Only when you move your caret away from that `rawReference` +do we actually put it in the `pendingReferences`. + +Your caret can stop touching a `rawReference` can happen in a variety of ways: + + - As you type, we only tokenize after you type a space or move with the arrow keys + - On blur, we consider your caret not touching anything + +--- + + - When you click the "Add related issues"(in the `AddIssuableForm`), + we submit the `pendingReferences` to the server and they come back as actual `relatedIssues` + - When you click the "Cancel"(in the `AddIssuableForm`), we clear out `pendingReferences` + and hide the `AddIssuableForm` area. + +*/ +import { deprecatedCreateFlash as Flash } from '~/flash'; +import { __ } from '~/locale'; +import RelatedIssuesBlock from './related_issues_block.vue'; +import RelatedIssuesStore from '../stores/related_issues_store'; +import RelatedIssuesService from '../services/related_issues_service'; +import { + relatedIssuesRemoveErrorMap, + pathIndeterminateErrorMap, + addRelatedIssueErrorMap, + issuableTypesMap, + PathIdSeparator, +} from '../constants'; + +export default { + name: 'RelatedIssuesRoot', + components: { + relatedIssuesBlock: RelatedIssuesBlock, + }, + props: { + endpoint: { + type: String, + required: true, + }, + canAdmin: { + type: Boolean, + required: false, + default: false, + }, + canReorder: { + type: Boolean, + required: false, + default: false, + }, + helpPath: { + type: String, + required: false, + default: '', + }, + issuableType: { + type: String, + required: false, + default: issuableTypesMap.ISSUE, + }, + allowAutoComplete: { + type: Boolean, + required: false, + default: true, + }, + pathIdSeparator: { + type: String, + required: false, + default: PathIdSeparator.Issue, + }, + cssClass: { + type: String, + required: false, + default: '', + }, + showCategorizedIssues: { + type: Boolean, + required: false, + default: true, + }, + }, + data() { + this.store = new RelatedIssuesStore(); + + return { + state: this.store.state, + isFetching: false, + isSubmitting: false, + isFormVisible: false, + inputValue: '', + }; + }, + computed: { + autoCompleteSources() { + if (!this.allowAutoComplete) return {}; + return gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources; + }, + }, + created() { + this.service = new RelatedIssuesService(this.endpoint); + this.fetchRelatedIssues(); + }, + methods: { + findRelatedIssueById(id) { + return this.state.relatedIssues.find(issue => issue.id === id); + }, + onRelatedIssueRemoveRequest(idToRemove) { + const issueToRemove = this.findRelatedIssueById(idToRemove); + + if (issueToRemove) { + RelatedIssuesService.remove(issueToRemove.relationPath) + .then(({ data }) => { + this.store.setRelatedIssues(data.issuables); + }) + .catch(res => { + if (res && res.status !== 404) { + Flash(relatedIssuesRemoveErrorMap[this.issuableType]); + } + }); + } else { + Flash(pathIndeterminateErrorMap[this.issuableType]); + } + }, + onToggleAddRelatedIssuesForm() { + this.isFormVisible = !this.isFormVisible; + }, + onPendingIssueRemoveRequest(indexToRemove) { + this.store.removePendingRelatedIssue(indexToRemove); + }, + onPendingFormSubmit(event) { + this.processAllReferences(event.pendingReferences); + + if (this.state.pendingReferences.length > 0) { + this.isSubmitting = true; + this.service + .addRelatedIssues(this.state.pendingReferences, event.linkedIssueType) + .then(({ data }) => { + // We could potentially lose some pending issues in the interim here + this.store.setPendingReferences([]); + this.store.setRelatedIssues(data.issuables); + + // Close the form on submission + this.isFormVisible = false; + }) + .catch(({ response }) => { + let errorMessage = addRelatedIssueErrorMap[this.issuableType]; + if (response && response.data && response.data.message) { + errorMessage = response.data.message; + } + Flash(errorMessage); + }) + .finally(() => { + this.isSubmitting = false; + }); + } + }, + onPendingFormCancel() { + this.isFormVisible = false; + this.store.setPendingReferences([]); + this.inputValue = ''; + }, + fetchRelatedIssues() { + this.isFetching = true; + this.service + .fetchRelatedIssues() + .then(({ data }) => { + this.store.setRelatedIssues(data); + }) + .catch(() => { + this.store.setRelatedIssues([]); + Flash(__('An error occurred while fetching issues.')); + }) + .finally(() => { + this.isFetching = false; + }); + }, + saveIssueOrder({ issueId, beforeId, afterId, oldIndex, newIndex }) { + const issueToReorder = this.findRelatedIssueById(issueId); + + if (issueToReorder) { + RelatedIssuesService.saveOrder({ + endpoint: issueToReorder.relationPath, + move_before_id: beforeId, + move_after_id: afterId, + }) + .then(({ data }) => { + if (!data.message) { + this.store.updateIssueOrder(oldIndex, newIndex); + } + }) + .catch(() => { + Flash(__('An error occurred while reordering issues.')); + }); + } + }, + onInput({ untouchedRawReferences, touchedReference }) { + this.store.addPendingReferences(untouchedRawReferences); + + this.inputValue = `${touchedReference}`; + }, + onBlur(newValue) { + this.processAllReferences(newValue); + }, + processAllReferences(value = '') { + const rawReferences = value.split(/\s+/).filter(reference => reference.trim().length > 0); + + this.store.addPendingReferences(rawReferences); + this.inputValue = ''; + }, + }, +}; +</script> + +<template> + <related-issues-block + :class="cssClass" + :help-path="helpPath" + :is-fetching="isFetching" + :is-submitting="isSubmitting" + :related-issues="state.relatedIssues" + :can-admin="canAdmin" + :can-reorder="canReorder" + :pending-references="state.pendingReferences" + :is-form-visible="isFormVisible" + :input-value="inputValue" + :auto-complete-sources="autoCompleteSources" + :issuable-type="issuableType" + :path-id-separator="pathIdSeparator" + :show-categorized-issues="showCategorizedIssues" + @saveReorder="saveIssueOrder" + @toggleAddRelatedIssuesForm="onToggleAddRelatedIssuesForm" + @addIssuableFormInput="onInput" + @addIssuableFormBlur="onBlur" + @addIssuableFormSubmit="onPendingFormSubmit" + @addIssuableFormCancel="onPendingFormCancel" + @pendingIssuableRemoveRequest="onPendingIssueRemoveRequest" + @relatedIssueRemoveRequest="onRelatedIssueRemoveRequest" + /> +</template> |