Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-08-26 21:11:25 +00:00
parent a56971e97f
commit 9f6c0ac9fd
49 changed files with 1105 additions and 276 deletions

View File

@ -1 +1,35 @@
export { Blockquote as default } from '@tiptap/extension-blockquote';
import { Blockquote } from '@tiptap/extension-blockquote';
import { wrappingInputRule } from 'prosemirror-inputrules';
import { getParents } from '~/lib/utils/dom_utils';
import { getMarkdownSource } from '../services/markdown_sourcemap';
export const multilineInputRegex = /^\s*>>>\s$/gm;
export default Blockquote.extend({
addAttributes() {
return {
...this.parent?.(),
multiline: {
default: false,
parseHTML: (element) => {
const source = getMarkdownSource(element);
const parentsIncludeBlockquote = getParents(element).some(
(p) => p.nodeName.toLowerCase() === 'blockquote',
);
return {
multiline: source && !source.startsWith('>') && !parentsIncludeBlockquote,
};
},
},
};
},
addInputRules() {
return [
...this.parent?.(),
wrappingInputRule(multilineInputRegex, this.type, () => ({ multiline: true })),
];
},
});

View File

@ -1 +1,17 @@
export { OrderedList as default } from '@tiptap/extension-ordered-list';
import { OrderedList } from '@tiptap/extension-ordered-list';
import { getMarkdownSource } from '../services/markdown_sourcemap';
export default OrderedList.extend({
addAttributes() {
return {
...this.parent?.(),
parens: {
default: false,
parseHTML: (element) => ({
parens: /^[0-9]+\)/.test(getMarkdownSource(element)),
}),
},
};
},
});

View File

@ -1,17 +1,32 @@
import { mergeAttributes } from '@tiptap/core';
import { TaskList } from '@tiptap/extension-task-list';
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
import { getMarkdownSource } from '../services/markdown_sourcemap';
export default TaskList.extend({
addAttributes() {
return {
type: {
default: 'ul',
parseHTML: (element) => {
return {
type: element.tagName.toLowerCase() === 'ol' ? 'ol' : 'ul',
};
},
numeric: {
default: false,
parseHTML: (element) => ({
numeric: element.tagName.toLowerCase() === 'ol',
}),
},
start: {
default: 1,
parseHTML: (element) => ({
start: element.hasAttribute('start')
? parseInt(element.getAttribute('start') || '', 10)
: 1,
}),
},
parens: {
default: false,
parseHTML: (element) => ({
parens: /^[0-9]+\)/.test(getMarkdownSource(element)),
}),
},
};
},
@ -25,7 +40,7 @@ export default TaskList.extend({
];
},
renderHTML({ HTMLAttributes: { type, ...HTMLAttributes } }) {
return [type, mergeAttributes(HTMLAttributes, { 'data-type': 'taskList' }), 0];
renderHTML({ HTMLAttributes: { numeric, ...HTMLAttributes } }) {
return [numeric ? 'ol' : 'ul', mergeAttributes(HTMLAttributes, { 'data-type': 'taskList' }), 0];
},
});

View File

@ -32,12 +32,14 @@ import TaskItem from '../extensions/task_item';
import TaskList from '../extensions/task_list';
import Text from '../extensions/text';
import {
isPlainURL,
renderHardBreak,
renderTable,
renderTableCell,
renderTableRow,
openTag,
closeTag,
renderOrderedList,
} from './serialization_helpers';
const defaultSerializerConfig = {
@ -57,14 +59,15 @@ const defaultSerializerConfig = {
},
},
[Link.name]: {
open() {
return '[';
open(state, mark, parent, index) {
return isPlainURL(mark, parent, index, 1) ? '<' : '[';
},
close(state, mark) {
close(state, mark, parent, index) {
const href = mark.attrs.canonicalSrc || mark.attrs.href;
return `](${state.esc(href)}${
mark.attrs.title ? ` ${state.quote(mark.attrs.title)}` : ''
})`;
return isPlainURL(mark, parent, index, -1)
? '>'
: `](${state.esc(href)}${mark.attrs.title ? ` ${state.quote(mark.attrs.title)}` : ''})`;
},
},
[Strike.name]: {
@ -89,7 +92,18 @@ const defaultSerializerConfig = {
},
nodes: {
[Blockquote.name]: defaultMarkdownSerializer.nodes.blockquote,
[Blockquote.name]: (state, node) => {
if (node.attrs.multiline) {
state.write('>>>');
state.ensureNewLine();
state.renderContent(node);
state.ensureNewLine();
state.write('>>>');
state.closeBlock(node);
} else {
state.wrapBlock('> ', null, node, () => state.renderContent(node));
}
},
[BulletList.name]: defaultMarkdownSerializer.nodes.bullet_list,
[CodeBlockHighlight.name]: (state, node) => {
state.write(`\`\`\`${node.attrs.language || ''}\n`);
@ -113,7 +127,7 @@ const defaultSerializerConfig = {
state.write(`![${state.esc(alt || '')}](${state.esc(canonicalSrc || src)}${quotedTitle})`);
},
[ListItem.name]: defaultMarkdownSerializer.nodes.list_item,
[OrderedList.name]: defaultMarkdownSerializer.nodes.ordered_list,
[OrderedList.name]: renderOrderedList,
[Paragraph.name]: defaultMarkdownSerializer.nodes.paragraph,
[Reference.name]: (state, node) => {
state.write(node.attrs.originalText || node.attrs.text);
@ -127,8 +141,8 @@ const defaultSerializerConfig = {
state.renderContent(node);
},
[TaskList.name]: (state, node) => {
if (node.attrs.type === 'ul') defaultMarkdownSerializer.nodes.bullet_list(state, node);
else defaultMarkdownSerializer.nodes.ordered_list(state, node);
if (node.attrs.numeric) renderOrderedList(state, node);
else defaultMarkdownSerializer.nodes.bullet_list(state, node);
},
[Text.name]: defaultMarkdownSerializer.nodes.text,
},

View File

@ -8,6 +8,22 @@ const defaultAttrs = {
const tableMap = new WeakMap();
// Source taken from
// prosemirror-markdown/src/to_markdown.js
export function isPlainURL(link, parent, index, side) {
if (link.attrs.title || !/^\w+:/.test(link.attrs.href)) return false;
const content = parent.child(index + (side < 0 ? -1 : 0));
if (
!content.isText ||
content.text !== link.attrs.href ||
content.marks[content.marks.length - 1] !== link
)
return false;
if (index === (side < 0 ? 1 : parent.childCount - 1)) return true;
const next = parent.child(index + (side < 0 ? -2 : 1));
return !link.isInSet(next.marks);
}
function shouldRenderCellInline(cell) {
if (cell.childCount === 1) {
const parent = cell.child(0);
@ -206,6 +222,19 @@ function renderTableRowAsHTML(state, node) {
renderTagClose(state, 'tr');
}
export function renderOrderedList(state, node) {
const { parens } = node.attrs;
const start = node.attrs.start || 1;
const maxW = String(start + node.childCount - 1).length;
const space = state.repeat(' ', maxW + 2);
const delimiter = parens ? ')' : '.';
state.renderList(node, space, (i) => {
const nStr = String(start + i);
return `${state.repeat(' ', maxW - nStr.length) + nStr}${delimiter} `;
});
}
export function renderTableCell(state, node) {
if (!isBlockTablesFeatureEnabled()) {
state.renderInline(node);

View File

@ -18,6 +18,7 @@ export default {
data() {
return {
formEnvironment: {
id: this.environment.id,
name: this.environment.name,
externalUrl: this.environment.external_url,
},
@ -33,7 +34,6 @@ export default {
axios
.put(this.updateEnvironmentPath, {
id: this.environment.id,
name: this.formEnvironment.name,
external_url: this.formEnvironment.externalUrl,
})
.then(({ data: { path } }) => visitUrl(path))

View File

@ -39,12 +39,17 @@ export default {
),
nameLabel: __('Name'),
nameFeedback: __('This field is required'),
nameDisabledHelp: __("You cannot rename an environment after it's created."),
nameDisabledLinkText: __('How do I rename an environment?'),
urlLabel: __('External URL'),
urlFeedback: __('The URL should start with http:// or https://'),
save: __('Save'),
cancel: __('Cancel'),
},
helpPagePath: helpPagePath('ci/environments/index.md'),
renamingDisabledHelpPagePath: helpPagePath('ci/environments/index.md', {
anchor: 'rename-an-environment',
}),
data() {
return {
visited: {
@ -54,6 +59,9 @@ export default {
};
},
computed: {
isNameDisabled() {
return Boolean(this.environment.id);
},
valid() {
return {
name: this.visited.name && this.environment.name !== '',
@ -102,10 +110,17 @@ export default {
:state="valid.name"
:invalid-feedback="$options.i18n.nameFeedback"
>
<template v-if="isNameDisabled" #description>
{{ $options.i18n.nameDisabledHelp }}
<gl-link :href="$options.renamingDisabledHelpPagePath" target="_blank">
{{ $options.i18n.nameDisabledLinkText }}
</gl-link>
</template>
<gl-form-input
id="environment_name"
:value="environment.name"
:state="valid.name"
:disabled="isNameDisabled"
name="environment[name]"
required
@input="onChange({ ...environment, name: $event })"

View File

@ -77,3 +77,15 @@ export const isElementVisible = (element) =>
* @returns {Boolean} `true` if the element is currently hidden, otherwise false
*/
export const isElementHidden = (element) => !isElementVisible(element);
export const getParents = (element) => {
const parents = [];
let parent = element.parentNode;
do {
parents.push(parent);
parent = parent.parentNode;
} while (parent);
return parents;
};

View File

@ -213,8 +213,14 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
end
def allowed_environment_attributes
attributes = [:external_url]
attributes << :name if action_name == "create"
attributes
end
def environment_params
params.require(:environment).permit(:name, :external_url)
params.require(:environment).permit(allowed_environment_attributes)
end
def environment

View File

@ -11,7 +11,7 @@ class SearchController < ApplicationController
around_action :allow_gitaly_ref_name_caching
before_action :block_anonymous_global_searches, except: :opensearch
before_action :block_anonymous_global_searches, :check_scope_global_search_enabled, except: :opensearch
skip_before_action :authenticate_user!
requires_cross_project_access if: -> do
search_term_present = params[:search].present? || params[:term].present?
@ -156,6 +156,29 @@ class SearchController < ApplicationController
redirect_to new_user_session_path, alert: _('You must be logged in to search across all of GitLab')
end
def check_scope_global_search_enabled
return if params[:project_id].present? || params[:group_id].present?
search_allowed = case params[:scope]
when 'blobs'
Feature.enabled?(:global_search_code_tab, current_user, type: :ops, default_enabled: true)
when 'commits'
Feature.enabled?(:global_search_commits_tab, current_user, type: :ops, default_enabled: true)
when 'issues'
Feature.enabled?(:global_search_issues_tab, current_user, type: :ops, default_enabled: true)
when 'merge_requests'
Feature.enabled?(:global_search_merge_requests_tab, current_user, type: :ops, default_enabled: true)
when 'wiki_blobs'
Feature.enabled?(:global_search_wiki_tab, current_user, type: :ops, default_enabled: true)
else
true
end
return if search_allowed
redirect_to search_path, alert: _('Global Search is disabled for this scope')
end
def render_timeout(exception)
raise exception unless action_name.to_sym.in?(RESCUE_FROM_TIMEOUT_ACTIONS)

View File

@ -443,6 +443,10 @@ module SearchHelper
_("Open")
end
end
def feature_flag_tab_enabled?(flag)
@group || Feature.enabled?(flag, current_user, type: :ops, default_enabled: true)
end
end
SearchHelper.prepend_mod_with('SearchHelper')

View File

@ -57,7 +57,11 @@ module Issuable
items.each do |issuable|
next unless can?(current_user, :"update_#{type}", issuable)
update_class.new(**update_class.constructor_container_arg(issuable.issuing_parent), current_user: current_user, params: params).execute(issuable)
update_class.new(
**update_class.constructor_container_arg(issuable.issuing_parent),
current_user: current_user,
params: params.dup
).execute(issuable)
end
items

View File

@ -27,11 +27,11 @@
= search_filter_link 'snippet_titles', _("Titles and Descriptions"), search: { snippets: true, group_id: nil, project_id: nil }
- else
= search_filter_link 'projects', _("Projects"), data: { qa_selector: 'projects_tab' }
= render_if_exists 'search/category_code'
= render_if_exists 'search/category_code' if feature_flag_tab_enabled?(:global_search_code_tab)
= render_if_exists 'search/epics_filter_link'
= search_filter_link 'issues', _("Issues")
= search_filter_link 'merge_requests', _("Merge requests")
= render_if_exists 'search/category_wiki'
= search_filter_link 'issues', _("Issues") if feature_flag_tab_enabled?(:global_search_issues_tab)
= search_filter_link 'merge_requests', _("Merge requests") if feature_flag_tab_enabled?(:global_search_merge_requests_tab)
= render_if_exists 'search/category_wiki' if feature_flag_tab_enabled?(:global_search_wiki_tab)
= render_if_exists 'search/category_elasticsearch'
= search_filter_link 'milestones', _("Milestones")
= users

View File

@ -0,0 +1,8 @@
---
name: global_search_code_tab
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68640
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339207
milestone: '14.3'
type: ops
group: group::global search
default_enabled: true

View File

@ -0,0 +1,8 @@
---
name: global_search_commits_tab
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68640
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339207
milestone: '14.3'
type: ops
group: group::global search
default_enabled: true

View File

@ -0,0 +1,8 @@
---
name: global_search_issues_tab
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68640
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339207
milestone: '14.3'
type: ops
group: group::global search
default_enabled: true

View File

@ -0,0 +1,8 @@
---
name: global_search_merge_requests_tab
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68640
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339207
milestone: '14.3'
type: ops
group: group::global search
default_enabled: true

View File

@ -0,0 +1,8 @@
---
name: global_search_wiki_tab
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68640
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339207
milestone: '14.3'
type: ops
group: group::global search
default_enabled: true

View File

@ -0,0 +1,22 @@
---
# Suggestion: gitlab.UnclearAntecedent
#
# Checks for words that need a noun for clarity.
#
# For a list of all options, see https://errata-ai.gitbook.io/vale/getting-started/styles
extends: existence
message: "'%s' is not precise. Try rewriting with a specific subject and verb."
link: https://docs.gitlab.com/ee/development/documentation/styleguide/word_list.html#this-these-that-those
level: suggestion
ignorecase: false
tokens:
- 'That is'
- 'That was'
- 'These are'
- 'These were'
- 'There are'
- 'There were'
- 'This is'
- 'This was'
- 'Those are'
- 'Those were'

View File

@ -194,7 +194,7 @@ PUT /projects/:id/environments/:environments_id
| --------------- | ------- | --------------------------------- | ------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user |
| `environment_id` | integer | yes | The ID of the environment |
| `name` | string | no | The new name of the environment |
| `name` | string | no | [Deprecated and will be removed in GitLab 15.0](https://gitlab.com/gitlab-org/gitlab/-/issues/338897) |
| `external_url` | string | no | The new `external_url` |
```shell

View File

@ -728,6 +728,19 @@ like [Review Apps](../review_apps/index.md) (`review/*`).
The most specific spec takes precedence over the other wildcard matching. In this case,
the `review/feature-1` spec takes precedence over `review/*` and `*` specs.
### Rename an environment
> Renaming environments through the UI [was removed in GitLab 14.3](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68550). Renaming environments through the API was deprected and [will be removed in GitLab 15.0](https://gitlab.com/gitlab-org/gitlab/-/issues/338897).
Renaming an environment through the UI is not possible.
Instead, you need to delete the old environment and create a new one:
1. On the top bar, select **Menu > Projects** and find your project.
1. On the left sidebar, select **Deployments > Environments**.
1. Find the environment and stop it.
1. Delete the environment.
1. Create a new environment with your preferred name.
## Related topics
- [Use GitLab CI to deploy to multiple environments (blog post)](https://about.gitlab.com/blog/2021/02/05/ci-deployment-and-environments/)

View File

@ -131,10 +131,10 @@ The following metadata should be added when a page is moved to another location:
- `redirect_to`: The relative path and filename (with an `.md` extension) of the
location to which visitors should be redirected for a moved page.
[Learn more](#move-or-rename-a-page).
[Learn more](redirects.md).
- `disqus_identifier`: Identifier for Disqus commenting system. Used to keep
comments with a page that's been moved to a new URL.
[Learn more](#redirections-for-pages-with-disqus-comments).
[Learn more](redirects.md#redirections-for-pages-with-disqus-comments).
### Comments metadata
@ -156,133 +156,7 @@ Nanoc layout), which is displayed at the top of the page if defined.
## Move or rename a page
Moving or renaming a document is the same as changing its location. Be sure to
assign a technical writer to any merge request that renames or moves a page.
Technical Writers can help with any questions and can review your change.
When moving or renaming a page, you must redirect browsers to the new page.
This ensures users find the new page, and have the opportunity to update their
bookmarks.
There are two types of redirects:
- Redirect codes added into the documentation files themselves, for users who
view the docs in `/help` on self-managed instances. For example,
[`/help` on GitLab.com](https://gitlab.com/help).
- [GitLab Pages redirects](../../user/project/pages/redirects.md),
for users who view the docs on [`docs.gitlab.com`](https://docs.gitlab.com).
The Technical Writing team manages the [process](https://gitlab.com/gitlab-org/technical-writing/-/blob/main/.gitlab/issue_templates/tw-monthly-tasks.md)
to regularly update the [`redirects.yaml`](https://gitlab.com/gitlab-org/gitlab-docs/-/blob/main/content/_data/redirects.yaml)
file.
To add a redirect:
1. In the repository (`gitlab`, `gitlab-runner`, `omnibus-gitlab`, or `charts`),
create a new documentation file. Don't delete the old one. The easiest
way is to copy it. For example:
```shell
cp doc/user/search/old_file.md doc/api/new_file.md
```
1. Add the redirect code to the old documentation file by running the
following Rake task. The first argument is the path of the old file,
and the second argument is the path of the new file:
- To redirect to a page in the same project, use relative paths and
the `.md` extension. Both old and new paths start from the same location.
In the following example, both paths are relative to `doc/`:
```shell
bundle exec rake "gitlab:docs:redirect[doc/user/search/old_file.md, doc/api/new_file.md]"
```
- To redirect to a page in a different project or site, use the full URL (with `https://`) :
```shell
bundle exec rake "gitlab:docs:redirect[doc/user/search/old_file.md, https://example.com]"
```
Alternatively, you can omit the arguments and be asked to enter their values:
```shell
bundle exec rake gitlab:docs:redirect
```
If you don't want to use the Rake task, you can use the following template.
However, the file paths must be relative to the `doc` or `docs` directory.
Replace the value of `redirect_to` with the new file path and `YYYY-MM-DD`
with the date the file should be removed.
Redirect files that link to docs in internal documentation projects
are removed after three months. Redirect files that link to external sites are
removed after one year:
```markdown
---
redirect_to: '../newpath/to/file/index.md'
remove_date: 'YYYY-MM-DD'
---
This document was moved to [another location](../path/to/file/index.md).
<!-- This redirect file can be deleted after <YYYY-MM-DD>. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->
```
1. If the documentation page being moved has any Disqus comments, follow the steps
described in [Redirections for pages with Disqus comments](#redirections-for-pages-with-disqus-comments).
1. Open a merge request with your changes. If a documentation page
you're removing includes images that aren't used
with any other documentation pages, be sure to use your merge request to delete
those images from the repository.
1. Assign the merge request to a technical writer for review and merge.
1. Search for links to the old documentation file. You must find and update all
links that point to the old documentation file:
- In <https://gitlab.com/gitlab-com/www-gitlab-com>, search for full URLs:
`grep -r "docs.gitlab.com/ee/path/to/file.html" .`
- In <https://gitlab.com/gitlab-org/gitlab-docs/-/tree/master/content/_data>,
search the navigation bar configuration files for the path with `.html`:
`grep -r "path/to/file.html" .`
- In any of the four internal projects, search for links in the docs
and codebase. Search for all variations, including full URL and just the path.
For example, go to the root directory of the `gitlab` project and run:
```shell
grep -r "docs.gitlab.com/ee/path/to/file.html" .
grep -r "path/to/file.html" .
grep -r "path/to/file.md" .
grep -r "path/to/file" .
```
You may need to try variations of relative links, such as `../path/to/file` or
`../file` to find every case.
### Redirections for pages with Disqus comments
If the documentation page being relocated already has Disqus comments,
we need to preserve the Disqus thread.
Disqus uses an identifier per page, and for <https://docs.gitlab.com>, the page identifier
is configured to be the page URL. Therefore, when we change the document location,
we need to preserve the old URL as the same Disqus identifier.
To do that, add to the front matter the variable `disqus_identifier`,
using the old URL as value. For example, let's say we moved the document
available under `https://docs.gitlab.com/my-old-location/README.html` to a new location,
`https://docs.gitlab.com/my-new-location/index.html`.
Into the **new document** front matter, we add the following information. You must
include the filename in the `disqus_identifier` URL, even if it's `index.html` or `README.html`.
```yaml
---
disqus_identifier: 'https://docs.gitlab.com/my-old-location/README.html'
---
```
See [redirects](redirects.md).
## Merge requests for GitLab documentation

View File

@ -0,0 +1,136 @@
---
stage: none
group: Documentation Guidelines
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
description: Learn how to contribute to GitLab Documentation.
---
# Redirects in GitLab documentation
Moving or renaming a document is the same as changing its location. Be sure
to assign a technical writer to any merge request that renames or moves a page.
Technical Writers can help with any questions and can review your change.
When moving or renaming a page, you must redirect browsers to the new page.
This ensures users find the new page, and have the opportunity to update their
bookmarks.
There are two types of redirects:
- Redirect added into the documentation files themselves, for users who
view the docs in `/help` on self-managed instances. For example,
[`/help` on GitLab.com](https://gitlab.com/help).
- [GitLab Pages redirects](../../user/project/pages/redirects.md),
for users who view the docs on [`docs.gitlab.com`](https://docs.gitlab.com).
The Technical Writing team manages the [process](https://gitlab.com/gitlab-org/technical-writing/-/blob/main/.gitlab/issue_templates/tw-monthly-tasks.md)
to regularly update the [`redirects.yaml`](https://gitlab.com/gitlab-org/gitlab-docs/-/blob/main/content/_data/redirects.yaml)
file.
To add a redirect:
1. In the repository (`gitlab`, `gitlab-runner`, `omnibus-gitlab`, or `charts`),
create a new documentation file. Don't delete the old one. The easiest
way is to copy it. For example:
```shell
cp doc/user/search/old_file.md doc/api/new_file.md
```
1. Add the redirect code to the old documentation file by running the
following Rake task. The first argument is the path of the old file,
and the second argument is the path of the new file:
- To redirect to a page in the same project, use relative paths and
the `.md` extension. Both old and new paths start from the same location.
In the following example, both paths are relative to `doc/`:
```shell
bundle exec rake "gitlab:docs:redirect[doc/user/search/old_file.md, doc/api/new_file.md]"
```
- To redirect to a page in a different project or site, use the full URL (with `https://`) :
```shell
bundle exec rake "gitlab:docs:redirect[doc/user/search/old_file.md, https://example.com]"
```
Alternatively, you can omit the arguments and be asked to enter their values:
```shell
bundle exec rake gitlab:docs:redirect
```
If you don't want to use the Rake task, you can use the following template.
However, the file paths must be relative to the `doc` or `docs` directory.
Replace the value of `redirect_to` with the new file path and `YYYY-MM-DD`
with the date the file should be removed.
Redirect files that link to docs in internal documentation projects
are removed after three months. Redirect files that link to external sites are
removed after one year:
```markdown
---
redirect_to: '../newpath/to/file/index.md'
remove_date: 'YYYY-MM-DD'
---
This document was moved to [another location](../path/to/file/index.md).
<!-- This redirect file can be deleted after <YYYY-MM-DD>. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->
```
1. If the documentation page being moved has any Disqus comments, follow the steps
described in [Redirections for pages with Disqus comments](#redirections-for-pages-with-disqus-comments).
1. Open a merge request with your changes. If a documentation page
you're removing includes images that aren't used
with any other documentation pages, be sure to use your merge request to delete
those images from the repository.
1. Assign the merge request to a technical writer for review and merge.
1. Search for links to the old documentation file. You must find and update all
links that point to the old documentation file:
- In <https://gitlab.com/gitlab-com/www-gitlab-com>, search for full URLs:
`grep -r "docs.gitlab.com/ee/path/to/file.html" .`
- In <https://gitlab.com/gitlab-org/gitlab-docs/-/tree/master/content/_data>,
search the navigation bar configuration files for the path with `.html`:
`grep -r "path/to/file.html" .`
- In any of the four internal projects, search for links in the docs
and codebase. Search for all variations, including full URL and just the path.
For example, go to the root directory of the `gitlab` project and run:
```shell
grep -r "docs.gitlab.com/ee/path/to/file.html" .
grep -r "path/to/file.html" .
grep -r "path/to/file.md" .
grep -r "path/to/file" .
```
You may need to try variations of relative links, such as `../path/to/file` or
`../file` to find every case.
## Redirections for pages with Disqus comments
If the documentation page being relocated already has Disqus comments,
we need to preserve the Disqus thread.
Disqus uses an identifier per page, and for <https://docs.gitlab.com>, the page identifier
is configured to be the page URL. Therefore, when we change the document location,
we need to preserve the old URL as the same Disqus identifier.
To do that, add to the front matter the variable `disqus_identifier`,
using the old URL as value. For example, let's say we moved the document
available under `https://docs.gitlab.com/my-old-location/README.html` to a new location,
`https://docs.gitlab.com/my-new-location/index.html`.
Into the **new document** front matter, we add the following information. You must
include the filename in the `disqus_identifier` URL, even if it's `index.html` or `README.html`.
```yaml
---
disqus_identifier: 'https://docs.gitlab.com/my-old-location/README.html'
---
```

View File

@ -420,6 +420,11 @@ Some contractions, however, should be avoided:
| Requests to localhost are not allowed. | Requests to localhost aren't allowed. |
| Specified URL cannot be used. | Specified URL can't be used. |
### Acronyms
If you use an acronym, spell it out on first use on a page. You do not need to spell it out more than once on a page.
When possible, try to avoid acronyms in headings.
## Text
- [Write in Markdown](#markdown).

View File

@ -6,10 +6,11 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Downgrading from EE to CE
If you ever decide to downgrade your Enterprise Edition back to the Community
Edition, there are a few steps you need take before installing the CE package
on top of the current EE package, or, if you are in an installation from source,
before you change remotes and fetch the latest CE code.
If you ever decide to downgrade your Enterprise Edition back to the
Community Edition, there are a few steps you need take beforehand. On Omnibus GitLab
installations, these steps are made before installing the CE package on top of
the current EE package. On installations from source, they are done before
you change remotes and fetch the latest CE code.
## Disable Enterprise-only features
@ -17,8 +18,8 @@ First thing to do is to disable the following features.
### Authentication mechanisms
Kerberos and Atlassian Crowd are only available on the Enterprise Edition, so
you should disable these mechanisms before downgrading and you should provide
Kerberos and Atlassian Crowd are only available on the Enterprise Edition. You
should disable these mechanisms before downgrading. Be sure to provide
alternative authentication methods to your users.
### Remove Service Integration entries from the database
@ -35,63 +36,63 @@ column if you didn't intend it to be used for storing the inheritance class or o
use another column for that information.)
```
All integrations are created automatically for every project you have, so in order
to avoid getting this error, you need to remove all records with the type set to
All integrations are created automatically for every project you have.
To avoid getting this error, you must remove all records with the type set to
`GithubService` from your database:
**Omnibus Installation**
- **Omnibus Installation**
```shell
sudo gitlab-rails runner "Integration.where(type: ['GithubService']).delete_all"
```
```shell
sudo gitlab-rails runner "Integration.where(type: ['GithubService']).delete_all"
```
**Source Installation**
- **Source Installation**
```shell
bundle exec rails runner "Integration.where(type: ['GithubService']).delete_all" production
```
```shell
bundle exec rails runner "Integration.where(type: ['GithubService']).delete_all" production
```
NOTE:
If you are running `GitLab =< v13.0` you need to also remove `JenkinsDeprecatedService` records
and if you are running `GitLab =< v13.6` you need to also remove `JenkinsService` records.
If you are running `GitLab =< v13.0` you must also remove `JenkinsDeprecatedService` records
and if you are running `GitLab =< v13.6` you must remove `JenkinsService` records.
### Variables environment scopes
If you're using this feature and there are variables sharing the same
key, but they have different scopes in a project, then you might want to
revisit the environment scope setting for those variables.
In GitLab Community Edition, [environment scopes](../user/group/clusters/index.md#environment-scopes)
are completely ignored, so if you are using this feature there may be some
necessary adjustments to your configuration. This is especially true if
configuration variables share the same key, but have different
scopes in a project. In cases like these you could accidentally get a variable
which you're not expecting for a particular environment. Make sure that you have
the right variables in this case.
In CE, environment scopes are completely ignored, therefore you could
accidentally get a variable which you're not expecting for a particular
environment. Make sure that you have the right variables in this case.
Data is completely preserved, so you could always upgrade back to EE and
restore the behavior if you leave it alone.
Your data is completely preserved in the transition, so you could always upgrade
back to EE and restore the behavior if you leave it alone.
## Downgrade to CE
After performing the above mentioned steps, you are now ready to downgrade your
GitLab installation to the Community Edition.
**Omnibus Installation**
- **Omnibus Installation**
To downgrade an Omnibus installation, it is sufficient to install the Community
Edition package on top of the currently installed one. You can do this manually,
by directly [downloading the package](https://packages.gitlab.com/gitlab/gitlab-ce)
you need, or by adding our CE package repository and following the
[CE installation instructions](https://about.gitlab.com/install/?version=ce).
To downgrade an Omnibus installation, it is sufficient to install the Community
Edition package on top of the currently installed one. You can do this manually,
by directly [downloading the package](https://packages.gitlab.com/gitlab/gitlab-ce)
you need, or by adding our CE package repository and following the
[CE installation instructions](https://about.gitlab.com/install/?version=ce).
**Source Installation**
- **Source Installation**
To downgrade a source installation, you need to replace the current remote of
your GitLab installation with the Community Edition's remote, fetch the latest
changes, and checkout the latest stable branch:
```shell
git remote set-url origin git@gitlab.com:gitlab-org/gitlab-foss.git
git fetch --all
git checkout 8-x-stable
```
To downgrade a source installation, you must replace the current remote of
your GitLab installation with the Community Edition's remote. After that, you
can fetch the latest changes, and checkout the latest stable branch:
```shell
git remote set-url origin git@gitlab.com:gitlab-org/gitlab-foss.git
git fetch --all
git checkout 8-x-stable
```
Remember to follow the correct [update guides](../update/index.md) to make
sure all dependencies are up to date.

View File

@ -9,8 +9,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
To enable the Auth0 OmniAuth provider, you must create an Auth0 account, and an
application.
1. Sign in to the [Auth0 Console](https://auth0.com/auth/login). If you need to
create an account, you can do so at the same link.
1. Sign in to the [Auth0 Console](https://auth0.com/auth/login). You can also
create an account using the same link.
1. Select **New App/API**.

View File

@ -6,7 +6,11 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# CAS OmniAuth Provider **(FREE)**
To enable the CAS OmniAuth provider you must register your application with your CAS instance. This requires the service URL GitLab supplies to CAS. It should be something like: `https://gitlab.example.com:443/users/auth/cas3/callback?url`. By default handling for SLO is enabled, you only need to configure CAS for back-channel logout.
To enable the CAS OmniAuth provider you must register your application with your
CAS instance. This requires the service URL GitLab supplies to CAS. It should be
something like: `https://gitlab.example.com:443/users/auth/cas3/callback?url`.
Handling for Single Logout (SLO) is enabled by default, so you only have to
configure CAS for back-channel logout.
1. On your GitLab server, open the configuration file.

View File

@ -4,9 +4,10 @@ group: Integrations
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Facebook OAuth2 OmniAuth Provider **(FREE)**
# Facebook OAuth 2.0 OmniAuth Provider **(FREE)**
To enable the Facebook OmniAuth provider you must register your application with Facebook. Facebook generates an app ID and secret key for you to use.
To enable the Facebook OmniAuth provider you must register your application with
Facebook. Facebook generates an app ID and secret key for you to use.
1. Sign in to the [Facebook Developer Platform](https://developers.facebook.com/).
@ -14,8 +15,9 @@ To enable the Facebook OmniAuth provider you must register your application with
1. Select the type "Website"
1. Enter a name for your app. This can be anything. Consider something like "&lt;Organization&gt;'s GitLab" or "&lt;Your Name&gt;'s GitLab" or
something else descriptive.
1. Enter a name for your app. This can be anything. Consider something like
"&lt;Organization&gt;'s GitLab" or "&lt;Your Name&gt;'s GitLab" or something
else descriptive.
1. Choose "Create New Facebook App ID"
@ -49,7 +51,8 @@ To enable the Facebook OmniAuth provider you must register your application with
1. Choose "Show" next to the hidden "App Secret"
1. You should now see an app key and app secret (see screenshot). Keep this page open as you continue configuration.
1. You should now see an app key and app secret (see screenshot). Keep this page
open as you continue configuration.
![Facebook API Keys](img/facebook_api_keys.png)
@ -101,4 +104,7 @@ To enable the Facebook OmniAuth provider you must register your application with
1. [Reconfigure](../administration/restart_gitlab.md#omnibus-gitlab-reconfigure) or [restart GitLab](../administration/restart_gitlab.md#installations-from-source) for the changes to take effect if you
installed GitLab via Omnibus or from source respectively.
On the sign in page there should now be a Facebook icon below the regular sign in form. Click the icon to begin the authentication process. Facebook asks the user to sign in and authorize the GitLab application. If everything goes well the user is returned to GitLab and signed in.
On the sign in page there should now be a Facebook icon below the regular sign
in form. Click the icon to begin the authentication process. Facebook asks the
user to sign in and authorize the GitLab application. If everything goes well
the user is returned to GitLab and signed in.

View File

@ -12,10 +12,11 @@ If correctly set up, emails that require an action are marked in Gmail.
![GMail actions button](img/gmail_action_buttons_for_gitlab.png)
To get this functioning, you need to be registered with Google. For instructions, see
To get this functioning, you must be registered with Google. For instructions, see
[Register with Google](https://developers.google.com/gmail/markup/registering-with-google).
This process has many steps. Make sure that you fulfill all requirements set by Google to avoid your application being rejected by Google.
This process has many steps. Make sure that you fulfill all requirements set by
Google to avoid your application being rejected by Google.
In particular, note:

View File

@ -4,12 +4,12 @@ group: Integrations
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Google OAuth2 OmniAuth Provider **(FREE)**
# Google OAuth 2.0 OmniAuth Provider **(FREE)**
To enable the Google OAuth2 OmniAuth provider you must register your application
To enable the Google OAuth 2.0 OmniAuth provider you must register your application
with Google. Google generates a client ID and secret key for you to use.
## Enabling Google OAuth
## Enable Google OAuth
In Google's side:
@ -47,7 +47,7 @@ In Google's side:
- Cloud Resource Manager API
- Cloud Billing API
To do so you need to:
To do so you should:
1. Go to the [Google API Console](https://console.developers.google.com/apis/dashboard).
1. Click on **ENABLE APIS AND SERVICES** button at the top of the page.
@ -98,8 +98,8 @@ On your GitLab server:
1. Change `YOUR_APP_ID` to the client ID from the Google Developer page
1. Similarly, change `YOUR_APP_SECRET` to the client secret
1. Make sure that you configure GitLab to use a fully-qualified domain name, as Google doesn't accept
raw IP addresses.
1. Make sure that you configure GitLab to use a fully-qualified domain name, as
Google doesn't accept raw IP addresses.
For Omnibus packages:
@ -115,8 +115,10 @@ On your GitLab server:
```
1. Save the configuration file.
1. [Reconfigure](../administration/restart_gitlab.md#omnibus-gitlab-reconfigure) or [restart GitLab](../administration/restart_gitlab.md#installations-from-source) for the changes to take effect if you
installed GitLab via Omnibus or from source respectively.
1. [Reconfigure](../administration/restart_gitlab.md#omnibus-gitlab-reconfigure)
or [restart GitLab](../administration/restart_gitlab.md#installations-from-source) for
the changes to take effect if you installed GitLab via Omnibus or from source
respectively.
On the sign in page there should now be a Google icon below the regular sign in
form. Click the icon to begin the authentication process. Google asks the

View File

@ -40,7 +40,7 @@ In GitLab, perform the following steps.
### Read access to repository
Jenkins needs read access to the GitLab repository. We already specified a
private key to use in Jenkins, now we need to add a public one to the GitLab
private key to use in Jenkins, now we must add a public one to the GitLab
project. For that case we need a Deploy key. Read the documentation on
[how to set up a Deploy key](../user/project/deploy_keys/index.md).
@ -50,7 +50,8 @@ Now navigate to GitLab services page and activate Jenkins
![screen](img/jenkins_gitlab_service.png)
Done! When you push to GitLab, it creates a build for Jenkins. You can view the merge request build status with a link to the Jenkins build.
Done! When you push to GitLab, it creates a build for Jenkins. You can view the
merge request build status with a link to the Jenkins build.
### Multi-project Configuration

View File

@ -73,10 +73,10 @@ self-managed GitLab instances with Jira Cloud, you can either:
You can configure your Atlassian Cloud instance to allow you to install applications
from outside the Marketplace, which allows you to install the application:
1. Sign in to your Jira instance as a user with administrator permissions.
1. Sign in to your Jira instance as a user with an Administrator role.
1. Place your Jira instance into
[development mode](https://developer.atlassian.com/cloud/jira/platform/getting-started-with-connect/#step-2--enable-development-mode).
1. Sign in to your GitLab application as a user with [Administrator](../../user/permissions.md) permissions.
1. Sign in to your GitLab application as a user with an [Administrator](../../user/permissions.md) role.
1. Install the GitLab application from your self-managed GitLab instance, as
described in the [Atlassian developer guides](https://developer.atlassian.com/cloud/jira/platform/getting-started-with-connect/#step-3--install-and-test-your-app):
1. In your Jira instance, go to **Apps > Manage Apps** and click **Upload app**:
@ -104,7 +104,7 @@ application.
### Create a Marketplace listing **(FREE SELF)**
If you prefer to not use development mode on your Jira instance, you can create
your own Marketplace listing for your instance, which enables your application
your own Marketplace listing for your instance. This enables your application
to be installed from the Atlassian Marketplace.
For full instructions, review the Atlassian [guide to creating a marketplace listing](https://developer.atlassian.com/platform/marketplace/installing-cloud-apps/#creating-the-marketplace-listing). To create a
@ -124,9 +124,12 @@ for details.
NOTE:
DVCS means distributed version control system.
## Troubleshooting GitLab.com for Jira Cloud app
## Troubleshoot GitLab.com for Jira Cloud app
The GitLab.com for Jira Cloud app uses an iframe to add namespaces on the settings page. Some browsers block cross-site cookies, which can lead to a message saying that the user needs to log in on GitLab.com even though the user is already logged in.
The GitLab.com for Jira Cloud app uses an iframe to add namespaces on the
settings page. Some browsers block cross-site cookies, which can lead to a
message saying that the user needs to log in on GitLab.com even though the user
is already logged in.
> "You need to sign in or sign up before continuing."

View File

@ -9,7 +9,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
Use the Jira DVCS (distributed version control system) connector if you self-host
your Jira instance, and you want to sync information
between GitLab and Jira. If you use Jira Cloud and GitLab.com, you should use the
[GitLab.com for Jira Cloud app](connect-app.md) unless you specifically need the DVCS connector.
[GitLab.com for Jira Cloud app](connect-app.md) unless you specifically need the
DVCS connector.
When you configure the Jira DVCS connector, make sure your GitLab and Jira instances
are accessible.
@ -61,14 +62,13 @@ you can still perform multiple actions in a single commit:
## Configure a GitLab application for DVCS
We recommend you create and use a `jira` user in GitLab, and use the account only
for integration work. A separate account ensures regular account maintenance does not affect
your integration.
We recommend you create and use a `jira` user in GitLab, and use the account
only for integration work. A separate account ensures regular account
maintenance does not affect your integration.
1. In GitLab, [create a user](../../user/profile/account/create_accounts.md) for Jira to
use to connect to GitLab. For Jira to access all projects,
a user with [administrator](../../user/permissions.md) permissions must
create the user with administrator permissions.
this user must have an [Administrator](../../user/permissions.md) role.
1. Sign in as the `jira` user.
1. In the top right corner, click the account's avatar, and select **Edit profile**.
1. In the left sidebar, select **Applications**.
@ -141,7 +141,7 @@ can refresh the data manually from the Jira interface:
column, select the icon:
![Refresh GitLab information in Jira](img/jira_dev_panel_manual_refresh.png)
## Troubleshooting your DVCS connection
## Troubleshoot your DVCS connection
Refer to the items in this section if you're having problems with your DVCS connector.
@ -174,7 +174,8 @@ Error obtaining access token. Cannot access https://gitlab.example.com from Jira
must have the appropriate certificate (such as your organization's
root certificate) added to it .
Refer to Atlassian's documentation and Atlassian Support for assistance setting up Jira correctly:
Refer to Atlassian's documentation and Atlassian Support for assistance setting
up Jira correctly:
- [Add a certificate](https://confluence.atlassian.com/kb/how-to-import-a-public-ssl-certificate-into-a-jvm-867025849.html)
to the trust store.
@ -234,7 +235,7 @@ To resolve this issue:
### Fix synchronization issues
If Jira displays incorrect information, such as deleted branches, you may need to
If Jira displays incorrect information, such as deleted branches, you may have to
resynchronize the information. To do so:
1. In Jira, go to **Jira Administration > Applications > DVCS accounts**.

View File

@ -25,7 +25,7 @@ This process creates a user named `gitlab` and adds it to a new group named `git
1. Create a new user account (`gitlab`) with write access to
projects in Jira.
- **Email address**: Jira requires a valid email address, and sends a verification
email, which you need to set up the password.
email, which is required to set up the password.
- **Username**: Jira creates the username by using the email prefix. You can change
this username later.
- **Password**: You must create a password, because the GitLab integration doesn't

View File

@ -119,3 +119,24 @@ You can search a specific issue or merge request by its ID with a special prefix
- To search by issue ID, use prefix `#` followed by issue ID. For example, [#23456](https://gitlab.com/search?snippets=&scope=issues&repository_ref=&search=%2323456&group_id=9970&project_id=278964)
- To search by merge request ID, use prefix `!` followed by merge request ID. For example [!23456](https://gitlab.com/search?snippets=&scope=merge_requests&repository_ref=&search=%2123456&group_id=9970&project_id=278964)
## Global search scopes **(FREE SELF)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68640) in GitLab 14.3.
To improve the performance of your instance's global search, you can limit
the scope of the search. To do so, you can exclude global search scopes by disabling
[`ops` feature flags](../../development/feature_flags/index.md#ops-type).
Global search has all its scopes **enabled** by default in GitLab SaaS and
self-managed instances. A GitLab administrator can disable the following `ops`
feature flags to limit the scope of your instance's global search and optimize
its performance:
| Scope | Feature flag | Description |
|--|--|--|
| Code | `global_search_code_tab` | When enabled, the global search includes code as part of the search. |
| Commits | `global_search_commits_tab` | When enabled, the global search includes commits as part of the search. |
| Issues | `global_search_issues_tab` | When enabled, the global search includes issues as part of the search. |
| Merge Requests | `global_search_merge_requests_tab` | When enabled, the global search includes merge requests as part of the search. |
| Wiki | `global_search_wiki_tab` | When enabled, the global search includes wiki as part of the search. |

View File

@ -58,7 +58,8 @@ module API
end
params do
requires :environment_id, type: Integer, desc: 'The environment ID'
optional :name, type: String, desc: 'The new environment name'
# TODO: disallow renaming via the API https://gitlab.com/gitlab-org/gitlab/-/issues/338897
optional :name, type: String, desc: 'DEPRECATED: Renaming environment can lead to errors, this will be removed in 15.0'
optional :external_url, type: String, desc: 'The new URL on which this deployment is viewable'
optional :slug, absence: { message: "is automatically generated and cannot be changed" }
end

View File

@ -15488,6 +15488,9 @@ msgstr ""
msgid "Given epic is already related to this epic."
msgstr ""
msgid "Global Search is disabled for this scope"
msgstr ""
msgid "Global Shortcuts"
msgstr ""
@ -16635,6 +16638,9 @@ msgstr ""
msgid "How do I mirror repositories?"
msgstr ""
msgid "How do I rename an environment?"
msgstr ""
msgid "How do I set up a Google Chat webhook?"
msgstr ""
@ -38260,6 +38266,9 @@ msgstr ""
msgid "You cannot play this scheduled pipeline at the moment. Please wait a minute."
msgstr ""
msgid "You cannot rename an environment after it's created."
msgstr ""
msgid "You cannot write to a read-only secondary GitLab Geo instance. Please use %{link_to_primary_node} instead."
msgstr ""

View File

@ -222,6 +222,16 @@ RSpec.describe Projects::EnvironmentsController do
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'when name is passed' do
let(:params) { environment_params.merge(environment: { name: "new name" }) }
it 'ignores name' do
expect do
subject
end.not_to change { environment.reload.name }
end
end
end
describe 'PATCH #stop' do

View File

@ -182,6 +182,37 @@ RSpec.describe SearchController do
end
end
end
context 'tab feature flags' do
subject { get :show, params: { scope: scope, search: 'term' }, format: :html }
where(:feature_flag, :scope) do
:global_search_code_tab | 'blobs'
:global_search_issues_tab | 'issues'
:global_search_merge_requests_tab | 'merge_requests'
:global_search_wiki_tab | 'wiki_blobs'
:global_search_commits_tab | 'commits'
end
with_them do
it 'returns 200 if flag is enabled' do
stub_feature_flags(feature_flag => true)
subject
expect(response).to have_gitlab_http_status(:ok)
end
it 'redirects with alert if flag is disabled' do
stub_feature_flags(feature_flag => false)
subject
expect(response).to redirect_to search_path
expect(controller).to set_flash[:alert].to(/Global Search is disabled for this scope/)
end
end
end
end
it 'finds issue comments' do

View File

@ -7,9 +7,17 @@ import Image from '~/content_editor/extensions/image';
import Link from '~/content_editor/extensions/link';
import Loading from '~/content_editor/extensions/loading';
import httpStatus from '~/lib/utils/http_status';
import { loadMarkdownApiResult } from '../markdown_processing_examples';
import { createTestEditor, createDocBuilder } from '../test_utils';
const PROJECT_WIKI_ATTACHMENT_IMAGE_HTML = `<p data-sourcepos="1:1-1:27" dir="auto">
<a class="no-attachment-icon" href="/group1/project1/-/wikis/test-file.png" target="_blank" rel="noopener noreferrer" data-canonical-src="test-file.png">
<img alt="test-file" class="lazy" data-src="/group1/project1/-/wikis/test-file.png" data-canonical-src="test-file.png">
</a>
</p>`;
const PROJECT_WIKI_ATTACHMENT_LINK_HTML = `<p data-sourcepos="1:1-1:26" dir="auto">
<a href="/group1/project1/-/wikis/test-file.zip" data-canonical-src="test-file.zip">test-file</a>
</p>`;
describe('content_editor/extensions/attachment', () => {
let tiptapEditor;
let eq;
@ -76,7 +84,7 @@ describe('content_editor/extensions/attachment', () => {
const base64EncodedFile = 'data:image/png;base64,Zm9v';
beforeEach(() => {
renderMarkdown.mockResolvedValue(loadMarkdownApiResult('project_wiki_attachment_image'));
renderMarkdown.mockResolvedValue(PROJECT_WIKI_ATTACHMENT_IMAGE_HTML);
});
describe('when uploading succeeds', () => {
@ -151,7 +159,7 @@ describe('content_editor/extensions/attachment', () => {
});
describe('when the file has a zip (or any other attachment) mime type', () => {
const markdownApiResult = loadMarkdownApiResult('project_wiki_attachment_link');
const markdownApiResult = PROJECT_WIKI_ATTACHMENT_LINK_HTML;
beforeEach(() => {
renderMarkdown.mockResolvedValue(markdownApiResult);

View File

@ -0,0 +1,19 @@
import { multilineInputRegex } from '~/content_editor/extensions/blockquote';
describe('content_editor/extensions/blockquote', () => {
describe.each`
input | matches
${'>>> '} | ${true}
${' >>> '} | ${true}
${'\t>>> '} | ${true}
${'>> '} | ${false}
${'>>>x '} | ${false}
${'> '} | ${false}
`('multilineInputRegex', ({ input, matches }) => {
it(`${matches ? 'matches' : 'does not match'}: "${input}"`, () => {
const match = new RegExp(multilineInputRegex).test(input);
expect(match).toBe(matches);
});
});
});

View File

@ -1,9 +1,15 @@
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
import { loadMarkdownApiResult } from '../markdown_processing_examples';
import { createTestEditor } from '../test_utils';
const CODE_BLOCK_HTML = `<pre class="code highlight js-syntax-highlight language-javascript" lang="javascript" v-pre="true">
<code>
<span id="LC1" class="line" lang="javascript">
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">hello world</span><span class="dl">'</span><span class="p">)</span>
</span>
</code>
</pre>`;
describe('content_editor/extensions/code_block_highlight', () => {
let codeBlockHtmlFixture;
let parsedCodeBlockHtmlFixture;
let tiptapEditor;
@ -12,10 +18,9 @@ describe('content_editor/extensions/code_block_highlight', () => {
beforeEach(() => {
tiptapEditor = createTestEditor({ extensions: [CodeBlockHighlight] });
codeBlockHtmlFixture = loadMarkdownApiResult('code_block');
parsedCodeBlockHtmlFixture = parseHTML(codeBlockHtmlFixture);
parsedCodeBlockHtmlFixture = parseHTML(CODE_BLOCK_HTML);
tiptapEditor.commands.setContent(codeBlockHtmlFixture);
tiptapEditor.commands.setContent(CODE_BLOCK_HTML);
});
it('extracts language and params attributes from Markdown API output', () => {

View File

@ -8,6 +8,7 @@ import HardBreak from '~/content_editor/extensions/hard_break';
import Heading from '~/content_editor/extensions/heading';
import HorizontalRule from '~/content_editor/extensions/horizontal_rule';
import Image from '~/content_editor/extensions/image';
import InlineDiff from '~/content_editor/extensions/inline_diff';
import Italic from '~/content_editor/extensions/italic';
import Link from '~/content_editor/extensions/link';
import ListItem from '~/content_editor/extensions/list_item';
@ -18,6 +19,8 @@ import Table from '~/content_editor/extensions/table';
import TableCell from '~/content_editor/extensions/table_cell';
import TableHeader from '~/content_editor/extensions/table_header';
import TableRow from '~/content_editor/extensions/table_row';
import TaskItem from '~/content_editor/extensions/task_item';
import TaskList from '~/content_editor/extensions/task_list';
import Text from '~/content_editor/extensions/text';
import markdownSerializer from '~/content_editor/services/markdown_serializer';
import { createTestEditor, createDocBuilder } from '../test_utils';
@ -40,6 +43,7 @@ const tiptapEditor = createTestEditor({
Heading,
HorizontalRule,
Image,
InlineDiff,
Italic,
Link,
ListItem,
@ -50,6 +54,8 @@ const tiptapEditor = createTestEditor({
TableCell,
TableHeader,
TableRow,
TaskItem,
TaskList,
Text,
],
});
@ -67,6 +73,7 @@ const {
hardBreak,
horizontalRule,
image,
inlineDiff,
italic,
link,
listItem,
@ -77,6 +84,8 @@ const {
tableCell,
tableHeader,
tableRow,
taskItem,
taskList,
},
} = createDocBuilder({
tiptapEditor,
@ -91,6 +100,7 @@ const {
heading: { nodeType: Heading.name },
horizontalRule: { nodeType: HorizontalRule.name },
image: { nodeType: Image.name },
inlineDiff: { markType: InlineDiff.name },
italic: { nodeType: Italic.name },
link: { markType: Link.name },
listItem: { nodeType: ListItem.name },
@ -101,6 +111,8 @@ const {
tableCell: { nodeType: TableCell.name },
tableHeader: { nodeType: TableHeader.name },
tableRow: { nodeType: TableRow.name },
taskItem: { nodeType: TaskItem.name },
taskList: { nodeType: TaskList.name },
},
});
@ -111,6 +123,25 @@ const serialize = (...content) =>
});
describe('markdownSerializer', () => {
it('correctly serializes bold', () => {
expect(serialize(paragraph(bold('bold')))).toBe('**bold**');
});
it('correctly serializes italics', () => {
expect(serialize(paragraph(italic('italics')))).toBe('_italics_');
});
it('correctly serializes inline diff', () => {
expect(
serialize(
paragraph(
inlineDiff({ type: 'addition' }, '+30 lines'),
inlineDiff({ type: 'deletion' }, '-10 lines'),
),
),
).toBe('{++30 lines+}{--10 lines-}');
});
it('correctly serializes a line break', () => {
expect(serialize(paragraph('hello', hardBreak(), 'world'))).toBe('hello\\\nworld');
});
@ -121,6 +152,12 @@ describe('markdownSerializer', () => {
);
});
it('correctly serializes a plain URL link', () => {
expect(serialize(paragraph(link({ href: 'https://example.com' }, 'https://example.com')))).toBe(
'<https://example.com>',
);
});
it('correctly serializes a link with a title', () => {
expect(
serialize(
@ -129,6 +166,16 @@ describe('markdownSerializer', () => {
).toBe('[example url](https://example.com "click this link")');
});
it('correctly serializes a plain URL link with a title', () => {
expect(
serialize(
paragraph(
link({ href: 'https://example.com', title: 'link title' }, 'https://example.com'),
),
),
).toBe('[https://example.com](https://example.com "link title")');
});
it('correctly serializes a link with a canonicalSrc', () => {
expect(
serialize(
@ -146,6 +193,115 @@ describe('markdownSerializer', () => {
).toBe('[download file](file.zip "click here to download")');
});
it('correctly serializes strikethrough', () => {
expect(serialize(paragraph(strike('deleted content')))).toBe('~~deleted content~~');
});
it('correctly serializes blockquotes with hard breaks', () => {
expect(serialize(blockquote('some text', hardBreak(), hardBreak(), 'new line'))).toBe(
`
> some text\\
> \\
> new line
`.trim(),
);
});
it('correctly serializes blockquote with multiple block nodes', () => {
expect(serialize(blockquote(paragraph('some paragraph'), codeBlock('var x = 10;')))).toBe(
`
> some paragraph
>
> \`\`\`
> var x = 10;
> \`\`\`
`.trim(),
);
});
it('correctly serializes a multiline blockquote', () => {
expect(
serialize(
blockquote(
{ multiline: true },
paragraph('some paragraph with ', bold('bold')),
codeBlock('var y = 10;'),
),
),
).toBe(
`
>>>
some paragraph with **bold**
\`\`\`
var y = 10;
\`\`\`
>>>
`.trim(),
);
});
it('correctly serializes a code block with language', () => {
expect(
serialize(
codeBlock(
{ language: 'json' },
'this is not really json but just trying out whether this case works or not',
),
),
).toBe(
`
\`\`\`json
this is not really json but just trying out whether this case works or not
\`\`\`
`.trim(),
);
});
it('correctly serializes emoji', () => {
expect(serialize(paragraph(emoji({ name: 'dog' })))).toBe(':dog:');
});
it('correctly serializes headings', () => {
expect(
serialize(
heading({ level: 1 }, 'Heading 1'),
heading({ level: 2 }, 'Heading 2'),
heading({ level: 3 }, 'Heading 3'),
heading({ level: 4 }, 'Heading 4'),
heading({ level: 5 }, 'Heading 5'),
heading({ level: 6 }, 'Heading 6'),
),
).toBe(
`
# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
###### Heading 6
`.trim(),
);
});
it('correctly serializes horizontal rule', () => {
expect(serialize(horizontalRule(), horizontalRule(), horizontalRule())).toBe(
`
---
---
---
`.trim(),
);
});
it('correctly serializes an image', () => {
expect(serialize(paragraph(image({ src: 'img.jpg', alt: 'foo bar' })))).toBe(
'![foo bar](img.jpg)',
@ -173,6 +329,210 @@ describe('markdownSerializer', () => {
).toBe('![this is an image](file.png "foo bar baz")');
});
it('correctly serializes bullet list', () => {
expect(
serialize(
bulletList(
listItem(paragraph('list item 1')),
listItem(paragraph('list item 2')),
listItem(paragraph('list item 3')),
),
),
).toBe(
`
* list item 1
* list item 2
* list item 3
`.trim(),
);
});
it('correctly serializes bullet list with different bullet styles', () => {
expect(
serialize(
bulletList(
{ bullet: '+' },
listItem(paragraph('list item 1')),
listItem(paragraph('list item 2')),
listItem(
paragraph('list item 3'),
bulletList(
{ bullet: '-' },
listItem(paragraph('sub-list item 1')),
listItem(paragraph('sub-list item 2')),
),
),
),
),
).toBe(
`
+ list item 1
+ list item 2
+ list item 3
- sub-list item 1
- sub-list item 2
`.trim(),
);
});
it('correctly serializes a numeric list', () => {
expect(
serialize(
orderedList(
listItem(paragraph('list item 1')),
listItem(paragraph('list item 2')),
listItem(paragraph('list item 3')),
),
),
).toBe(
`
1. list item 1
2. list item 2
3. list item 3
`.trim(),
);
});
it('correctly serializes a numeric list with parens', () => {
expect(
serialize(
orderedList(
{ parens: true },
listItem(paragraph('list item 1')),
listItem(paragraph('list item 2')),
listItem(paragraph('list item 3')),
),
),
).toBe(
`
1) list item 1
2) list item 2
3) list item 3
`.trim(),
);
});
it('correctly serializes a numeric list with a different start order', () => {
expect(
serialize(
orderedList(
{ start: 17 },
listItem(paragraph('list item 1')),
listItem(paragraph('list item 2')),
listItem(paragraph('list item 3')),
),
),
).toBe(
`
17. list item 1
18. list item 2
19. list item 3
`.trim(),
);
});
it('correctly serializes a numeric list with an invalid start order', () => {
expect(
serialize(
orderedList(
{ start: NaN },
listItem(paragraph('list item 1')),
listItem(paragraph('list item 2')),
listItem(paragraph('list item 3')),
),
),
).toBe(
`
1. list item 1
2. list item 2
3. list item 3
`.trim(),
);
});
it('correctly serializes a bullet list inside an ordered list', () => {
expect(
serialize(
orderedList(
{ start: 17 },
listItem(paragraph('list item 1')),
listItem(paragraph('list item 2')),
listItem(
paragraph('list item 3'),
bulletList(
listItem(paragraph('sub-list item 1')),
listItem(paragraph('sub-list item 2')),
),
),
),
),
).toBe(
// notice that 4 space indent works fine in this case,
// when it usually wouldn't
`
17. list item 1
18. list item 2
19. list item 3
* sub-list item 1
* sub-list item 2
`.trim(),
);
});
it('correctly serializes a task list', () => {
expect(
serialize(
taskList(
taskItem({ checked: true }, paragraph('list item 1')),
taskItem(paragraph('list item 2')),
taskItem(
paragraph('list item 3'),
taskList(
taskItem({ checked: true }, paragraph('sub-list item 1')),
taskItem(paragraph('sub-list item 2')),
),
),
),
),
).toBe(
`
* [x] list item 1
* [ ] list item 2
* [ ] list item 3
* [x] sub-list item 1
* [ ] sub-list item 2
`.trim(),
);
});
it('correctly serializes a numeric task list + with start order', () => {
expect(
serialize(
taskList(
{ numeric: true },
taskItem({ checked: true }, paragraph('list item 1')),
taskItem(paragraph('list item 2')),
taskItem(
paragraph('list item 3'),
taskList(
{ numeric: true, start: 1351, parens: true },
taskItem({ checked: true }, paragraph('sub-list item 1')),
taskItem(paragraph('sub-list item 2')),
),
),
),
),
).toBe(
`
1. [x] list item 1
2. [ ] list item 2
3. [ ] list item 3
1351) [x] sub-list item 1
1352) [ ] sub-list item 2
`.trim(),
);
});
it('correctly serializes a table with inline content', () => {
expect(
serialize(

View File

@ -4,9 +4,20 @@ import ListItem from '~/content_editor/extensions/list_item';
import Paragraph from '~/content_editor/extensions/paragraph';
import markdownSerializer from '~/content_editor/services/markdown_serializer';
import { getMarkdownSource } from '~/content_editor/services/markdown_sourcemap';
import { loadMarkdownApiResult, loadMarkdownApiExample } from '../markdown_processing_examples';
import { createTestEditor, createDocBuilder } from '../test_utils';
const BULLET_LIST_MARKDOWN = `+ list item 1
+ list item 2
- embedded list item 3`;
const BULLET_LIST_HTML = `<ul data-sourcepos="1:1-3:24" dir="auto">
<li data-sourcepos="1:1-1:13">list item 1</li>
<li data-sourcepos="2:1-3:24">list item 2
<ul data-sourcepos="3:3-3:24">
<li data-sourcepos="3:3-3:24">embedded list item 3</li>
</ul>
</li>
</ul>`;
const SourcemapExtension = Extension.create({
// lets add `source` attribute to every element using `getMarkdownSource`
addGlobalAttributes() {
@ -44,11 +55,11 @@ const {
describe('content_editor/services/markdown_sourcemap', () => {
it('gets markdown source for a rendered HTML element', async () => {
const deserialized = await markdownSerializer({
render: () => loadMarkdownApiResult('bullet_list_style_3'),
render: () => BULLET_LIST_HTML,
serializerConfig: {},
}).deserialize({
schema: tiptapEditor.schema,
content: loadMarkdownApiExample('bullet_list_style_3'),
content: BULLET_LIST_MARKDOWN,
});
const expected = doc(

View File

@ -15,15 +15,12 @@ const DEFAULT_OPTS = {
projectEnvironmentsPath: '/projects/environments',
updateEnvironmentPath: '/proejcts/environments/1',
},
propsData: { environment: { name: 'foo', externalUrl: 'https://foo.example.com' } },
propsData: { environment: { id: '0', name: 'foo', external_url: 'https://foo.example.com' } },
};
describe('~/environments/components/edit.vue', () => {
let wrapper;
let mock;
let name;
let url;
let form;
const createWrapper = (opts = {}) =>
mountExtended(EditEnvironment, {
@ -34,9 +31,6 @@ describe('~/environments/components/edit.vue', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
wrapper = createWrapper();
name = wrapper.findByLabelText('Name');
url = wrapper.findByLabelText('External URL');
form = wrapper.findByRole('form', { name: 'Edit environment' });
});
afterEach(() => {
@ -44,19 +38,22 @@ describe('~/environments/components/edit.vue', () => {
wrapper.destroy();
});
const findNameInput = () => wrapper.findByLabelText('Name');
const findExternalUrlInput = () => wrapper.findByLabelText('External URL');
const findForm = () => wrapper.findByRole('form', { name: 'Edit environment' });
const showsLoading = () => wrapper.find(GlLoadingIcon).exists();
const submitForm = async (expected, response) => {
mock
.onPut(DEFAULT_OPTS.provide.updateEnvironmentPath, {
name: expected.name,
external_url: expected.url,
id: '0',
})
.reply(...response);
await name.setValue(expected.name);
await url.setValue(expected.url);
await findExternalUrlInput().setValue(expected.url);
await form.trigger('submit');
await findForm().trigger('submit');
await waitForPromises();
};
@ -65,18 +62,8 @@ describe('~/environments/components/edit.vue', () => {
expect(header.exists()).toBe(true);
});
it.each`
input | value
${() => name} | ${'test'}
${() => url} | ${'https://example.org'}
`('it changes the value of the input to $value', async ({ input, value }) => {
await input().setValue(value);
expect(input().element.value).toBe(value);
});
it('shows loader after form is submitted', async () => {
const expected = { name: 'test', url: 'https://google.ca' };
const expected = { url: 'https://google.ca' };
expect(showsLoading()).toBe(false);
@ -86,7 +73,7 @@ describe('~/environments/components/edit.vue', () => {
});
it('submits the updated environment on submit', async () => {
const expected = { name: 'test', url: 'https://google.ca' };
const expected = { url: 'https://google.ca' };
await submitForm(expected, [200, { path: '/test' }]);
@ -94,11 +81,24 @@ describe('~/environments/components/edit.vue', () => {
});
it('shows errors on error', async () => {
const expected = { name: 'test', url: 'https://google.ca' };
const expected = { url: 'https://google.ca' };
await submitForm(expected, [400, { message: ['name taken'] }]);
await submitForm(expected, [400, { message: ['uh oh!'] }]);
expect(createFlash).toHaveBeenCalledWith({ message: 'name taken' });
expect(createFlash).toHaveBeenCalledWith({ message: 'uh oh!' });
expect(showsLoading()).toBe(false);
});
it('renders a disabled "Name" field', () => {
const nameInput = findNameInput();
expect(nameInput.attributes().disabled).toBe('disabled');
expect(nameInput.element.value).toBe('foo');
});
it('renders an "External URL" field', () => {
const urlInput = findExternalUrlInput();
expect(urlInput.element.value).toBe('https://foo.example.com');
});
});

View File

@ -102,4 +102,52 @@ describe('~/environments/components/form.vue', () => {
wrapper = createWrapper({ loading: true });
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
describe('when a new environment is being created', () => {
beforeEach(() => {
wrapper = createWrapper({
environment: {
name: '',
externalUrl: '',
},
});
});
it('renders an enabled "Name" field', () => {
const nameInput = wrapper.findByLabelText('Name');
expect(nameInput.attributes().disabled).toBeUndefined();
expect(nameInput.element.value).toBe('');
});
it('renders an "External URL" field', () => {
const urlInput = wrapper.findByLabelText('External URL');
expect(urlInput.element.value).toBe('');
});
});
describe('when an existing environment is being edited', () => {
beforeEach(() => {
wrapper = createWrapper({
environment: {
id: 1,
name: 'test',
externalUrl: 'https://example.com',
},
});
});
it('renders a disabled "Name" field', () => {
const nameInput = wrapper.findByLabelText('Name');
expect(nameInput.attributes().disabled).toBe('disabled');
expect(nameInput.element.value).toBe('test');
});
it('renders an "External URL" field', () => {
const urlInput = wrapper.findByLabelText('External URL');
expect(urlInput.element.value).toBe('https://example.com');
});
});
});

View File

@ -99,6 +99,11 @@
1. list item 1
2. list item 2
3. list item 3
- name: ordered_list_with_start_order
markdown: |-
134. list item 1
135. list item 2
136. list item 3
- name: task_list
markdown: |-
* [x] hello
@ -115,6 +120,11 @@
1. [ ] of nested
1. [x] task list
2. [ ] items
- name: ordered_task_list_with_order
markdown: |-
4893. [x] hello
4894. [x] world
4895. [ ] example
- name: image
markdown: '![alt text](https://gitlab.com/logo.png)'
- name: hard_break

View File

@ -5,6 +5,7 @@ import {
parseBooleanDataAttributes,
isElementVisible,
isElementHidden,
getParents,
} from '~/lib/utils/dom_utils';
const TEST_MARGIN = 5;
@ -193,4 +194,18 @@ describe('DOM Utils', () => {
});
},
);
describe('getParents', () => {
it('gets all parents of an element', () => {
const el = document.createElement('div');
el.innerHTML = '<p><span><strong><mark>hello world';
expect(getParents(el.querySelector('mark'))).toEqual([
el.querySelector('strong'),
el.querySelector('span'),
el.querySelector('p'),
el,
]);
});
});
});