diff options
Diffstat (limited to 'app/assets/javascripts/vue_shared/components/gl_mentions.vue')
-rw-r--r-- | app/assets/javascripts/vue_shared/components/gl_mentions.vue | 224 |
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) { |