From c5ac71d93e2fad318713fde9f3263441c3583705 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 9 May 2024 03:13:16 +0000 Subject: [PATCH] Add latest changes from gitlab-org/gitlab@master --- .rubocop_todo/rspec/named_subject.yml | 1 - GITALY_SERVER_VERSION | 2 +- .../behaviors/shortcuts/shortcuts_issuable.js | 2 +- .../shortcuts/shortcuts_work_items.js | 116 ++++++++++++++- .../components/advanced_settings.vue | 4 +- app/assets/javascripts/issues/list/index.js | 4 + .../lib/apollo/correlation_id_link.js | 22 +++ app/assets/javascripts/lib/graphql.js | 2 + .../javascripts/lib/utils/breadcrumbs.js | 27 +++- .../components/harbor_registry_breadcrumb.vue | 2 +- .../shared/components/registry_breadcrumb.vue | 2 +- .../super_sidebar/super_sidebar_bundle.js | 8 +- app/assets/javascripts/work_items/index.js | 5 +- .../javascripts/work_items/router/index.js | 7 +- .../stylesheets/framework/breadcrumbs.scss | 23 ++- app/graphql/mutations/timelogs/create.rb | 8 +- app/helpers/work_items_helper.rb | 3 +- app/services/ml/create_candidate_service.rb | 2 +- app/views/import/github/new.html.haml | 2 +- .../nav/breadcrumbs/_breadcrumbs.html.haml | 5 +- config/application.rb | 2 +- doc/.vale/gitlab/Badges-Tiers.yml | 6 +- .../processing_specific_job_classes.md | 5 +- doc/api/graphql/reference/index.md | 2 +- doc/api/groups.md | 69 ++++++++- doc/api/rest/deprecations.md | 15 ++ doc/api/settings.md | 44 +++++- doc/ci/secrets/convert-to-id-tokens.md | 1 + .../sast/customize_rulesets.md | 2 +- doc/user/project/import/github.md | 4 +- doc/user/project/time_tracking.md | 3 +- .../overdue_finalize_background_migration.rb | 4 +- lib/gitlab/gon_helper.rb | 1 + locale/gitlab.pot | 6 +- .../issuables/shortcuts_issuable_spec.rb | 42 ++++-- .../work_items/shortcuts_work_item_spec.rb | 42 +++++- .../shortcuts/shortcuts_issuable_spec.js | 4 +- .../components/advanced_settings_spec.js | 2 +- .../lib/apollo/correlation_id_link_spec.js | 64 ++++++++ spec/frontend/lib/utils/breadcrumbs_spec.js | 137 +++++++++++++----- spec/support/helpers/selection_helper.rb | 2 +- .../timelogs/create_shared_examples.rb | 34 ++++- 42 files changed, 629 insertions(+), 109 deletions(-) create mode 100644 app/assets/javascripts/lib/apollo/correlation_id_link.js create mode 100644 spec/frontend/lib/apollo/correlation_id_link_spec.js diff --git a/.rubocop_todo/rspec/named_subject.yml b/.rubocop_todo/rspec/named_subject.yml index 07858875f28..efbb4c5d9d0 100644 --- a/.rubocop_todo/rspec/named_subject.yml +++ b/.rubocop_todo/rspec/named_subject.yml @@ -456,7 +456,6 @@ RSpec/NamedSubject: - 'ee/spec/lib/gitlab/llm/chat_storage_spec.rb' - 'ee/spec/lib/gitlab/llm/completions/chat_spec.rb' - 'ee/spec/lib/gitlab/llm/concerns/exponential_backoff_spec.rb' - - 'ee/spec/lib/gitlab/llm/graphql_subscription_response_service_spec.rb' - 'ee/spec/lib/gitlab/llm/templates/categorize_question_spec.rb' - 'ee/spec/lib/gitlab/llm/templates/explain_vulnerability_spec.rb' - 'ee/spec/lib/gitlab/llm/templates/fill_in_merge_request_template_spec.rb' diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 9d02af0e103..b966878efd3 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -d5833f9560b9a225a108e766bf052103b899ee76 +902f1b82628d4df113f87b7cbb87ac108e028209 diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js index cde6d59b210..b8dd365d535 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js @@ -67,7 +67,7 @@ export default class ShortcutsIssuable { } static replyWithSelectedText() { - let $replyField = $('.js-main-target-form .js-vue-comment-form'); + let $replyField = $('.js-main-target-form .js-gfm-input'); // Ensure that markdown input is still present in the DOM // otherwise fall back to main comment input field. diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_work_items.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_work_items.js index 37d29f25200..be420a0e89a 100644 --- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_work_items.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_work_items.js @@ -1,13 +1,17 @@ import ClipboardJS from 'clipboard'; import toast from '~/vue_shared/plugins/global_toast'; +import { getSelectedFragment } from '~/lib/utils/common_utils'; +import { isElementVisible } from '~/lib/utils/dom_utils'; import { s__ } from '~/locale'; import { DEBOUNCE_DROPDOWN_DELAY } from '~/sidebar/components/labels/labels_select_widget/constants'; +import { CopyAsGFM } from '../markdown/copy_as_gfm'; import { ISSUE_MR_CHANGE_ASSIGNEE, ISSUE_MR_CHANGE_MILESTONE, ISSUABLE_CHANGE_LABEL, ISSUABLE_EDIT_DESCRIPTION, ISSUABLE_COPY_REF, + ISSUABLE_COMMENT_OR_REPLY, } from './keybindings'; export default class ShortcutsWorkItem { @@ -22,17 +26,41 @@ export default class ShortcutsWorkItem { }); shortcuts.addAll([ - [ISSUE_MR_CHANGE_ASSIGNEE, () => ShortcutsWorkItem.openSidebarDropdown('assignee')], - [ISSUE_MR_CHANGE_MILESTONE, () => ShortcutsWorkItem.openSidebarDropdown('milestone')], - [ISSUABLE_CHANGE_LABEL, () => ShortcutsWorkItem.openSidebarDropdown('labels')], + [ISSUE_MR_CHANGE_ASSIGNEE, () => ShortcutsWorkItem.openSidebarDropdown('js-assignee')], + [ISSUE_MR_CHANGE_MILESTONE, () => ShortcutsWorkItem.openSidebarDropdown('js-milestone')], + [ISSUABLE_CHANGE_LABEL, () => ShortcutsWorkItem.openSidebarDropdown('js-labels')], [ISSUABLE_EDIT_DESCRIPTION, ShortcutsWorkItem.editDescription], [ISSUABLE_COPY_REF, () => this.copyReference()], + [ISSUABLE_COMMENT_OR_REPLY, ShortcutsWorkItem.replyWithSelectedText], ]); + + /** + * We're attaching a global focus event listener on document for + * every markdown input field. + */ + document.addEventListener('focus', this.handleMarkdownFieldFocus); } - static openSidebarDropdown(name) { + destroy() { + document.removeEventListener('focus', this.handleMarkdownFieldFocus); + } + + /** + * This event handler preserves last focused markdown input field. + * @param {Object} event + */ + static handleMarkdownFieldFocus({ target }) { + if (target.matches('.js-vue-markdown-field .js-gfm-input')) { + ShortcutsWorkItem.lastFocusedReplyField = target; + } + } + + static openSidebarDropdown(selector) { setTimeout(() => { - const editBtn = document.querySelector(`.js-${name} .shortcut-sidebar-dropdown-toggle`); + const shortcutSelector = `.${selector} .shortcut-sidebar-dropdown-toggle`; + const editBtn = + document.querySelector(`.is-modal ${shortcutSelector}`) || + document.querySelector(shortcutSelector); editBtn?.click(); }, DEBOUNCE_DROPDOWN_DELAY); return false; @@ -56,4 +84,82 @@ export default class ShortcutsWorkItem { this.refInMemoryButton.dispatchEvent(new CustomEvent('click')); } } + + static replyWithSelectedText() { + const gfmSelector = '.js-vue-markdown-field .js-gfm-input'; + let replyField = + document.querySelector(`.modal ${gfmSelector}`) || document.querySelector(gfmSelector); + + // Ensure that markdown input is still present in the DOM + // otherwise fall back to main comment input field. + if ( + ShortcutsWorkItem.lastFocusedReplyField && + isElementVisible(ShortcutsWorkItem.lastFocusedReplyField) + ) { + replyField = ShortcutsWorkItem.lastFocusedReplyField; + } + + if (!replyField || !isElementVisible(replyField)) { + return false; + } + + const documentFragment = getSelectedFragment(document.querySelector('#content-body')); + + if (!documentFragment) { + replyField.focus(); + return false; + } + + // Sanity check: Make sure the selected text comes from a discussion : it can either contain a message... + let foundMessage = Boolean(documentFragment.querySelector('.md')); + + // ... Or come from a message + if (!foundMessage) { + if (documentFragment.originalNodes) { + documentFragment.originalNodes.forEach((e) => { + let node = e; + do { + // Text nodes don't define the `matches` method + if (node.matches && node.matches('.md')) { + foundMessage = true; + } + node = node.parentNode; + } while (node && !foundMessage); + }); + } + + // If there is no message, just select the reply field + if (!foundMessage) { + replyField.focus(); + return false; + } + } + + const el = CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true)); + const blockquoteEl = document.createElement('blockquote'); + blockquoteEl.appendChild(el); + CopyAsGFM.nodeToGFM(blockquoteEl) + .then((text) => { + if (text.trim() === '') { + return false; + } + + // If replyField already has some content, add a newline before our quote + const separator = (replyField.value.trim() !== '' && '\n\n') || ''; + replyField.value = `${replyField.value}${separator}${text}\n\n`; + + // Trigger autosize + const event = document.createEvent('Event'); + event.initEvent('autosize:update', true, false); + replyField.dispatchEvent(event); + + // Focus the input field + replyField.focus(); + + return false; + }) + .catch(() => {}); + + return false; + } } diff --git a/app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue b/app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue index 0875b5015c2..d39fdabdb3f 100644 --- a/app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue +++ b/app/assets/javascripts/import_entities/import_projects/components/advanced_settings.vue @@ -48,13 +48,13 @@ export default { > {{ - s__('ImportProjects|The more information you select, the longer it will take to import') + s__('ImportProjects|The more information you select, the longer it will take to import.') }}

diff --git a/app/assets/javascripts/issues/list/index.js b/app/assets/javascripts/issues/list/index.js index c2507b4b82c..e2c0f523d39 100644 --- a/app/assets/javascripts/issues/list/index.js +++ b/app/assets/javascripts/issues/list/index.js @@ -2,6 +2,8 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import VueRouter from 'vue-router'; import IssuesListApp from 'ee_else_ce/issues/list/components/issues_list_app.vue'; +import { addShortcutsExtension } from '~/behaviors/shortcuts'; +import ShortcutsWorkItems from '~/behaviors/shortcuts/shortcuts_work_items'; import { parseBoolean } from '~/lib/utils/common_utils'; import JiraIssuesImportStatusApp from './components/jira_issues_import_status_app.vue'; import { gqlClient } from './graphql'; @@ -49,6 +51,8 @@ export async function mountIssuesListApp() { return null; } + addShortcutsExtension(ShortcutsWorkItems); + Vue.use(VueApollo); Vue.use(VueRouter); diff --git a/app/assets/javascripts/lib/apollo/correlation_id_link.js b/app/assets/javascripts/lib/apollo/correlation_id_link.js new file mode 100644 index 00000000000..731d7181a72 --- /dev/null +++ b/app/assets/javascripts/lib/apollo/correlation_id_link.js @@ -0,0 +1,22 @@ +import { ApolloLink } from '@apollo/client/core'; + +function getCorrelationId(operation) { + const { + response: { headers }, + } = operation.getContext(); + + return headers?.get('X-Request-Id') || headers?.get('x-request-id'); +} + +/** + * An ApolloLink used to get the correlation_id from the X-Request-Id response header. + * + * The correlationId is added to the response so our components can read and use it: + * const { correlationId } = await this.$apollo.mutate({ ... + */ +export const correlationIdLink = new ApolloLink((operation, forward) => + forward(operation).map((response) => ({ + ...response, + correlationId: getCorrelationId(operation), + })), +); diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js index 95ef96e2ad8..b0134b3278e 100644 --- a/app/assets/javascripts/lib/graphql.js +++ b/app/assets/javascripts/lib/graphql.js @@ -13,6 +13,7 @@ import { getInstrumentationLink } from './apollo/instrumentation_link'; import { getSuppressNetworkErrorsDuringNavigationLink } from './apollo/suppress_network_errors_during_navigation_link'; import { getPersistLink } from './apollo/persist_link'; import { persistenceMapper } from './apollo/persistence_mapper'; +import { correlationIdLink } from './apollo/correlation_id_link'; export const fetchPolicies = { CACHE_FIRST: 'cache-first', @@ -249,6 +250,7 @@ function createApolloClient(resolvers = {}, config = {}) { [ getSuppressNetworkErrorsDuringNavigationLink(), getInstrumentationLink(), + correlationIdLink, requestCounterLink, performanceBarLink, new StartupJSLink(), diff --git a/app/assets/javascripts/lib/utils/breadcrumbs.js b/app/assets/javascripts/lib/utils/breadcrumbs.js index 9404c2695d5..d8a6673ea74 100644 --- a/app/assets/javascripts/lib/utils/breadcrumbs.js +++ b/app/assets/javascripts/lib/utils/breadcrumbs.js @@ -1,7 +1,32 @@ import Vue from 'vue'; -// TODO: Review replacing this when a breadcrumbs ViewComponent has been created https://gitlab.com/gitlab-org/gitlab/-/issues/367326 +export const staticBreadcrumbs = Vue.observable({}); + export const injectVueAppBreadcrumbs = (router, BreadcrumbsComponent, apolloProvider = null) => { + if (gon.features.vuePageBreadcrumbs) { + const injectBreadcrumbEl = document.querySelector('#js-injected-page-breadcrumbs'); + + if (!injectBreadcrumbEl) { + return false; + } + + // Hide the last of the static breadcrumbs by nulling its values. + // This way, the separator "/" stays visible and also the new "last" static item isn't displayed in bold font. + staticBreadcrumbs.items[staticBreadcrumbs.items.length - 1].text = ''; + staticBreadcrumbs.items[staticBreadcrumbs.items.length - 1].href = ''; + + return new Vue({ + el: injectBreadcrumbEl, + router, + apolloProvider, + render(createElement) { + return createElement(BreadcrumbsComponent, { + class: injectBreadcrumbEl.className, + }); + }, + }); + } + const breadcrumbEls = document.querySelectorAll('nav .js-breadcrumbs-list li'); if (breadcrumbEls.length < 1) { diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/components/harbor_registry_breadcrumb.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/components/harbor_registry_breadcrumb.vue index b8caf56fbd7..27fa26fd567 100644 --- a/app/assets/javascripts/packages_and_registries/harbor_registry/components/harbor_registry_breadcrumb.vue +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/components/harbor_registry_breadcrumb.vue @@ -57,5 +57,5 @@ export default { diff --git a/app/assets/javascripts/packages_and_registries/shared/components/registry_breadcrumb.vue b/app/assets/javascripts/packages_and_registries/shared/components/registry_breadcrumb.vue index b34516a3eb7..c8b7ef2f141 100644 --- a/app/assets/javascripts/packages_and_registries/shared/components/registry_breadcrumb.vue +++ b/app/assets/javascripts/packages_and_registries/shared/components/registry_breadcrumb.vue @@ -45,5 +45,5 @@ export default { diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js index ce8c32a0732..6bc3728b30d 100644 --- a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js +++ b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js @@ -3,6 +3,7 @@ import { GlBreadcrumb, GlToast } from '@gitlab/ui'; import VueApollo from 'vue-apollo'; import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils'; import createDefaultClient from '~/lib/graphql'; +import { staticBreadcrumbs } from '~/lib/utils/breadcrumbs'; import { JS_TOGGLE_EXPAND_CLASS, CONTEXT_NAMESPACE_GROUPS } from './constants'; import createStore from './components/global_search/store'; import { @@ -196,17 +197,14 @@ export function initPageBreadcrumbs() { if (!el) return false; const { breadcrumbsJson } = el.dataset; - const props = { - items: JSON.parse(breadcrumbsJson), - }; + staticBreadcrumbs.items = JSON.parse(breadcrumbsJson); return new Vue({ el, render(h) { return h(GlBreadcrumb, { - props, + props: staticBreadcrumbs, attrs: { 'data-testid': 'breadcrumb-links' }, - class: 'gl-flex-grow-1', }); }, }); diff --git a/app/assets/javascripts/work_items/index.js b/app/assets/javascripts/work_items/index.js index 5900c0a3ee0..99d20434895 100644 --- a/app/assets/javascripts/work_items/index.js +++ b/app/assets/javascripts/work_items/index.js @@ -3,6 +3,7 @@ import VueApollo from 'vue-apollo'; import { WORKSPACE_GROUP } from '~/issues/constants'; import { addShortcutsExtension } from '~/behaviors/shortcuts'; import ShortcutsWorkItems from '~/behaviors/shortcuts/shortcuts_work_items'; +import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import { parseBoolean } from '~/lib/utils/common_utils'; import { apolloProvider } from '~/graphql_shared/issuable_client'; import App from './components/app.vue'; @@ -17,6 +18,7 @@ export const initWorkItemsRoot = ({ workItemType, workspaceType } = {}) => { return undefined; } + addShortcutsExtension(ShortcutsNavigation); addShortcutsExtension(ShortcutsWorkItems); const { @@ -31,6 +33,7 @@ export const initWorkItemsRoot = ({ workItemType, workspaceType } = {}) => { hasIssuableHealthStatusFeature, newCommentTemplatePaths, reportAbusePath, + defaultBranch, } = el.dataset; const isGroup = workspaceType === WORKSPACE_GROUP; @@ -38,7 +41,7 @@ export const initWorkItemsRoot = ({ workItemType, workspaceType } = {}) => { return new Vue({ el, name: 'WorkItemsRoot', - router: createRouter({ fullPath, workItemType, workspaceType }), + router: createRouter({ fullPath, workItemType, workspaceType, defaultBranch }), apolloProvider, provide: { fullPath, diff --git a/app/assets/javascripts/work_items/router/index.js b/app/assets/javascripts/work_items/router/index.js index dcf9dcb10f5..4bc5d611523 100644 --- a/app/assets/javascripts/work_items/router/index.js +++ b/app/assets/javascripts/work_items/router/index.js @@ -2,7 +2,7 @@ import { GlToast } from '@gitlab/ui'; import Vue from 'vue'; import VueRouter from 'vue-router'; import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants'; -import { joinPaths } from '~/lib/utils/url_utility'; +import { joinPaths, webIDEUrl } from '~/lib/utils/url_utility'; import { routes } from './routes'; Vue.use(GlToast); @@ -12,9 +12,14 @@ export function createRouter({ fullPath, workItemType = 'work_items', workspaceType = WORKSPACE_PROJECT, + defaultBranch, }) { const workspacePath = workspaceType === WORKSPACE_GROUP ? '/groups' : ''; + if (workspaceType === WORKSPACE_PROJECT) { + window.gl.webIDEPath = webIDEUrl(joinPaths('/', fullPath, 'edit/', defaultBranch, '/-/')); + } + return new VueRouter({ routes: routes(), mode: 'history', diff --git a/app/assets/stylesheets/framework/breadcrumbs.scss b/app/assets/stylesheets/framework/breadcrumbs.scss index 551e7def761..c56cd09c5d4 100644 --- a/app/assets/stylesheets/framework/breadcrumbs.scss +++ b/app/assets/stylesheets/framework/breadcrumbs.scss @@ -12,6 +12,27 @@ } } +#js-vue-page-breadcrumbs-wrapper { + display: flex; + flex-grow: 1; + + /* + * Our auto-resizing GlBreadcrumb component works best when it is set to grow with the available space. + * Only this way can its ResizeObserver detect that more space than currently taken is available, + * so it can uncollapse items when more space becomes available. + * But we do *not* want this effect on pages that use the injectVueAppBreadcrumbs() mechanism. + * There, we want the lefthand breadcrumbs only take as much space as they needed on their first size calc, + * so that the second, injected GlBreadcrumb component sits right next to it, with no "grow effect" taking + * empty space between them. + * The only downside to this approach is that on such injected pages, the lefthand breadcrumbs won't + * uncollapse themselves when more space becomes available, as they won't "grow" into it, not triggering + * their ResizeObserver. + */ + nav.gl-breadcrumbs:only-of-type { + flex-grow: 1; + } +} + /* * This temporarily restores the legacy breadcrumbs styles on the primary HAML breadcrumbs. * Those styles got changed in https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/3663, @@ -32,4 +53,4 @@ } } } -} \ No newline at end of file +} diff --git a/app/graphql/mutations/timelogs/create.rb b/app/graphql/mutations/timelogs/create.rb index 1be023eed8a..4caea414175 100644 --- a/app/graphql/mutations/timelogs/create.rb +++ b/app/graphql/mutations/timelogs/create.rb @@ -12,8 +12,8 @@ module Mutations argument :spent_at, Types::TimeType, - required: true, - description: 'When the time was spent.' + required: false, + description: 'Timestamp of when the time was spent. If empty, defaults to current time.' argument :summary, GraphQL::Types::String, @@ -27,7 +27,7 @@ module Mutations authorize :create_timelog - def resolve(issuable_id:, time_spent:, spent_at:, summary:, **args) + def resolve(issuable_id:, time_spent:, summary:, **args) parsed_time_spent = Gitlab::TimeTrackingFormatter.parse(time_spent) if parsed_time_spent.nil? return { timelog: nil, errors: [_('Time spent must be formatted correctly. For example: 1h 30m.')] } @@ -35,6 +35,8 @@ module Mutations issuable = authorized_find!(id: issuable_id) + spent_at = args[:spent_at].nil? ? DateTime.current : args[:spent_at] + result = ::Timelogs::CreateService.new( issuable, parsed_time_spent, spent_at, summary, current_user ).execute diff --git a/app/helpers/work_items_helper.rb b/app/helpers/work_items_helper.rb index c4f6d68fed9..a1364c312f9 100644 --- a/app/helpers/work_items_helper.rb +++ b/app/helpers/work_items_helper.rb @@ -11,7 +11,8 @@ module WorkItemsHelper register_path: new_user_registration_path(redirect_to_referer: 'yes'), sign_in_path: new_session_path(:user, redirect_to_referer: 'yes'), new_comment_template_paths: new_comment_template_paths(group).to_json, - report_abuse_path: add_category_abuse_reports_path + report_abuse_path: add_category_abuse_reports_path, + default_branch: resource_parent.is_a?(Project) ? resource_parent.default_branch_or_main : nil } end diff --git a/app/services/ml/create_candidate_service.rb b/app/services/ml/create_candidate_service.rb index 53913c3fb19..28870bce827 100644 --- a/app/services/ml/create_candidate_service.rb +++ b/app/services/ml/create_candidate_service.rb @@ -28,7 +28,7 @@ module Ml end def random_candidate_name - parts = Array.new(3).map { FFaker::Animal.common_name.downcase.delete(' ') } << rand(10000) + parts = Array.new(3).map { FFaker::AnimalUS.common_name.downcase.delete(' ') } << rand(10000) parts.join('-').truncate(255) end diff --git a/app/views/import/github/new.html.haml b/app/views/import/github/new.html.haml index a0e2ba02f4f..19c2dd6a750 100644 --- a/app/views/import/github/new.html.haml +++ b/app/views/import/github/new.html.haml @@ -41,7 +41,7 @@ %li= safe_format(s_('GithubImporter|%{code_start}repo%{code_end}: Used to display a list of your public and private repositories that are available to connect to.'), code_pair) - else %li= safe_format(s_('GithubImporter|%{code_start}repo%{code_end}: Used to display a list of your public and private repositories that are available to import from.'), code_pair) - %li= safe_format(s_('GithubImporter|%{code_start}read:org%{code_end} (optional): Used to import collaborators from GitHub repositories.'), code_pair) + %li= safe_format(s_('GithubImporter|%{code_start}read:org%{code_end} (optional): Used to import collaborators from GitHub repositories, or if your project has Git LFS files.'), code_pair) - docs_link = link_to('', help_page_path('user/project/import/github', anchor: 'use-a-github-personal-access-token'), target: '_blank', rel: 'noopener noreferrer') - docs_link_tag_pair = tag_pair(docs_link, :link_start, :link_end) = safe_format(s_('GithubImport|%{link_start}Learn more%{link_end}.'), docs_link_tag_pair) diff --git a/app/views/layouts/nav/breadcrumbs/_breadcrumbs.html.haml b/app/views/layouts/nav/breadcrumbs/_breadcrumbs.html.haml index 33a22dc870f..dc33ed49283 100644 --- a/app/views/layouts/nav/breadcrumbs/_breadcrumbs.html.haml +++ b/app/views/layouts/nav/breadcrumbs/_breadcrumbs.html.haml @@ -11,7 +11,10 @@ #{schema_breadcrumb_json} - if Feature.enabled?(:vue_page_breadcrumbs) - #js-vue-page-breadcrumbs{ data: { breadcrumbs_json: breadcrumbs_as_json } } + #js-vue-page-breadcrumbs-wrapper + #js-vue-page-breadcrumbs{ data: { breadcrumbs_json: breadcrumbs_as_json } } + #js-injected-page-breadcrumbs + - else %nav.breadcrumbs.gl-breadcrumbs.tmp-breadcrumbs-fix{ 'aria-label': _('Breadcrumbs'), data: { testid: 'breadcrumb-links' } } %ul.breadcrumb.gl-breadcrumb-list.js-breadcrumbs-list.gl-flex-grow-1 diff --git a/config/application.rb b/config/application.rb index 9ec69276670..f6b849a8714 100644 --- a/config/application.rb +++ b/config/application.rb @@ -444,7 +444,7 @@ module Gitlab # Allow access to GitLab API from other domains config.middleware.insert_before Warden::Manager, Rack::Cors do - headers_to_expose = %w[Link X-Total X-Total-Pages X-Per-Page X-Page X-Next-Page X-Prev-Page X-Gitlab-Blob-Id X-Gitlab-Commit-Id X-Gitlab-Content-Sha256 X-Gitlab-Encoding X-Gitlab-File-Name X-Gitlab-File-Path X-Gitlab-Last-Commit-Id X-Gitlab-Ref X-Gitlab-Size] + headers_to_expose = %w[Link X-Total X-Total-Pages X-Per-Page X-Page X-Next-Page X-Prev-Page X-Gitlab-Blob-Id X-Gitlab-Commit-Id X-Gitlab-Content-Sha256 X-Gitlab-Encoding X-Gitlab-File-Name X-Gitlab-File-Path X-Gitlab-Last-Commit-Id X-Gitlab-Ref X-Gitlab-Size X-Request-Id] allow do origins Gitlab.config.gitlab.url diff --git a/doc/.vale/gitlab/Badges-Tiers.yml b/doc/.vale/gitlab/Badges-Tiers.yml index c9cd63b15ca..4b53d9ffaae 100644 --- a/doc/.vale/gitlab/Badges-Tiers.yml +++ b/doc/.vale/gitlab/Badges-Tiers.yml @@ -4,9 +4,9 @@ # # For a list of all options, see https://docs.gitlab.com/ee/development/documentation/styleguide/#available-product-tier-badges extends: existence -message: "Tiers should be capitalized, comma-separated, and ordered lowest to highest without `and`." +message: "Tiers should be capitalized, comma-separated, and ordered lowest to highest." link: https://docs.gitlab.com/ee/development/documentation/styleguide/#available-product-tier-badges -level: suggestion +level: error scope: raw raw: -- (?<=\n\*\*Tier:\*\*)[^\n]*(and|free|premium|ultimate|, Free|Ultimate,) +- (?<=\n\*\*Tier:\*\*)[^\n]*(free|premium|ultimate|, Free|Ultimate,) diff --git a/doc/administration/sidekiq/processing_specific_job_classes.md b/doc/administration/sidekiq/processing_specific_job_classes.md index 0feeaf246c9..21c75b452a0 100644 --- a/doc/administration/sidekiq/processing_specific_job_classes.md +++ b/doc/administration/sidekiq/processing_specific_job_classes.md @@ -64,8 +64,9 @@ because they only change the arguments to the launched Sidekiq process. ### Detailed example -This is a comprehensive example intended to show different possibilities. It is -not a recommendation. +This is a comprehensive example intended to show different possibilities. +A [Helm chart example is also available](https://docs.gitlab.com/charts/charts/gitlab/sidekiq/#queues). +They are not recommendations. 1. Edit `/etc/gitlab/gitlab.rb`: diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 01c4ea61abb..5e7b38edc15 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -8236,7 +8236,7 @@ Input type: `TimelogCreateInput` | ---- | ---- | ----------- | | `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | `issuableId` | [`IssuableID!`](#issuableid) | Global ID of the issuable (Issue, WorkItem or MergeRequest). | -| `spentAt` | [`Time!`](#time) | When the time was spent. | +| `spentAt` | [`Time`](#time) | Timestamp of when the time was spent. If empty, defaults to current time. | | `summary` | [`String!`](#string) | Summary of time spent. | | `timeSpent` | [`String!`](#string) | Amount of time spent. | diff --git a/doc/api/groups.md b/doc/api/groups.md index 75c10343abe..cabfe85619c 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -67,6 +67,19 @@ GET /groups "lfs_enabled": true, "default_branch": null, "default_branch_protection": 2, + "default_branch_protection_defaults": { + "allowed_to_push": [ + { + "access_level": 40 + } + ], + "allow_force_push": false, + "allowed_to_merge": [ + { + "access_level": 40 + } + ] + }, "avatar_url": "http://localhost:3000/uploads/group/avatar/1/foo.jpg", "web_url": "http://localhost:3000/groups/foo-bar", "request_access_enabled": false, @@ -107,6 +120,19 @@ GET /groups?statistics=true "lfs_enabled": true, "default_branch": null, "default_branch_protection": 2, + "default_branch_protection_defaults": { + "allowed_to_push": [ + { + "access_level": 40 + } + ], + "allow_force_push": false, + "allowed_to_merge": [ + { + "access_level": 40 + } + ] + }, "avatar_url": "http://localhost:3000/uploads/group/avatar/1/foo.jpg", "web_url": "http://localhost:3000/groups/foo-bar", "request_access_enabled": false, @@ -196,6 +222,19 @@ GET /groups/:id/subgroups "lfs_enabled": true, "default_branch": null, "default_branch_protection": 2, + "default_branch_protection_defaults": { + "allowed_to_push": [ + { + "access_level": 40 + } + ], + "allow_force_push": false, + "allowed_to_merge": [ + { + "access_level": 40 + } + ] + }, "avatar_url": "http://gitlab.example.com/uploads/group/avatar/1/foo.jpg", "web_url": "http://gitlab.example.com/groups/foo-bar", "request_access_enabled": false, @@ -258,6 +297,19 @@ GET /groups/:id/descendant_groups "lfs_enabled": true, "default_branch": null, "default_branch_protection": 2, + "default_branch_protection_defaults": { + "allowed_to_push": [ + { + "access_level": 40 + } + ], + "allow_force_push": false, + "allowed_to_merge": [ + { + "access_level": 40 + } + ] + }, "avatar_url": "http://gitlab.example.com/uploads/group/avatar/1/bar.jpg", "web_url": "http://gitlab.example.com/groups/foo/bar", "request_access_enabled": false, @@ -285,6 +337,19 @@ GET /groups/:id/descendant_groups "lfs_enabled": true, "default_branch": null, "default_branch_protection": 2, + "default_branch_protection_defaults": { + "allowed_to_push": [ + { + "access_level": 40 + } + ], + "allow_force_push": false, + "allowed_to_merge": [ + { + "access_level": 40 + } + ] + }, "avatar_url": "http://gitlab.example.com/uploads/group/avatar/1/baz.jpg", "web_url": "http://gitlab.example.com/groups/foo/bar/baz", "request_access_enabled": false, @@ -821,7 +886,7 @@ Parameters: | `auto_devops_enabled` | boolean | no | Default to Auto DevOps pipeline for all projects within this group. | | `avatar` | mixed | no | Image file for avatar of the group. | | `default_branch` | string | no | The [default branch](../user/project/repository/branches/default.md) name for group's projects. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/442298) in GitLab 16.11. | -| `default_branch_protection` | integer | no | See [Options for `default_branch_protection`](#options-for-default_branch_protection). Default to the global level default branch protection setting. | +| `default_branch_protection` | integer | no | [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/408314) in GitLab 17.0. Use `default_branch_protection_defaults` instead. | | `default_branch_protection_defaults` | hash | no | See [Options for `default_branch_protection_defaults`](#options-for-default_branch_protection_defaults). | | `description` | string | no | The group's description. | | `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `all` to allow both protocols. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/436618) in GitLab 16.9. | @@ -993,7 +1058,7 @@ PUT /groups/:id | `auto_devops_enabled` | boolean | no | Default to Auto DevOps pipeline for all projects within this group. | | `avatar` | mixed | no | Image file for avatar of the group. | | `default_branch` | string | no | The [default branch](../user/project/repository/branches/default.md) name for group's projects. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/442298) in GitLab 16.11. | -| `default_branch_protection` | integer | no | See [Options for `default_branch_protection`](#options-for-default_branch_protection). | +| `default_branch_protection` | integer | no | [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/408314) in GitLab 17.0. Use `default_branch_protection_defaults` instead. | | `default_branch_protection_defaults` | hash | no | See [Options for `default_branch_protection_defaults`](#options-for-default_branch_protection_defaults). | | `description` | string | no | The description of the group. | | `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `all` to allow both protocols. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/436618) in GitLab 16.9. | diff --git a/doc/api/rest/deprecations.md b/doc/api/rest/deprecations.md index 24e49e0724c..e205b210807 100644 --- a/doc/api/rest/deprecations.md +++ b/doc/api/rest/deprecations.md @@ -124,3 +124,18 @@ Breaking change. [Related issue](https://gitlab.com/gitlab-org/gitlab/-/issues/4 In GitLab 18.0, the [Runners API](../runners.md) will return `""` in place of `version`, `revision`, `platform`, and `architecture` for runners. In v5 of the REST API, the fields will be removed. + +## `default_branch_protection` API field + +Breaking change. [Related issue](https://gitlab.com/gitlab-org/gitlab/-/issues/408315). + +The `default_branch_protection` field is deprecated in GitLab 17.0 for the following APIs: + +- [New group API](../groups.md#new-group). +- [Update group API](../groups.md#update-group). +- [Application API](../settings.md#change-application-settings) + +You should use the `default_branch_protection_defaults` field instead, which provides more finer grained control +over the default branch protections. + +The `default_branch_protection` field will be removed in v5 of the GitLab REST API. diff --git a/doc/api/settings.md b/doc/api/settings.md index 5cf87a2ebbb..2950a068967 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -46,6 +46,19 @@ Example response: "signup_enabled" : true, "id" : 1, "default_branch_protection" : 2, + "default_branch_protection_defaults": { + "allowed_to_push": [ + { + "access_level": 40 + } + ], + "allow_force_push": false, + "allowed_to_merge": [ + { + "access_level": 40 + } + ] + }, "default_preferred_language" : "en", "failed_login_attempts_unlock_period_in_minutes": 30, "restricted_visibility_levels" : [], @@ -179,6 +192,7 @@ these parameters: > - `always_perform_delayed_deletion` feature flag [enabled](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113332) in GitLab 15.11. > - `delayed_project_deletion` and `delayed_group_deletion` attributes removed in GitLab 16.0. > - `user_email_lookup_limit` attribute [removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/136886) in GitLab 16.7. +> - `default_branch_protection` [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/408314) in GitLab 17.0. Use `default_branch_protection_defaults` instead. Use an API call to modify GitLab instance [application settings](#list-of-settings-that-can-be-accessed-via-api-calls). @@ -206,6 +220,19 @@ Example response: "updated_at": "2015-06-30T13:22:42.210Z", "home_page_url": "", "default_branch_protection": 2, + "default_branch_protection_defaults": { + "allowed_to_push": [ + { + "access_level": 40 + } + ], + "allow_force_push": false, + "allowed_to_merge": [ + { + "access_level": 40 + } + ] + }, "restricted_visibility_levels": [], "max_attachment_size": 10, "max_decompressed_archive_size": 25600, @@ -389,7 +416,8 @@ listed in the descriptions of the relevant settings. | `decompress_archive_file_timeout` | integer | no | Default timeout for decompressing archived files, in seconds. Set to 0 to disable timeouts. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/129161) in GitLab 16.4. | | `default_artifacts_expire_in` | string | no | Set the default expiration time for each job's artifacts. | | `default_branch_name` | string | no | [Instance-level custom initial branch name](../user/project/repository/branches/default.md#instance-level-custom-initial-branch-name). | -| `default_branch_protection` | integer | no | Determine if developers can push to the default branch. Can take: `0` _(not protected, both users with the Developer role or Maintainer role can push new commits and force push)_, `1` _(partially protected, users with the Developer role or Maintainer role can push new commits, but cannot force push)_ or `2` _(fully protected, users with the Developer or Maintainer role cannot push new commits, but users with the Developer or Maintainer role can; no one can force push)_ as a parameter. Default is `2`. | +| `default_branch_protection` | integer | no | [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/408314) in GitLab 17.0. Use `default_branch_protection_defaults` instead. | +| `default_branch_protection_defaults` | hash | no | For available options, see [Options for `default_branch_protection_defaults`](#options-for-default_branch_protection_defaults). | | `default_ci_config_path` | string | no | Default CI/CD configuration file and path for new projects (`.gitlab-ci.yml` if not set). | | `default_group_visibility` | string | no | What visibility level new groups receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. [Changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131203) in GitLab 16.4: cannot be set to any levels in `restricted_visibility_levels`.| | `default_preferred_language` | string | no | Default preferred language for users who are not logged in. | @@ -704,3 +732,17 @@ to be set, or _all_ of these values to be set: The package file size limits are not part of the Application settings API. Instead, these settings can be accessed using the [Plan limits API](plan_limits.md). + +### Options for `default_branch_protection_defaults` + +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/408314) in GitLab 17.0. + +The `default_branch_protection_defaults` attribute describes the default branch +protection defaults. All parameters are optional. + +| Key | Type | Description | +|:-----------------------------|:--------|:------------| +| `allowed_to_push` | array | An array of access levels allowed to push. Supports Developer (30) or Maintainer (40). | +| `allow_force_push` | boolean | Allow force push for all users with push access. | +| `allowed_to_merge` | array | An array of access levels allowed to merge. Supports Developer (30) or Maintainer (40). | +| `developer_can_initial_push` | boolean | Allow developers to initial push. | diff --git a/doc/ci/secrets/convert-to-id-tokens.md b/doc/ci/secrets/convert-to-id-tokens.md index d5e193a4211..42fc2fd41c7 100644 --- a/doc/ci/secrets/convert-to-id-tokens.md +++ b/doc/ci/secrets/convert-to-id-tokens.md @@ -92,6 +92,7 @@ Roles are bound to a specific authentication path so you need to add new roles f "policies": ["myproject-staging"], "token_explicit_max_ttl": 60, "user_claim": "user_email", + "bound_audiences": ["https://vault.example.com"], "bound_claims": { "project_id": "22", "ref": "master", diff --git a/doc/user/application_security/sast/customize_rulesets.md b/doc/user/application_security/sast/customize_rulesets.md index 090d08b5106..4801fd64296 100644 --- a/doc/user/application_security/sast/customize_rulesets.md +++ b/doc/user/application_security/sast/customize_rulesets.md @@ -58,7 +58,7 @@ You can completely replace the predefined rules of some SAST analyzers: can replace the default [njsscan configuration file](https://github.com/ajinabraham/njsscan#configure-njsscan) with your own. - [semgrep](https://gitlab.com/gitlab-org/security-products/analyzers/semgrep) - you can replace - the [GitLab-maintained ruleset](https://gitlab.com/gitlab-org/security-products/analyzers/semgrep/-/tree/main/rules) + the [GitLab-maintained ruleset](https://gitlab.com/gitlab-org/security-products/sast-rules) with your own. You provide your customizations via passthroughs, which are composed into a diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md index e6bc981c2c8..f563124ac7c 100644 --- a/doc/user/project/import/github.md +++ b/doc/user/project/import/github.md @@ -122,7 +122,7 @@ To import your GitHub repository using a GitHub Personal Access Token: 1. Go to . 1. In the **Note** field, enter a token description. 1. Select the `repo` scope. - 1. Optional. To [import collaborators](#select-additional-items-to-import), select the `read:org` scope. + 1. Optional. To [import collaborators](#select-additional-items-to-import), or if your project has [Git LFS files](../../../topics/git/lfs/index.md), select the `read:org` scope. 1. Select **Generate token**. 1. On the GitLab left sidebar, at the top, select **Create new** (**{plus}**) and **New project/repository**. 1. Select **Import project** and then **GitHub**. @@ -151,7 +151,7 @@ To import your GitHub repository using the GitLab REST API: 1. Go to . 1. In the **Note** field, enter a token description. 1. Select the `repo` scope. - 1. Optional. To [import collaborators](#select-additional-items-to-import), select the `read:org` scope. + 1. Optional. To [import collaborators](#select-additional-items-to-import), or if your project has [Git LFS files](../../../topics/git/lfs/index.md), select the `read:org` scope. 1. Select **Generate token**. 1. Use the [GitLab REST API](../../../api/import.md#import-repository-from-github) to import your GitHub repository. diff --git a/doc/user/project/time_tracking.md b/doc/user/project/time_tracking.md index b15966722b6..03666abb09b 100644 --- a/doc/user/project/time_tracking.md +++ b/doc/user/project/time_tracking.md @@ -83,6 +83,7 @@ Prerequisites: #### Using the user interface > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/101563) in GitLab 15.7. +> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/150564) in GitLab 17.0. When you don't specify when time was spent, current time is used. To add a time entry using the user interface: @@ -90,7 +91,7 @@ To add a time entry using the user interface: 1. Enter: - The amount of time spent. - - Optional. When it was spent. + - Optional. When it was spent. If empty, uses current time. - Optional. A summary. 1. Select **Save**. diff --git a/keeps/overdue_finalize_background_migration.rb b/keeps/overdue_finalize_background_migration.rb index c3870a3d279..8c3b1b92434 100644 --- a/keeps/overdue_finalize_background_migration.rb +++ b/keeps/overdue_finalize_background_migration.rb @@ -24,8 +24,6 @@ module Keeps # -k Keeps::OverdueFinalizeBackgroundMigration # ``` class OverdueFinalizeBackgroundMigration < ::Gitlab::Housekeeper::Keep - CUTOFF_MILESTONE = '16.8' # Only finalize migrations added before this - def each_change each_batched_background_migration do |migration_yaml_file, migration| next unless before_cuttoff_milestone?(migration['milestone']) @@ -218,7 +216,7 @@ module Keeps end def before_cuttoff_milestone?(milestone) - Gem::Version.new(milestone) < Gem::Version.new(CUTOFF_MILESTONE) + Gem::Version.new(milestone) <= Gem::Version.new(::Gitlab::Database::MIN_SCHEMA_GITLAB_VERSION) end def each_batched_background_migration diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index de4dde89f0a..e9d91b71722 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -74,6 +74,7 @@ module Gitlab push_frontend_feature_flag(:organization_switching, current_user) # To be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/399248 push_frontend_feature_flag(:remove_monitor_metrics) + push_frontend_feature_flag(:vue_page_breadcrumbs) end # Exposes the state of a feature flag to the frontend code. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ab260a59fe4..a7ebe92c2c9 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -23493,7 +23493,7 @@ msgstr "" msgid "Gitea import" msgstr "" -msgid "GithubImporter|%{code_start}read:org%{code_end} (optional): Used to import collaborators from GitHub repositories." +msgid "GithubImporter|%{code_start}read:org%{code_end} (optional): Used to import collaborators from GitHub repositories, or if your project has Git LFS files." msgstr "" msgid "GithubImporter|%{code_start}repo%{code_end}: Used to display a list of your public and private repositories that are available to connect to." @@ -26637,7 +26637,7 @@ msgstr "" msgid "ImportProjects|Select the repositories you want to import" msgstr "" -msgid "ImportProjects|The more information you select, the longer it will take to import" +msgid "ImportProjects|The more information you select, the longer it will take to import." msgstr "" msgid "ImportProjects|The remote data could not be imported." @@ -26646,7 +26646,7 @@ msgstr "" msgid "ImportProjects|The repository could not be created." msgstr "" -msgid "ImportProjects|To import collaborators, you must use a classic personal access token with %{codeStart}read:org%{codeEnd} scope. %{linkStart}Learn more%{linkEnd}." +msgid "ImportProjects|To import collaborators, or if your project has Git LFS files, you must use a classic personal access token with %{codeStart}read:org%{codeEnd} scope. %{linkStart}Learn more%{linkEnd}." msgstr "" msgid "ImportProjects|Update of imported projects with realtime changes failed" diff --git a/spec/features/issuables/shortcuts_issuable_spec.rb b/spec/features/issuables/shortcuts_issuable_spec.rb index 13bec61e4da..5502df71020 100644 --- a/spec/features/issuables/shortcuts_issuable_spec.rb +++ b/spec/features/issuables/shortcuts_issuable_spec.rb @@ -3,32 +3,40 @@ require 'spec_helper' RSpec.describe 'Blob shortcuts', :js, feature_category: :team_planning do - let(:user) { create(:user) } - let(:project) { create(:project, :public, :repository) } - let(:issue) { create(:issue, project: project, author: user) } + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :public, :repository) } + let_it_be(:issue) { create(:issue, project: project, author: user) } let(:merge_request) { create(:merge_request, source_project: project) } let(:note_text) { 'I got this!' } - before do + before_all do project.add_developer(user) - - sign_in(user) end shared_examples "quotes the selected text" do - it 'quotes the selected text in main comment form', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/356388' do - select_element('#notes-list .note:first-child .note-text') + it 'focuses main comment field by default' do find('body').native.send_key('r') - expect(find('.js-main-target-form .js-vue-comment-form').value).to include(note_text) + expect(page).to have_selector('.js-main-target-form .js-gfm-input:focus') end - it 'quotes the selected text in the discussion reply form', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/356388' do - find('#notes-list .note:first-child .js-reply-button').click - select_element('#notes-list .note:first-child .note-text') + it 'quotes the selected text in main comment form' do + select_element('#notes-list .note-comment:first-child .note-text') find('body').native.send_key('r') - expect(find('#notes-list .note:first-child .js-vue-markdown-field .js-gfm-input').value).to include(note_text) + page.within('.js-main-target-form') do + expect(page).to have_field('Write a comment or drag your files here…', with: "> #{note_text}\n\n") + end + end + + it 'quotes the selected text in the discussion reply form' do + find('#notes-list .note:first-child .js-reply-button').click + select_element('#notes-list .note-comment:first-child .note-text') + find('body').native.send_key('r') + + page.within('.notes .discussion-reply-holder') do + expect(page).to have_field('Write a comment or drag your files here…', with: "> #{note_text}\n\n") + end end end @@ -36,6 +44,7 @@ RSpec.describe 'Blob shortcuts', :js, feature_category: :team_planning do describe 'On an Issue' do before do create(:note, noteable: issue, project: project, note: note_text) + sign_in(user) visit project_issue_path(project, issue) wait_for_requests end @@ -46,6 +55,7 @@ RSpec.describe 'Blob shortcuts', :js, feature_category: :team_planning do describe 'On a Merge Request' do before do create(:note, noteable: merge_request, project: project, note: note_text) + sign_in(user) visit project_merge_request_path(project, merge_request) wait_for_requests end @@ -65,6 +75,7 @@ RSpec.describe 'Blob shortcuts', :js, feature_category: :team_planning do describe 'pressing "a"' do describe 'On an Issue' do before do + sign_in(user) visit project_issue_path(project, issue) wait_for_requests end @@ -74,6 +85,7 @@ RSpec.describe 'Blob shortcuts', :js, feature_category: :team_planning do describe 'On a Merge Request' do before do + sign_in(user) visit project_merge_request_path(project, merge_request) wait_for_requests end @@ -93,6 +105,7 @@ RSpec.describe 'Blob shortcuts', :js, feature_category: :team_planning do describe 'pressing "m"' do describe 'On an Issue' do before do + sign_in(user) visit project_issue_path(project, issue) wait_for_requests end @@ -102,6 +115,7 @@ RSpec.describe 'Blob shortcuts', :js, feature_category: :team_planning do describe 'On a Merge Request' do before do + sign_in(user) visit project_merge_request_path(project, merge_request) wait_for_requests end @@ -121,6 +135,7 @@ RSpec.describe 'Blob shortcuts', :js, feature_category: :team_planning do describe 'pressing "l"' do describe 'On an Issue' do before do + sign_in(user) visit project_issue_path(project, issue) wait_for_requests end @@ -130,6 +145,7 @@ RSpec.describe 'Blob shortcuts', :js, feature_category: :team_planning do describe 'On a Merge Request' do before do + sign_in(user) visit project_merge_request_path(project, merge_request) wait_for_requests end diff --git a/spec/features/projects/work_items/shortcuts_work_item_spec.rb b/spec/features/projects/work_items/shortcuts_work_item_spec.rb index 746e608a1c5..74701caa975 100644 --- a/spec/features/projects/work_items/shortcuts_work_item_spec.rb +++ b/spec/features/projects/work_items/shortcuts_work_item_spec.rb @@ -6,7 +6,8 @@ RSpec.describe 'Work item keyboard shortcuts', :js, feature_category: :team_plan let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, :public, :repository) } let_it_be(:work_item) { create(:work_item, project: project) } - let(:work_items_path) { project_work_item_path(project, work_item.iid) } + let_it_be(:work_items_path) { project_work_item_path(project, work_item.iid) } + let_it_be(:note_text) { 'I got this!' } context 'for signed in user' do before_all do @@ -14,6 +15,7 @@ RSpec.describe 'Work item keyboard shortcuts', :js, feature_category: :team_plan end before do + create(:note, noteable: work_item, project: project, note: note_text) sign_in(user) visit work_items_path @@ -46,5 +48,43 @@ RSpec.describe 'Work item keyboard shortcuts', :js, feature_category: :team_plan expect(page).to have_selector('form textarea#work-item-description') end end + + describe 'pressing r' do + it 'focuses main comment field by default' do + find('body').native.send_key('r') + + expect(page).to have_selector('.js-main-target-form .js-gfm-input:focus') + end + + it 'quotes the selected text in main comment form' do + select_element('.notes .note-comment:first-child .note-text') + find('body').native.send_key('r') + + page.within('.js-main-target-form') do + expect(page).to have_field('Write a comment or drag your files here…', with: "> #{note_text}\n\n") + end + end + + it 'quotes the selected text in the discussion reply form' do + click_button 'Reply to comment' + + select_element('.notes .note-comment:first-child .note-text') + + find('body').native.send_key('r') + page.within('.notes .discussion-reply-holder') do + expect(page).to have_field('Write a comment or drag your files here…', with: "> #{note_text}\n\n") + end + end + end + + describe 'navigation' do + it 'pressing . opens web IDE' do + new_tab = window_opened_by { find('body').native.send_key('.') } + + within_window new_tab do + expect(page).to have_selector('.ide-view') + end + end + end end end diff --git a/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js b/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js index 3951714c64e..206716a3397 100644 --- a/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js +++ b/spec/frontend/behaviors/shortcuts/shortcuts_issuable_spec.js @@ -22,13 +22,13 @@ describe('ShortcutsIssuable', () => { }); describe('replyWithSelectedText', () => { - const FORM_SELECTOR = '.js-main-target-form .js-vue-comment-form'; + const FORM_SELECTOR = '.js-main-target-form .js-gfm-input'; beforeEach(() => { setHTMLFixture(htmlSnippetsShow); $('body').append( `

- +
`, ); document.querySelector('.js-new-note-form').classList.add('js-main-target-form'); diff --git a/spec/frontend/import_entities/import_projects/components/advanced_settings_spec.js b/spec/frontend/import_entities/import_projects/components/advanced_settings_spec.js index ae259d2b627..355c100dd6b 100644 --- a/spec/frontend/import_entities/import_projects/components/advanced_settings_spec.js +++ b/spec/frontend/import_entities/import_projects/components/advanced_settings_spec.js @@ -35,7 +35,7 @@ describe('Import Advanced Settings', () => { it('renders a warning message', () => { expect(findAlert().text()).toMatchInterpolatedText( - 'The more information you select, the longer it will take to import To import collaborators, you must use a classic personal access token with read:org scope. Learn more.', + 'The more information you select, the longer it will take to import. To import collaborators, or if your project has Git LFS files, you must use a classic personal access token with read:org scope. Learn more.', ); }); diff --git a/spec/frontend/lib/apollo/correlation_id_link_spec.js b/spec/frontend/lib/apollo/correlation_id_link_spec.js new file mode 100644 index 00000000000..35a845229f4 --- /dev/null +++ b/spec/frontend/lib/apollo/correlation_id_link_spec.js @@ -0,0 +1,64 @@ +import { ApolloLink, execute, Observable } from '@apollo/client/core'; +import { correlationIdLink } from '~/lib/apollo/correlation_id_link'; + +describe('getCorrelationIdLink', () => { + let subscription; + const mockCorrelationId = 'abc123'; + const mockData = { foo: { id: 1 } }; + + afterEach(() => subscription?.unsubscribe()); + + const makeMockTerminatingLink = () => + new ApolloLink(() => + Observable.of({ + data: mockData, + }), + ); + + const createSubscription = (link, observer, headerName) => { + const mockOperation = { + operationName: 'someMockOperation', + context: { + response: { + headers: { + get: (name) => (name === headerName ? mockCorrelationId : null), + }, + }, + }, + }; + subscription = execute(link, mockOperation).subscribe(observer); + }; + + describe.each(['X-Request-Id', 'x-request-id'])('when header name is %s', (headerName) => { + let link; + beforeEach(() => { + link = correlationIdLink.concat(makeMockTerminatingLink()); + }); + + it('adds the correlation ID to the response', () => { + return new Promise((resolve) => { + createSubscription( + link, + ({ correlationId }) => { + expect(correlationId).toBe(mockCorrelationId); + resolve(); + }, + headerName, + ); + }); + }); + + it('does not modify the original response', () => { + return new Promise((resolve) => { + createSubscription( + link, + (response) => { + expect(response.data).toEqual(mockData); + resolve(); + }, + headerName, + ); + }); + }); + }); +}); diff --git a/spec/frontend/lib/utils/breadcrumbs_spec.js b/spec/frontend/lib/utils/breadcrumbs_spec.js index 481e3db521c..9400d5ce073 100644 --- a/spec/frontend/lib/utils/breadcrumbs_spec.js +++ b/spec/frontend/lib/utils/breadcrumbs_spec.js @@ -1,30 +1,10 @@ import { createWrapper } from '@vue/test-utils'; import Vue from 'vue'; -import { injectVueAppBreadcrumbs } from '~/lib/utils/breadcrumbs'; +import { injectVueAppBreadcrumbs, staticBreadcrumbs } from '~/lib/utils/breadcrumbs'; import { resetHTMLFixture, setHTMLFixture } from 'helpers/fixtures'; import createMockApollo from 'helpers/mock_apollo_helper'; describe('Breadcrumbs utils', () => { - const breadcrumbsHTML = ` - - `; - - const emptyBreadcrumbsHTML = ` - - `; - const mockRouter = jest.fn(); const MockComponent = Vue.component('MockComponent', { @@ -43,36 +23,115 @@ describe('Breadcrumbs utils', () => { }); describe('injectVueAppBreadcrumbs', () => { - describe('without any breadcrumbs', () => { + describe('when vue_page_breadcrumbs feature flag is enabled', () => { beforeEach(() => { - setHTMLFixture(emptyBreadcrumbsHTML); + window.gon = { features: { vuePageBreadcrumbs: true } }; }); - it('returns early and stops trying to inject', () => { - expect(injectVueAppBreadcrumbs(mockRouter, MockComponent)).toBe(false); + describe('when inject target id is not present', () => { + const emptyBreadcrumbsHTML = ``; + + beforeEach(() => { + setHTMLFixture(emptyBreadcrumbsHTML); + }); + + it('returns early and stops trying to inject', () => { + expect(injectVueAppBreadcrumbs(mockRouter, MockComponent)).toBe(false); + }); + }); + + describe('when inject target id is present', () => { + const breadcrumbsHTML = ` +
+ +
+
+ `; + + beforeEach(() => { + setHTMLFixture(breadcrumbsHTML); + staticBreadcrumbs.items = [ + { text: 'First', href: '/first' }, + { text: 'Last', href: '/last' }, + ]; + }); + + it('nulls text and href of the last static breadcrumb item', () => { + injectVueAppBreadcrumbs(mockRouter, MockComponent); + expect(staticBreadcrumbs.items[0].text).toBe('First'); + expect(staticBreadcrumbs.items[0].href).toBe('/first'); + expect(staticBreadcrumbs.items[1].text).toBe(''); + expect(staticBreadcrumbs.items[1].href).toBe(''); + }); + + it('mounts given component at the inject target id', () => { + const wrapper = createWrapper( + injectVueAppBreadcrumbs(mockRouter, MockComponent, mockApolloProvider), + ); + expect(wrapper.exists()).toBe(true); + expect( + document.querySelectorAll('#js-vue-page-breadcrumbs + [data-testid="mock-component"]'), + ).toHaveLength(1); + }); }); }); - describe('with breadcrumbs', () => { + describe('when vue_page_breadcrumbs feature flag is disabled', () => { + const breadcrumbsHTML = ` + + `; + + const emptyBreadcrumbsHTML = ` + + `; + beforeEach(() => { - setHTMLFixture(breadcrumbsHTML); + window.gon = { features: { vuePageBreadcrumbs: false } }; }); - describe.each` - testLabel | apolloProvider - ${'set'} | ${mockApolloProvider} - ${'not set'} | ${null} - `('given the apollo provider is $testLabel', ({ apolloProvider }) => { + describe('without any breadcrumbs', () => { beforeEach(() => { - createWrapper(injectVueAppBreadcrumbs(mockRouter, MockComponent, apolloProvider)); + setHTMLFixture(emptyBreadcrumbsHTML); }); - it('returns a new breadcrumbs component replacing the inject HTML', () => { - // Using `querySelectorAll` because we're not testing a full Vue app. - // We are testing a partial Vue app added into the pages HTML. - expect(document.querySelectorAll('[data-testid="existing-crumb"]')).toHaveLength(1); - expect(document.querySelectorAll('[data-testid="last-crumb"]')).toHaveLength(0); - expect(document.querySelectorAll('[data-testid="mock-component"]')).toHaveLength(1); + it('returns early and stops trying to inject', () => { + expect(injectVueAppBreadcrumbs(mockRouter, MockComponent)).toBe(false); + }); + }); + + describe('with breadcrumbs', () => { + beforeEach(() => { + setHTMLFixture(breadcrumbsHTML); + }); + + describe.each` + testLabel | apolloProvider + ${'set'} | ${mockApolloProvider} + ${'not set'} | ${null} + `('given the apollo provider is $testLabel', ({ apolloProvider }) => { + beforeEach(() => { + createWrapper(injectVueAppBreadcrumbs(mockRouter, MockComponent, apolloProvider)); + }); + + it('returns a new breadcrumbs component replacing the inject HTML', () => { + // Using `querySelectorAll` because we're not testing a full Vue app. + // We are testing a partial Vue app added into the pages HTML. + expect(document.querySelectorAll('[data-testid="existing-crumb"]')).toHaveLength(1); + expect(document.querySelectorAll('[data-testid="last-crumb"]')).toHaveLength(0); + expect(document.querySelectorAll('[data-testid="mock-component"]')).toHaveLength(1); + }); }); }); }); diff --git a/spec/support/helpers/selection_helper.rb b/spec/support/helpers/selection_helper.rb index a5f9ca76f6e..697f4b523fa 100644 --- a/spec/support/helpers/selection_helper.rb +++ b/spec/support/helpers/selection_helper.rb @@ -3,6 +3,6 @@ module SelectionHelper def select_element(selector) find(selector) - execute_script("let range = document.createRange(); let sel = window.getSelection(); range.selectNodeContents(document.querySelector('#{selector}')); sel.addRange(range);") + execute_script("let sel = window.getSelection(); sel.removeAllRanges(); let range = document.createRange(); range.selectNodeContents(document.querySelector('#{selector}')); sel.addRange(range);") end end diff --git a/spec/support/shared_examples/graphql/mutations/timelogs/create_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/timelogs/create_shared_examples.rb index c6402a89f02..23dd195e626 100644 --- a/spec/support/shared_examples/graphql/mutations/timelogs/create_shared_examples.rb +++ b/spec/support/shared_examples/graphql/mutations/timelogs/create_shared_examples.rb @@ -2,14 +2,16 @@ RSpec.shared_examples 'issuable supports timelog creation mutation' do let(:mutation_response) { graphql_mutation_response(:timelog_create) } - let(:mutation) do - variables = { + let(:spent_at) { '2022-11-16T12:59:35+0100' } + let(:mutation) { graphql_mutation(:timelogCreate, variables) } + + let(:variables) do + { 'time_spent' => time_spent, - 'spent_at' => '2022-11-16T12:59:35+0100', + 'spent_at' => spent_at, 'summary' => 'Test summary', 'issuable_id' => issuable.to_global_id.to_s } - graphql_mutation(:timelogCreate, variables) end context 'when the user is anonymous' do @@ -56,6 +58,30 @@ RSpec.shared_examples 'issuable supports timelog creation mutation' do end end + context 'when spent_at is not provided', time_travel_to: '2024-04-23 22:50:00 +0200' do + let(:variables) do + { + 'time_spent' => time_spent, + 'summary' => 'Test summary', + 'issuable_id' => issuable.to_global_id.to_s + } + end + + it 'creates the timelog using the current time' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + end.to change { Timelog.count }.by(1) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['errors']).to be_empty + expect(mutation_response['timelog']).to include( + 'timeSpent' => 3600, + 'spentAt' => '2024-04-23T20:50:00Z', + 'summary' => 'Test summary' + ) + end + end + context 'with invalid time_spent' do let(:time_spent) { '3h e' }