gitlab-org--gitlab-foss/app/assets/javascripts/related_issues/components/related_issues_root.vue

257 lines
7.6 KiB
Vue

<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 {
relatedIssuesRemoveErrorMap,
pathIndeterminateErrorMap,
addRelatedIssueErrorMap,
issuableTypesMap,
PathIdSeparator,
} from '../constants';
import RelatedIssuesService from '../services/related_issues_service';
import RelatedIssuesStore from '../stores/related_issues_store';
import RelatedIssuesBlock from './related_issues_block.vue';
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.formatInput(touchedReference);
},
formatInput(touchedReference = '') {
const startsWithNumber = String(touchedReference).match(/^[0-9]/) !== null;
if (startsWithNumber) {
this.inputValue = `#${touchedReference}`;
} else {
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>