Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
d87faae128
commit
85435943cd
|
|
@ -630,7 +630,6 @@ Layout/LineLength:
|
|||
- 'config/application.rb'
|
||||
- 'config/initializers/01_secret_token.rb'
|
||||
- 'config/initializers/1_settings.rb'
|
||||
- 'config/initializers/5_backend.rb'
|
||||
- 'config/initializers/7_prometheus_metrics.rb'
|
||||
- 'config/initializers/8_devise.rb'
|
||||
- 'config/initializers/active_record_force_reconnects.rb'
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
<script>
|
||||
import { GlAvatar, GlLoadingIcon } from '@gitlab/ui';
|
||||
import { GlAvatar, GlLoadingIcon, GlIcon } from '@gitlab/ui';
|
||||
import SafeHtml from '~/vue_shared/directives/safe_html';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlAvatar,
|
||||
GlLoadingIcon,
|
||||
GlIcon,
|
||||
},
|
||||
|
||||
directives: {
|
||||
|
|
@ -98,6 +99,10 @@ export default {
|
|||
return this.isReference && this.nodeProps.referenceType === 'milestone';
|
||||
},
|
||||
|
||||
isWiki() {
|
||||
return this.nodeProps.referenceType === 'wiki';
|
||||
},
|
||||
|
||||
isEmoji() {
|
||||
return this.nodeType === 'emoji';
|
||||
},
|
||||
|
|
@ -146,6 +151,8 @@ export default {
|
|||
return item.reference;
|
||||
case 'vulnerability':
|
||||
return `[vulnerability:${item.id}]`;
|
||||
case 'wiki':
|
||||
return item.title;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
|
@ -178,6 +185,16 @@ export default {
|
|||
});
|
||||
}
|
||||
|
||||
if (this.isWiki) {
|
||||
Object.assign(props, {
|
||||
text: item.title,
|
||||
href: item.path,
|
||||
isGollumLink: true,
|
||||
isWikiPage: true,
|
||||
canonicalSrc: item.slug,
|
||||
});
|
||||
}
|
||||
|
||||
Object.assign(props, this.nodeProps);
|
||||
|
||||
return props;
|
||||
|
|
@ -312,6 +329,15 @@ export default {
|
|||
<span v-safe-html:safeHtmlConfig="highlight(item.title)"></span>
|
||||
<span v-if="item.expired">{{ __('(expired)') }}</span>
|
||||
</span>
|
||||
<span v-if="isWiki">
|
||||
<gl-icon class="gl-mr-2" name="document" />
|
||||
<span v-safe-html:[$options.safeHtmlConfig]="highlight(item.title)"></span>
|
||||
<small
|
||||
v-if="item.title.toLowerCase() !== item.slug.toLowerCase()"
|
||||
v-safe-html:[$options.safeHtmlConfig]="highlight(`(${item.slug})`)"
|
||||
class="gl-text-gray-500"
|
||||
></small>
|
||||
</span>
|
||||
<span v-if="isLabel" class="gl-display-flex">
|
||||
<span
|
||||
data-testid="label-color-box"
|
||||
|
|
|
|||
|
|
@ -52,6 +52,17 @@ export default Link.extend({
|
|||
title: null,
|
||||
parseHTML: (element) => element.getAttribute('title'),
|
||||
},
|
||||
// only for gollum links (wikis)
|
||||
isGollumLink: {
|
||||
default: false,
|
||||
parseHTML: (element) => Boolean(element.dataset.gollum),
|
||||
renderHTML: () => '',
|
||||
},
|
||||
isWikiPage: {
|
||||
default: false,
|
||||
parseHTML: (element) => Boolean(element.classList.contains('gfm-gollum-wiki-page')),
|
||||
renderHTML: ({ isWikiPage }) => (isWikiPage ? { class: 'gfm-gollum-wiki-page' } : {}),
|
||||
},
|
||||
canonicalSrc: {
|
||||
default: null,
|
||||
parseHTML: (element) => element.dataset.canonicalSrc,
|
||||
|
|
|
|||
|
|
@ -23,14 +23,24 @@ function createSuggestionPlugin({
|
|||
pluginKey: new PluginKey(uniqueId('suggestions')),
|
||||
|
||||
command: ({ editor: tiptapEditor, range, props }) => {
|
||||
tiptapEditor
|
||||
.chain()
|
||||
.focus()
|
||||
.insertContentAt(range, [
|
||||
let content;
|
||||
|
||||
if (nodeType === 'link') {
|
||||
content = [
|
||||
{
|
||||
type: 'text',
|
||||
text: props.text,
|
||||
marks: [{ type: 'link', attrs: props }],
|
||||
},
|
||||
];
|
||||
} else {
|
||||
content = [
|
||||
{ type: nodeType, attrs: props },
|
||||
{ type: 'text', text: ` ${insertionMap[props.text] || ''}` },
|
||||
])
|
||||
.run();
|
||||
];
|
||||
}
|
||||
|
||||
tiptapEditor.chain().focus().insertContentAt(range, content).run();
|
||||
},
|
||||
|
||||
async items({ query, editor: tiptapEditor }) {
|
||||
|
|
@ -153,6 +163,7 @@ export default Node.create({
|
|||
createPlugin('[vulnerability:', 'reference', 'vulnerability'),
|
||||
createPlugin('%', 'reference', 'milestone'),
|
||||
createPlugin(':', 'emoji', 'emoji'),
|
||||
createPlugin('[[', 'link', 'wiki'),
|
||||
createPlugin('/', 'reference', 'command', {
|
||||
cache: false,
|
||||
limit: 100,
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ export default class AutocompleteHelper {
|
|||
merge_request: this.dataSourceUrls.mergeRequests,
|
||||
vulnerability: this.dataSourceUrls.vulnerabilities,
|
||||
command: this.dataSourceUrls.commands,
|
||||
wiki: this.dataSourceUrls.wikis,
|
||||
};
|
||||
|
||||
const searchFields = {
|
||||
|
|
@ -151,6 +152,7 @@ export default class AutocompleteHelper {
|
|||
merge_request: ['iid', 'title'],
|
||||
milestone: ['title', 'iid'],
|
||||
command: ['name'],
|
||||
wiki: ['title'],
|
||||
emoji: [],
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -586,6 +586,10 @@ export const italic = {
|
|||
expelEnclosingWhitespace: true,
|
||||
};
|
||||
|
||||
function getMarkText(mark, parent) {
|
||||
return findChildWithMark(mark, parent).child?.text || '';
|
||||
}
|
||||
|
||||
const generateCodeTag = (wrapTagName = openTag) => {
|
||||
const isOpen = wrapTagName === openTag;
|
||||
|
||||
|
|
@ -596,7 +600,7 @@ const generateCodeTag = (wrapTagName = openTag) => {
|
|||
return wrapTagName(type.substring(1));
|
||||
}
|
||||
|
||||
const childText = findChildWithMark(mark, parent).child?.text || '';
|
||||
const childText = getMarkText(mark, parent);
|
||||
if (childText.includes('`')) {
|
||||
let tag = '``';
|
||||
if (childText.startsWith('`') || childText.endsWith('`'))
|
||||
|
|
@ -694,12 +698,13 @@ export const link = {
|
|||
return isBracketAutoLink(mark.attrs.sourceMarkdown) ? '<' : '';
|
||||
}
|
||||
|
||||
const { href, title, sourceMarkdown } = mark.attrs;
|
||||
const { href, title, sourceMarkdown, isGollumLink } = mark.attrs;
|
||||
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
if (href.startsWith('data:') || href.startsWith('blob:')) return '';
|
||||
|
||||
if (linkType(sourceMarkdown) === LINK_MARKDOWN) {
|
||||
if (isGollumLink) return '[[';
|
||||
return '[';
|
||||
}
|
||||
|
||||
|
|
@ -718,7 +723,14 @@ export const link = {
|
|||
return isBracketAutoLink(mark.attrs.sourceMarkdown) ? '>' : '';
|
||||
}
|
||||
|
||||
const { href = '', title, sourceMarkdown, isReference } = mark.attrs;
|
||||
const {
|
||||
href = '',
|
||||
title,
|
||||
sourceMarkdown,
|
||||
isReference,
|
||||
isGollumLink,
|
||||
canonicalSrc,
|
||||
} = mark.attrs;
|
||||
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
if (href.startsWith('data:') || href.startsWith('blob:')) return '';
|
||||
|
|
@ -731,6 +743,17 @@ export const link = {
|
|||
return closeTag('a');
|
||||
}
|
||||
|
||||
if (isGollumLink) {
|
||||
const text = getMarkText(mark, parent);
|
||||
const escapedCanonicalSrc = state.esc(canonicalSrc);
|
||||
|
||||
if (text.toLowerCase() === escapedCanonicalSrc.toLowerCase()) {
|
||||
return ']]';
|
||||
}
|
||||
|
||||
return `|${escapedCanonicalSrc}]]`;
|
||||
}
|
||||
|
||||
return `](${state.esc(getLinkHref(mark, state.options.useCanonicalSrc))}${
|
||||
title ? ` ${state.quote(title)}` : ''
|
||||
})`;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ const MERGEREQUESTS_ALIAS = 'mergerequests';
|
|||
const LABELS_ALIAS = 'labels';
|
||||
const SNIPPETS_ALIAS = 'snippets';
|
||||
const CONTACTS_ALIAS = 'contacts';
|
||||
const WIKIS_ALIAS = 'wikis';
|
||||
|
||||
const CADENCE_REFERENCE_PREFIX = '[cadence:';
|
||||
const ITERATION_REFERENCE_PREFIX = '*iteration:';
|
||||
|
|
@ -130,6 +131,7 @@ export const defaultAutocompleteConfig = {
|
|||
snippets: true,
|
||||
vulnerabilities: true,
|
||||
contacts: true,
|
||||
wikis: true,
|
||||
};
|
||||
|
||||
class GfmAutoComplete {
|
||||
|
|
@ -172,6 +174,7 @@ class GfmAutoComplete {
|
|||
if (this.enableMap.labels) this.setupLabels($input);
|
||||
if (this.enableMap.snippets) this.setupSnippets($input);
|
||||
if (this.enableMap.contacts) this.setupContacts($input);
|
||||
if (this.enableMap.wikis) this.setupWikis($input);
|
||||
|
||||
$input.filter('[data-supports-quick-actions="true"]').atwho({
|
||||
at: '/',
|
||||
|
|
@ -741,6 +744,47 @@ class GfmAutoComplete {
|
|||
showAndHideHelper($input, SNIPPETS_ALIAS);
|
||||
}
|
||||
|
||||
setupWikis($input) {
|
||||
$input.atwho({
|
||||
at: '[[',
|
||||
suffix: ']]',
|
||||
alias: WIKIS_ALIAS,
|
||||
searchKey: 'title',
|
||||
data: GfmAutoComplete.defaultLoadingData,
|
||||
displayTpl(value) {
|
||||
let tmpl = GfmAutoComplete.Loading.template;
|
||||
if (value.title != null) {
|
||||
tmpl = GfmAutoComplete.Wikis.templateFunction(value);
|
||||
}
|
||||
return tmpl;
|
||||
},
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
insertTpl: '${atwho-at}${title}|${slug}',
|
||||
callbacks: {
|
||||
...this.getDefaultCallbacks(),
|
||||
beforeInsert(value) {
|
||||
const [title, slug] = value.substr(2).split('|');
|
||||
if (title.toLowerCase() === slug.toLowerCase()) {
|
||||
return `[[${slug}`;
|
||||
}
|
||||
return `[[${title}|${slug}`;
|
||||
},
|
||||
beforeSave(wikis) {
|
||||
return $.map(wikis, (m) => {
|
||||
if (m.title == null) {
|
||||
return m;
|
||||
}
|
||||
return {
|
||||
title: m.title,
|
||||
slug: m.slug,
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
showAndHideHelper($input, WIKIS_ALIAS);
|
||||
}
|
||||
|
||||
setupContacts($input) {
|
||||
const fetchData = this.fetchData.bind(this);
|
||||
let command = '';
|
||||
|
|
@ -1011,6 +1055,7 @@ GfmAutoComplete.atTypeMap = {
|
|||
'[vulnerability:': 'vulnerabilities',
|
||||
$: 'snippets',
|
||||
'[contact:': 'contacts',
|
||||
'[[': 'wikis',
|
||||
};
|
||||
|
||||
GfmAutoComplete.typesWithBackendFiltering = ['vulnerabilities'];
|
||||
|
|
@ -1116,6 +1161,14 @@ GfmAutoComplete.Contacts = {
|
|||
return `<li><small>${escape(firstName)} ${escape(lastName)}</small> ${escape(email)}</li>`;
|
||||
},
|
||||
};
|
||||
GfmAutoComplete.Wikis = {
|
||||
templateFunction({ title, slug }) {
|
||||
const icon = spriteIcon('document', 's16 vertical-align-middle gl-mr-2');
|
||||
const slugSpan =
|
||||
title.toLowerCase() !== slug.toLowerCase() ? ` <small>(${escape(slug)})</small>` : '';
|
||||
return `<li>${icon} ${escape(title)} ${slugSpan}</li>`;
|
||||
},
|
||||
};
|
||||
|
||||
const loadingSpinner = loadingIconForLegacyJS({
|
||||
inline: true,
|
||||
|
|
|
|||
|
|
@ -484,20 +484,29 @@
|
|||
}
|
||||
}
|
||||
|
||||
a.gfm-gollum-wiki-page {
|
||||
&::before {
|
||||
margin-right: 4px;
|
||||
vertical-align: -2px;
|
||||
|
||||
@include gl-dark-invert-keep-hue;
|
||||
content: url('sprite_icons/document.svg');
|
||||
}
|
||||
}
|
||||
|
||||
a.with-attachment-icon,
|
||||
a[href*='/uploads/'],
|
||||
a[href*='storage.googleapis.com/google-code-attachments/'] {
|
||||
&::before {
|
||||
margin-right: 4px;
|
||||
vertical-align: -2px;
|
||||
|
||||
font-style: normal;
|
||||
font-size: inherit;
|
||||
text-rendering: auto;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
content: '📎';
|
||||
@include gl-dark-invert-keep-hue;
|
||||
content: url('sprite_icons/paperclip.svg');
|
||||
}
|
||||
}
|
||||
|
||||
a[href*='/wikis/'],
|
||||
a[href*='/uploads/'],
|
||||
a[href*='storage.googleapis.com/google-code-attachments/'] {
|
||||
&.no-attachment-icon {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
|
|||
before_action :authorize_read_milestone!, only: :milestones
|
||||
before_action :authorize_read_crm_contact!, only: :contacts
|
||||
|
||||
feature_category :team_planning, [:issues, :labels, :milestones, :commands, :contacts]
|
||||
feature_category :team_planning, [:issues, :labels, :milestones, :commands, :contacts, :wikis]
|
||||
feature_category :code_review_workflow, [:merge_requests]
|
||||
feature_category :groups_and_projects, [:members]
|
||||
feature_category :source_code_management, [:snippets]
|
||||
|
|
@ -46,6 +46,10 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
|
|||
render json: autocomplete_service.contacts(target)
|
||||
end
|
||||
|
||||
def wikis
|
||||
render json: autocomplete_service.wikis
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def autocomplete_service
|
||||
|
|
|
|||
|
|
@ -456,7 +456,8 @@ module ApplicationHelper
|
|||
milestones: milestones_project_autocomplete_sources_path(object),
|
||||
commands: commands_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
|
||||
snippets: snippets_project_autocomplete_sources_path(object),
|
||||
contacts: contacts_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id])
|
||||
contacts: contacts_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
|
||||
wikis: object.feature_available?(:wiki, current_user) ? wikis_project_autocomplete_sources_path(object) : nil
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
module Projects
|
||||
class AutocompleteService < BaseService
|
||||
include LabelsAsHash
|
||||
include Routing::WikiHelper
|
||||
|
||||
def issues
|
||||
IssuesFinder.new(current_user, project_id: project.id, state: 'opened').execute.select([:iid, :title])
|
||||
end
|
||||
|
|
@ -33,6 +35,16 @@ module Projects
|
|||
SnippetsFinder.new(current_user, project: project).execute.select([:id, :title])
|
||||
end
|
||||
|
||||
def wikis
|
||||
wiki = Wiki.for_container(project, current_user)
|
||||
return [] unless can?(current_user, :read_wiki, wiki.container)
|
||||
|
||||
wiki
|
||||
.list_pages(limit: 5000)
|
||||
.reject { |page| page.slug.start_with?('templates/') }
|
||||
.map { |page| { path: wiki_page_path(page.wiki, page), slug: page.slug, title: page.title } }
|
||||
end
|
||||
|
||||
def contacts(target)
|
||||
available_contacts = Crm::ContactsFinder.new(current_user, group: project.group).execute
|
||||
.select([:id, :email, :first_name, :last_name, :state])
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
unless Rails.env.test?
|
||||
required_version = Gitlab::VersionInfo.parse(Gitlab::Shell.version_required)
|
||||
current_version = Gitlab::VersionInfo.parse(Gitlab::Shell.version)
|
||||
|
||||
unless current_version.valid? && required_version <= current_version
|
||||
warn "WARNING: This version of GitLab depends on gitlab-shell #{required_version}, but you're running #{current_version}. Please update gitlab-shell."
|
||||
end
|
||||
end
|
||||
|
|
@ -172,6 +172,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
|||
get 'commands'
|
||||
get 'snippets'
|
||||
get 'contacts'
|
||||
get 'wikis'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ Some features are still in development. View details about [support for each sta
|
|||
| Automates repetitive tasks and helps catch bugs early. | [Test generation](gitlab_duo_chat.md#write-tests-in-the-ide) | **Tier:** Ultimate <br>**Offering:** GitLab.com, Self-managed, GitLab Dedicated <br>**Status:** Beta |
|
||||
| Generates a description for the merge request based on the contents of the template. | [Merge request template population](project/merge_requests/ai_in_merge_requests.md#fill-in-merge-request-templates) | **Tier:** Ultimate<br>**Offering:** GitLab.com <br>**Status:** Experiment |
|
||||
| Assists in creating faster and higher-quality reviews by automatically suggesting reviewers for your merge request. <br><br><i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Watch overview](https://www.youtube.com/watch?v=ivwZQgh4Rxw) | [Suggested Reviewers](project/merge_requests/reviews/index.md#gitlab-duo-suggested-reviewers) | **Tier:** Ultimate <br>**Offering:** GitLab.com<br>**Status:** Generally Available |
|
||||
| Efficiently communicates the impact of your merge request changes. | [Merge request summary](project/merge_requests/ai_in_merge_requests.md#summarize-merge-request-changes) | **Tier:** Ultimate <br>**Offering:** GitLab.com <br>**Status:** Experiment |
|
||||
| Efficiently communicates the impact of your merge request changes. | [Merge request summary](project/merge_requests/ai_in_merge_requests.md#summarize-merge-request-changes) | **Tier:** Ultimate <br>**Offering:** GitLab.com <br>**Status:** Beta |
|
||||
| Helps ease merge request handoff between authors and reviewers and help reviewers efficiently understand suggestions. | [Code review summary](project/merge_requests/ai_in_merge_requests.md#summarize-my-merge-request-review) | **Tier:** Ultimate <br>**Offering:** GitLab.com <br>**Status:** Experiment |
|
||||
| Helps you remediate vulnerabilities more efficiently, boost your skills, and write more secure code. <br><br><i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Watch overview](https://www.youtube.com/watch?v=6sDf73QOav8) | [Vulnerability explanation](application_security/vulnerabilities/index.md#explaining-a-vulnerability) | **Tier:** Ultimate <br>**Offering:** GitLab.com <br>**Status:** Beta |
|
||||
| Generates a merge request containing the changes required to mitigate a vulnerability. | [Vulnerability resolution](application_security/vulnerabilities/index.md#vulnerability-resolution) | **Tier:** Ultimate <br>**Offering:** GitLab.com <br>**Status:** Experiment |
|
||||
|
|
|
|||
|
|
@ -682,6 +682,8 @@ The selected diagram is replaced with an updated version.
|
|||
|
||||
## GitLab-specific references
|
||||
|
||||
> - Autocomplete for wiki pages [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/442229) in GitLab 16.11.
|
||||
|
||||
GitLab Flavored Markdown renders GitLab-specific references. For example, you can reference
|
||||
an issue, a commit, a team member, or even an entire project team. GitLab Flavored Markdown turns
|
||||
that reference into a link so you can navigate between them. All references to projects should use the
|
||||
|
|
@ -692,35 +694,37 @@ version to reference other projects from the same namespace.
|
|||
|
||||
GitLab Flavored Markdown recognizes the following:
|
||||
|
||||
| References | Input | Cross-project reference | Shortcut inside the same namespace |
|
||||
| -------------------------------------------------------------- | ------------------------------ | --------------------------------------- | ---------------------------------- |
|
||||
| Specific user | `@user_name` | | |
|
||||
| Specific group | `@group_name` | | |
|
||||
| Entire team | [`@all`](discussions/index.md#mentioning-all-members) | | |
|
||||
| Project | `namespace/project>` | | |
|
||||
| Issue | ``#123`` | `namespace/project#123` | `project#123` |
|
||||
| Merge request | `!123` | `namespace/project!123` | `project!123` |
|
||||
| Snippet | `$123` | `namespace/project$123` | `project$123` |
|
||||
| [Epic](group/epics/index.md) | `&123` | `group1/subgroup&123` | |
|
||||
| [Iteration](group/iterations/index.md) | `*iteration:"iteration title"` | | |
|
||||
| [Iteration cadence](group/iterations/index.md) by ID<sup>1</sup> | `[cadence:123]` | | |
|
||||
| [Iteration cadence](group/iterations/index.md) by title (one word)<sup>1</sup> | `[cadence:plan]` | | |
|
||||
| [Iteration cadence](group/iterations/index.md) by title (multiple words)<sup>1</sup> | `[cadence:"plan a"]` | | |
|
||||
| [Vulnerability](application_security/vulnerabilities/index.md) | `[vulnerability:123]` | `[vulnerability:namespace/project/123]` | `[vulnerability:project/123]` |
|
||||
| Feature flag | `[feature_flag:123]` | `[feature_flag:namespace/project/123]` | `[feature_flag:project/123]` |
|
||||
| Label by ID | `~123` | `namespace/project~123` | `project~123` |
|
||||
| Label by name (one word) | `~bug` | `namespace/project~bug` | `project~bug` |
|
||||
| Label by name (multiple words) | `~"feature request"` | `namespace/project~"feature request"` | `project~"feature request"` |
|
||||
| Label by name (scoped) | `~"priority::high"` | `namespace/project~"priority::high"` | `project~"priority::high"` |
|
||||
| Project milestone by ID <sup>2</sup> | `%123` | `namespace/project%123` | `project%123` |
|
||||
| Milestone by name (one word) <sup>2</sup> | `%v1.23` | `namespace/project%v1.23` | `project%v1.23` |
|
||||
| Milestone by name (multiple words) <sup>2</sup> | `%"release candidate"` | `namespace/project%"release candidate"` | `project%"release candidate"` |
|
||||
| Commit (specific) | `9ba12248` | `namespace/project@9ba12248` | `project@9ba12248` |
|
||||
| Commit range comparison | `9ba12248...b19a04f5` | `namespace/project@9ba12248...b19a04f5` | `project@9ba12248...b19a04f5` |
|
||||
| Repository file reference | `[README](doc/README.md)` | | |
|
||||
| Repository file reference (specific line) | `[README](doc/README.md#L13)` | | |
|
||||
| [Alert](../operations/incident_management/alerts.md) | `^alert#123` | `namespace/project^alert#123` | `project^alert#123` |
|
||||
| [Contact](crm/index.md#contacts) | `[contact:test@example.com]` | | |
|
||||
| References | Input | Cross-project reference | Shortcut inside the same namespace |
|
||||
|--------------------------------------------------------------------------------------|-------------------------------------------------------|-----------------------------------------|------------------------------------|
|
||||
| Specific user | `@user_name` | | |
|
||||
| Specific group | `@group_name` | | |
|
||||
| Entire team | [`@all`](discussions/index.md#mentioning-all-members) | | |
|
||||
| Project | `namespace/project>` | | |
|
||||
| Issue | ``#123`` | `namespace/project#123` | `project#123` |
|
||||
| Merge request | `!123` | `namespace/project!123` | `project!123` |
|
||||
| Snippet | `$123` | `namespace/project$123` | `project$123` |
|
||||
| [Epic](group/epics/index.md) | `&123` | `group1/subgroup&123` | |
|
||||
| [Iteration](group/iterations/index.md) | `*iteration:"iteration title"` | | |
|
||||
| [Iteration cadence](group/iterations/index.md) by ID<sup>1</sup> | `[cadence:123]` | | |
|
||||
| [Iteration cadence](group/iterations/index.md) by title (one word)<sup>1</sup> | `[cadence:plan]` | | |
|
||||
| [Iteration cadence](group/iterations/index.md) by title (multiple words)<sup>1</sup> | `[cadence:"plan a"]` | | |
|
||||
| [Vulnerability](application_security/vulnerabilities/index.md) | `[vulnerability:123]` | `[vulnerability:namespace/project/123]` | `[vulnerability:project/123]` |
|
||||
| Feature flag | `[feature_flag:123]` | `[feature_flag:namespace/project/123]` | `[feature_flag:project/123]` |
|
||||
| Label by ID | `~123` | `namespace/project~123` | `project~123` |
|
||||
| Label by name (one word) | `~bug` | `namespace/project~bug` | `project~bug` |
|
||||
| Label by name (multiple words) | `~"feature request"` | `namespace/project~"feature request"` | `project~"feature request"` |
|
||||
| Label by name (scoped) | `~"priority::high"` | `namespace/project~"priority::high"` | `project~"priority::high"` |
|
||||
| Project milestone by ID <sup>2</sup> | `%123` | `namespace/project%123` | `project%123` |
|
||||
| Milestone by name (one word) <sup>2</sup> | `%v1.23` | `namespace/project%v1.23` | `project%v1.23` |
|
||||
| Milestone by name (multiple words) <sup>2</sup> | `%"release candidate"` | `namespace/project%"release candidate"` | `project%"release candidate"` |
|
||||
| Commit (specific) | `9ba12248` | `namespace/project@9ba12248` | `project@9ba12248` |
|
||||
| Commit range comparison | `9ba12248...b19a04f5` | `namespace/project@9ba12248...b19a04f5` | `project@9ba12248...b19a04f5` |
|
||||
| Repository file reference | `[README](doc/README.md)` | | |
|
||||
| Repository file reference (specific line) | `[README](doc/README.md#L13)` | | |
|
||||
| [Alert](../operations/incident_management/alerts.md) | `^alert#123` | `namespace/project^alert#123` | `project^alert#123` |
|
||||
| [Contact](crm/index.md#contacts) | `[contact:test@example.com]` | | |
|
||||
| [Wiki page](project/wiki/index.md) (if the page slug is the same as the title) | `[[Home]]` | | |
|
||||
| [Wiki page](project/wiki/index.md) (if the page slug is different from the title) | `[[How to use GitLab\|how-to-use-gitlab]]` | | |
|
||||
|
||||
**Footnotes:**
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@ This process is different than [importing from Bitbucket Cloud](bitbucket.md).
|
|||
must be enabled. If not enabled, ask your GitLab administrator to enable it. The Bitbucket Server import source is enabled
|
||||
by default on GitLab.com.
|
||||
- At least the Maintainer role on the destination group to import to.
|
||||
- Bitbucket Server authentication token with administrator access.
|
||||
- Bitbucket Server authentication token with administrator access. Without administrator access, some data is
|
||||
[not imported](https://gitlab.com/gitlab-org/gitlab/-/issues/446218).
|
||||
|
||||
## Import repositories
|
||||
|
||||
|
|
|
|||
|
|
@ -15,45 +15,17 @@ AI-assisted features in merge requests are designed to provide contextually rele
|
|||
|
||||
Additional information on enabling these features and maturity can be found in our [GitLab Duo overview](../../ai_features.md).
|
||||
|
||||
## Fill in merge request templates
|
||||
|
||||
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10591) in GitLab 16.3 as an [Experiment](../../../policy/experiment-beta-support.md#experiment).
|
||||
|
||||
This feature is an [Experiment](../../../policy/experiment-beta-support.md) on GitLab.com.
|
||||
|
||||
Merge requests in projects often have [templates](../description_templates.md#create-a-merge-request-template) defined that need to be filled out. This helps reviewers and other users understand the purpose and changes a merge request might propose.
|
||||
|
||||
When creating a merge request, GitLab Duo can generate a description for the merge request based on the contents of the template. This fills in the template and replaces the current contents of the description.
|
||||
|
||||
To generate the description:
|
||||
|
||||
1. [Create a new merge request](creating_merge_requests.md), and go to the **Description** field.
|
||||
1. Select **AI Actions** (**{tanuki}**).
|
||||
1. Select **Fill in merge request template**.
|
||||
|
||||
The updated description is applied to the box. You can edit or revise this description before you finish creating your merge request.
|
||||
|
||||
Provide feedback on this experimental feature in [issue 416537](https://gitlab.com/gitlab-org/gitlab/-/issues/416537).
|
||||
|
||||
**Data usage**: When you use this feature, the following data is sent to the large language model referenced above:
|
||||
|
||||
- Title of the merge request
|
||||
- Contents of the description
|
||||
- Diff of changes between the source branch's head and the target branch
|
||||
|
||||
## Summarize merge request changes
|
||||
|
||||
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10401) in GitLab 16.2 as an [Experiment](../../../policy/experiment-beta-support.md#experiment).
|
||||
|
||||
This feature is an [Experiment](../../../policy/experiment-beta-support.md) on GitLab.com.
|
||||
This feature is in [Beta](../../../policy/experiment-beta-support.md) on GitLab.com.
|
||||
|
||||
GitLab Duo Merge request summaries are available on the merge request page in:
|
||||
GitLab Duo Merge request summaries can be added to your merge request description when creating or editing a merge request. To add a summary, select **Summarize code changes**. The generated summary is added to the merge request description where your cursor is.
|
||||
|
||||
- The **Merge request summaries** dialog.
|
||||
- The To-Do list.
|
||||
- Email notifications.
|
||||

|
||||
|
||||
Provide feedback on this experimental feature in [issue 408726](https://gitlab.com/gitlab-org/gitlab/-/issues/408726).
|
||||
Provide feedback on this feature in [issue 443236](https://gitlab.com/gitlab-org/gitlab/-/issues/443236).
|
||||
|
||||
**Data usage**: The diff of changes between the source branch's head and the target branch is sent to the large language model.
|
||||
|
||||
|
|
@ -80,6 +52,33 @@ Provide feedback on this experimental feature in [issue 408991](https://gitlab.c
|
|||
|
||||
- Draft comment's text
|
||||
|
||||
## Fill in merge request templates
|
||||
|
||||
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10591) in GitLab 16.3 as an [Experiment](../../../policy/experiment-beta-support.md#experiment).
|
||||
> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/429882) to Beta in GitLab 16.10
|
||||
|
||||
This feature is in [Beta](../../../policy/experiment-beta-support.md) on GitLab.com.
|
||||
|
||||
Merge requests in projects often have [templates](../description_templates.md#create-a-merge-request-template) defined that need to be filled out. This helps reviewers and other users understand the purpose and changes a merge request might propose.
|
||||
|
||||
When creating a merge request, GitLab Duo can generate a description for the merge request based on the contents of the template. This fills in the template and replaces the current contents of the description.
|
||||
|
||||
To generate the description:
|
||||
|
||||
1. [Create a new merge request](creating_merge_requests.md), and go to the **Description** field.
|
||||
1. Select **AI Actions** (**{tanuki}**).
|
||||
1. Select **Fill in merge request template**.
|
||||
|
||||
The updated description is applied to the box. You can edit or revise this description before you finish creating your merge request.
|
||||
|
||||
Provide feedback on this experimental feature in [issue 416537](https://gitlab.com/gitlab-org/gitlab/-/issues/416537).
|
||||
|
||||
**Data usage**: When you use this feature, the following data is sent to the large language model referenced above:
|
||||
|
||||
- Title of the merge request
|
||||
- Contents of the description
|
||||
- Diff of changes between the source branch's head and the target branch
|
||||
|
||||
## Generate messages for merge or squash commits
|
||||
|
||||
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/10453) in GitLab 16.2 as an [Experiment](../../../policy/experiment-beta-support.md#experiment).
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
File diff suppressed because it is too large
Load Diff
|
|
@ -64,13 +64,11 @@ module Banzai
|
|||
next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
|
||||
next unless node.content =~ TAGS_PATTERN
|
||||
|
||||
html = process_tag(Regexp.last_match(1))
|
||||
html = node.content.gsub(TAGS_PATTERN) do
|
||||
process_tag(Regexp.last_match(1)) || Regexp.last_match(0)
|
||||
end
|
||||
|
||||
next unless html && html != node.content
|
||||
|
||||
new_node = Banzai::Filter::SanitizationFilter.new(html).call
|
||||
new_node = new_node&.children&.first&.add_class('gfm')
|
||||
node.replace(new_node.to_html) if new_node
|
||||
node.replace(html)
|
||||
end
|
||||
|
||||
doc
|
||||
|
|
@ -105,12 +103,12 @@ module Banzai
|
|||
path =
|
||||
if url?(content)
|
||||
content
|
||||
elsif file = wiki.find_file(content, load_content: false)
|
||||
elsif wiki && file = wiki.find_file(content, load_content: false)
|
||||
file.path
|
||||
end
|
||||
|
||||
if path
|
||||
content_tag(:img, nil, src: path, class: 'gfm')
|
||||
sanitized_content_tag(:img, nil, src: path, class: 'gfm')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -135,29 +133,48 @@ module Banzai
|
|||
name, reference = *parts.compact.map(&:strip)
|
||||
end
|
||||
|
||||
href =
|
||||
if url?(reference)
|
||||
reference
|
||||
else
|
||||
::File.join(wiki_base_path, reference)
|
||||
end
|
||||
class_list = 'gfm'
|
||||
additional_data = {
|
||||
'canonical-src': reference,
|
||||
link: true,
|
||||
gollum: true
|
||||
}
|
||||
|
||||
content_tag(:a, name || reference, href: href, class: 'gfm')
|
||||
if url?(reference)
|
||||
href = reference
|
||||
elsif wiki
|
||||
href = ::File.join(wiki_base_path, reference)
|
||||
class_list += " gfm-gollum-wiki-page"
|
||||
|
||||
additional_data['reference-type'] = 'wiki_page'
|
||||
additional_data[:project] = context[:project].id if context[:project]
|
||||
additional_data[:group] = context[:group]&.id if context[:group]
|
||||
end
|
||||
|
||||
if href
|
||||
sanitized_content_tag(:a, name || reference, href: href, class: class_list, data: additional_data)
|
||||
end
|
||||
end
|
||||
|
||||
def wiki
|
||||
context[:wiki]
|
||||
context[:wiki] || context[:project]&.wiki || context[:group]&.wiki
|
||||
end
|
||||
|
||||
def wiki_base_path
|
||||
wiki&.wiki_base_path
|
||||
end
|
||||
|
||||
# Ensure that a :wiki key exists in context
|
||||
#
|
||||
# Note that while the key might exist, its value could be nil!
|
||||
def validate
|
||||
needs :wiki
|
||||
def sanitized_content_tag(name, content, options = {})
|
||||
html = content_tag(name, content, options)
|
||||
node = Banzai::Filter::SanitizationFilter.new(html).call
|
||||
link_node = node&.children&.first
|
||||
|
||||
link_node.add_class(options[:class])
|
||||
options[:data]&.each do |key, value|
|
||||
link_node.set_attribute("data-#{key}", value)
|
||||
end
|
||||
|
||||
link_node
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ module Banzai
|
|||
FilterArray[
|
||||
Filter::AsciiDocSanitizationFilter,
|
||||
Filter::CodeLanguageFilter,
|
||||
Filter::GollumTagsFilter,
|
||||
Filter::AssetProxyFilter,
|
||||
Filter::ExternalLinkFilter,
|
||||
Filter::PlantumlFilter,
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ module Banzai
|
|||
Filter::SpacedLinkFilter,
|
||||
Filter::SanitizationFilter,
|
||||
Filter::KrokiFilter,
|
||||
Filter::GollumTagsFilter,
|
||||
Filter::AssetProxyFilter,
|
||||
Filter::MathFilter,
|
||||
Filter::ColorFilter,
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ module Banzai
|
|||
module Pipeline
|
||||
class WikiPipeline < FullPipeline
|
||||
def self.filters
|
||||
@filters ||= super.insert_before(Filter::ImageLazyLoadFilter, Filter::GollumTagsFilter)
|
||||
.insert_before(Filter::TaskListFilter, Filter::WikiLinkFilter)
|
||||
@filters ||= super.insert_before(Filter::TaskListFilter, Filter::WikiLinkFilter)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Banzai
|
||||
module ReferenceParser
|
||||
class WikiPageParser < BaseParser
|
||||
self.reference_type = :wiki_page
|
||||
|
||||
def nodes_visible_to_user(user, nodes)
|
||||
project_attr = 'data-project'
|
||||
group_attr = 'data-group'
|
||||
|
||||
projects = lazy { projects_for_nodes(nodes) }
|
||||
groups = lazy { grouped_objects_for_nodes(nodes, Group, group_attr) }
|
||||
|
||||
preload_associations(projects, user)
|
||||
|
||||
nodes.select do |node|
|
||||
if node.has_attribute?(project_attr)
|
||||
can_read_reference?(user, projects[node], node)
|
||||
elsif node.has_attribute?(group_attr)
|
||||
can_read_reference?(user, groups[node], node)
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def can_read_reference?(user, project_or_group, _node)
|
||||
can?(user, :read_wiki, project_or_group)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -67,6 +67,9 @@ module Banzai
|
|||
original_content = node.attr('data-original')
|
||||
original_content = CGI.escape_html(original_content) if original_content
|
||||
|
||||
# Redact gollum wiki links completely
|
||||
redacted_content = _('[redacted]') if node.attr('data-reference-type') == 'wiki_page'
|
||||
|
||||
# Build the raw <a> tag just with a link as href and content if
|
||||
# it's originally a link pattern. We shouldn't return a plain text href.
|
||||
original_link =
|
||||
|
|
@ -78,7 +81,7 @@ module Banzai
|
|||
|
||||
# The reference should be replaced by the original link's content,
|
||||
# which is not always the same as the rendered one.
|
||||
original_link || original_content || node.inner_html
|
||||
redacted_content || original_link || original_content || node.inner_html
|
||||
end
|
||||
|
||||
def redact_cross_project_references(documents)
|
||||
|
|
|
|||
|
|
@ -44737,7 +44737,7 @@ msgstr ""
|
|||
msgid "ScanResultPolicy|When %{scanType} in an open merge request targeting %{branches} %{branchExceptions} and the licenses match all of the following criteria:"
|
||||
msgstr ""
|
||||
|
||||
msgid "ScanResultPolicy|When %{scanType} in an open that targets %{branches} %{branchExceptions} with %{commitType}"
|
||||
msgid "ScanResultPolicy|When %{scanType} that targets %{branches} %{branchExceptions} with %{commitType}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ScanResultPolicy|When %{scanners} find scanner specified conditions in an open merge request targeting the %{branches} %{branchExceptions} and match %{boldDescription} of the following criteria"
|
||||
|
|
@ -59295,6 +59295,9 @@ msgstr ""
|
|||
msgid "[Supports GitLab-flavored markdown, including quick actions]"
|
||||
msgstr ""
|
||||
|
||||
msgid "[redacted]"
|
||||
msgstr ""
|
||||
|
||||
msgid "`.campfirenow.com` subdomain when you're signed in."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ gem 'capybara', '~> 3.40.0'
|
|||
gem 'capybara-screenshot', '~> 1.0.26'
|
||||
gem 'rake', '~> 13', '>= 13.1.0'
|
||||
gem 'rspec', '~> 3.13'
|
||||
gem 'selenium-webdriver', '= 4.18.1'
|
||||
gem 'selenium-webdriver', '= 4.19.0'
|
||||
gem 'airborne', '~> 0.3.7', require: false # airborne is messing with rspec sandboxed mode so not requiring by default
|
||||
gem 'rest-client', '~> 2.1.0'
|
||||
gem 'rspec-retry', '~> 0.6.2', require: 'rspec/retry'
|
||||
|
|
|
|||
|
|
@ -304,7 +304,7 @@ GEM
|
|||
sawyer (0.9.2)
|
||||
addressable (>= 2.3.5)
|
||||
faraday (>= 0.17.3, < 3)
|
||||
selenium-webdriver (4.18.1)
|
||||
selenium-webdriver (4.19.0)
|
||||
base64 (~> 0.2)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
rubyzip (>= 1.2.2, < 3.0)
|
||||
|
|
@ -379,7 +379,7 @@ DEPENDENCIES
|
|||
rspec-retry (~> 0.6.2)
|
||||
rspec_junit_formatter (~> 0.6.0)
|
||||
ruby-debug-ide (~> 0.7.3)
|
||||
selenium-webdriver (= 4.18.1)
|
||||
selenium-webdriver (= 4.19.0)
|
||||
slack-notifier (~> 2.4)
|
||||
terminal-table (~> 3.0.2)
|
||||
warning (~> 1.3)
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ module QA
|
|||
end
|
||||
|
||||
it(
|
||||
'publishes a composer package and deletes it',
|
||||
'publishes a composer package and deletes it', :blocking,
|
||||
testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348016'
|
||||
) do
|
||||
Page::Project::Menu.perform(&:go_to_package_registry)
|
||||
|
|
|
|||
|
|
@ -287,6 +287,44 @@ RSpec.describe Projects::AutocompleteSourcesController do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'GET wikis' do
|
||||
before do
|
||||
create(:wiki_page, project: project, title: 'foo')
|
||||
create(:wiki_page, project: project, title: 'templates/template1')
|
||||
end
|
||||
|
||||
context 'when user can read wiki pages' do
|
||||
before do
|
||||
group.add_owner(user)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'lists wiki pages (except templates)' do
|
||||
get :wikis, format: :json, params: { namespace_id: group.path, project_id: project.path }
|
||||
|
||||
expect(json_response.pluck('title')).to eq(['foo'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user cannot read wiki pages' do
|
||||
let_it_be(:group2) { create(:group, :public) }
|
||||
let_it_be(:project2) { create(:project, :public, namespace: group2) }
|
||||
|
||||
before do
|
||||
create(:wiki_page, project: project2, title: 'foo')
|
||||
|
||||
# set wikis feature to members only
|
||||
project2.project_feature.update!(wiki_access_level: ProjectFeature::PRIVATE)
|
||||
end
|
||||
|
||||
it 'returns an empty list' do
|
||||
get :wikis, format: :json, params: { namespace_id: group2.path, project_id: project2.path }
|
||||
|
||||
expect(json_response).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET contacts' do
|
||||
let_it_be(:contact_1) { create(:contact, group: group) }
|
||||
let_it_be(:contact_2) { create(:contact, group: group) }
|
||||
|
|
|
|||
|
|
@ -254,6 +254,24 @@ RSpec.describe 'GFM autocomplete', :js, feature_category: :team_planning do
|
|||
|
||||
it_behaves_like 'autocomplete user mentions'
|
||||
|
||||
context 'autocomplete wiki pages' do
|
||||
let_it_be(:wiki_page1) { create(:wiki_page, project: project, title: 'Home') }
|
||||
let_it_be(:wiki_page2) { create(:wiki_page, project: project, title: 'How to use GitLab') }
|
||||
|
||||
it 'shows wiki pages in the autocomplete menu' do
|
||||
fill_in 'Comment', with: '[[ho'
|
||||
|
||||
wait_for_requests
|
||||
|
||||
expect(find_autocomplete_menu).to have_text('Home')
|
||||
expect(find_autocomplete_menu).to have_text('How to use GitLab (How-to-use-GitLab)')
|
||||
|
||||
send_keys [:arrow_down, :enter]
|
||||
|
||||
expect(find_field('Comment').value).to have_text('[[How to use GitLab|How-to-use-GitLab]]')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when mention_autocomplete_backend_filtering is disabled' do
|
||||
before do
|
||||
stub_feature_flags(mention_autocomplete_backend_filtering: false)
|
||||
|
|
|
|||
|
|
@ -78,6 +78,16 @@ describe('~/content_editor/components/suggestions_dropdown', () => {
|
|||
},
|
||||
fieldValue: 'smiley',
|
||||
};
|
||||
const exampleWiki = {
|
||||
title: 'Home',
|
||||
slug: 'home',
|
||||
path: '/path/to/project/-/wikis/home',
|
||||
};
|
||||
const exampleWiki2 = {
|
||||
title: 'Changelog',
|
||||
slug: 'docs/changelog',
|
||||
path: '/path/to/project/-/wikis/docs/changelog',
|
||||
};
|
||||
|
||||
const insertedEmojiProps = {
|
||||
name: 'smiley',
|
||||
|
|
@ -147,6 +157,7 @@ describe('~/content_editor/components/suggestions_dropdown', () => {
|
|||
${'reference'} | ${'snippet'} | ${exampleSnippet} | ${'project'} | ${'<strong class="gl-text-body!">Project</strong> creation QueryRecorder logs'}
|
||||
${'reference'} | ${'snippet'} | ${exampleSnippet} | ${'242'} | ${'<strong class="gl-text-body!">242</strong>0859'}
|
||||
${'emoji'} | ${'emoji'} | ${exampleEmoji} | ${'sm'} | ${'<strong class="gl-text-body!">sm</strong>iley'}
|
||||
${'wiki'} | ${'wiki'} | ${exampleWiki} | ${'home'} | ${'<strong class="gl-text-body!">Home</strong>'}
|
||||
`(
|
||||
'highlights query as bolded in $referenceType text',
|
||||
({ nodeType, referenceType, reference, query, expectedHTML }) => {
|
||||
|
|
@ -185,6 +196,7 @@ describe('~/content_editor/components/suggestions_dropdown', () => {
|
|||
${'reference'} | ${'vulnerability'} | ${'[vulnerability:'} | ${exampleVulnerability} | ${`[vulnerability:60850147]`} | ${{}}
|
||||
${'reference'} | ${'snippet'} | ${'$'} | ${exampleSnippet} | ${`$2420859`} | ${{}}
|
||||
${'emoji'} | ${'emoji'} | ${':'} | ${exampleEmoji} | ${`😃`} | ${insertedEmojiProps}
|
||||
${'link'} | ${'wiki'} | ${'[['} | ${exampleWiki} | ${`Home`} | ${{ canonicalSrc: 'home', href: '/path/to/project/-/wikis/home', isGollumLink: true, isWikiPage: true }}
|
||||
`(
|
||||
'runs a command to insert the selected $referenceType',
|
||||
async ({ char, nodeType, referenceType, reference, insertedText, insertedProps }) => {
|
||||
|
|
@ -308,6 +320,40 @@ describe('~/content_editor/components/suggestions_dropdown', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('rendering wiki references', () => {
|
||||
it('displays wiki title', () => {
|
||||
buildWrapper({
|
||||
propsData: {
|
||||
char: '[[',
|
||||
nodeType: 'link',
|
||||
nodeProps: {
|
||||
referenceType: 'wiki',
|
||||
},
|
||||
items: [exampleWiki],
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain(exampleWiki.title);
|
||||
expect(wrapper.text()).not.toContain(exampleWiki.slug);
|
||||
});
|
||||
|
||||
it('displays wiki slug if title is not the same as the slug', () => {
|
||||
buildWrapper({
|
||||
propsData: {
|
||||
char: '[[',
|
||||
nodeType: 'link',
|
||||
nodeProps: {
|
||||
referenceType: 'wiki',
|
||||
},
|
||||
items: [exampleWiki2],
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.text()).toContain(exampleWiki2.title);
|
||||
expect(wrapper.text()).toContain(exampleWiki2.slug);
|
||||
});
|
||||
});
|
||||
|
||||
describe('rendering epic references', () => {
|
||||
it('displays epic title and reference', () => {
|
||||
buildWrapper({
|
||||
|
|
|
|||
|
|
@ -254,3 +254,10 @@ Array [
|
|||
"Cross Site Scripting (Persistent)",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`AutocompleteHelper for reference type "wiki", searches for "ho" correctly 1`] = `
|
||||
Array [
|
||||
"Home",
|
||||
"How to use GitLab",
|
||||
]
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import {
|
|||
MOCK_MERGE_REQUESTS,
|
||||
MOCK_ASSIGNEES,
|
||||
MOCK_REVIEWERS,
|
||||
MOCK_WIKIS,
|
||||
} from './autocomplete_mock_data';
|
||||
|
||||
jest.mock('~/emoji', () => ({
|
||||
|
|
@ -122,6 +123,7 @@ describe('AutocompleteHelper', () => {
|
|||
mergeRequests: '/mergeRequests',
|
||||
vulnerabilities: '/vulnerabilities',
|
||||
commands: '/commands',
|
||||
wikis: '/wikis',
|
||||
};
|
||||
|
||||
mock.onGet('/members').reply(200, MOCK_MEMBERS);
|
||||
|
|
@ -133,6 +135,7 @@ describe('AutocompleteHelper', () => {
|
|||
mock.onGet('/mergeRequests').reply(200, MOCK_MERGE_REQUESTS);
|
||||
mock.onGet('/vulnerabilities').reply(200, MOCK_VULNERABILITIES);
|
||||
mock.onGet('/commands').reply(200, MOCK_COMMANDS);
|
||||
mock.onGet('/wikis').reply(200, MOCK_WIKIS);
|
||||
|
||||
const sidebarMediator = {
|
||||
store: {
|
||||
|
|
@ -170,6 +173,7 @@ describe('AutocompleteHelper', () => {
|
|||
${'merge_request'} | ${'n'}
|
||||
${'vulnerability'} | ${'cross'}
|
||||
${'command'} | ${'re'}
|
||||
${'wiki'} | ${'ho'}
|
||||
`(
|
||||
'for reference type "$referenceType", searches for "$query" correctly',
|
||||
async ({ referenceType, query }) => {
|
||||
|
|
|
|||
|
|
@ -965,3 +965,21 @@ export const MOCK_COMMANDS = [
|
|||
params: ['\u003c#issue | group/project#issue | issue URL\u003e'],
|
||||
},
|
||||
];
|
||||
|
||||
export const MOCK_WIKIS = [
|
||||
{
|
||||
title: 'Home',
|
||||
slug: 'home',
|
||||
path: '/gitlab-org/gitlab-test/-/wikis/home',
|
||||
},
|
||||
{
|
||||
title: 'How to use GitLab',
|
||||
slug: 'how-to-use-gitlab',
|
||||
path: '/gitlab-org/gitlab-test/-/wikis/how-to-use-gitlab',
|
||||
},
|
||||
{
|
||||
title: 'Changelog',
|
||||
slug: 'changelog',
|
||||
path: '/gitlab-org/gitlab-test/-/wikis/changelog',
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -272,6 +272,31 @@ describe('markdownSerializer', () => {
|
|||
).toBe('[GitLab][gitlab-url]');
|
||||
});
|
||||
|
||||
it.each`
|
||||
title | canonicalSrc | serialized
|
||||
${'Usage'} | ${'usage'} | ${'[[Usage]]'}
|
||||
${'Changelog'} | ${'docs/changelog'} | ${'[[Changelog|docs/changelog]]'}
|
||||
`(
|
||||
'correctly serializes a gollum (wiki) link: $serialized',
|
||||
({ title, canonicalSrc, serialized }) => {
|
||||
expect(
|
||||
serialize(
|
||||
paragraph(
|
||||
link(
|
||||
{
|
||||
isGollumLink: true,
|
||||
isWikiPage: true,
|
||||
href: '/gitlab-org/gitlab-test/-/wikis/link/to/some/wiki/page',
|
||||
canonicalSrc,
|
||||
},
|
||||
title,
|
||||
),
|
||||
),
|
||||
),
|
||||
).toBe(serialized);
|
||||
},
|
||||
);
|
||||
|
||||
it('correctly serializes image references', () => {
|
||||
expect(
|
||||
serialize(
|
||||
|
|
|
|||
|
|
@ -23,6 +23,8 @@ import {
|
|||
crmContactsMock,
|
||||
} from 'ee_else_ce_jest/gfm_auto_complete/mock_data';
|
||||
|
||||
const mockSpriteIcons = '/icons.svg';
|
||||
|
||||
describe('escape', () => {
|
||||
it.each`
|
||||
xssPayload | escapedPayload
|
||||
|
|
@ -55,6 +57,10 @@ describe('GfmAutoComplete', () => {
|
|||
jest.runOnlyPendingTimers();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
window.gon = { sprite_icons: mockSpriteIcons };
|
||||
});
|
||||
|
||||
describe('DefaultOptions.filter', () => {
|
||||
let items;
|
||||
|
||||
|
|
@ -577,7 +583,7 @@ describe('GfmAutoComplete', () => {
|
|||
title: 'My Group',
|
||||
search: 'MyGroup my-group',
|
||||
icon:
|
||||
'<svg class="s16 vertical-align-middle gl-ml-2"><use xlink:href="undefined#notifications-off" /></svg>',
|
||||
'<svg class="s16 vertical-align-middle gl-ml-2"><use xlink:href="/icons.svg#notifications-off" /></svg>',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
|
@ -772,6 +778,54 @@ describe('GfmAutoComplete', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('GfmAutoComplete.Wikis', () => {
|
||||
const wikiPage1 = {
|
||||
title: 'My Wiki Page',
|
||||
slug: 'my-wiki-page',
|
||||
path: '/path/to/project/-/wikis/my-wiki-page',
|
||||
};
|
||||
const wikiPage2 = {
|
||||
title: 'Home',
|
||||
slug: 'home',
|
||||
path: '/path/to/project/-/wikis/home',
|
||||
};
|
||||
|
||||
describe('templateFunction', () => {
|
||||
it('shows both title and slug, if they are different', () => {
|
||||
expect(GfmAutoComplete.Wikis.templateFunction(wikiPage1)).toMatchInlineSnapshot(`
|
||||
<li>
|
||||
<svg
|
||||
class="gl-mr-2 s16 vertical-align-middle"
|
||||
>
|
||||
<use
|
||||
xlink:href="/icons.svg#document"
|
||||
/>
|
||||
</svg>
|
||||
My Wiki Page
|
||||
<small>
|
||||
(my-wiki-page)
|
||||
</small>
|
||||
</li>
|
||||
`);
|
||||
});
|
||||
|
||||
it('shows only title, if title and slug are the same', () => {
|
||||
expect(GfmAutoComplete.Wikis.templateFunction(wikiPage2)).toMatchInlineSnapshot(`
|
||||
<li>
|
||||
<svg
|
||||
class="gl-mr-2 s16 vertical-align-middle"
|
||||
>
|
||||
<use
|
||||
xlink:href="/icons.svg#document"
|
||||
/>
|
||||
</svg>
|
||||
Home
|
||||
</li>
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('labels', () => {
|
||||
const dataSources = {
|
||||
labels: `${TEST_HOST}/autocomplete_sources/labels`,
|
||||
|
|
|
|||
|
|
@ -452,7 +452,7 @@ RSpec.describe ApplicationHelper do
|
|||
|
||||
it 'returns paths for autocomplete_sources_controller' do
|
||||
sources = helper.autocomplete_data_sources(project, noteable_type)
|
||||
expect(sources.keys).to match_array([:members, :issues, :mergeRequests, :labels, :milestones, :commands, :snippets, :contacts])
|
||||
expect(sources.keys).to match_array([:members, :issues, :mergeRequests, :labels, :milestones, :commands, :snippets, :contacts, :wikis])
|
||||
sources.keys.each do |key|
|
||||
expect(sources[key]).not_to be_nil
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,14 +5,9 @@ require 'spec_helper'
|
|||
RSpec.describe Banzai::Filter::GollumTagsFilter, feature_category: :wiki do
|
||||
include FilterSpecHelper
|
||||
|
||||
let(:project) { create(:project) }
|
||||
let(:wiki) { ProjectWiki.new(project, nil) }
|
||||
|
||||
describe 'validation' do
|
||||
it 'ensure that a :wiki key exists in context' do
|
||||
expect { filter("See [[images/image.jpg]]", {}) }.to raise_error ArgumentError, "Missing context keys for Banzai::Filter::GollumTagsFilter: :wiki"
|
||||
end
|
||||
end
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:wiki) { create(:project_wiki, project: project) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
|
||||
context 'linking internal images' do
|
||||
it 'creates img tag if image exists' do
|
||||
|
|
@ -68,6 +63,13 @@ RSpec.describe Banzai::Filter::GollumTagsFilter, feature_category: :wiki do
|
|||
expect(doc.at_css('a').text).to eq 'link-text'
|
||||
expect(doc.at_css('a')['href']).to eq 'http://example.com/pdfs/gollum.pdf'
|
||||
end
|
||||
|
||||
it 'does not add `gfm-gollum-wiki-page` class to the link' do
|
||||
tag = '[[http://example.com]]'
|
||||
doc = filter("See #{tag}", wiki: wiki)
|
||||
|
||||
expect(doc.at_css('a')['class']).to eq 'gfm'
|
||||
end
|
||||
end
|
||||
|
||||
context 'linking internal resources' do
|
||||
|
|
@ -78,6 +80,12 @@ RSpec.describe Banzai::Filter::GollumTagsFilter, feature_category: :wiki do
|
|||
|
||||
expect(doc.at_css('a').text).to eq 'wiki-slug'
|
||||
expect(doc.at_css('a')['href']).to eq expected_path
|
||||
expect(doc.at_css('a')['data-reference-type']).to eq 'wiki_page'
|
||||
expect(doc.at_css('a')['data-canonical-src']).to eq 'wiki-slug'
|
||||
expect(doc.at_css('a')['data-gollum']).to eq 'true'
|
||||
expect(doc.at_css('a')['data-project']).to eq project.id.to_s
|
||||
expect(doc.at_css('a')['data-group']).to be_nil
|
||||
expect(doc.at_css('a')['class']).to eq 'gfm gfm-gollum-wiki-page'
|
||||
end
|
||||
|
||||
it "the created link's text will be link-text" do
|
||||
|
|
@ -89,24 +97,46 @@ RSpec.describe Banzai::Filter::GollumTagsFilter, feature_category: :wiki do
|
|||
expect(doc.at_css('a')['href']).to eq expected_path
|
||||
end
|
||||
|
||||
it "inside back ticks will be exempt from linkification" do
|
||||
it 'inside back ticks will be exempt from linkification' do
|
||||
doc = filter('<code>[[link-in-backticks]]</code>', wiki: wiki)
|
||||
|
||||
expect(doc.at_css('code').text).to eq '[[link-in-backticks]]'
|
||||
end
|
||||
|
||||
it "sanitizes the href attribute (case 1)" do
|
||||
tag = '[[a|http:\'"injected=attribute><img/src="0"onerror="alert(0)">https://gitlab.com/gitlab-org/gitlab/-/issues/1]]'
|
||||
doc = filter("See #{tag}", wiki: wiki)
|
||||
it 'handles group wiki links' do
|
||||
tag = '[[wiki-slug]]'
|
||||
doc = filter("See #{tag}", project: nil, group: group, wiki: wiki)
|
||||
expected_path = ::File.join(wiki.wiki_base_path, 'wiki-slug')
|
||||
|
||||
expect(doc.at_css('a').to_html).to eq '<a href="http:\'%22injected=attribute><img/src=%220%22onerror=%22alert(0)%22>https://gitlab.com/gitlab-org/gitlab/-/issues/1" class="gfm">a</a>'
|
||||
end
|
||||
|
||||
it "sanitizes the href attribute (case 2)" do
|
||||
tag = '<i>[[a|\'"><svg><i/class=gl-show-field-errors><input/title="<script>alert(0)</script>"/></svg>https://gitlab.com/gitlab-org/gitlab/-/issues/1]]'
|
||||
doc = filter("See #{tag}", wiki: wiki)
|
||||
|
||||
expect(doc.at_css('i a').to_html).to eq "<a href=\"#{wiki.wiki_base_path}/\'%22><svg><i/class=gl-show-field-errors><input/title=%22<script>alert(0)</script>%22/></svg>https://gitlab.com/gitlab-org/gitlab/-/issues/1\" class=\"gfm\">a</a>"
|
||||
expect(doc.at_css('a').text).to eq 'wiki-slug'
|
||||
expect(doc.at_css('a')['href']).to eq expected_path
|
||||
expect(doc.at_css('a')['data-reference-type']).to eq 'wiki_page'
|
||||
expect(doc.at_css('a')['data-canonical-src']).to eq 'wiki-slug'
|
||||
expect(doc.at_css('a')['data-gollum']).to eq 'true'
|
||||
expect(doc.at_css('a')['data-project']).to be_nil
|
||||
expect(doc.at_css('a')['data-group']).to eq group.id.to_s
|
||||
expect(doc.at_css('a')['class']).to eq 'gfm gfm-gollum-wiki-page'
|
||||
end
|
||||
end
|
||||
|
||||
it 'adds `gfm-gollum-wiki-page` classes to the link' do
|
||||
tag = '[[wiki-slug]]'
|
||||
doc = filter("See #{tag}", wiki: wiki)
|
||||
|
||||
expect(doc.at_css('a')['class']).to eq 'gfm gfm-gollum-wiki-page'
|
||||
end
|
||||
|
||||
it 'sanitizes the href attribute (case 1)' do
|
||||
tag = '[[a|http:\'"injected=attribute><img/src="0"onerror="alert(0)">https://gitlab.com/gitlab-org/gitlab/-/issues/1]]'
|
||||
doc = filter("See #{tag}", wiki: wiki)
|
||||
|
||||
expect(doc.at_css('a').to_html).to eq '<a href="http:\'%22injected=attribute><img/src=%220%22onerror=%22alert(0)%22>https://gitlab.com/gitlab-org/gitlab/-/issues/1" class="gfm" data-canonical-src="http:\'"injected=attribute><img/src="0"onerror="alert(0)">https://gitlab.com/gitlab-org/gitlab/-/issues/1" data-link="true" data-gollum="true">a</a>'
|
||||
end
|
||||
|
||||
it 'sanitizes the href attribute (case 2)' do
|
||||
tag = '<i>[[a|\'"><svg><i/class=gl-show-field-errors><input/title="<script>alert(0)</script>"/></svg>https://gitlab.com/gitlab-org/gitlab/-/issues/1]]'
|
||||
doc = filter("See #{tag}", wiki: wiki)
|
||||
|
||||
expect(doc.at_css('i a').to_html).to eq "<a href=\"#{wiki.wiki_base_path}/'%22><svg><i/class=gl-show-field-errors><input/title=%22<script>alert(0)</script>%22/></svg>https://gitlab.com/gitlab-org/gitlab/-/issues/1\" class=\"gfm gfm-gollum-wiki-page\" data-canonical-src=\"'"><svg><i/class=gl-show-field-errors><input/title="<script>alert(0)</script>"/></svg>https://gitlab.com/gitlab-org/gitlab/-/issues/1\" data-link=\"true\" data-gollum=\"true\" data-reference-type=\"wiki_page\" data-project=\"#{project.id}\">a</a>"
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,57 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Banzai::ReferenceParser::WikiPageParser, feature_category: :team_planning do
|
||||
include ReferenceParserHelpers
|
||||
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
let(:link) { empty_html_link }
|
||||
|
||||
describe '#nodes_visible_to_user' do
|
||||
before do
|
||||
link['data-reference-type'] = 'wiki_page'
|
||||
end
|
||||
|
||||
context 'when the link has a data-project attribute' do
|
||||
before do
|
||||
link['data-project'] = project.id
|
||||
end
|
||||
|
||||
it 'redacts the link if the user cannot read the project' do
|
||||
nodes = described_class
|
||||
.new(Banzai::RenderContext.new(project, user))
|
||||
.nodes_visible_to_user(user, [link])
|
||||
|
||||
expect(nodes).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the link has a data-group attribute' do
|
||||
before do
|
||||
link['data-group'] = group.id
|
||||
end
|
||||
|
||||
it 'redacts the link if the user cannot read the group' do
|
||||
nodes = described_class
|
||||
.new(Banzai::RenderContext.new(group, user))
|
||||
.nodes_visible_to_user(user, [link])
|
||||
|
||||
expect(nodes).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'if no data-project or data-group attribute is present' do
|
||||
it 'returns the link' do
|
||||
nodes = described_class
|
||||
.new(Banzai::RenderContext.new(project, user))
|
||||
.nodes_visible_to_user(user, [link])
|
||||
|
||||
expect(nodes).to eq([link])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -103,6 +103,18 @@ RSpec.describe Banzai::ReferenceRedactor, feature_category: :team_planning do
|
|||
expect(doc2.to_html).to eq(doc2_html)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when reference is a gollum wiki page link that is not visible to user' do
|
||||
it 'redacts the wiki page title and href' do
|
||||
doc = Nokogiri::HTML.fragment('<a class="gfm" href="https://gitlab.com/path/to/project/-/wikis/foo" data-reference-type="wiki_page" data-gollum="true">foo</a>')
|
||||
|
||||
expect(redactor).to receive(:nodes_visible_to_user).and_return([])
|
||||
|
||||
redactor.redact([doc])
|
||||
|
||||
expect(doc.to_html).to eq('[redacted]')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the user cannot read cross project' do
|
||||
|
|
|
|||
|
|
@ -821,6 +821,8 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process', feature_category: :team
|
|||
"uploading": false,
|
||||
"href": "/uploads/groups-test-file",
|
||||
"title": null,
|
||||
"isGollumLink": false,
|
||||
"isWikiPage": false,
|
||||
"canonicalSrc": "/uploads/groups-test-file",
|
||||
"isReference": false
|
||||
}
|
||||
|
|
@ -848,6 +850,8 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process', feature_category: :team
|
|||
"uploading": false,
|
||||
"href": "projects-test-file",
|
||||
"title": null,
|
||||
"isGollumLink": false,
|
||||
"isWikiPage": false,
|
||||
"canonicalSrc": "projects-test-file",
|
||||
"isReference": false
|
||||
}
|
||||
|
|
@ -905,6 +909,8 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process', feature_category: :team
|
|||
"uploading": false,
|
||||
"href": "project-wikis-test-file",
|
||||
"title": null,
|
||||
"isGollumLink": false,
|
||||
"isWikiPage": false,
|
||||
"canonicalSrc": "project-wikis-test-file",
|
||||
"isReference": false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -153,6 +153,39 @@ RSpec.describe Projects::AutocompleteService, feature_category: :groups_and_proj
|
|||
end
|
||||
end
|
||||
|
||||
describe '#wikis' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group) { create(:group, :public) }
|
||||
let_it_be(:project) { create(:project, :public, group: group) }
|
||||
let_it_be(:wiki) { create(:project_wiki, project: project) }
|
||||
|
||||
before do
|
||||
create(:wiki_page, wiki: wiki, title: 'page1', content: 'content1')
|
||||
create(:wiki_page, wiki: wiki, title: 'templates/page2', content: 'content2')
|
||||
end
|
||||
|
||||
context 'when user can read wiki' do
|
||||
it 'returns wiki pages (except templates)' do
|
||||
service = described_class.new(project, user)
|
||||
results = service.wikis
|
||||
|
||||
expect(results.size).to eq(1)
|
||||
expect(results.first).to include(path: "/#{project.full_path}/-/wikis/page1", slug: 'page1', title: 'page1')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user cannot read wiki' do
|
||||
it 'returns empty array' do
|
||||
project.project_feature.update!(wiki_access_level: ProjectFeature::PRIVATE)
|
||||
|
||||
service = described_class.new(project, nil)
|
||||
results = service.wikis
|
||||
|
||||
expect(results).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#contacts' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:contact_1) { create(:contact, group: group) }
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ RSpec.shared_examples 'rich text editor - autocomplete' do |params = {
|
|||
create(:merge_request, source_project: project, source_branch: 'branch-1', title: 'My Cool Merge Request')
|
||||
create(:label, project: project, title: 'My Cool Label')
|
||||
create(:milestone, project: project, title: 'My Cool Milestone')
|
||||
create(:wiki_page, wiki: project.wiki, title: 'My Cool Wiki Page', content: 'Example')
|
||||
|
||||
project.add_maintainer(create(:user, name: 'abc123', username: 'abc123'))
|
||||
else # group wikis
|
||||
|
|
@ -26,6 +27,7 @@ RSpec.shared_examples 'rich text editor - autocomplete' do |params = {
|
|||
create(:merge_request, source_project: project, source_branch: 'branch-1', title: 'My Cool Merge Request')
|
||||
create(:group_label, group: group, title: 'My Cool Label')
|
||||
create(:milestone, group: group, title: 'My Cool Milestone')
|
||||
create(:wiki_page, wiki: group.wiki, title: 'My Cool Wiki Page', content: 'Example')
|
||||
|
||||
project.add_maintainer(create(:user, name: 'abc123', username: 'abc123'))
|
||||
end
|
||||
|
|
@ -232,6 +234,23 @@ RSpec.shared_examples 'rich text editor - autocomplete' do |params = {
|
|||
expect(page).to have_text('😄')
|
||||
end
|
||||
|
||||
it 'shows suggestions for wiki pages' do
|
||||
type_in_content_editor '[[My'
|
||||
|
||||
expect(find(suggestions_dropdown)).to have_text('My Cool Wiki Page')
|
||||
|
||||
send_keys :enter
|
||||
|
||||
expect(page).not_to have_css(suggestions_dropdown)
|
||||
expect(page).to have_text('My Cool Wiki Page')
|
||||
|
||||
click_button 'Switch to plain text editing'
|
||||
wait_for_requests
|
||||
|
||||
# ensure serialized markdown is in correct format (gollum/wikilinks)
|
||||
expect(page.find('textarea').value).to include('[[My Cool Wiki Page|My-Cool-Wiki-Page]]')
|
||||
end
|
||||
|
||||
it 'doesn\'t show suggestions dropdown if there are no suggestions to show' do
|
||||
type_in_content_editor '%'
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ RSpec.shared_examples 'autocompletes items' do
|
|||
create(:merge_request, source_project: project, title: 'My Cool Merge Request')
|
||||
create(:label, project: project, title: 'My Cool Label')
|
||||
create(:milestone, project: project, title: 'My Cool Milestone')
|
||||
create(:wiki_page, wiki: project.wiki, title: 'My Cool Wiki Page', content: 'Example')
|
||||
|
||||
project.add_maintainer(create(:user, name: 'JohnDoe123'))
|
||||
project.add_maintainer(create(:user, name: 'ReallyLongUsername1234567890'))
|
||||
|
|
@ -17,6 +18,7 @@ RSpec.shared_examples 'autocompletes items' do
|
|||
create(:merge_request, source_project: project, title: 'My Cool Merge Request')
|
||||
create(:group_label, group: group, title: 'My Cool Label')
|
||||
create(:milestone, group: group, title: 'My Cool Milestone')
|
||||
create(:wiki_page, wiki: group.wiki, title: 'My Cool Wiki Page', content: 'Example')
|
||||
|
||||
project.add_maintainer(create(:user, name: 'JohnDoe123'))
|
||||
project.add_maintainer(create(:user, name: 'ReallyLongUsername1234567890'))
|
||||
|
|
@ -41,6 +43,9 @@ RSpec.shared_examples 'autocompletes items' do
|
|||
|
||||
fill_in :wiki_content, with: ':smil'
|
||||
expect(page).to have_text 'smile_cat'
|
||||
|
||||
fill_in :wiki_content, with: '[[My'
|
||||
expect(page).to have_text 'My Cool Wiki Page'
|
||||
end
|
||||
|
||||
it 'autocompletes items with long names' do
|
||||
|
|
|
|||
Loading…
Reference in New Issue