diff --git a/app/assets/javascripts/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue new file mode 100644 index 00000000000..c45666e69eb --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue @@ -0,0 +1,90 @@ + diff --git a/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js new file mode 100644 index 00000000000..b2e995d0f17 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/gfm_autocomplete/utils.js @@ -0,0 +1,142 @@ +import { escape, last } from 'lodash'; +import { spriteIcon } from '~/lib/utils/common_utils'; + +const groupType = 'Group'; // eslint-disable-line @gitlab/require-i18n-strings + +const nonWordOrInteger = /\W|^\d+$/; + +export const GfmAutocompleteType = { + Issues: 'issues', + Labels: 'labels', + Members: 'members', + MergeRequests: 'mergeRequests', + Milestones: 'milestones', + Snippets: 'snippets', +}; + +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); +} + +export const tributeConfig = { + [GfmAutocompleteType.Issues]: { + config: { + trigger: '#', + lookup: value => value.iid + value.title, + menuItemTemplate: ({ original }) => + `${original.reference || original.iid} ${escape(original.title)}`, + selectTemplate: ({ original }) => original.reference || `#${original.iid}`, + }, + }, + + [GfmAutocompleteType.Labels]: { + config: { + trigger: '~', + lookup: 'title', + menuItemTemplate: ({ original }) => ` + + ${escape(original.title)}`, + selectTemplate: ({ original }) => + nonWordOrInteger.test(original.title) + ? `~"${escape(original.title)}"` + : `~${escape(original.title)}`, + }, + filterValues({ collection, fullText, selectionStart }) { + if (doesCurrentLineStartWith('/label', fullText, selectionStart)) { + return collection.filter(label => !label.set); + } + + if (doesCurrentLineStartWith('/unlabel', fullText, selectionStart)) { + return collection.filter(label => label.set); + } + + return collection; + }, + }, + + [GfmAutocompleteType.Members]: { + config: { + trigger: '@', + fillAttr: 'username', + lookup: value => + value.type === groupType ? last(value.name.split(' / ')) : value.name + value.username, + menuItemTemplate: ({ original }) => { + const commonClasses = 'gl-avatar gl-avatar-s24 gl-flex-shrink-0'; + const noAvatarClasses = `${commonClasses} gl-rounded-small + gl-display-flex gl-align-items-center gl-justify-content-center`; + + const avatar = original.avatar_url + ? `` + : ``; + + let displayName = original.name; + let parentGroupOrUsername = `@${original.username}`; + + if (original.type === groupType) { + const splitName = original.name.split(' / '); + displayName = splitName.pop(); + parentGroupOrUsername = splitName.pop(); + } + + const count = original.count && !original.mentionsDisabled ? ` (${original.count})` : ''; + + const disabledMentionsIcon = original.mentionsDisabled + ? spriteIcon('notifications-off', 's16 gl-ml-3') + : ''; + + return ` +
+ ${avatar} +
+
${escape(displayName)}${count}
+
${escape(parentGroupOrUsername)}
+
+ ${disabledMentionsIcon} +
+ `; + }, + }, + filterValues({ assignees, collection, fullText, selectionStart }) { + if (doesCurrentLineStartWith('/assign', fullText, selectionStart)) { + return collection.filter(member => !assignees.includes(member.username)); + } + + if (doesCurrentLineStartWith('/unassign', fullText, selectionStart)) { + return collection.filter(member => assignees.includes(member.username)); + } + + return collection; + }, + }, + + [GfmAutocompleteType.MergeRequests]: { + config: { + trigger: '!', + lookup: value => value.iid + value.title, + menuItemTemplate: ({ original }) => + `${original.reference || original.iid} ${escape(original.title)}`, + selectTemplate: ({ original }) => original.reference || `!${original.iid}`, + }, + }, + + [GfmAutocompleteType.Milestones]: { + config: { + trigger: '%', + lookup: 'title', + menuItemTemplate: ({ original }) => escape(original.title), + selectTemplate: ({ original }) => `%"${escape(original.title)}"`, + }, + }, + + [GfmAutocompleteType.Snippets]: { + config: { + trigger: '$', + fillAttr: 'id', + lookup: value => value.id + value.title, + menuItemTemplate: ({ original }) => `${original.id} ${escape(original.title)}`, + }, + }, +}; diff --git a/app/assets/javascripts/vue_shared/components/gl_mentions.vue b/app/assets/javascripts/vue_shared/components/gl_mentions.vue deleted file mode 100644 index dde7e3ebe13..00000000000 --- a/app/assets/javascripts/vue_shared/components/gl_mentions.vue +++ /dev/null @@ -1,238 +0,0 @@ - diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 9cfba85e0d8..0d703545073 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -10,14 +10,14 @@ import { deprecatedCreateFlash as Flash } from '~/flash'; import GLForm from '~/gl_form'; import MarkdownHeader from './header.vue'; import MarkdownToolbar from './toolbar.vue'; -import GlMentions from '~/vue_shared/components/gl_mentions.vue'; +import GfmAutocomplete from '~/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue'; import Suggestions from '~/vue_shared/components/markdown/suggestions.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import axios from '~/lib/utils/axios_utils'; export default { components: { - GlMentions, + GfmAutocomplete, MarkdownHeader, MarkdownToolbar, GlIcon, @@ -246,9 +246,9 @@ export default { />
- + - + Users**. You will find the option of the access level in + **Admin Area > Users**. The option of the access level is located in the 'Access' section. ![Admin Area Form](img/auditor_access_form.png) diff --git a/doc/administration/auth/ldap/index.md b/doc/administration/auth/ldap/index.md index 9d88d66bf46..a9823c36d44 100644 --- a/doc/administration/auth/ldap/index.md +++ b/doc/administration/auth/ldap/index.md @@ -170,7 +170,7 @@ production: | `label` | A human-friendly name for your LDAP server. It will be displayed on your sign-in page. | yes | `'Paris'` or `'Acme, Ltd.'` | | `host` | IP address or domain name of your LDAP server. | yes | `'ldap.mydomain.com'` | | `port` | The port to connect with on your LDAP server. Always an integer, not a string. | yes | `389` or `636` (for SSL) | -| `uid` | LDAP attribute for username. Should be the attribute, not the value that maps to the `uid`. | yes | `'sAMAccountName'`, `'uid'`, `'userPrincipalName'` | +| `uid` | LDAP attribute for username. Should be the attribute, not the value that maps to the `uid`. | yes | `'sAMAccountName'` or `'uid'` or `'userPrincipalName'` | | `bind_dn` | The full DN of the user you will bind with. | no | `'america\momo'` or `'CN=Gitlab,OU=Users,DC=domain,DC=com'` | | `password` | The password of the bind user. | no | `'your_great_password'` | | `encryption` | Encryption method. The `method` key is deprecated in favor of `encryption`. | yes | `'start_tls'` or `'simple_tls'` or `'plain'` | diff --git a/doc/administration/index.md b/doc/administration/index.md index 0aa94b86371..f667bdb5d2e 100644 --- a/doc/administration/index.md +++ b/doc/administration/index.md @@ -135,7 +135,7 @@ Learn how to install, configure, update, and maintain your GitLab instance. - [Issue closing pattern](issue_closing_pattern.md): Customize how to close an issue from commit messages. - [Gitaly](gitaly/index.md): Configuring Gitaly, GitLab's Git repository storage service. -- [Default labels](../user/admin_area/labels.md): Create labels that will be automatically added to every new project. +- [Default labels](../user/admin_area/labels.md): Create labels that are automatically added to every new project. - [Restrict the use of public or internal projects](../public_access/public_access.md#restricting-the-use-of-public-or-internal-projects): Restrict the use of visibility levels for users when they create a project or a snippet. - [Custom project templates](../user/admin_area/custom_project_templates.md): Configure a set of projects to be used as custom templates when creating a new project. **(PREMIUM ONLY)** diff --git a/doc/administration/libravatar.md b/doc/administration/libravatar.md index a92e6fade03..5d92a0162bf 100644 --- a/doc/administration/libravatar.md +++ b/doc/administration/libravatar.md @@ -41,7 +41,7 @@ the configuration options as follows: ### Your own Libravatar server If you are [running your own Libravatar service](https://wiki.libravatar.org/running_your_own/), -the URL will be different in the configuration, but you must provide the same +the URL is different in the configuration, but you must provide the same placeholders so GitLab can parse the URL correctly. For example, you host a service on `http://libravatar.example.com` and the diff --git a/doc/administration/load_balancer.md b/doc/administration/load_balancer.md index 410381ff2b0..1ab87fb4b4c 100644 --- a/doc/administration/load_balancer.md +++ b/doc/administration/load_balancer.md @@ -7,17 +7,17 @@ type: reference # Load Balancer for multi-node GitLab -In an multi-node GitLab configuration, you will need a load balancer to route +In an multi-node GitLab configuration, you need a load balancer to route traffic to the application servers. The specifics on which load balancer to use or the exact configuration is beyond the scope of GitLab documentation. We hope that if you're managing HA systems like GitLab you have a load balancer of choice already. Some examples including HAProxy (open-source), F5 Big-IP LTM, -and Citrix Net Scaler. This documentation will outline what ports and protocols +and Citrix Net Scaler. This documentation outlines what ports and protocols you need to use with GitLab. ## SSL -How will you handle SSL in your multi-node environment? There are several different +How do you want to handle SSL in your multi-node environment? There are several different options: - Each application node terminates SSL @@ -29,8 +29,8 @@ options: ### Application nodes terminate SSL Configure your load balancer(s) to pass connections on port 443 as 'TCP' rather -than 'HTTP(S)' protocol. This will pass the connection to the application nodes -NGINX service untouched. NGINX will have the SSL certificate and listen on port 443. +than 'HTTP(S)' protocol. This passes the connection to the application nodes +NGINX service untouched. NGINX has the SSL certificate and listen on port 443. See [NGINX HTTPS documentation](https://docs.gitlab.com/omnibus/settings/nginx.html#enable-https) for details on managing SSL certificates and configuring NGINX. @@ -38,10 +38,10 @@ for details on managing SSL certificates and configuring NGINX. ### Load Balancer(s) terminate SSL without backend SSL Configure your load balancer(s) to use the 'HTTP(S)' protocol rather than 'TCP'. -The load balancer(s) will then be responsible for managing SSL certificates and +The load balancer(s) is be responsible for managing SSL certificates and terminating SSL. -Since communication between the load balancer(s) and GitLab will not be secure, +Since communication between the load balancer(s) and GitLab isn't secure, there is some additional configuration needed. See [NGINX Proxied SSL documentation](https://docs.gitlab.com/omnibus/settings/nginx.html#supporting-proxied-ssl) for details. @@ -49,12 +49,12 @@ for details. ### Load Balancer(s) terminate SSL with backend SSL Configure your load balancer(s) to use the 'HTTP(S)' protocol rather than 'TCP'. -The load balancer(s) will be responsible for managing SSL certificates that -end users will see. +The load balancer(s) is responsible for managing SSL certificates that +end users see. -Traffic will also be secure between the load balancer(s) and NGINX in this +Traffic is secure between the load balancer(s) and NGINX in this scenario. There is no need to add configuration for proxied SSL since the -connection will be secure all the way. However, configuration will need to be +connection is secure all the way. However, configuration must be added to GitLab to configure SSL certificates. See [NGINX HTTPS documentation](https://docs.gitlab.com/omnibus/settings/nginx.html#enable-https) for details on managing SSL certificates and configuring NGINX. @@ -75,13 +75,13 @@ for details on managing SSL certificates and configuring NGINX. to pass through the `Connection` and `Upgrade` hop-by-hop headers. See the [web terminal](integration/terminal.md) integration guide for more details. -- (*2*): When using HTTPS protocol for port 443, you will need to add an SSL +- (*2*): When using HTTPS protocol for port 443, you must add an SSL certificate to the load balancers. If you wish to terminate SSL at the GitLab application server instead, use TCP protocol. ### GitLab Pages Ports -If you're using GitLab Pages with custom domain support you will need some +If you're using GitLab Pages with custom domain support you need some additional port configurations. GitLab Pages requires a separate virtual IP address. Configure DNS to point the `pages_external_url` from `/etc/gitlab/gitlab.rb` at the new virtual IP address. See the @@ -103,7 +103,7 @@ GitLab Pages requires a separate virtual IP address. Configure DNS to point the Some organizations have policies against opening SSH port 22. In this case, it may be helpful to configure an alternate SSH hostname that allows users -to use SSH on port 443. An alternate SSH hostname will require a new virtual IP address +to use SSH on port 443. An alternate SSH hostname requires a new virtual IP address compared to the other GitLab HTTP configuration above. Configure DNS for an alternate SSH hostname such as `altssh.gitlab.example.com`. @@ -114,7 +114,7 @@ Configure DNS for an alternate SSH hostname such as `altssh.gitlab.example.com`. ## Readiness check -It is strongly recommend that multi-node deployments configure load balancers to use the [readiness check](../user/admin_area/monitoring/health_check.md#readiness) to ensure a node is ready to accept traffic, before routing traffic to it. This is especially important when utilizing Puma, as there is a brief period during a restart where Puma will not accept requests. +It is strongly recommend that multi-node deployments configure load balancers to use the [readiness check](../user/admin_area/monitoring/health_check.md#readiness) to ensure a node is ready to accept traffic, before routing traffic to it. This is especially important when utilizing Puma, as there is a brief period during a restart where Puma doesn't accept requests.
+
@@ -40,7 +40,7 @@ exports[`AlertsSettingsFormNew with default values renders the initial template
@@ -56,7 +56,7 @@ exports[`AlertsSettingsFormNew with default values renders the initial template
diff --git a/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap index de40e03b598..6f28573c808 100644 --- a/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap +++ b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap @@ -53,6 +53,7 @@ exports[`Remove cluster confirmation modal renders splitbutton with modal includ type="button" >

{ state, }); - expect(findReferenceIcon().attributes('aria-label')).toBe(state); + expect(findReferenceIcon().props('ariaLabel')).toBe(state); expect(findReference().text()).toBe(displayReference); expect(findTitle().text()).toBe(title); }); diff --git a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap index fe6d9a34078..c40b7c90c72 100644 --- a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap +++ b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap @@ -120,6 +120,7 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] = class="gl-search-box-by-type" > 123456 Project context issue title <script>alert('hi')</script>"`; + +exports[`gfm_autocomplete/utils issues config shows the reference and title in the menu item within a group context 1`] = `"gitlab#987654 Group context issue title <script>alert('hi')</script>"`; + +exports[`gfm_autocomplete/utils labels config shows the title in the menu item 1`] = ` +" + + bug <script>alert('hi')</script>" +`; + +exports[`gfm_autocomplete/utils members config shows an avatar character, name, parent name, and count in the menu item for a group 1`] = ` +" +

+
+ G
+
+
1-1s <script>alert('hi')</script> (2)
+
GitLab Support Team
+
+ +
+ " +`; + +exports[`gfm_autocomplete/utils members config shows the avatar, name and username in the menu item for a user 1`] = ` +" +
+ \\"\\" +
+
My Name <script>alert('hi')</script>
+
@myusername
+
+ +
+ " +`; + +exports[`gfm_autocomplete/utils merge requests config shows the iid and title in the menu item within a project context 1`] = `"123456 Project context merge request title <script>alert('hi')</script>"`; + +exports[`gfm_autocomplete/utils merge requests config shows the reference and title in the menu item within a group context 1`] = `"gitlab!456789 Group context merge request title <script>alert('hi')</script>"`; + +exports[`gfm_autocomplete/utils milestones config shows the title in the menu item 1`] = `"13.2 <script>alert('hi')</script>"`; + +exports[`gfm_autocomplete/utils snippets config shows the id and title in the menu item 1`] = `"123456 Snippet title <script>alert('hi')</script>"`; diff --git a/spec/frontend/vue_shared/components/gl_mentions_spec.js b/spec/frontend/vue_shared/components/gfm_autocomplete/gfm_autocomplete_spec.js similarity index 77% rename from spec/frontend/vue_shared/components/gl_mentions_spec.js rename to spec/frontend/vue_shared/components/gfm_autocomplete/gfm_autocomplete_spec.js index 32fc055a77d..d12faacca75 100644 --- a/spec/frontend/vue_shared/components/gl_mentions_spec.js +++ b/spec/frontend/vue_shared/components/gfm_autocomplete/gfm_autocomplete_spec.js @@ -1,15 +1,15 @@ import { shallowMount } from '@vue/test-utils'; import Tribute from 'tributejs'; -import GlMentions from '~/vue_shared/components/gl_mentions.vue'; +import GfmAutocomplete from '~/vue_shared/components/gfm_autocomplete/gfm_autocomplete.vue'; -describe('GlMentions', () => { +describe('GfmAutocomplete', () => { let wrapper; - describe('Tribute', () => { + describe('tribute', () => { const mentions = '/gitlab-org/gitlab-test/-/autocomplete_sources/members?type=Issue&type_id=1'; beforeEach(() => { - wrapper = shallowMount(GlMentions, { + wrapper = shallowMount(GfmAutocomplete, { propsData: { dataSources: { mentions, diff --git a/spec/frontend/vue_shared/components/gfm_autocomplete/utils_spec.js b/spec/frontend/vue_shared/components/gfm_autocomplete/utils_spec.js new file mode 100644 index 00000000000..647f8c6e000 --- /dev/null +++ b/spec/frontend/vue_shared/components/gfm_autocomplete/utils_spec.js @@ -0,0 +1,344 @@ +import { escape, last } from 'lodash'; +import { GfmAutocompleteType, tributeConfig } from '~/vue_shared/components/gfm_autocomplete/utils'; + +describe('gfm_autocomplete/utils', () => { + describe('issues config', () => { + const issuesConfig = tributeConfig[GfmAutocompleteType.Issues].config; + const groupContextIssue = { + iid: 987654, + reference: 'gitlab#987654', + title: "Group context issue title ", + }; + const projectContextIssue = { + id: null, + iid: 123456, + time_estimate: 0, + title: "Project context issue title ", + }; + + it('uses # as the trigger', () => { + expect(issuesConfig.trigger).toBe('#'); + }); + + it('searches using both the iid and title', () => { + expect(issuesConfig.lookup(projectContextIssue)).toBe( + `${projectContextIssue.iid}${projectContextIssue.title}`, + ); + }); + + it('shows the reference and title in the menu item within a group context', () => { + expect(issuesConfig.menuItemTemplate({ original: groupContextIssue })).toMatchSnapshot(); + }); + + it('shows the iid and title in the menu item within a project context', () => { + expect(issuesConfig.menuItemTemplate({ original: projectContextIssue })).toMatchSnapshot(); + }); + + it('inserts the reference on autocomplete selection within a group context', () => { + expect(issuesConfig.selectTemplate({ original: groupContextIssue })).toBe( + groupContextIssue.reference, + ); + }); + + it('inserts the iid on autocomplete selection within a project context', () => { + expect(issuesConfig.selectTemplate({ original: projectContextIssue })).toBe( + `#${projectContextIssue.iid}`, + ); + }); + }); + + describe('labels config', () => { + const labelsConfig = tributeConfig[GfmAutocompleteType.Labels].config; + const labelsFilter = tributeConfig[GfmAutocompleteType.Labels].filterValues; + const label = { + color: '#123456', + textColor: '#FFFFFF', + title: `bug `, + type: 'GroupLabel', + }; + const singleWordLabel = { + color: '#456789', + textColor: '#DDD', + title: `bug`, + type: 'GroupLabel', + }; + const numericalLabel = { + color: '#abcdef', + textColor: '#AAA', + title: 123456, + type: 'ProjectLabel', + }; + + it('uses ~ as the trigger', () => { + expect(labelsConfig.trigger).toBe('~'); + }); + + it('searches using `title`', () => { + expect(labelsConfig.lookup).toBe('title'); + }); + + it('shows the title in the menu item', () => { + expect(labelsConfig.menuItemTemplate({ original: label })).toMatchSnapshot(); + }); + + it('inserts the title on autocomplete selection', () => { + expect(labelsConfig.selectTemplate({ original: singleWordLabel })).toBe( + `~${escape(singleWordLabel.title)}`, + ); + }); + + it('inserts the title enclosed with quotes on autocomplete selection when the title is numerical', () => { + expect(labelsConfig.selectTemplate({ original: numericalLabel })).toBe( + `~"${escape(numericalLabel.title)}"`, + ); + }); + + it('inserts the title enclosed with quotes on autocomplete selection when the title contains multiple words', () => { + expect(labelsConfig.selectTemplate({ original: label })).toBe(`~"${escape(label.title)}"`); + }); + + describe('filter', () => { + const collection = [label, singleWordLabel, { ...numericalLabel, set: true }]; + + describe('/label quick action', () => { + describe('when the line starts with `/label`', () => { + it('shows labels that are not currently selected', () => { + const fullText = '/label ~'; + const selectionStart = 8; + + expect(labelsFilter({ collection, fullText, selectionStart })).toEqual([ + collection[0], + collection[1], + ]); + }); + }); + + describe('when the line does not start with `/label`', () => { + it('shows all labels', () => { + const fullText = '~'; + const selectionStart = 1; + + expect(labelsFilter({ collection, fullText, selectionStart })).toEqual(collection); + }); + }); + }); + + describe('/unlabel quick action', () => { + describe('when the line starts with `/unlabel`', () => { + it('shows labels that are currently selected', () => { + const fullText = '/unlabel ~'; + const selectionStart = 10; + + expect(labelsFilter({ collection, fullText, selectionStart })).toEqual([collection[2]]); + }); + }); + + describe('when the line does not start with `/unlabel`', () => { + it('shows all labels', () => { + const fullText = '~'; + const selectionStart = 1; + + expect(labelsFilter({ collection, fullText, selectionStart })).toEqual(collection); + }); + }); + }); + }); + }); + + describe('members config', () => { + const membersConfig = tributeConfig[GfmAutocompleteType.Members].config; + const membersFilter = tributeConfig[GfmAutocompleteType.Members].filterValues; + const userMember = { + type: 'User', + username: 'myusername', + name: "My Name ", + avatar_url: '/uploads/-/system/user/avatar/123456/avatar.png', + availability: null, + }; + const groupMember = { + type: 'Group', + username: 'gitlab-com/support/1-1s', + name: "GitLab.com / GitLab Support Team / 1-1s ", + avatar_url: null, + count: 2, + mentionsDisabled: null, + }; + + it('uses @ as the trigger', () => { + expect(membersConfig.trigger).toBe('@'); + }); + + it('inserts the username on autocomplete selection', () => { + expect(membersConfig.fillAttr).toBe('username'); + }); + + it('searches using both the name and username for a user', () => { + expect(membersConfig.lookup(userMember)).toBe(`${userMember.name}${userMember.username}`); + }); + + it('searches using only its own name and not its ancestors for a group', () => { + expect(membersConfig.lookup(groupMember)).toBe(last(groupMember.name.split(' / '))); + }); + + it('shows the avatar, name and username in the menu item for a user', () => { + expect(membersConfig.menuItemTemplate({ original: userMember })).toMatchSnapshot(); + }); + + it('shows an avatar character, name, parent name, and count in the menu item for a group', () => { + expect(membersConfig.menuItemTemplate({ original: groupMember })).toMatchSnapshot(); + }); + + describe('filter', () => { + const assignees = [userMember.username]; + const collection = [userMember, groupMember]; + + describe('/assign quick action', () => { + describe('when the line starts with `/assign`', () => { + it('shows members that are not currently selected', () => { + const fullText = '/assign @'; + const selectionStart = 9; + + expect(membersFilter({ assignees, collection, fullText, selectionStart })).toEqual([ + collection[1], + ]); + }); + }); + + describe('when the line does not start with `/assign`', () => { + it('shows all labels', () => { + const fullText = '@'; + const selectionStart = 1; + + expect(membersFilter({ assignees, collection, fullText, selectionStart })).toEqual( + collection, + ); + }); + }); + }); + + describe('/unassign quick action', () => { + describe('when the line starts with `/unassign`', () => { + it('shows members that are currently selected', () => { + const fullText = '/unassign @'; + const selectionStart = 11; + + expect(membersFilter({ assignees, collection, fullText, selectionStart })).toEqual([ + collection[0], + ]); + }); + }); + + describe('when the line does not start with `/unassign`', () => { + it('shows all members', () => { + const fullText = '@'; + const selectionStart = 1; + + expect(membersFilter({ assignees, collection, fullText, selectionStart })).toEqual( + collection, + ); + }); + }); + }); + }); + }); + + describe('merge requests config', () => { + const mergeRequestsConfig = tributeConfig[GfmAutocompleteType.MergeRequests].config; + const groupContextMergeRequest = { + iid: 456789, + reference: 'gitlab!456789', + title: "Group context merge request title ", + }; + const projectContextMergeRequest = { + id: null, + iid: 123456, + time_estimate: 0, + title: "Project context merge request title ", + }; + + it('uses ! as the trigger', () => { + expect(mergeRequestsConfig.trigger).toBe('!'); + }); + + it('searches using both the iid and title', () => { + expect(mergeRequestsConfig.lookup(projectContextMergeRequest)).toBe( + `${projectContextMergeRequest.iid}${projectContextMergeRequest.title}`, + ); + }); + + it('shows the reference and title in the menu item within a group context', () => { + expect( + mergeRequestsConfig.menuItemTemplate({ original: groupContextMergeRequest }), + ).toMatchSnapshot(); + }); + + it('shows the iid and title in the menu item within a project context', () => { + expect( + mergeRequestsConfig.menuItemTemplate({ original: projectContextMergeRequest }), + ).toMatchSnapshot(); + }); + + it('inserts the reference on autocomplete selection within a group context', () => { + expect(mergeRequestsConfig.selectTemplate({ original: groupContextMergeRequest })).toBe( + groupContextMergeRequest.reference, + ); + }); + + it('inserts the iid on autocomplete selection within a project context', () => { + expect(mergeRequestsConfig.selectTemplate({ original: projectContextMergeRequest })).toBe( + `!${projectContextMergeRequest.iid}`, + ); + }); + }); + + describe('milestones config', () => { + const milestonesConfig = tributeConfig[GfmAutocompleteType.Milestones].config; + const milestone = { + id: null, + iid: 49, + title: "13.2 ", + }; + + it('uses % as the trigger', () => { + expect(milestonesConfig.trigger).toBe('%'); + }); + + it('searches using the title', () => { + expect(milestonesConfig.lookup).toBe('title'); + }); + + it('shows the title in the menu item', () => { + expect(milestonesConfig.menuItemTemplate({ original: milestone })).toMatchSnapshot(); + }); + + it('inserts the title on autocomplete selection', () => { + expect(milestonesConfig.selectTemplate({ original: milestone })).toBe( + `%"${escape(milestone.title)}"`, + ); + }); + }); + + describe('snippets config', () => { + const snippetsConfig = tributeConfig[GfmAutocompleteType.Snippets].config; + const snippet = { + id: 123456, + title: "Snippet title ", + }; + + it('uses $ as the trigger', () => { + expect(snippetsConfig.trigger).toBe('$'); + }); + + it('inserts the id on autocomplete selection', () => { + expect(snippetsConfig.fillAttr).toBe('id'); + }); + + it('searches using both the id and title', () => { + expect(snippetsConfig.lookup(snippet)).toBe(`${snippet.id}${snippet.title}`); + }); + + it('shows the id and title in the menu item', () => { + expect(snippetsConfig.menuItemTemplate({ original: snippet })).toMatchSnapshot(); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap b/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap index ecea151fc8a..da49778f216 100644 --- a/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap +++ b/spec/frontend/vue_shared/components/registry/__snapshots__/code_instruction_spec.js.snap @@ -47,6 +47,7 @@ exports[`Package code instruction single line to match the default snapshot 1`]