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:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-12-07 21:07:33 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-12-07 21:07:33 +0300
commit1bdc6c89c32a7380a81598629b9ad05ba9a2a94f (patch)
tree778f1dc16130b3138ab3b641e664038648046a40 /app/assets/javascripts
parent9a940dabf04df126e7978c0ab4b8770b86dcaaa8 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts')
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor.vue2
-rw-r--r--app/assets/javascripts/content_editor/components/suggestions_dropdown.vue124
-rw-r--r--app/assets/javascripts/content_editor/extensions/emoji.js2
-rw-r--r--app/assets/javascripts/content_editor/extensions/suggestions.js171
-rw-r--r--app/assets/javascripts/content_editor/services/create_content_editor.js9
-rw-r--r--app/assets/javascripts/content_editor/services/data_source_factory.js213
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js4
-rw-r--r--app/assets/javascripts/groups/init_overview_tabs.js2
-rw-r--r--app/assets/javascripts/groups/service/archived_projects_service.js2
9 files changed, 363 insertions, 166 deletions
diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue
index 2e9388c1e20..a48245f732d 100644
--- a/app/assets/javascripts/content_editor/components/content_editor.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor.vue
@@ -6,6 +6,7 @@ import { VARIANT_DANGER } from '~/alert';
import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue';
import { CONTENT_EDITOR_READY_EVENT } from '~/vue_shared/constants';
import markdownEditorEventHub from '~/vue_shared/components/markdown/eventhub';
+import SidebarMediator from '~/sidebar/sidebar_mediator';
import { createContentEditor } from '../services/create_content_editor';
import { ALERT_EVENT, TIPTAP_AUTOFOCUS_OPTIONS } from '../constants';
import ContentEditorAlert from './content_editor_alert.vue';
@@ -157,6 +158,7 @@ export default {
enableAutocomplete,
autocompleteDataSources,
codeSuggestionsConfig,
+ sidebarMediator: SidebarMediator.singleton,
tiptapOptions: {
autofocus,
editable,
diff --git a/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue b/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue
index b34ebe85eb4..dcca76b0786 100644
--- a/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue
+++ b/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue
@@ -1,12 +1,17 @@
<script>
-import { GlAvatarLabeled, GlLoadingIcon } from '@gitlab/ui';
+import { GlAvatar, GlLoadingIcon } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
export default {
components: {
- GlAvatarLabeled,
+ GlAvatar,
GlLoadingIcon,
},
+ directives: {
+ SafeHtml,
+ },
+
props: {
char: {
type: String,
@@ -38,6 +43,12 @@ export default {
required: false,
default: false,
},
+
+ query: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
@@ -90,20 +101,30 @@ export default {
isEmoji() {
return this.nodeType === 'emoji';
},
+
+ shouldSelectFirstItem() {
+ return this.items.length && this.query;
+ },
},
watch: {
items() {
- this.selectedIndex = -1;
+ this.selectedIndex = this.shouldSelectFirstItem ? 0 : -1;
},
selectedIndex() {
this.scrollIntoView();
},
},
+ mounted() {
+ if (this.shouldSelectFirstItem) {
+ this.selectedIndex = 0;
+ }
+ },
+
methods: {
getText(item) {
- if (this.isEmoji) return item.e;
+ if (this.isEmoji) return item.emoji.e;
switch (this.isReference && this.nodeProps.referenceType) {
case 'user':
@@ -133,10 +154,10 @@ export default {
if (this.isEmoji) {
Object.assign(props, {
- name: item.name,
- unicodeVersion: item.u,
- title: item.d,
- moji: item.e,
+ name: item.emoji.name,
+ unicodeVersion: item.emoji.u,
+ title: item.emoji.d,
+ moji: item.emoji.e,
});
}
@@ -173,7 +194,7 @@ export default {
return true;
}
- if (event.key === 'Enter') {
+ if (event.key === 'Enter' || event.key === 'Tab') {
this.enterHandler();
return true;
}
@@ -194,7 +215,7 @@ export default {
},
scrollIntoView() {
- this.$refs.dropdownItems[this.selectedIndex]?.scrollIntoView({ block: 'nearest' });
+ this.$refs.dropdownItems?.[this.selectedIndex]?.scrollIntoView({ block: 'nearest' });
},
selectItem(index) {
@@ -211,7 +232,17 @@ export default {
avatarSubLabel(item) {
return item.count ? `${item.name} (${item.count})` : item.name;
},
+
+ highlight(text) {
+ return this.query
+ ? String(text).replace(
+ new RegExp(this.query, 'i'),
+ (match) => `<strong class="gl-text-body!">${match}</strong>`,
+ )
+ : text;
+ },
},
+ safeHtmlConfig: { ALLOWED_TAGS: ['strong'] },
};
</script>
@@ -238,29 +269,45 @@ export default {
@click="selectItem(index)"
>
<div class="gl-new-dropdown-item-text-wrapper">
- <gl-avatar-labeled
- v-if="isUser"
- :label="item.username"
- :sub-label="avatarSubLabel(item)"
- :src="item.avatar_url"
- :entity-name="item.username"
- :shape="item.type === 'Group' ? 'rect' : 'circle'"
- :size="32"
- />
+ <span v-if="isUser" class="gl-flex">
+ <gl-avatar
+ :src="item.avatar_url"
+ :entity-name="item.username"
+ :size="24"
+ :shape="item.type === 'Group' ? 'rect' : 'circle'"
+ class="gl-vertical-align-middle gl-mx-2"
+ />
+ <span class="gl-vertical-align-middle">
+ <span v-safe-html:safeHtmlConfig="highlight(item.username)"></span>
+ <small
+ v-safe-html:safeHtmlConfig="highlight(avatarSubLabel(item))"
+ class="gl-text-gray-500"
+ ></small>
+ </span>
+ </span>
<span v-if="isIssue || isMergeRequest">
- <small>{{ item.iid }}</small>
- {{ item.title }}
+ <small
+ v-safe-html:safeHtmlConfig="highlight(item.iid)"
+ class="gl-text-gray-500"
+ ></small>
+ <span v-safe-html:safeHtmlConfig="highlight(item.title)"></span>
</span>
<span v-if="isVulnerability || isSnippet">
- <small>{{ item.id }}</small>
- {{ item.title }}
+ <small
+ v-safe-html:safeHtmlConfig="highlight(item.id)"
+ class="gl-text-gray-500"
+ ></small>
+ <span v-safe-html:safeHtmlConfig="highlight(item.title)"></span>
</span>
<span v-if="isEpic">
- <small>{{ item.reference }}</small>
- {{ item.title }}
+ <small
+ v-safe-html:safeHtmlConfig="highlight(item.reference)"
+ class="gl-text-gray-500"
+ ></small>
+ <span v-safe-html:safeHtmlConfig="highlight(item.title)"></span>
</span>
<span v-if="isMilestone">
- {{ item.title }}
+ <span v-safe-html:safeHtmlConfig="highlight(item.title)"></span>
</span>
<span v-if="isLabel" class="gl-display-flex">
<span
@@ -268,20 +315,31 @@ export default {
class="dropdown-label-box gl-flex-shrink-0 gl-top-0 gl-mr-3"
:style="{ backgroundColor: item.color }"
></span>
- {{ item.title }}
+ <span v-safe-html:safeHtmlConfig="highlight(item.title)"></span>
</span>
<div v-if="isCommand">
<div class="gl-mb-1">
- <span class="gl-font-weight-bold">/{{ item.name }}</span>
- <em class="gl-text-gray-500 gl-font-sm">{{ item.params[0] }}</em>
+ /<span v-safe-html:safeHtmlConfig="highlight(item.name)"></span>
+ <span class="gl-text-gray-500 gl-font-sm">{{ item.params[0] }}</span>
</div>
- <small class="gl-text-gray-500"> {{ item.description }} </small>
+ <em
+ v-safe-html:safeHtmlConfig="highlight(item.description)"
+ class="gl-text-gray-500 gl-font-sm"
+ ></em>
</div>
<div v-if="isEmoji" class="gl-display-flex gl-align-items-center">
- <div class="gl-pr-4 gl-font-lg">{{ item.e }}</div>
+ <div class="gl-pr-4 gl-font-lg">
+ <gl-emoji
+ :key="item.emoji.e"
+ :data-name="item.emoji.name"
+ :title="item.emoji.d"
+ :data-unicode-version="item.emoji.u"
+ :data-fallback-src="item.emoji.src"
+ >{{ item.emoji.e }}</gl-emoji
+ >
+ </div>
<div class="gl-flex-grow-1">
- {{ item.name }}<br />
- <small>{{ item.d }}</small>
+ <span v-safe-html:safeHtmlConfig="highlight(item.fieldValue)"></span>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/content_editor/extensions/emoji.js b/app/assets/javascripts/content_editor/extensions/emoji.js
index be6ecb6cafd..96e03dfe598 100644
--- a/app/assets/javascripts/content_editor/extensions/emoji.js
+++ b/app/assets/javascripts/content_editor/extensions/emoji.js
@@ -46,7 +46,7 @@ export default Node.create({
title: node.attrs.title,
'data-unicode-version': node.attrs.unicodeVersion,
},
- node.attrs.moji,
+ node.attrs.moji || '',
];
},
diff --git a/app/assets/javascripts/content_editor/extensions/suggestions.js b/app/assets/javascripts/content_editor/extensions/suggestions.js
index f7ff2fd6647..d309210404a 100644
--- a/app/assets/javascripts/content_editor/extensions/suggestions.js
+++ b/app/assets/javascripts/content_editor/extensions/suggestions.js
@@ -3,29 +3,20 @@ import { VueRenderer } from '@tiptap/vue-2';
import tippy from 'tippy.js';
import Suggestion from '@tiptap/suggestion';
import { PluginKey } from '@tiptap/pm/state';
-import { isFunction, uniqueId, memoize } from 'lodash';
-import axios from '~/lib/utils/axios_utils';
-import { initEmojiMap, getAllEmoji } from '~/emoji';
+import { uniqueId } from 'lodash';
import SuggestionsDropdown from '../components/suggestions_dropdown.vue';
-function find(haystack, needle) {
- return String(haystack).toLocaleLowerCase().includes(String(needle).toLocaleLowerCase());
-}
-
function createSuggestionPlugin({
editor,
char,
- dataSource,
- search,
- limit = 15,
+ limit = 5,
nodeType,
- nodeProps = {},
+ referenceType,
+ cache = true,
insertionMap = {},
+ serializer,
+ autocompleteHelper,
}) {
- const fetchData = memoize(
- isFunction(dataSource) ? dataSource : async () => (await axios.get(dataSource)).data,
- );
-
return Suggestion({
editor,
char,
@@ -42,16 +33,17 @@ function createSuggestionPlugin({
.run();
},
- async items({ query }) {
- if (!dataSource) return [];
-
- try {
- const items = await fetchData();
-
- return items.filter(search(query)).slice(0, limit);
- } catch {
- return [];
- }
+ async items({ query, editor: tiptapEditor }) {
+ const slice = tiptapEditor.state.doc.slice(0, tiptapEditor.state.selection.to);
+ const markdownLine = serializer.serialize({ doc: slice.content }).split('\n').pop();
+
+ return autocompleteHelper
+ .getDataSource(referenceType, {
+ command: markdownLine.match(/\/\w+/)?.[0],
+ cache,
+ limit,
+ })
+ .search(query);
},
render: () => {
@@ -76,7 +68,7 @@ function createSuggestionPlugin({
...props,
char,
nodeType,
- nodeProps,
+ nodeProps: { referenceType },
loading: true,
},
editor: props.editor,
@@ -132,101 +124,38 @@ export default Node.create({
addOptions() {
return {
- autocompleteDataSources: {},
+ autocompleteHelper: {},
+ serializer: null,
};
},
addProseMirrorPlugins() {
- return [
- createSuggestionPlugin({
- editor: this.editor,
- char: '@',
- dataSource: this.options.autocompleteDataSources.members,
- nodeType: 'reference',
- nodeProps: {
- referenceType: 'user',
- },
- search: (query) => ({ name, username }) => find(name, query) || find(username, query),
- }),
- createSuggestionPlugin({
- editor: this.editor,
- char: '#',
- dataSource: this.options.autocompleteDataSources.issues,
- nodeType: 'reference',
- nodeProps: {
- referenceType: 'issue',
- },
- search: (query) => ({ iid, title }) => find(iid, query) || find(title, query),
- }),
- createSuggestionPlugin({
- editor: this.editor,
- char: '$',
- dataSource: this.options.autocompleteDataSources.snippets,
- nodeType: 'reference',
- nodeProps: {
- referenceType: 'snippet',
- },
- search: (query) => ({ id, title }) => find(id, query) || find(title, query),
- }),
- createSuggestionPlugin({
- editor: this.editor,
- char: '~',
- dataSource: this.options.autocompleteDataSources.labels,
- nodeType: 'referenceLabel',
- nodeProps: {
- referenceType: 'label',
- },
- search: (query) => ({ title }) => find(title, query),
- }),
- createSuggestionPlugin({
- editor: this.editor,
- char: '&',
- dataSource: this.options.autocompleteDataSources.epics,
- nodeType: 'reference',
- nodeProps: {
- referenceType: 'epic',
- },
- search: (query) => ({ iid, title }) => find(iid, query) || find(title, query),
- }),
- createSuggestionPlugin({
- editor: this.editor,
- char: '[vulnerability:',
- dataSource: this.options.autocompleteDataSources.vulnerabilities,
- nodeType: 'reference',
- nodeProps: {
- referenceType: 'vulnerability',
- },
- search: (query) => ({ id, title }) => find(id, query) || find(title, query),
- }),
- createSuggestionPlugin({
- editor: this.editor,
- char: '!',
- dataSource: this.options.autocompleteDataSources.mergeRequests,
- nodeType: 'reference',
- nodeProps: {
- referenceType: 'merge_request',
- },
- search: (query) => ({ iid, title }) => find(iid, query) || find(title, query),
- }),
- createSuggestionPlugin({
- editor: this.editor,
- char: '%',
- dataSource: this.options.autocompleteDataSources.milestones,
- nodeType: 'reference',
- nodeProps: {
- referenceType: 'milestone',
- },
- search: (query) => ({ iid, title }) => find(iid, query) || find(title, query),
- }),
+ const { serializer, autocompleteHelper } = this.options;
+
+ const createPlugin = (char, nodeType, referenceType, options = {}) =>
createSuggestionPlugin({
editor: this.editor,
- char: '/',
- dataSource: this.options.autocompleteDataSources.commands,
- nodeType: 'reference',
- nodeProps: {
- referenceType: 'command',
- },
- search: (query) => ({ name }) => find(name, query),
+ char,
+ nodeType,
+ referenceType,
+ serializer,
+ autocompleteHelper,
+ ...options,
+ });
+
+ return [
+ createPlugin('@', 'reference', 'user', { limit: 10 }),
+ createPlugin('#', 'reference', 'issue'),
+ createPlugin('$', 'reference', 'snippet'),
+ createPlugin('~', 'referenceLabel', 'label', { limit: 20 }),
+ createPlugin('&', 'reference', 'epic'),
+ createPlugin('!', 'reference', 'merge_request'),
+ createPlugin('[vulnerability:', 'reference', 'vulnerability'),
+ createPlugin('%', 'reference', 'milestone'),
+ createPlugin(':', 'emoji', 'emoji'),
+ createPlugin('/', 'reference', 'command', {
+ cache: false,
+ limit: 100,
insertionMap: {
'/label': '~',
'/unlabel': '~',
@@ -241,18 +170,6 @@ export default Node.create({
'/milestone': '%',
},
}),
- createSuggestionPlugin({
- editor: this.editor,
- char: ':',
- dataSource: () => getAllEmoji(),
- nodeType: 'emoji',
- search: (query) => ({ d, name }) => find(d, query) || find(name, query),
- limit: 10,
- }),
];
},
-
- onCreate() {
- initEmojiMap();
- },
});
diff --git a/app/assets/javascripts/content_editor/services/create_content_editor.js b/app/assets/javascripts/content_editor/services/create_content_editor.js
index 51e41ceefaf..5c48c0b1d43 100644
--- a/app/assets/javascripts/content_editor/services/create_content_editor.js
+++ b/app/assets/javascripts/content_editor/services/create_content_editor.js
@@ -70,6 +70,7 @@ import createGlApiMarkdownDeserializer from './gl_api_markdown_deserializer';
import createRemarkMarkdownDeserializer from './remark_markdown_deserializer';
import AssetResolver from './asset_resolver';
import trackInputRulesAndShortcuts from './track_input_rules_and_shortcuts';
+import DataSourceFactory from './data_source_factory';
const createTiptapEditor = ({ extensions = [], ...options } = {}) =>
new Editor({
@@ -86,6 +87,7 @@ export const createContentEditor = ({
drawioEnabled = false,
enableAutocomplete,
autocompleteDataSources = {},
+ sidebarMediator = {},
codeSuggestionsConfig = {},
} = {}) => {
if (!isFunction(renderMarkdown)) {
@@ -95,6 +97,10 @@ export const createContentEditor = ({
const eventHub = eventHubFactory();
const assetResolver = new AssetResolver({ renderMarkdown });
const serializer = new MarkdownSerializer({ serializerConfig });
+ const autocompleteHelper = new DataSourceFactory({
+ dataSourceUrls: autocompleteDataSources,
+ sidebarMediator,
+ });
const deserializer = window.gon?.features?.preserveUnchangedMarkdown
? createRemarkMarkdownDeserializer()
: createGlApiMarkdownDeserializer({
@@ -166,7 +172,8 @@ export const createContentEditor = ({
const allExtensions = [...builtInContentEditorExtensions, ...extensions];
- if (enableAutocomplete) allExtensions.push(Suggestions.configure({ autocompleteDataSources }));
+ if (enableAutocomplete)
+ allExtensions.push(Suggestions.configure({ autocompleteHelper, serializer }));
if (drawioEnabled) allExtensions.push(DrawioDiagram.configure({ uploadsPath, assetResolver }));
const trackedExtensions = allExtensions.map(trackInputRulesAndShortcuts);
diff --git a/app/assets/javascripts/content_editor/services/data_source_factory.js b/app/assets/javascripts/content_editor/services/data_source_factory.js
new file mode 100644
index 00000000000..a0f0e106f1d
--- /dev/null
+++ b/app/assets/javascripts/content_editor/services/data_source_factory.js
@@ -0,0 +1,213 @@
+import { identity, memoize, throttle } from 'lodash';
+import { sprintf, __ } from '~/locale';
+import { initEmojiMap, getAllEmoji, searchEmoji } from '~/emoji';
+import { parsePikadayDate } from '~/lib/utils/datetime_utility';
+import axios from '~/lib/utils/axios_utils';
+
+export function defaultSorter(searchFields) {
+ return (items, query) => {
+ if (!query) return items;
+
+ const sortOrdersMap = new WeakMap();
+
+ items.forEach((item) => {
+ const sortOrders = searchFields.map((searchField) => {
+ const haystack = String(item[searchField]).toLocaleLowerCase();
+ const needle = query.toLocaleLowerCase();
+
+ const i = haystack.indexOf(needle);
+ if (i < 0) return i;
+ return Number.MAX_SAFE_INTEGER - i;
+ });
+
+ sortOrdersMap.set(item, Math.max(...sortOrders));
+ });
+
+ return items.sort((a, b) => sortOrdersMap.get(b) - sortOrdersMap.get(a));
+ };
+}
+
+export function customSorter(sorter) {
+ return (items) => items.sort(sorter);
+}
+
+const milestonesMap = new WeakMap();
+
+function parseMilestone(milestone) {
+ if (!milestone.title) {
+ return milestone;
+ }
+
+ const dueDate = milestone.due_date ? parsePikadayDate(milestone.due_date) : null;
+ const expired = dueDate ? Date.now() > dueDate.getTime() : false;
+
+ return {
+ id: milestone.iid,
+ title: expired
+ ? sprintf(__('%{milestone} (expired)'), {
+ milestone: milestone.title,
+ })
+ : milestone.title,
+ expired,
+ dueDate,
+ };
+}
+
+function mapMilestone(milestone) {
+ if (!milestonesMap.has(milestone)) {
+ milestonesMap.set(milestone, parseMilestone(milestone));
+ }
+
+ return milestonesMap.get(milestone);
+}
+
+function sortMilestones(milestoneA, milestoneB) {
+ const mappedA = mapMilestone(milestoneA);
+ const mappedB = mapMilestone(milestoneB);
+
+ // Move all expired milestones to the bottom.
+ if (milestoneA.expired) return 1;
+ if (milestoneB.expired) return -1;
+
+ // Move milestones without due dates just above expired milestones.
+ if (!milestoneA.dueDate) return 1;
+ if (!milestoneB.dueDate) return -1;
+
+ return mappedA.dueDate - mappedB.dueDate;
+}
+
+export function createDataSource({
+ source,
+ searchFields,
+ filter,
+ mapper = identity,
+ sorter = defaultSorter(searchFields),
+ cache = true,
+ limit = 15,
+}) {
+ const fetchData = source ? async () => (await axios.get(source)).data : () => [];
+ let items = [];
+
+ const sync = async function sync() {
+ try {
+ items = await fetchData();
+ } catch {
+ items = [];
+ }
+ };
+
+ const init = memoize(sync);
+ const throttledSync = throttle(sync, 5000);
+
+ return {
+ search: async (query) => {
+ await init();
+ if (!cache) throttledSync();
+
+ let results = items.map(mapper);
+ if (filter) results = filter(items, query);
+
+ if (query) {
+ results = results.filter((item) => {
+ if (!searchFields.length) return true;
+ return searchFields.some((field) =>
+ String(item[field]).toLocaleLowerCase().includes(query.toLocaleLowerCase()),
+ );
+ });
+ }
+
+ return sorter(results, query).slice(0, limit);
+ },
+ };
+}
+
+export default class DataSourceFactory {
+ constructor({ dataSourceUrls, sidebarMediator }) {
+ this.dataSourceUrls = dataSourceUrls;
+ this.sidebarMediator = sidebarMediator;
+
+ initEmojiMap();
+ }
+
+ getDataSource = memoize(
+ (referenceType, config = {}) => {
+ const sources = {
+ user: this.dataSourceUrls.members,
+ issue: this.dataSourceUrls.issues,
+ snippet: this.dataSourceUrls.snippets,
+ label: this.dataSourceUrls.labels,
+ epic: this.dataSourceUrls.epics,
+ milestone: this.dataSourceUrls.milestones,
+ merge_request: this.dataSourceUrls.mergeRequests,
+ vulnerability: this.dataSourceUrls.vulnerabilities,
+ command: this.dataSourceUrls.commands,
+ };
+
+ const searchFields = {
+ user: ['username', 'name'],
+ issue: ['iid', 'title'],
+ snippet: ['id', 'title'],
+ label: ['title'],
+ epic: ['iid', 'title'],
+ vulnerability: ['id', 'title'],
+ merge_request: ['iid', 'title'],
+ milestone: ['title', 'iid'],
+ command: ['name'],
+ emoji: [],
+ };
+
+ const filters = {
+ label: (items) =>
+ items.filter((item) => {
+ if (config.command === '/unlabel') return item.set;
+ if (config.command === '/label') return !item.set;
+
+ return true;
+ }),
+ user: (items) =>
+ items.filter((item) => {
+ const assigned = this.sidebarMediator?.store?.assignees.some(
+ (assignee) => assignee.username === item.username,
+ );
+ const assignedReviewer = this.sidebarMediator?.store?.reviewers.some(
+ (reviewer) => reviewer.username === item.username,
+ );
+
+ if (config.command === '/assign') return !assigned;
+ if (config.command === '/assign_reviewer') return !assignedReviewer;
+ if (config.command === '/unassign') return assigned;
+ if (config.command === '/unassign_reviewer') return assignedReviewer;
+
+ return true;
+ }),
+ emoji: (_, query) =>
+ query
+ ? searchEmoji(query)
+ : getAllEmoji().map((emoji) => ({ emoji, fieldValue: emoji.name })),
+ };
+
+ const sorters = {
+ milestone: customSorter(sortMilestones),
+ default: defaultSorter(searchFields[referenceType]),
+ // do not sort emoji
+ emoji: customSorter(() => 0),
+ };
+
+ const mappers = {
+ milestone: mapMilestone,
+ default: identity,
+ };
+
+ return createDataSource({
+ source: sources[referenceType],
+ searchFields: searchFields[referenceType],
+ mapper: mappers[referenceType] || mappers.default,
+ sorter: sorters[referenceType] || sorters.default,
+ filter: filters[referenceType],
+ cache: config.cache,
+ limit: config.limit,
+ });
+ },
+ (referenceType, config) => JSON.stringify({ referenceType, config }),
+ );
+}
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index e116f64f927..be76ce2c28b 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -82,8 +82,8 @@ export function membersBeforeSave(members) {
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 imgAvatar = `<img src="${member.avatar_url}" alt="${member.username}" class="avatar ${rectAvatarClass} avatar-inline s24 gl-mr-2"/>`;
+ const txtAvatar = `<div class="avatar ${rectAvatarClass} avatar-inline s24 gl-mr-2">${autoCompleteAvatar}</div>`;
const avatarIcon = member.mentionsDisabled
? spriteIcon('notifications-off', 's16 vertical-align-middle gl-ml-2')
: '';
diff --git a/app/assets/javascripts/groups/init_overview_tabs.js b/app/assets/javascripts/groups/init_overview_tabs.js
index b831ae7b9d6..80dd1d36734 100644
--- a/app/assets/javascripts/groups/init_overview_tabs.js
+++ b/app/assets/javascripts/groups/init_overview_tabs.js
@@ -21,7 +21,7 @@ export const createRouter = () => {
const router = new VueRouter({
routes,
mode: 'history',
- base: '/',
+ base: gon.relative_url_root || '/',
});
return router;
diff --git a/app/assets/javascripts/groups/service/archived_projects_service.js b/app/assets/javascripts/groups/service/archived_projects_service.js
index b9d48cc660e..7558b8d6713 100644
--- a/app/assets/javascripts/groups/service/archived_projects_service.js
+++ b/app/assets/javascripts/groups/service/archived_projects_service.js
@@ -32,7 +32,7 @@ export default class ArchivedProjectsService {
markdown_description: project.description_html,
visibility: project.visibility,
avatar_url: project.avatar_url,
- relative_path: `/${project.path_with_namespace}`,
+ relative_path: `${gon.relative_url_root}/${project.path_with_namespace}`,
edit_path: null,
leave_path: null,
can_edit: false,