From 9f6c0ac9fd6921bc0b5190ed4d4eaf0ab1e1f2d7 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 26 Aug 2021 21:11:25 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .../content_editor/extensions/blockquote.js | 36 +- .../content_editor/extensions/ordered_list.js | 18 +- .../content_editor/extensions/task_list.js | 33 +- .../services/markdown_serializer.js | 34 +- .../services/serialization_helpers.js | 29 ++ .../components/edit_environment.vue | 2 +- .../components/environment_form.vue | 15 + app/assets/javascripts/lib/utils/dom_utils.js | 12 + .../projects/environments_controller.rb | 8 +- app/controllers/search_controller.rb | 25 +- app/helpers/search_helper.rb | 4 + app/services/issuable/bulk_update_service.rb | 6 +- app/views/search/_category.html.haml | 8 +- .../ops/global_search_code_tab.yml | 8 + .../ops/global_search_commits_tab.yml | 8 + .../ops/global_search_issues_tab.yml | 8 + .../ops/global_search_merge_requests_tab.yml | 8 + .../ops/global_search_wiki_tab.yml | 8 + doc/.vale/gitlab/UnclearAntecedent.yml | 22 ++ doc/api/environments.md | 2 +- doc/ci/environments/index.md | 13 + doc/development/documentation/index.md | 132 +------ doc/development/documentation/redirects.md | 136 +++++++ .../documentation/styleguide/index.md | 5 + doc/downgrade_ee_to_ce/index.md | 87 ++--- doc/integration/auth0.md | 4 +- doc/integration/cas.md | 6 +- doc/integration/facebook.md | 18 +- .../gmail_action_buttons_for_gitlab.md | 5 +- doc/integration/google.md | 18 +- doc/integration/jenkins_deprecated.md | 5 +- doc/integration/jira/connect-app.md | 13 +- doc/integration/jira/dvcs.md | 19 +- .../jira/jira_server_configuration.md | 2 +- doc/user/search/advanced_search.md | 21 + lib/api/environments.rb | 3 +- locale/gitlab.pot | 9 + .../projects/environments_controller_spec.rb | 10 + spec/controllers/search_controller_spec.rb | 31 ++ .../extensions/attachment_spec.js | 14 +- .../extensions/blockquote_spec.js | 19 + .../extensions/code_block_highlight_spec.js | 15 +- .../services/markdown_serializer_spec.js | 360 ++++++++++++++++++ .../services/markdown_sourcemap_spec.js | 17 +- .../environments/edit_environment_spec.js | 52 +-- .../environments/environment_form_spec.js | 48 +++ spec/frontend/fixtures/api_markdown.yml | 10 + spec/frontend/lib/utils/dom_utils_spec.js | 15 + ...rs_table_helpers.rb => members_helpers.rb} | 0 49 files changed, 1105 insertions(+), 276 deletions(-) create mode 100644 config/feature_flags/ops/global_search_code_tab.yml create mode 100644 config/feature_flags/ops/global_search_commits_tab.yml create mode 100644 config/feature_flags/ops/global_search_issues_tab.yml create mode 100644 config/feature_flags/ops/global_search_merge_requests_tab.yml create mode 100644 config/feature_flags/ops/global_search_wiki_tab.yml create mode 100644 doc/.vale/gitlab/UnclearAntecedent.yml create mode 100644 doc/development/documentation/redirects.md create mode 100644 spec/frontend/content_editor/extensions/blockquote_spec.js rename spec/support/helpers/features/{members_table_helpers.rb => members_helpers.rb} (100%) diff --git a/app/assets/javascripts/content_editor/extensions/blockquote.js b/app/assets/javascripts/content_editor/extensions/blockquote.js index 45f53fe230b..f9fb31cae1d 100644 --- a/app/assets/javascripts/content_editor/extensions/blockquote.js +++ b/app/assets/javascripts/content_editor/extensions/blockquote.js @@ -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 })), + ]; + }, +}); diff --git a/app/assets/javascripts/content_editor/extensions/ordered_list.js b/app/assets/javascripts/content_editor/extensions/ordered_list.js index 9a79187d9c1..ccbb29167d3 100644 --- a/app/assets/javascripts/content_editor/extensions/ordered_list.js +++ b/app/assets/javascripts/content_editor/extensions/ordered_list.js @@ -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)), + }), + }, + }; + }, +}); diff --git a/app/assets/javascripts/content_editor/extensions/task_list.js b/app/assets/javascripts/content_editor/extensions/task_list.js index 72806c944fb..821f578d9fc 100644 --- a/app/assets/javascripts/content_editor/extensions/task_list.js +++ b/app/assets/javascripts/content_editor/extensions/task_list.js @@ -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]; }, }); diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js index d1f7e88b1db..aca8c2c8488 100644 --- a/app/assets/javascripts/content_editor/services/markdown_serializer.js +++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js @@ -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, }, diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js index e1d0388227b..54c51703b59 100644 --- a/app/assets/javascripts/content_editor/services/serialization_helpers.js +++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js @@ -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); diff --git a/app/assets/javascripts/environments/components/edit_environment.vue b/app/assets/javascripts/environments/components/edit_environment.vue index 1cd960d7cd6..96742a11ebb 100644 --- a/app/assets/javascripts/environments/components/edit_environment.vue +++ b/app/assets/javascripts/environments/components/edit_environment.vue @@ -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)) diff --git a/app/assets/javascripts/environments/components/environment_form.vue b/app/assets/javascripts/environments/components/environment_form.vue index 6db8fe24e72..1d1d8d61b66 100644 --- a/app/assets/javascripts/environments/components/environment_form.vue +++ b/app/assets/javascripts/environments/components/environment_form.vue @@ -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" > + * @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; +}; diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index cac0aa9d513..23dabd885c8 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -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 diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index dbddb35d358..5f1b3750e41 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -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) diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 409a3e65fe3..b8e58e3afb1 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -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') diff --git a/app/services/issuable/bulk_update_service.rb b/app/services/issuable/bulk_update_service.rb index cd32cd78728..155f3c37aa3 100644 --- a/app/services/issuable/bulk_update_service.rb +++ b/app/services/issuable/bulk_update_service.rb @@ -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 diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml index 7f8a530deb8..ca6f2369bd8 100644 --- a/app/views/search/_category.html.haml +++ b/app/views/search/_category.html.haml @@ -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 diff --git a/config/feature_flags/ops/global_search_code_tab.yml b/config/feature_flags/ops/global_search_code_tab.yml new file mode 100644 index 00000000000..540f742a147 --- /dev/null +++ b/config/feature_flags/ops/global_search_code_tab.yml @@ -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 diff --git a/config/feature_flags/ops/global_search_commits_tab.yml b/config/feature_flags/ops/global_search_commits_tab.yml new file mode 100644 index 00000000000..7f6e5978d48 --- /dev/null +++ b/config/feature_flags/ops/global_search_commits_tab.yml @@ -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 diff --git a/config/feature_flags/ops/global_search_issues_tab.yml b/config/feature_flags/ops/global_search_issues_tab.yml new file mode 100644 index 00000000000..101b2588386 --- /dev/null +++ b/config/feature_flags/ops/global_search_issues_tab.yml @@ -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 diff --git a/config/feature_flags/ops/global_search_merge_requests_tab.yml b/config/feature_flags/ops/global_search_merge_requests_tab.yml new file mode 100644 index 00000000000..7f4570e5134 --- /dev/null +++ b/config/feature_flags/ops/global_search_merge_requests_tab.yml @@ -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 diff --git a/config/feature_flags/ops/global_search_wiki_tab.yml b/config/feature_flags/ops/global_search_wiki_tab.yml new file mode 100644 index 00000000000..ff7b777ac05 --- /dev/null +++ b/config/feature_flags/ops/global_search_wiki_tab.yml @@ -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 diff --git a/doc/.vale/gitlab/UnclearAntecedent.yml b/doc/.vale/gitlab/UnclearAntecedent.yml new file mode 100644 index 00000000000..863bbd4e109 --- /dev/null +++ b/doc/.vale/gitlab/UnclearAntecedent.yml @@ -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' diff --git a/doc/api/environments.md b/doc/api/environments.md index aa3697c54ac..b9a1dc47310 100644 --- a/doc/api/environments.md +++ b/doc/api/environments.md @@ -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 diff --git a/doc/ci/environments/index.md b/doc/ci/environments/index.md index 4a77e6d84b4..db9fbec85ae 100644 --- a/doc/ci/environments/index.md +++ b/doc/ci/environments/index.md @@ -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/) diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md index c6d8695975e..a597ea512c6 100644 --- a/doc/development/documentation/index.md +++ b/doc/development/documentation/index.md @@ -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). - - - - ``` - -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 , search for full URLs: - `grep -r "docs.gitlab.com/ee/path/to/file.html" .` - - In , - 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 , 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 diff --git a/doc/development/documentation/redirects.md b/doc/development/documentation/redirects.md new file mode 100644 index 00000000000..5f230d3aa6d --- /dev/null +++ b/doc/development/documentation/redirects.md @@ -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). + + + + ``` + +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 , search for full URLs: + `grep -r "docs.gitlab.com/ee/path/to/file.html" .` + - In , + 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 , 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' +--- +``` diff --git a/doc/development/documentation/styleguide/index.md b/doc/development/documentation/styleguide/index.md index 954359a73ae..280c12c8d38 100644 --- a/doc/development/documentation/styleguide/index.md +++ b/doc/development/documentation/styleguide/index.md @@ -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). diff --git a/doc/downgrade_ee_to_ce/index.md b/doc/downgrade_ee_to_ce/index.md index 00e59c46da1..865d60fed73 100644 --- a/doc/downgrade_ee_to_ce/index.md +++ b/doc/downgrade_ee_to_ce/index.md @@ -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. diff --git a/doc/integration/auth0.md b/doc/integration/auth0.md index 34ee326d6d5..870da6cdb3d 100644 --- a/doc/integration/auth0.md +++ b/doc/integration/auth0.md @@ -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**. diff --git a/doc/integration/cas.md b/doc/integration/cas.md index be54c31ec01..60ce728fa55 100644 --- a/doc/integration/cas.md +++ b/doc/integration/cas.md @@ -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. diff --git a/doc/integration/facebook.md b/doc/integration/facebook.md index ded89dd93a4..58c53db7996 100644 --- a/doc/integration/facebook.md +++ b/doc/integration/facebook.md @@ -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. diff --git a/doc/integration/gmail_action_buttons_for_gitlab.md b/doc/integration/gmail_action_buttons_for_gitlab.md index f0bcc00c0fa..0468e5d0a42 100644 --- a/doc/integration/gmail_action_buttons_for_gitlab.md +++ b/doc/integration/gmail_action_buttons_for_gitlab.md @@ -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: diff --git a/doc/integration/google.md b/doc/integration/google.md index a08944f65f1..4a2c61577ac 100644 --- a/doc/integration/google.md +++ b/doc/integration/google.md @@ -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 diff --git a/doc/integration/jenkins_deprecated.md b/doc/integration/jenkins_deprecated.md index b7e4c4f0e26..e349bb4e88f 100644 --- a/doc/integration/jenkins_deprecated.md +++ b/doc/integration/jenkins_deprecated.md @@ -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 diff --git a/doc/integration/jira/connect-app.md b/doc/integration/jira/connect-app.md index d8b1e9aa867..8bc08bf89d3 100644 --- a/doc/integration/jira/connect-app.md +++ b/doc/integration/jira/connect-app.md @@ -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." diff --git a/doc/integration/jira/dvcs.md b/doc/integration/jira/dvcs.md index 7d97312757e..14259628543 100644 --- a/doc/integration/jira/dvcs.md +++ b/doc/integration/jira/dvcs.md @@ -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**. diff --git a/doc/integration/jira/jira_server_configuration.md b/doc/integration/jira/jira_server_configuration.md index 52e7e5e412b..32a8cd430f9 100644 --- a/doc/integration/jira/jira_server_configuration.md +++ b/doc/integration/jira/jira_server_configuration.md @@ -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 diff --git a/doc/user/search/advanced_search.md b/doc/user/search/advanced_search.md index f29ac531d2e..f994539b9fc 100644 --- a/doc/user/search/advanced_search.md +++ b/doc/user/search/advanced_search.md @@ -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. | diff --git a/lib/api/environments.rb b/lib/api/environments.rb index e50da4264b5..c032b80e39b 100644 --- a/lib/api/environments.rb +++ b/lib/api/environments.rb @@ -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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 1d9ae54dbde..c73175dbba6 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index 7103d7df5c5..0fcdeb2edde 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -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 diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb index e0870e17d99..4e87a9fc1ba 100644 --- a/spec/controllers/search_controller_spec.rb +++ b/spec/controllers/search_controller_spec.rb @@ -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 diff --git a/spec/frontend/content_editor/extensions/attachment_spec.js b/spec/frontend/content_editor/extensions/attachment_spec.js index 97a33b28cdd..9e7c31cca72 100644 --- a/spec/frontend/content_editor/extensions/attachment_spec.js +++ b/spec/frontend/content_editor/extensions/attachment_spec.js @@ -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 = `

+ + test-file + +

`; +const PROJECT_WIKI_ATTACHMENT_LINK_HTML = `

+ test-file +

`; + 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); diff --git a/spec/frontend/content_editor/extensions/blockquote_spec.js b/spec/frontend/content_editor/extensions/blockquote_spec.js new file mode 100644 index 00000000000..c5b5044352d --- /dev/null +++ b/spec/frontend/content_editor/extensions/blockquote_spec.js @@ -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); + }); + }); +}); diff --git a/spec/frontend/content_editor/extensions/code_block_highlight_spec.js b/spec/frontend/content_editor/extensions/code_block_highlight_spec.js index 828fdb224fc..6a0a0c76825 100644 --- a/spec/frontend/content_editor/extensions/code_block_highlight_spec.js +++ b/spec/frontend/content_editor/extensions/code_block_highlight_spec.js @@ -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 = `
+  
+    
+      console.log('hello world')
+    
+  
+
`; + 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', () => { diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js index 868473faa14..cd4560677f8 100644 --- a/spec/frontend/content_editor/services/markdown_serializer_spec.js +++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js @@ -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( + '', + ); + }); + 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( diff --git a/spec/frontend/content_editor/services/markdown_sourcemap_spec.js b/spec/frontend/content_editor/services/markdown_sourcemap_spec.js index 0ef822942ea..a6ebe204078 100644 --- a/spec/frontend/content_editor/services/markdown_sourcemap_spec.js +++ b/spec/frontend/content_editor/services/markdown_sourcemap_spec.js @@ -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 = `
    +
  • list item 1
  • +
  • list item 2 +
      +
    • embedded list item 3
    • +
    +
  • +
`; + 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( diff --git a/spec/frontend/environments/edit_environment_spec.js b/spec/frontend/environments/edit_environment_spec.js index 3e7f5dd5ff4..2c8c054ccbd 100644 --- a/spec/frontend/environments/edit_environment_spec.js +++ b/spec/frontend/environments/edit_environment_spec.js @@ -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'); + }); }); diff --git a/spec/frontend/environments/environment_form_spec.js b/spec/frontend/environments/environment_form_spec.js index ed8fda71dab..f1af08bcf32 100644 --- a/spec/frontend/environments/environment_form_spec.js +++ b/spec/frontend/environments/environment_form_spec.js @@ -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'); + }); + }); }); diff --git a/spec/frontend/fixtures/api_markdown.yml b/spec/frontend/fixtures/api_markdown.yml index ff9da33c116..32bbb5db745 100644 --- a/spec/frontend/fixtures/api_markdown.yml +++ b/spec/frontend/fixtures/api_markdown.yml @@ -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 diff --git a/spec/frontend/lib/utils/dom_utils_spec.js b/spec/frontend/lib/utils/dom_utils_spec.js index 7c4c20e651f..cb8b1c7ca9a 100644 --- a/spec/frontend/lib/utils/dom_utils_spec.js +++ b/spec/frontend/lib/utils/dom_utils_spec.js @@ -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 = '

hello world'; + + expect(getParents(el.querySelector('mark'))).toEqual([ + el.querySelector('strong'), + el.querySelector('span'), + el.querySelector('p'), + el, + ]); + }); + }); }); diff --git a/spec/support/helpers/features/members_table_helpers.rb b/spec/support/helpers/features/members_helpers.rb similarity index 100% rename from spec/support/helpers/features/members_table_helpers.rb rename to spec/support/helpers/features/members_helpers.rb