Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
a56971e97f
commit
9f6c0ac9fd
|
@ -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 })),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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)),
|
||||
}),
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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];
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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 })"
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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'
|
|
@ -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
|
||||
|
|
|
@ -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/)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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'
|
||||
---
|
||||
```
|
|
@ -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).
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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**.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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 "<Organization>'s GitLab" or "<Your Name>'s GitLab" or
|
||||
something else descriptive.
|
||||
1. Enter a name for your app. This can be anything. Consider something like
|
||||
"<Organization>'s GitLab" or "<Your Name>'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.
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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."
|
||||
|
||||
|
|
|
@ -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**.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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. |
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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', () => {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue