2018-03-09 15:18:59 -05:00
|
|
|
import $ from 'jquery';
|
2020-09-10 02:08:37 -04:00
|
|
|
import '~/lib/utils/jquery_at_who';
|
2021-04-09 14:09:24 -04:00
|
|
|
import { escape, sortBy, template } from 'lodash';
|
2021-02-14 13:09:20 -05:00
|
|
|
import * as Emoji from '~/emoji';
|
|
|
|
import axios from '~/lib/utils/axios_utils';
|
2021-03-10 07:09:14 -05:00
|
|
|
import { s__, __, sprintf } from '~/locale';
|
2020-11-17 10:09:28 -05:00
|
|
|
import { isUserBusy } from '~/set_status_modal/utils';
|
2021-02-14 13:09:20 -05:00
|
|
|
import SidebarMediator from '~/sidebar/sidebar_mediator';
|
2017-06-23 15:08:06 -04:00
|
|
|
import AjaxCache from './lib/utils/ajax_cache';
|
2019-12-03 07:06:34 -05:00
|
|
|
import { spriteIcon } from './lib/utils/common_utils';
|
2021-03-10 07:09:14 -05:00
|
|
|
import { parsePikadayDate } from './lib/utils/datetime_utility';
|
2021-02-14 13:09:20 -05:00
|
|
|
import glRegexp from './lib/utils/regexp';
|
2017-02-27 23:44:34 -05:00
|
|
|
|
2017-03-11 02:30:44 -05:00
|
|
|
function sanitize(str) {
|
|
|
|
return str.replace(/<(?:.|\n)*?>/gm, '');
|
|
|
|
}
|
2016-11-06 19:16:39 -05:00
|
|
|
|
2021-01-28 07:09:54 -05:00
|
|
|
function createMemberSearchString(member) {
|
|
|
|
return `${member.name.replace(/ /g, '')} ${member.username}`;
|
|
|
|
}
|
|
|
|
|
2019-12-03 07:06:34 -05:00
|
|
|
export function membersBeforeSave(members) {
|
2020-12-23 19:10:25 -05:00
|
|
|
return members.map((member) => {
|
2019-12-03 07:06:34 -05:00
|
|
|
const GROUP_TYPE = 'Group';
|
|
|
|
|
|
|
|
let title = '';
|
|
|
|
if (member.username == null) {
|
|
|
|
return member;
|
|
|
|
}
|
|
|
|
title = member.name;
|
|
|
|
if (member.count && !member.mentionsDisabled) {
|
|
|
|
title += ` (${member.count})`;
|
|
|
|
}
|
|
|
|
|
|
|
|
const autoCompleteAvatar = member.avatar_url || member.username.charAt(0).toUpperCase();
|
|
|
|
|
|
|
|
const rectAvatarClass = member.type === GROUP_TYPE ? 'rect-avatar' : '';
|
|
|
|
const imgAvatar = `<img src="${member.avatar_url}" alt="${member.username}" class="avatar ${rectAvatarClass} avatar-inline center s26"/>`;
|
|
|
|
const txtAvatar = `<div class="avatar ${rectAvatarClass} center avatar-inline s26">${autoCompleteAvatar}</div>`;
|
|
|
|
const avatarIcon = member.mentionsDisabled
|
2020-07-14 02:09:17 -04:00
|
|
|
? spriteIcon('notifications-off', 's16 vertical-align-middle gl-ml-2')
|
2019-12-03 07:06:34 -05:00
|
|
|
: '';
|
|
|
|
|
|
|
|
return {
|
|
|
|
username: member.username,
|
|
|
|
avatarTag: autoCompleteAvatar.length === 1 ? txtAvatar : imgAvatar,
|
|
|
|
title: sanitize(title),
|
2021-01-28 07:09:54 -05:00
|
|
|
search: sanitize(createMemberSearchString(member)),
|
2019-12-03 07:06:34 -05:00
|
|
|
icon: avatarIcon,
|
2020-11-20 04:09:06 -05:00
|
|
|
availability: member?.availability,
|
2019-12-03 07:06:34 -05:00
|
|
|
};
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-06-21 07:52:43 -04:00
|
|
|
export const defaultAutocompleteConfig = {
|
|
|
|
emojis: true,
|
|
|
|
members: true,
|
|
|
|
issues: true,
|
|
|
|
mergeRequests: true,
|
2018-06-28 02:41:56 -04:00
|
|
|
epics: true,
|
2018-06-21 07:52:43 -04:00
|
|
|
milestones: true,
|
|
|
|
labels: true,
|
2018-10-05 05:42:38 -04:00
|
|
|
snippets: true,
|
2020-11-17 16:09:19 -05:00
|
|
|
vulnerabilities: true,
|
2018-06-21 07:52:43 -04:00
|
|
|
};
|
|
|
|
|
2017-05-16 14:42:06 -04:00
|
|
|
class GfmAutoComplete {
|
2020-02-05 16:09:02 -05:00
|
|
|
constructor(dataSources = {}) {
|
|
|
|
this.dataSources = dataSources;
|
2017-05-16 14:42:06 -04:00
|
|
|
this.cachedData = {};
|
|
|
|
this.isLoadingData = {};
|
2020-11-17 16:09:19 -05:00
|
|
|
this.previousQuery = '';
|
2017-05-16 14:42:06 -04:00
|
|
|
}
|
2016-08-30 08:49:31 -04:00
|
|
|
|
2018-06-21 07:52:43 -04:00
|
|
|
setup(input, enableMap = defaultAutocompleteConfig) {
|
2017-03-11 02:30:44 -05:00
|
|
|
// Add GFM auto-completion to all input fields, that accept GFM input.
|
|
|
|
this.input = input || $('.js-gfm-input');
|
2017-05-03 11:47:59 -04:00
|
|
|
this.enableMap = enableMap;
|
2017-03-11 02:30:44 -05:00
|
|
|
this.setupLifecycle();
|
2017-05-16 14:42:06 -04:00
|
|
|
}
|
|
|
|
|
2017-03-11 02:30:44 -05:00
|
|
|
setupLifecycle() {
|
|
|
|
this.input.each((i, input) => {
|
|
|
|
const $input = $(input);
|
2020-09-09 17:08:33 -04:00
|
|
|
if (!$input.hasClass('js-gfm-input-initialized')) {
|
2020-12-08 16:10:06 -05:00
|
|
|
// eslint-disable-next-line @gitlab/no-global-event-off
|
2020-09-09 17:08:33 -04:00
|
|
|
$input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
|
|
|
|
$input.on('change.atwho', () => input.dispatchEvent(new Event('input')));
|
|
|
|
// This triggers at.js again
|
|
|
|
// Needed for quick actions with suffixes (ex: /label ~)
|
|
|
|
$input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
|
|
|
|
$input.on('clear-commands-cache.atwho', () => this.clearCache());
|
|
|
|
$input.addClass('js-gfm-input-initialized');
|
|
|
|
}
|
2017-03-11 02:30:44 -05:00
|
|
|
});
|
2017-05-16 14:42:06 -04:00
|
|
|
}
|
2017-05-03 11:47:59 -04:00
|
|
|
|
2017-05-16 14:42:06 -04:00
|
|
|
setupAtWho($input) {
|
2017-05-03 11:47:59 -04:00
|
|
|
if (this.enableMap.emojis) this.setupEmoji($input);
|
|
|
|
if (this.enableMap.members) this.setupMembers($input);
|
|
|
|
if (this.enableMap.issues) this.setupIssues($input);
|
|
|
|
if (this.enableMap.milestones) this.setupMilestones($input);
|
|
|
|
if (this.enableMap.mergeRequests) this.setupMergeRequests($input);
|
|
|
|
if (this.enableMap.labels) this.setupLabels($input);
|
2018-10-05 05:42:38 -04:00
|
|
|
if (this.enableMap.snippets) this.setupSnippets($input);
|
2017-05-03 11:47:59 -04:00
|
|
|
|
2017-05-31 01:50:53 -04:00
|
|
|
$input.filter('[data-supports-quick-actions="true"]').atwho({
|
2017-05-03 11:47:59 -04:00
|
|
|
at: '/',
|
|
|
|
alias: 'commands',
|
|
|
|
searchKey: 'search',
|
2021-01-12 10:10:37 -05:00
|
|
|
limit: 100,
|
2017-05-03 11:47:59 -04:00
|
|
|
skipSpecialCharacterTest: true,
|
2018-03-28 10:45:16 -04:00
|
|
|
skipMarkdownCharacterTest: true,
|
2017-05-16 14:42:06 -04:00
|
|
|
data: GfmAutoComplete.defaultLoadingData,
|
|
|
|
displayTpl(value) {
|
2018-11-21 09:22:41 -05:00
|
|
|
const cssClasses = [];
|
|
|
|
|
2017-05-16 14:42:06 -04:00
|
|
|
if (GfmAutoComplete.isLoading(value)) return GfmAutoComplete.Loading.template;
|
|
|
|
// eslint-disable-next-line no-template-curly-in-string
|
2018-11-21 09:22:41 -05:00
|
|
|
let tpl = '<li class="<%- className %>"><span class="name">/${name}</span>';
|
2017-05-03 11:47:59 -04:00
|
|
|
if (value.aliases.length > 0) {
|
2018-01-08 04:27:50 -05:00
|
|
|
tpl += ' <small class="aliases">(or /<%- aliases.join(", /") %>)</small>';
|
2017-05-03 11:47:59 -04:00
|
|
|
}
|
|
|
|
if (value.params.length > 0) {
|
2018-01-08 04:27:50 -05:00
|
|
|
tpl += ' <small class="params"><%- params.join(" ") %></small>';
|
2017-05-03 11:47:59 -04:00
|
|
|
}
|
2019-12-20 19:07:40 -05:00
|
|
|
if (value.warning && value.icon && value.icon === 'confidential') {
|
2020-07-14 17:09:03 -04:00
|
|
|
tpl += `<small class="description gl-display-flex gl-align-items-center">${spriteIcon(
|
|
|
|
'eye-slash',
|
|
|
|
's16 gl-mr-2',
|
|
|
|
)}<em><%- warning %></em></small>`;
|
2019-12-20 19:07:40 -05:00
|
|
|
} else if (value.warning) {
|
|
|
|
tpl += '<small class="description"><em><%- warning %></em></small>';
|
|
|
|
} else if (value.description !== '') {
|
|
|
|
tpl += '<small class="description"><em><%- description %></em></small>';
|
2017-05-03 11:47:59 -04:00
|
|
|
}
|
|
|
|
tpl += '</li>';
|
2018-11-21 09:22:41 -05:00
|
|
|
|
|
|
|
if (value.warning) {
|
|
|
|
cssClasses.push('has-warning');
|
|
|
|
}
|
|
|
|
|
2020-04-21 11:21:10 -04:00
|
|
|
return template(tpl)({
|
2018-11-21 09:22:41 -05:00
|
|
|
...value,
|
|
|
|
className: cssClasses.join(' '),
|
|
|
|
});
|
2017-05-16 14:42:06 -04:00
|
|
|
},
|
|
|
|
insertTpl(value) {
|
|
|
|
// eslint-disable-next-line no-template-curly-in-string
|
|
|
|
let tpl = '/${name} ';
|
|
|
|
let referencePrefix = null;
|
2017-05-03 11:47:59 -04:00
|
|
|
if (value.params.length > 0) {
|
2018-06-16 17:50:13 -04:00
|
|
|
[[referencePrefix]] = value.params;
|
2017-05-16 14:42:06 -04:00
|
|
|
if (/^[@%~]/.test(referencePrefix)) {
|
|
|
|
tpl += '<%- referencePrefix %>';
|
2017-05-03 11:47:59 -04:00
|
|
|
}
|
|
|
|
}
|
2020-04-21 11:21:10 -04:00
|
|
|
return template(tpl, { interpolate: /<%=([\s\S]+?)%>/g })({ referencePrefix });
|
2017-05-03 11:47:59 -04:00
|
|
|
},
|
|
|
|
suffix: '',
|
|
|
|
callbacks: {
|
2017-05-16 14:42:06 -04:00
|
|
|
...this.getDefaultCallbacks(),
|
|
|
|
beforeSave(commands) {
|
|
|
|
if (GfmAutoComplete.isLoading(commands)) return commands;
|
2020-12-23 19:10:25 -05:00
|
|
|
return $.map(commands, (c) => {
|
2017-05-16 14:42:06 -04:00
|
|
|
let search = c.name;
|
2017-05-03 11:47:59 -04:00
|
|
|
if (c.aliases.length > 0) {
|
2017-05-16 14:42:06 -04:00
|
|
|
search = `${search} ${c.aliases.join(' ')}`;
|
2017-05-03 11:47:59 -04:00
|
|
|
}
|
|
|
|
return {
|
|
|
|
name: c.name,
|
|
|
|
aliases: c.aliases,
|
|
|
|
params: c.params,
|
|
|
|
description: c.description,
|
2018-11-21 09:22:41 -05:00
|
|
|
warning: c.warning,
|
2019-12-20 19:07:40 -05:00
|
|
|
icon: c.icon,
|
2017-05-16 14:42:06 -04:00
|
|
|
search,
|
2017-05-03 11:47:59 -04:00
|
|
|
};
|
|
|
|
});
|
|
|
|
},
|
2017-05-16 14:42:06 -04:00
|
|
|
matcher(flag, subtext) {
|
|
|
|
const regexp = /(?:^|\n)\/([A-Za-z_]*)$/gi;
|
|
|
|
const match = regexp.exec(subtext);
|
2017-05-03 11:47:59 -04:00
|
|
|
if (match) {
|
|
|
|
return match[1];
|
|
|
|
}
|
2017-05-16 14:42:06 -04:00
|
|
|
return null;
|
|
|
|
},
|
|
|
|
},
|
2017-05-03 11:47:59 -04:00
|
|
|
});
|
2017-05-16 14:42:06 -04:00
|
|
|
}
|
2017-05-03 11:47:59 -04:00
|
|
|
|
|
|
|
setupEmoji($input) {
|
2021-02-06 04:09:11 -05:00
|
|
|
const fetchData = this.fetchData.bind(this);
|
2020-10-14 11:08:42 -04:00
|
|
|
|
2017-03-11 02:30:44 -05:00
|
|
|
// Emoji
|
|
|
|
$input.atwho({
|
|
|
|
at: ':',
|
2021-02-06 04:09:11 -05:00
|
|
|
displayTpl: GfmAutoComplete.Emoji.templateFunction,
|
2020-10-13 05:08:27 -04:00
|
|
|
insertTpl: GfmAutoComplete.Emoji.insertTemplateFunction,
|
2017-03-11 02:30:44 -05:00
|
|
|
skipSpecialCharacterTest: true,
|
2017-05-16 14:42:06 -04:00
|
|
|
data: GfmAutoComplete.defaultLoadingData,
|
2017-03-11 02:30:44 -05:00
|
|
|
callbacks: {
|
2021-02-06 04:09:11 -05:00
|
|
|
...this.getDefaultCallbacks(),
|
2017-05-16 14:42:06 -04:00
|
|
|
matcher(flag, subtext) {
|
2017-04-25 06:06:24 -04:00
|
|
|
const regexp = new RegExp(`(?:[^${glRegexp.unicodeLetters}0-9:]|\n|^):([^:]*)$`, 'gi');
|
2018-03-11 15:02:55 -04:00
|
|
|
const match = regexp.exec(subtext);
|
2017-04-25 06:06:24 -04:00
|
|
|
|
|
|
|
return match && match.length ? match[1] : null;
|
2017-05-16 14:42:06 -04:00
|
|
|
},
|
2021-02-06 04:09:11 -05:00
|
|
|
filter(query, items) {
|
|
|
|
if (GfmAutoComplete.isLoading(items)) {
|
|
|
|
fetchData(this.$inputor, this.at);
|
|
|
|
return items;
|
2020-10-14 11:08:42 -04:00
|
|
|
}
|
|
|
|
|
2021-02-06 04:09:11 -05:00
|
|
|
return GfmAutoComplete.Emoji.filter(query);
|
|
|
|
},
|
|
|
|
sorter(query, items) {
|
|
|
|
this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0;
|
|
|
|
if (GfmAutoComplete.isLoading(items)) {
|
|
|
|
this.setting.highlightFirst = false;
|
|
|
|
return items;
|
|
|
|
}
|
2020-10-14 11:08:42 -04:00
|
|
|
|
2021-02-06 04:09:11 -05:00
|
|
|
if (query.length === 0) {
|
|
|
|
return items;
|
|
|
|
}
|
2020-10-14 11:08:42 -04:00
|
|
|
|
2021-02-06 04:09:11 -05:00
|
|
|
return GfmAutoComplete.Emoji.sorter(items);
|
2020-10-14 11:08:42 -04:00
|
|
|
},
|
2017-05-16 14:42:06 -04:00
|
|
|
},
|
2017-03-11 02:30:44 -05:00
|
|
|
});
|
2017-05-16 14:42:06 -04:00
|
|
|
}
|
2017-05-03 11:47:59 -04:00
|
|
|
|
|
|
|
setupMembers($input) {
|
2020-02-05 16:09:02 -05:00
|
|
|
const fetchData = this.fetchData.bind(this);
|
|
|
|
const MEMBER_COMMAND = {
|
|
|
|
ASSIGN: '/assign',
|
|
|
|
UNASSIGN: '/unassign',
|
2021-04-26 05:09:53 -04:00
|
|
|
ASSIGN_REVIEWER: '/assign_reviewer',
|
|
|
|
UNASSIGN_REVIEWER: '/unassign_reviewer',
|
2020-02-05 16:09:02 -05:00
|
|
|
REASSIGN: '/reassign',
|
|
|
|
CC: '/cc',
|
|
|
|
};
|
|
|
|
let assignees = [];
|
2021-04-26 05:09:53 -04:00
|
|
|
let reviewers = [];
|
2020-02-05 16:09:02 -05:00
|
|
|
let command = '';
|
|
|
|
|
2017-03-11 02:30:44 -05:00
|
|
|
// Team Members
|
|
|
|
$input.atwho({
|
|
|
|
at: '@',
|
2018-11-12 04:21:25 -05:00
|
|
|
alias: 'users',
|
2017-05-16 14:42:06 -04:00
|
|
|
displayTpl(value) {
|
|
|
|
let tmpl = GfmAutoComplete.Loading.template;
|
2020-11-17 10:09:28 -05:00
|
|
|
const { avatarTag, username, title, icon, availability } = value;
|
2018-11-12 04:21:25 -05:00
|
|
|
if (username != null) {
|
|
|
|
tmpl = GfmAutoComplete.Members.templateFunction({
|
|
|
|
avatarTag,
|
|
|
|
username,
|
|
|
|
title,
|
2019-12-03 07:06:34 -05:00
|
|
|
icon,
|
2020-11-17 10:09:28 -05:00
|
|
|
availabilityStatus:
|
|
|
|
availability && isUserBusy(availability)
|
|
|
|
? `<span class="gl-text-gray-500"> ${s__('UserAvailability|(Busy)')}</span>`
|
|
|
|
: '',
|
2018-11-12 04:21:25 -05:00
|
|
|
});
|
2017-05-16 14:42:06 -04:00
|
|
|
}
|
|
|
|
return tmpl;
|
|
|
|
},
|
|
|
|
// eslint-disable-next-line no-template-curly-in-string
|
2017-03-11 02:30:44 -05:00
|
|
|
insertTpl: '${atwho-at}${username}',
|
2021-02-22 13:10:55 -05:00
|
|
|
limit: 10,
|
2017-03-11 02:30:44 -05:00
|
|
|
searchKey: 'search',
|
|
|
|
alwaysHighlightFirst: true,
|
|
|
|
skipSpecialCharacterTest: true,
|
2017-05-16 14:42:06 -04:00
|
|
|
data: GfmAutoComplete.defaultLoadingData,
|
2017-03-11 02:30:44 -05:00
|
|
|
callbacks: {
|
2017-05-16 14:42:06 -04:00
|
|
|
...this.getDefaultCallbacks(),
|
2019-12-03 07:06:34 -05:00
|
|
|
beforeSave: membersBeforeSave,
|
2020-02-05 16:09:02 -05:00
|
|
|
matcher(flag, subtext) {
|
2020-12-23 07:10:26 -05:00
|
|
|
const subtextNodes = subtext.split(/\n+/g).pop().split(GfmAutoComplete.regexSubtext);
|
2020-02-05 16:09:02 -05:00
|
|
|
|
|
|
|
// Check if @ is followed by '/assign', '/reassign', '/unassign' or '/cc' commands.
|
2020-12-23 19:10:25 -05:00
|
|
|
command = subtextNodes.find((node) => {
|
2020-02-05 16:09:02 -05:00
|
|
|
if (Object.values(MEMBER_COMMAND).includes(node)) {
|
|
|
|
return node;
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
});
|
|
|
|
|
2021-04-26 05:09:53 -04:00
|
|
|
// Cache assignees & reviewers list for easier filtering later
|
2020-03-23 17:09:46 -04:00
|
|
|
assignees =
|
2021-01-28 07:09:54 -05:00
|
|
|
SidebarMediator.singleton?.store?.assignees?.map(createMemberSearchString) || [];
|
2021-04-26 05:09:53 -04:00
|
|
|
reviewers =
|
|
|
|
SidebarMediator.singleton?.store?.reviewers?.map(createMemberSearchString) || [];
|
2020-02-05 16:09:02 -05:00
|
|
|
|
|
|
|
const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers);
|
|
|
|
return match && match.length ? match[1] : null;
|
|
|
|
},
|
|
|
|
filter(query, data, searchKey) {
|
|
|
|
if (GfmAutoComplete.isLoading(data)) {
|
|
|
|
fetchData(this.$inputor, this.at);
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (data === GfmAutoComplete.defaultLoadingData) {
|
|
|
|
return $.fn.atwho.default.callbacks.filter(query, data, searchKey);
|
|
|
|
}
|
|
|
|
|
2020-09-29 14:09:52 -04:00
|
|
|
if (command === MEMBER_COMMAND.ASSIGN) {
|
2020-02-05 16:09:02 -05:00
|
|
|
// Only include members which are not assigned to Issuable currently
|
2020-12-23 19:10:25 -05:00
|
|
|
return data.filter((member) => !assignees.includes(member.search));
|
2020-02-05 16:09:02 -05:00
|
|
|
} else if (command === MEMBER_COMMAND.UNASSIGN) {
|
|
|
|
// Only include members which are assigned to Issuable currently
|
2020-12-23 19:10:25 -05:00
|
|
|
return data.filter((member) => assignees.includes(member.search));
|
2021-04-26 05:09:53 -04:00
|
|
|
} else if (command === MEMBER_COMMAND.ASSIGN_REVIEWER) {
|
|
|
|
// Only include members which are not assigned as a reviewer to Issuable currently
|
|
|
|
return data.filter((member) => !reviewers.includes(member.search));
|
|
|
|
} else if (command === MEMBER_COMMAND.UNASSIGN_REVIEWER) {
|
|
|
|
// Only include members which are not assigned as a reviewer to Issuable currently
|
|
|
|
return data.filter((member) => reviewers.includes(member.search));
|
2020-02-05 16:09:02 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
return data;
|
|
|
|
},
|
2021-02-22 13:10:55 -05:00
|
|
|
sorter(query, items) {
|
|
|
|
// Disable auto-selecting the loading icon
|
|
|
|
this.setting.highlightFirst = this.setting.alwaysHighlightFirst;
|
|
|
|
if (GfmAutoComplete.isLoading(items)) {
|
|
|
|
this.setting.highlightFirst = false;
|
|
|
|
return items;
|
|
|
|
}
|
|
|
|
|
2021-02-24 07:10:54 -05:00
|
|
|
if (!query) {
|
|
|
|
return items;
|
|
|
|
}
|
|
|
|
|
2021-04-09 14:09:24 -04:00
|
|
|
return GfmAutoComplete.Members.sort(query, items);
|
2021-02-22 13:10:55 -05:00
|
|
|
},
|
2017-05-16 14:42:06 -04:00
|
|
|
},
|
2017-03-11 02:30:44 -05:00
|
|
|
});
|
2017-05-16 14:42:06 -04:00
|
|
|
}
|
2017-05-03 11:47:59 -04:00
|
|
|
|
|
|
|
setupIssues($input) {
|
2017-03-11 02:30:44 -05:00
|
|
|
$input.atwho({
|
|
|
|
at: '#',
|
|
|
|
alias: 'issues',
|
|
|
|
searchKey: 'search',
|
2017-05-16 14:42:06 -04:00
|
|
|
displayTpl(value) {
|
|
|
|
let tmpl = GfmAutoComplete.Loading.template;
|
|
|
|
if (value.title != null) {
|
2018-12-21 03:49:44 -05:00
|
|
|
tmpl = GfmAutoComplete.Issues.templateFunction(value);
|
2017-05-16 14:42:06 -04:00
|
|
|
}
|
|
|
|
return tmpl;
|
|
|
|
},
|
|
|
|
data: GfmAutoComplete.defaultLoadingData,
|
2018-12-21 03:49:44 -05:00
|
|
|
insertTpl: GfmAutoComplete.Issues.insertTemplateFunction,
|
|
|
|
skipSpecialCharacterTest: true,
|
2017-03-11 02:30:44 -05:00
|
|
|
callbacks: {
|
2017-05-16 14:42:06 -04:00
|
|
|
...this.getDefaultCallbacks(),
|
|
|
|
beforeSave(issues) {
|
2020-12-23 19:10:25 -05:00
|
|
|
return $.map(issues, (i) => {
|
2017-03-11 02:30:44 -05:00
|
|
|
if (i.title == null) {
|
|
|
|
return i;
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
id: i.iid,
|
|
|
|
title: sanitize(i.title),
|
2018-12-21 03:49:44 -05:00
|
|
|
reference: i.reference,
|
2017-05-16 14:42:06 -04:00
|
|
|
search: `${i.iid} ${i.title}`,
|
2017-03-11 02:30:44 -05:00
|
|
|
};
|
|
|
|
});
|
2017-05-16 14:42:06 -04:00
|
|
|
},
|
|
|
|
},
|
2017-03-11 02:30:44 -05:00
|
|
|
});
|
2017-05-16 14:42:06 -04:00
|
|
|
}
|
2017-05-03 11:47:59 -04:00
|
|
|
|
|
|
|
setupMilestones($input) {
|
2017-03-11 02:30:44 -05:00
|
|
|
$input.atwho({
|
|
|
|
at: '%',
|
|
|
|
alias: 'milestones',
|
|
|
|
searchKey: 'search',
|
2017-05-16 14:42:06 -04:00
|
|
|
// eslint-disable-next-line no-template-curly-in-string
|
2017-03-11 02:30:44 -05:00
|
|
|
insertTpl: '${atwho-at}${title}',
|
2017-05-16 14:42:06 -04:00
|
|
|
displayTpl(value) {
|
|
|
|
let tmpl = GfmAutoComplete.Loading.template;
|
|
|
|
if (value.title != null) {
|
2021-03-10 07:09:14 -05:00
|
|
|
tmpl = GfmAutoComplete.Milestones.templateFunction(value.title, value.expired);
|
2017-05-16 14:42:06 -04:00
|
|
|
}
|
|
|
|
return tmpl;
|
|
|
|
},
|
|
|
|
data: GfmAutoComplete.defaultLoadingData,
|
2017-03-11 02:30:44 -05:00
|
|
|
callbacks: {
|
2017-05-16 14:42:06 -04:00
|
|
|
...this.getDefaultCallbacks(),
|
|
|
|
beforeSave(milestones) {
|
2021-03-10 07:09:14 -05:00
|
|
|
const parsedMilestones = $.map(milestones, (m) => {
|
2017-03-11 02:30:44 -05:00
|
|
|
if (m.title == null) {
|
|
|
|
return m;
|
|
|
|
}
|
2021-03-10 07:09:14 -05:00
|
|
|
|
|
|
|
const dueDate = m.due_date ? parsePikadayDate(m.due_date) : null;
|
|
|
|
const expired = dueDate ? Date.now() > dueDate.getTime() : false;
|
|
|
|
|
2017-03-11 02:30:44 -05:00
|
|
|
return {
|
|
|
|
id: m.iid,
|
|
|
|
title: sanitize(m.title),
|
2017-05-16 14:42:06 -04:00
|
|
|
search: m.title,
|
2021-03-10 07:09:14 -05:00
|
|
|
expired,
|
|
|
|
dueDate,
|
2017-03-11 02:30:44 -05:00
|
|
|
};
|
|
|
|
});
|
2021-03-10 07:09:14 -05:00
|
|
|
|
|
|
|
// Sort milestones by due date when present.
|
|
|
|
if (typeof parsedMilestones[0] === 'object') {
|
|
|
|
return parsedMilestones.sort((mA, mB) => {
|
|
|
|
// Move all expired milestones to the bottom.
|
|
|
|
if (mA.expired) return 1;
|
|
|
|
if (mB.expired) return -1;
|
|
|
|
|
|
|
|
// Move milestones without due dates just above expired milestones.
|
|
|
|
if (!mA.dueDate) return 1;
|
|
|
|
if (!mB.dueDate) return -1;
|
|
|
|
|
|
|
|
// Sort by due date in ascending order.
|
|
|
|
return mA.dueDate - mB.dueDate;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return parsedMilestones;
|
2017-05-16 14:42:06 -04:00
|
|
|
},
|
|
|
|
},
|
2017-03-11 02:30:44 -05:00
|
|
|
});
|
2017-05-16 14:42:06 -04:00
|
|
|
}
|
2017-05-03 11:47:59 -04:00
|
|
|
|
|
|
|
setupMergeRequests($input) {
|
2017-03-11 02:30:44 -05:00
|
|
|
$input.atwho({
|
|
|
|
at: '!',
|
|
|
|
alias: 'mergerequests',
|
|
|
|
searchKey: 'search',
|
2017-05-16 14:42:06 -04:00
|
|
|
displayTpl(value) {
|
|
|
|
let tmpl = GfmAutoComplete.Loading.template;
|
|
|
|
if (value.title != null) {
|
2018-12-21 03:49:44 -05:00
|
|
|
tmpl = GfmAutoComplete.Issues.templateFunction(value);
|
2017-05-16 14:42:06 -04:00
|
|
|
}
|
|
|
|
return tmpl;
|
|
|
|
},
|
|
|
|
data: GfmAutoComplete.defaultLoadingData,
|
2018-12-21 03:49:44 -05:00
|
|
|
insertTpl: GfmAutoComplete.Issues.insertTemplateFunction,
|
|
|
|
skipSpecialCharacterTest: true,
|
2017-03-11 02:30:44 -05:00
|
|
|
callbacks: {
|
2017-05-16 14:42:06 -04:00
|
|
|
...this.getDefaultCallbacks(),
|
|
|
|
beforeSave(merges) {
|
2020-12-23 19:10:25 -05:00
|
|
|
return $.map(merges, (m) => {
|
2017-03-11 02:30:44 -05:00
|
|
|
if (m.title == null) {
|
|
|
|
return m;
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
id: m.iid,
|
|
|
|
title: sanitize(m.title),
|
2018-12-21 03:49:44 -05:00
|
|
|
reference: m.reference,
|
2017-05-16 14:42:06 -04:00
|
|
|
search: `${m.iid} ${m.title}`,
|
2017-03-11 02:30:44 -05:00
|
|
|
};
|
|
|
|
});
|
2017-05-16 14:42:06 -04:00
|
|
|
},
|
|
|
|
},
|
2017-03-11 02:30:44 -05:00
|
|
|
});
|
2017-05-16 14:42:06 -04:00
|
|
|
}
|
2017-05-03 11:47:59 -04:00
|
|
|
|
|
|
|
setupLabels($input) {
|
2019-06-25 06:19:29 -04:00
|
|
|
const instance = this;
|
2017-11-27 22:51:03 -05:00
|
|
|
const fetchData = this.fetchData.bind(this);
|
|
|
|
const LABEL_COMMAND = { LABEL: '/label', UNLABEL: '/unlabel', RELABEL: '/relabel' };
|
|
|
|
let command = '';
|
|
|
|
|
2017-03-11 02:30:44 -05:00
|
|
|
$input.atwho({
|
|
|
|
at: '~',
|
|
|
|
alias: 'labels',
|
|
|
|
searchKey: 'search',
|
2017-05-16 14:42:06 -04:00
|
|
|
data: GfmAutoComplete.defaultLoadingData,
|
|
|
|
displayTpl(value) {
|
2018-12-11 07:22:08 -05:00
|
|
|
let tmpl = GfmAutoComplete.Labels.templateFunction(value.color, value.title);
|
2017-05-16 14:42:06 -04:00
|
|
|
if (GfmAutoComplete.isLoading(value)) {
|
|
|
|
tmpl = GfmAutoComplete.Loading.template;
|
|
|
|
}
|
|
|
|
return tmpl;
|
|
|
|
},
|
|
|
|
// eslint-disable-next-line no-template-curly-in-string
|
2017-03-11 02:30:44 -05:00
|
|
|
insertTpl: '${atwho-at}${title}',
|
2019-10-04 11:06:38 -04:00
|
|
|
limit: 20,
|
2017-03-11 02:30:44 -05:00
|
|
|
callbacks: {
|
2017-05-16 14:42:06 -04:00
|
|
|
...this.getDefaultCallbacks(),
|
|
|
|
beforeSave(merges) {
|
|
|
|
if (GfmAutoComplete.isLoading(merges)) return merges;
|
2020-12-23 19:10:25 -05:00
|
|
|
return $.map(merges, (m) => ({
|
2017-05-16 14:42:06 -04:00
|
|
|
title: sanitize(m.title),
|
|
|
|
color: m.color,
|
|
|
|
search: m.title,
|
2017-11-27 22:51:03 -05:00
|
|
|
set: m.set,
|
2017-05-16 14:42:06 -04:00
|
|
|
}));
|
|
|
|
},
|
2017-11-27 22:51:03 -05:00
|
|
|
matcher(flag, subtext) {
|
2020-12-23 07:10:26 -05:00
|
|
|
const subtextNodes = subtext.split(/\n+/g).pop().split(GfmAutoComplete.regexSubtext);
|
2017-11-27 22:51:03 -05:00
|
|
|
|
|
|
|
// Check if ~ is followed by '/label', '/relabel' or '/unlabel' commands.
|
2020-12-23 19:10:25 -05:00
|
|
|
command = subtextNodes.find((node) => {
|
2018-10-30 16:28:31 -04:00
|
|
|
if (
|
|
|
|
node === LABEL_COMMAND.LABEL ||
|
|
|
|
node === LABEL_COMMAND.RELABEL ||
|
|
|
|
node === LABEL_COMMAND.UNLABEL
|
|
|
|
) {
|
|
|
|
return node;
|
|
|
|
}
|
2017-11-27 22:51:03 -05:00
|
|
|
return null;
|
|
|
|
});
|
|
|
|
|
2019-06-25 06:19:29 -04:00
|
|
|
// If any label matches the inserted text after the last `~`, suggest those labels,
|
|
|
|
// even if any spaces or funky characters were typed.
|
|
|
|
// This allows matching labels like "Accepting merge requests".
|
|
|
|
const labels = instance.cachedData[flag];
|
|
|
|
if (labels) {
|
|
|
|
if (!subtext.includes(flag)) {
|
|
|
|
// Do not match if there is no `~` before the cursor
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
const lastCandidate = subtext.split(flag).pop();
|
2020-12-23 19:10:25 -05:00
|
|
|
if (labels.find((label) => label.title.startsWith(lastCandidate))) {
|
2019-06-25 06:19:29 -04:00
|
|
|
return lastCandidate;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Load all labels into the autocompleter.
|
|
|
|
// This needs to happen if e.g. editing a label in an existing comment, because normally
|
|
|
|
// label data would only be loaded only once you type `~`.
|
|
|
|
fetchData(this.$inputor, this.at);
|
|
|
|
}
|
|
|
|
|
|
|
|
const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers);
|
2017-11-27 22:51:03 -05:00
|
|
|
return match && match.length ? match[1] : null;
|
|
|
|
},
|
|
|
|
filter(query, data, searchKey) {
|
|
|
|
if (GfmAutoComplete.isLoading(data)) {
|
|
|
|
fetchData(this.$inputor, this.at);
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (data === GfmAutoComplete.defaultLoadingData) {
|
|
|
|
return $.fn.atwho.default.callbacks.filter(query, data, searchKey);
|
|
|
|
}
|
|
|
|
|
|
|
|
// The `LABEL_COMMAND.RELABEL` is intentionally skipped
|
|
|
|
// because we want to return all the labels (unfiltered) for that command.
|
|
|
|
if (command === LABEL_COMMAND.LABEL) {
|
|
|
|
// Return labels with set: undefined.
|
2020-12-23 19:10:25 -05:00
|
|
|
return data.filter((label) => !label.set);
|
2017-11-27 22:51:03 -05:00
|
|
|
} else if (command === LABEL_COMMAND.UNLABEL) {
|
|
|
|
// Return labels with set: true.
|
2020-12-23 19:10:25 -05:00
|
|
|
return data.filter((label) => label.set);
|
2017-11-27 22:51:03 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
return data;
|
|
|
|
},
|
2017-05-16 14:42:06 -04:00
|
|
|
},
|
2017-03-11 02:30:44 -05:00
|
|
|
});
|
2017-05-16 14:42:06 -04:00
|
|
|
}
|
2017-05-03 11:47:59 -04:00
|
|
|
|
2018-10-05 05:42:38 -04:00
|
|
|
setupSnippets($input) {
|
|
|
|
$input.atwho({
|
|
|
|
at: '$',
|
|
|
|
alias: 'snippets',
|
|
|
|
searchKey: 'search',
|
|
|
|
displayTpl(value) {
|
|
|
|
let tmpl = GfmAutoComplete.Loading.template;
|
|
|
|
if (value.title != null) {
|
2018-12-21 03:49:44 -05:00
|
|
|
tmpl = GfmAutoComplete.Issues.templateFunction(value);
|
2018-10-05 05:42:38 -04:00
|
|
|
}
|
|
|
|
return tmpl;
|
|
|
|
},
|
|
|
|
data: GfmAutoComplete.defaultLoadingData,
|
|
|
|
// eslint-disable-next-line no-template-curly-in-string
|
|
|
|
insertTpl: '${atwho-at}${id}',
|
|
|
|
callbacks: {
|
|
|
|
...this.getDefaultCallbacks(),
|
|
|
|
beforeSave(snippets) {
|
2020-12-23 19:10:25 -05:00
|
|
|
return $.map(snippets, (m) => {
|
2018-10-05 05:42:38 -04:00
|
|
|
if (m.title == null) {
|
|
|
|
return m;
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
id: m.id,
|
|
|
|
title: sanitize(m.title),
|
|
|
|
search: `${m.id} ${m.title}`,
|
|
|
|
};
|
|
|
|
});
|
|
|
|
},
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-05-16 14:42:06 -04:00
|
|
|
getDefaultCallbacks() {
|
2020-11-17 16:09:19 -05:00
|
|
|
const self = this;
|
2017-05-16 14:42:06 -04:00
|
|
|
|
|
|
|
return {
|
|
|
|
sorter(query, items, searchKey) {
|
|
|
|
this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0;
|
|
|
|
if (GfmAutoComplete.isLoading(items)) {
|
|
|
|
this.setting.highlightFirst = false;
|
|
|
|
return items;
|
|
|
|
}
|
|
|
|
return $.fn.atwho.default.callbacks.sorter(query, items, searchKey);
|
|
|
|
},
|
|
|
|
filter(query, data, searchKey) {
|
|
|
|
if (GfmAutoComplete.isLoading(data)) {
|
2020-11-17 16:09:19 -05:00
|
|
|
self.fetchData(this.$inputor, this.at);
|
|
|
|
return data;
|
|
|
|
} else if (
|
|
|
|
GfmAutoComplete.isTypeWithBackendFiltering(this.at) &&
|
|
|
|
self.previousQuery !== query
|
|
|
|
) {
|
|
|
|
self.fetchData(this.$inputor, this.at, query);
|
|
|
|
self.previousQuery = query;
|
2017-05-16 14:42:06 -04:00
|
|
|
return data;
|
|
|
|
}
|
|
|
|
return $.fn.atwho.default.callbacks.filter(query, data, searchKey);
|
|
|
|
},
|
|
|
|
beforeInsert(value) {
|
2018-03-28 10:45:16 -04:00
|
|
|
let withoutAt = value.substring(1);
|
|
|
|
const at = value.charAt();
|
|
|
|
|
2017-05-16 14:42:06 -04:00
|
|
|
if (value && !this.setting.skipSpecialCharacterTest) {
|
2018-03-28 10:45:16 -04:00
|
|
|
const regex = at === '~' ? /\W|^\d+$/ : /\W/;
|
2017-11-13 09:30:22 -05:00
|
|
|
if (withoutAt && regex.test(withoutAt)) {
|
2018-03-28 10:45:16 -04:00
|
|
|
withoutAt = `"${withoutAt}"`;
|
2017-05-16 14:42:06 -04:00
|
|
|
}
|
|
|
|
}
|
2018-03-28 10:45:16 -04:00
|
|
|
|
|
|
|
// We can ignore this for quick actions because they are processed
|
|
|
|
// before Markdown.
|
|
|
|
if (!this.setting.skipMarkdownCharacterTest) {
|
Only use backslash escapes in autocomplete when needed
Autocompletion for references happens on the frontend. Those references
are turned into actual references on the backend, but only after
Markdown processing has happened. That means that if a reference
contains a character that Markdown might consume, it won't render
correctly. So we need to do some escaping on the frontend.
We have these potential problem characters:
https://docs.gitlab.com/ee/user/markdown.html#emphasis
1. ~ - this is ~~strikethrough~~, but only when doubled.
2. _ - used for _emphasis_, doubled is __bold__.
3. * - also used for *emphasis*, doubled is **bold** also.
4. ` - used for `code spans`, any number works.
We don't need to escape `-` any more. When it comes to being inside a
word:
1. a~~b~~ has strikethrough, so it needs to be escaped everywhere.
2. a_b_ has no emphasis (see [a]) so it only needs to be escaped at the
start and end of words.
3. a*b* has emphasis, so it needs to be escaped everywhere.
4. a`b` has a code span, so it needs to be escaped everywhere.
Or, in code terms:
1. Always escape ~~, *, and ` when being inserted by autocomplete.
2. Escape _ when it's either at the beginning or the end of a word.
[a]: https://docs.gitlab.com/ee/user/markdown.html#multiple-underscores-in-words
2019-04-17 07:52:25 -04:00
|
|
|
withoutAt = withoutAt
|
|
|
|
.replace(/(~~|`|\*)/g, '\\$1')
|
|
|
|
.replace(/(\b)(_+)/g, '$1\\$2') // only escape underscores at the start
|
|
|
|
.replace(/(_+)(\b)/g, '\\$1$2'); // or end of words
|
2018-03-28 10:45:16 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
return `${at}${withoutAt}`;
|
2017-05-16 14:42:06 -04:00
|
|
|
},
|
|
|
|
matcher(flag, subtext) {
|
2017-11-27 22:51:03 -05:00
|
|
|
const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers);
|
2017-05-16 14:42:06 -04:00
|
|
|
|
|
|
|
if (match) {
|
|
|
|
return match[1];
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
},
|
2019-04-25 04:11:20 -04:00
|
|
|
highlighter(li, query) {
|
|
|
|
// override default behaviour to escape dot character
|
|
|
|
// see https://github.com/ichord/At.js/pull/576
|
|
|
|
if (!query) {
|
|
|
|
return li;
|
|
|
|
}
|
|
|
|
const escapedQuery = query.replace(/[.+]/, '\\$&');
|
|
|
|
const regexp = new RegExp(`>\\s*([^<]*?)(${escapedQuery})([^<]*)\\s*<`, 'ig');
|
|
|
|
return li.replace(regexp, (str, $1, $2, $3) => `> ${$1}<strong>${$2}</strong>${$3} <`);
|
|
|
|
},
|
2017-05-16 14:42:06 -04:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-11-17 16:09:19 -05:00
|
|
|
fetchData($input, at, search) {
|
2017-03-11 02:30:44 -05:00
|
|
|
if (this.isLoadingData[at]) return;
|
2018-04-26 15:53:13 -04:00
|
|
|
|
2017-03-11 02:30:44 -05:00
|
|
|
this.isLoadingData[at] = true;
|
2018-04-26 15:53:13 -04:00
|
|
|
const dataSource = this.dataSources[GfmAutoComplete.atTypeMap[at]];
|
|
|
|
|
2020-11-17 16:09:19 -05:00
|
|
|
if (GfmAutoComplete.isTypeWithBackendFiltering(at)) {
|
|
|
|
axios
|
|
|
|
.get(dataSource, { params: { search } })
|
|
|
|
.then(({ data }) => {
|
|
|
|
this.loadData($input, at, data);
|
|
|
|
})
|
|
|
|
.catch(() => {
|
|
|
|
this.isLoadingData[at] = false;
|
|
|
|
});
|
|
|
|
} else if (this.cachedData[at]) {
|
2017-03-11 02:30:44 -05:00
|
|
|
this.loadData($input, at, this.cachedData[at]);
|
2017-05-16 14:42:06 -04:00
|
|
|
} else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
|
2020-10-13 05:08:27 -04:00
|
|
|
this.loadEmojiData($input, at).catch(() => {});
|
2018-04-26 15:53:13 -04:00
|
|
|
} else if (dataSource) {
|
|
|
|
AjaxCache.retrieve(dataSource, true)
|
2020-12-23 19:10:25 -05:00
|
|
|
.then((data) => {
|
2017-06-05 05:12:15 -04:00
|
|
|
this.loadData($input, at, data);
|
|
|
|
})
|
2018-10-30 16:28:31 -04:00
|
|
|
.catch(() => {
|
|
|
|
this.isLoadingData[at] = false;
|
|
|
|
});
|
2018-04-26 15:53:13 -04:00
|
|
|
} else {
|
|
|
|
this.isLoadingData[at] = false;
|
2017-03-11 02:30:44 -05:00
|
|
|
}
|
2017-05-16 14:42:06 -04:00
|
|
|
}
|
2017-06-05 05:12:15 -04:00
|
|
|
|
2017-05-16 14:42:06 -04:00
|
|
|
loadData($input, at, data) {
|
2017-03-11 02:30:44 -05:00
|
|
|
this.isLoadingData[at] = false;
|
|
|
|
this.cachedData[at] = data;
|
|
|
|
$input.atwho('load', at, data);
|
|
|
|
// This trigger at.js again
|
|
|
|
// otherwise we would be stuck with loading until the user types
|
|
|
|
return $input.trigger('keyup');
|
2017-05-16 14:42:06 -04:00
|
|
|
}
|
|
|
|
|
2020-10-13 05:08:27 -04:00
|
|
|
async loadEmojiData($input, at) {
|
|
|
|
await Emoji.initEmojiMap();
|
|
|
|
|
2021-02-06 04:09:11 -05:00
|
|
|
this.loadData($input, at, ['loaded']);
|
2020-10-14 11:08:42 -04:00
|
|
|
|
2020-10-13 05:08:27 -04:00
|
|
|
GfmAutoComplete.glEmojiTag = Emoji.glEmojiTag;
|
|
|
|
}
|
|
|
|
|
2017-06-05 05:12:15 -04:00
|
|
|
clearCache() {
|
|
|
|
this.cachedData = {};
|
|
|
|
}
|
|
|
|
|
2017-06-29 11:28:12 -04:00
|
|
|
destroy() {
|
|
|
|
this.input.each((i, input) => {
|
|
|
|
const $input = $(input);
|
|
|
|
$input.atwho('destroy');
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2017-05-16 14:42:06 -04:00
|
|
|
static isLoading(data) {
|
|
|
|
let dataToInspect = data;
|
2017-03-11 02:30:44 -05:00
|
|
|
if (data && data.length > 0) {
|
2018-06-16 17:50:13 -04:00
|
|
|
[dataToInspect] = data;
|
2016-12-22 06:56:10 -05:00
|
|
|
}
|
2017-03-11 02:30:44 -05:00
|
|
|
|
2017-05-16 14:42:06 -04:00
|
|
|
const loadingState = GfmAutoComplete.defaultLoadingData[0];
|
2018-10-30 16:28:31 -04:00
|
|
|
return dataToInspect && (dataToInspect === loadingState || dataToInspect.name === loadingState);
|
2017-03-11 02:30:44 -05:00
|
|
|
}
|
2017-11-27 22:51:03 -05:00
|
|
|
|
|
|
|
static defaultMatcher(flag, subtext, controllers) {
|
|
|
|
// The below is taken from At.js source
|
|
|
|
// Tweaked to commands to start without a space only if char before is a non-word character
|
|
|
|
// https://github.com/ichord/At.js
|
2018-10-30 16:28:31 -04:00
|
|
|
const atSymbolsWithBar = Object.keys(controllers)
|
|
|
|
.join('|')
|
2020-11-17 16:09:19 -05:00
|
|
|
.replace(/[$]/, '\\$&')
|
|
|
|
.replace(/([[\]:])/g, '\\$1');
|
|
|
|
|
2017-11-27 22:51:03 -05:00
|
|
|
const atSymbolsWithoutBar = Object.keys(controllers).join('');
|
|
|
|
const targetSubtext = subtext.split(GfmAutoComplete.regexSubtext).pop();
|
|
|
|
const resultantFlag = flag.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&');
|
|
|
|
|
|
|
|
const accentAChar = decodeURI('%C3%80');
|
|
|
|
const accentYChar = decodeURI('%C3%BF');
|
|
|
|
|
2019-06-25 06:19:29 -04:00
|
|
|
// Holy regex, batman!
|
2018-10-30 16:28:31 -04:00
|
|
|
const regexp = new RegExp(
|
2019-06-25 06:19:29 -04:00
|
|
|
`^(?:\\B|[^a-zA-Z0-9_\`${atSymbolsWithoutBar}]|\\s)${resultantFlag}(?!${atSymbolsWithBar})((?:[A-Za-z${accentAChar}-${accentYChar}0-9_'.+-:]|[^\\x00-\\x7a])*)$`,
|
2018-10-30 16:28:31 -04:00
|
|
|
'gi',
|
|
|
|
);
|
2017-11-27 22:51:03 -05:00
|
|
|
|
|
|
|
return regexp.exec(targetSubtext);
|
|
|
|
}
|
2017-05-16 14:42:06 -04:00
|
|
|
}
|
|
|
|
|
2017-11-27 22:51:03 -05:00
|
|
|
GfmAutoComplete.regexSubtext = new RegExp(/\s+/g);
|
|
|
|
|
2017-05-16 14:42:06 -04:00
|
|
|
GfmAutoComplete.defaultLoadingData = ['loading'];
|
|
|
|
|
|
|
|
GfmAutoComplete.atTypeMap = {
|
|
|
|
':': 'emojis',
|
|
|
|
'@': 'members',
|
|
|
|
'#': 'issues',
|
|
|
|
'!': 'mergeRequests',
|
2018-06-28 02:41:56 -04:00
|
|
|
'&': 'epics',
|
2017-05-16 14:42:06 -04:00
|
|
|
'~': 'labels',
|
|
|
|
'%': 'milestones',
|
|
|
|
'/': 'commands',
|
2020-11-17 16:09:19 -05:00
|
|
|
'[vulnerability:': 'vulnerabilities',
|
2018-10-05 05:42:38 -04:00
|
|
|
$: 'snippets',
|
2017-05-16 14:42:06 -04:00
|
|
|
};
|
|
|
|
|
2020-11-17 16:09:19 -05:00
|
|
|
GfmAutoComplete.typesWithBackendFiltering = ['vulnerabilities'];
|
2020-12-23 19:10:25 -05:00
|
|
|
GfmAutoComplete.isTypeWithBackendFiltering = (type) =>
|
2020-11-17 16:09:19 -05:00
|
|
|
GfmAutoComplete.typesWithBackendFiltering.includes(GfmAutoComplete.atTypeMap[type]);
|
|
|
|
|
2017-05-16 14:42:06 -04:00
|
|
|
// Emoji
|
2017-06-27 02:26:38 -04:00
|
|
|
GfmAutoComplete.glEmojiTag = null;
|
2017-05-16 14:42:06 -04:00
|
|
|
GfmAutoComplete.Emoji = {
|
2020-10-13 05:08:27 -04:00
|
|
|
insertTemplateFunction(value) {
|
2021-02-06 04:09:11 -05:00
|
|
|
return `:${value.emoji.name}:`;
|
2020-10-13 05:08:27 -04:00
|
|
|
},
|
2021-02-06 04:09:11 -05:00
|
|
|
templateFunction(item) {
|
|
|
|
if (GfmAutoComplete.isLoading(item)) {
|
|
|
|
return GfmAutoComplete.Loading.template;
|
|
|
|
}
|
|
|
|
|
|
|
|
const escapedFieldValue = escape(item.fieldValue);
|
|
|
|
if (!GfmAutoComplete.glEmojiTag) {
|
|
|
|
return `<li>${escapedFieldValue}</li>`;
|
|
|
|
}
|
2020-10-13 05:08:27 -04:00
|
|
|
|
2021-02-06 04:09:11 -05:00
|
|
|
return `<li>${escapedFieldValue} ${GfmAutoComplete.glEmojiTag(item.emoji.name)}</li>`;
|
|
|
|
},
|
|
|
|
filter(query) {
|
|
|
|
if (query.length === 0) {
|
|
|
|
return Object.values(Emoji.getAllEmoji())
|
|
|
|
.map((emoji) => ({
|
|
|
|
emoji,
|
|
|
|
fieldValue: emoji.name,
|
|
|
|
}))
|
|
|
|
.slice(0, 20);
|
2020-10-14 11:08:42 -04:00
|
|
|
}
|
|
|
|
|
2021-02-06 04:09:11 -05:00
|
|
|
return Emoji.searchEmoji(query);
|
|
|
|
},
|
|
|
|
sorter(items) {
|
|
|
|
return Emoji.sortEmoji(items);
|
2017-05-16 14:42:06 -04:00
|
|
|
},
|
2017-03-11 02:30:44 -05:00
|
|
|
};
|
2017-05-16 14:42:06 -04:00
|
|
|
// Team Members
|
|
|
|
GfmAutoComplete.Members = {
|
2020-11-17 10:09:28 -05:00
|
|
|
templateFunction({ avatarTag, username, title, icon, availabilityStatus }) {
|
|
|
|
return `<li>${avatarTag} ${username} <small>${escape(
|
|
|
|
title,
|
|
|
|
)}${availabilityStatus}</small> ${icon}</li>`;
|
2018-11-12 04:21:25 -05:00
|
|
|
},
|
2021-02-22 13:10:55 -05:00
|
|
|
nameOrUsernameStartsWith(member, query) {
|
|
|
|
// `member.search` is a name:username string like `MargeSimpson msimpson`
|
|
|
|
return member.search.split(' ').some((name) => name.toLowerCase().startsWith(query));
|
|
|
|
},
|
|
|
|
nameOrUsernameIncludes(member, query) {
|
|
|
|
// `member.search` is a name:username string like `MargeSimpson msimpson`
|
|
|
|
return member.search.toLowerCase().includes(query);
|
|
|
|
},
|
2021-04-09 14:09:24 -04:00
|
|
|
sort(query, members) {
|
|
|
|
const lowercaseQuery = query.toLowerCase();
|
|
|
|
const { nameOrUsernameStartsWith, nameOrUsernameIncludes } = GfmAutoComplete.Members;
|
|
|
|
|
2021-05-13 23:10:11 -04:00
|
|
|
return sortBy(
|
|
|
|
members.filter((member) => nameOrUsernameIncludes(member, lowercaseQuery)),
|
2021-04-09 14:09:24 -04:00
|
|
|
(member) => (nameOrUsernameStartsWith(member, lowercaseQuery) ? -1 : 0),
|
2021-05-13 23:10:11 -04:00
|
|
|
);
|
2021-04-09 14:09:24 -04:00
|
|
|
},
|
2017-05-16 14:42:06 -04:00
|
|
|
};
|
|
|
|
GfmAutoComplete.Labels = {
|
2018-12-11 07:22:08 -05:00
|
|
|
templateFunction(color, title) {
|
2020-04-21 14:09:31 -04:00
|
|
|
return `<li><span class="dropdown-label-box" style="background: ${escape(
|
|
|
|
color,
|
|
|
|
)}"></span> ${escape(title)}</li>`;
|
2018-12-11 07:22:08 -05:00
|
|
|
},
|
2017-05-16 14:42:06 -04:00
|
|
|
};
|
2018-10-05 05:42:38 -04:00
|
|
|
// Issues, MergeRequests and Snippets
|
2017-05-16 14:42:06 -04:00
|
|
|
GfmAutoComplete.Issues = {
|
2018-12-21 03:49:44 -05:00
|
|
|
insertTemplateFunction(value) {
|
|
|
|
// eslint-disable-next-line no-template-curly-in-string
|
|
|
|
return value.reference || '${atwho-at}${id}';
|
|
|
|
},
|
|
|
|
templateFunction({ id, title, reference }) {
|
2020-04-21 14:09:31 -04:00
|
|
|
return `<li><small>${reference || id}</small> ${escape(title)}</li>`;
|
2018-10-19 04:17:56 -04:00
|
|
|
},
|
2017-05-16 14:42:06 -04:00
|
|
|
};
|
|
|
|
// Milestones
|
|
|
|
GfmAutoComplete.Milestones = {
|
2021-03-10 07:09:14 -05:00
|
|
|
templateFunction(title, expired) {
|
|
|
|
if (expired) {
|
|
|
|
return `<li>${sprintf(__('%{milestone} (expired)'), {
|
|
|
|
milestone: escape(title),
|
|
|
|
})}</li>`;
|
|
|
|
}
|
2020-04-21 14:09:31 -04:00
|
|
|
return `<li>${escape(title)}</li>`;
|
2018-12-11 07:22:08 -05:00
|
|
|
},
|
2017-05-16 14:42:06 -04:00
|
|
|
};
|
|
|
|
GfmAutoComplete.Loading = {
|
2018-10-30 16:28:31 -04:00
|
|
|
template:
|
2020-02-17 10:09:01 -05:00
|
|
|
'<li style="pointer-events: none;"><span class="spinner align-text-bottom mr-1"></span>Loading...</li>',
|
2017-05-16 14:42:06 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
export default GfmAutoComplete;
|