Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/vue_shared/components/gl_mentions.vue')
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_mentions.vue224
1 files changed, 142 insertions, 82 deletions
diff --git a/app/assets/javascripts/vue_shared/components/gl_mentions.vue b/app/assets/javascripts/vue_shared/components/gl_mentions.vue
index 0ef4f1eda27..00bc46257ed 100644
--- a/app/assets/javascripts/vue_shared/components/gl_mentions.vue
+++ b/app/assets/javascripts/vue_shared/components/gl_mentions.vue
@@ -5,39 +5,102 @@ import axios from '~/lib/utils/axios_utils';
import { spriteIcon } from '~/lib/utils/common_utils';
import SidebarMediator from '~/sidebar/sidebar_mediator';
-/**
- * Creates the HTML template for each row of the mentions dropdown.
- *
- * @param original - An object from the array returned from the `autocomplete_sources/members` API
- * @returns {string} - An HTML template
- */
-function menuItemTemplate({ original }) {
- const rectAvatarClass = original.type === 'Group' ? 'rect-avatar' : '';
-
- const avatarClasses = `avatar avatar-inline center s26 ${rectAvatarClass}
- gl-display-inline-flex! gl-align-items-center gl-justify-content-center`;
-
- const avatarTag = original.avatar_url
- ? `<img
- src="${original.avatar_url}"
- alt="${original.username}'s avatar"
- class="${avatarClasses}"/>`
- : `<div class="${avatarClasses}">${original.username.charAt(0).toUpperCase()}</div>`;
-
- const name = escape(original.name);
-
- const count = original.count && !original.mentionsDisabled ? ` (${original.count})` : '';
-
- const icon = original.mentionsDisabled
- ? spriteIcon('notifications-off', 's16 gl-vertical-align-middle gl-ml-3')
- : '';
-
- return `${avatarTag}
- ${original.username}
- <small class="gl-text-small gl-font-weight-normal gl-reset-color">${name}${count}</small>
- ${icon}`;
+const AutoComplete = {
+ Issues: 'issues',
+ Labels: 'labels',
+ Members: 'members',
+};
+
+function doesCurrentLineStartWith(searchString, fullText, selectionStart) {
+ const currentLineNumber = fullText.slice(0, selectionStart).split('\n').length;
+ const currentLine = fullText.split('\n')[currentLineNumber - 1];
+ return currentLine.startsWith(searchString);
}
+const autoCompleteMap = {
+ [AutoComplete.Issues]: {
+ filterValues() {
+ return this[AutoComplete.Issues];
+ },
+ menuItemTemplate({ original }) {
+ return `<small>${original.reference || original.iid}</small> ${escape(original.title)}`;
+ },
+ },
+ [AutoComplete.Labels]: {
+ filterValues() {
+ const fullText = this.$slots.default?.[0]?.elm?.value;
+ const selectionStart = this.$slots.default?.[0]?.elm?.selectionStart;
+
+ if (doesCurrentLineStartWith('/label', fullText, selectionStart)) {
+ return this.labels.filter(label => !label.set);
+ }
+
+ if (doesCurrentLineStartWith('/unlabel', fullText, selectionStart)) {
+ return this.labels.filter(label => label.set);
+ }
+
+ return this.labels;
+ },
+ menuItemTemplate({ original }) {
+ return `
+ <span class="dropdown-label-box" style="background: ${escape(original.color)};"></span>
+ ${escape(original.title)}`;
+ },
+ },
+ [AutoComplete.Members]: {
+ filterValues() {
+ const fullText = this.$slots.default?.[0]?.elm?.value;
+ const selectionStart = this.$slots.default?.[0]?.elm?.selectionStart;
+
+ // Need to check whether sidebar store assignees has been updated
+ // in the case where the assignees AJAX response comes after the user does @ autocomplete
+ const isAssigneesLengthSame =
+ this.assignees?.length === SidebarMediator.singleton?.store?.assignees?.length;
+
+ if (!this.assignees || !isAssigneesLengthSame) {
+ this.assignees =
+ SidebarMediator.singleton?.store?.assignees?.map(assignee => assignee.username) || [];
+ }
+
+ if (doesCurrentLineStartWith('/assign', fullText, selectionStart)) {
+ return this.members.filter(member => !this.assignees.includes(member.username));
+ }
+
+ if (doesCurrentLineStartWith('/unassign', fullText, selectionStart)) {
+ return this.members.filter(member => this.assignees.includes(member.username));
+ }
+
+ return this.members;
+ },
+ menuItemTemplate({ original }) {
+ const rectAvatarClass = original.type === 'Group' ? 'rect-avatar' : '';
+
+ const avatarClasses = `avatar avatar-inline center s26 ${rectAvatarClass}
+ gl-display-inline-flex! gl-align-items-center gl-justify-content-center`;
+
+ const avatarTag = original.avatar_url
+ ? `<img
+ src="${original.avatar_url}"
+ alt="${original.username}'s avatar"
+ class="${avatarClasses}"/>`
+ : `<div class="${avatarClasses}">${original.username.charAt(0).toUpperCase()}</div>`;
+
+ const name = escape(original.name);
+
+ const count = original.count && !original.mentionsDisabled ? ` (${original.count})` : '';
+
+ const icon = original.mentionsDisabled
+ ? spriteIcon('notifications-off', 's16 gl-vertical-align-middle gl-ml-3')
+ : '';
+
+ return `${avatarTag}
+ ${original.username}
+ <small class="gl-text-small gl-font-weight-normal gl-reset-color">${name}${count}</small>
+ ${icon}`;
+ },
+ },
+};
+
export default {
name: 'GlMentions',
props: {
@@ -47,67 +110,64 @@ export default {
default: () => gl.GfmAutoComplete?.dataSources || {},
},
},
- data() {
- return {
- assignees: undefined,
- members: undefined,
- };
- },
mounted() {
+ const NON_WORD_OR_INTEGER = /\W|^\d+$/;
+
this.tribute = new Tribute({
- trigger: '@',
- fillAttr: 'username',
- lookup: value => value.name + value.username,
- menuItemTemplate,
- values: this.getMembers,
+ collection: [
+ {
+ trigger: '#',
+ lookup: value => value.iid + value.title,
+ menuItemTemplate: autoCompleteMap[AutoComplete.Issues].menuItemTemplate,
+ selectTemplate: ({ original }) => original.reference || `#${original.iid}`,
+ values: this.getValues(AutoComplete.Issues),
+ },
+ {
+ trigger: '@',
+ fillAttr: 'username',
+ lookup: value => value.name + value.username,
+ menuItemTemplate: autoCompleteMap[AutoComplete.Members].menuItemTemplate,
+ values: this.getValues(AutoComplete.Members),
+ },
+ {
+ trigger: '~',
+ lookup: 'title',
+ menuItemTemplate: autoCompleteMap[AutoComplete.Labels].menuItemTemplate,
+ selectTemplate: ({ original }) =>
+ NON_WORD_OR_INTEGER.test(original.title)
+ ? `~"${original.title}"`
+ : `~${original.title}`,
+ values: this.getValues(AutoComplete.Labels),
+ },
+ ],
});
- const input = this.$slots.default[0].elm;
+ const input = this.$slots.default?.[0]?.elm;
this.tribute.attach(input);
},
beforeDestroy() {
- const input = this.$slots.default[0].elm;
+ const input = this.$slots.default?.[0]?.elm;
this.tribute.detach(input);
},
methods: {
- /**
- * Creates the list of users to show in the mentions dropdown.
- *
- * @param inputText - The text entered by the user in the mentions input field
- * @param processValues - Callback function to set the list of users to show in the mentions dropdown
- */
- getMembers(inputText, processValues) {
- if (this.members) {
- processValues(this.getFilteredMembers());
- } else if (this.dataSources.members) {
- axios
- .get(this.dataSources.members)
- .then(response => {
- this.members = response.data;
- processValues(this.getFilteredMembers());
- })
- .catch(() => {});
- } else {
- processValues([]);
- }
- },
- getFilteredMembers() {
- const fullText = this.$slots.default[0].elm.value;
-
- if (!this.assignees) {
- this.assignees =
- SidebarMediator.singleton?.store?.assignees?.map(assignee => assignee.username) || [];
- }
-
- if (fullText.startsWith('/assign @')) {
- return this.members.filter(member => !this.assignees.includes(member.username));
- }
-
- if (fullText.startsWith('/unassign @')) {
- return this.members.filter(member => this.assignees.includes(member.username));
- }
-
- return this.members;
+ getValues(autoCompleteType) {
+ return (inputText, processValues) => {
+ if (this[autoCompleteType]) {
+ const filteredValues = autoCompleteMap[autoCompleteType].filterValues.call(this);
+ processValues(filteredValues);
+ } else if (this.dataSources[autoCompleteType]) {
+ axios
+ .get(this.dataSources[autoCompleteType])
+ .then(response => {
+ this[autoCompleteType] = response.data;
+ const filteredValues = autoCompleteMap[autoCompleteType].filterValues.call(this);
+ processValues(filteredValues);
+ })
+ .catch(() => {});
+ } else {
+ processValues([]);
+ }
+ };
},
},
render(createElement) {