Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-05-09 03:13:16 +00:00
parent 26ae9cfbfb
commit c5ac71d93e
42 changed files with 629 additions and 109 deletions

View File

@ -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'

View File

@ -1 +1 @@
d5833f9560b9a225a108e766bf052103b899ee76
902f1b82628d4df113f87b7cbb87ac108e028209

View File

@ -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.

View File

@ -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;
}
}

View File

@ -48,13 +48,13 @@ export default {
>
<gl-alert variant="warning" class="gl-mb-5" :dismissible="false"
>{{
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.')
}}
<p class="mb-0">
<gl-sprintf
:message="
s__(
'ImportProjects|To import collaborators, you must use a classic personal access token with %{codeStart}read:org%{codeEnd} scope. %{linkStart}Learn more%{linkEnd}.',
'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}.',
)
"
>

View File

@ -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);

View File

@ -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),
})),
);

View File

@ -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(),

View File

@ -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) {

View File

@ -57,5 +57,5 @@ export default {
</script>
<template>
<gl-breadcrumb :key="isLoaded" :items="allCrumbs" />
<gl-breadcrumb :key="isLoaded" :items="allCrumbs" :auto-resize="false" />
</template>

View File

@ -45,5 +45,5 @@ export default {
</script>
<template>
<gl-breadcrumb :key="isLoaded" :items="allCrumbs" />
<gl-breadcrumb :key="isLoaded" :items="allCrumbs" :auto-resize="false" />
</template>

View File

@ -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',
});
},
});

View File

@ -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,

View File

@ -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',

View File

@ -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 @@
}
}
}
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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,)

View File

@ -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`:

View File

@ -8236,7 +8236,7 @@ Input type: `TimelogCreateInput`
| ---- | ---- | ----------- |
| <a id="mutationtimelogcreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationtimelogcreateissuableid"></a>`issuableId` | [`IssuableID!`](#issuableid) | Global ID of the issuable (Issue, WorkItem or MergeRequest). |
| <a id="mutationtimelogcreatespentat"></a>`spentAt` | [`Time!`](#time) | When the time was spent. |
| <a id="mutationtimelogcreatespentat"></a>`spentAt` | [`Time`](#time) | Timestamp of when the time was spent. If empty, defaults to current time. |
| <a id="mutationtimelogcreatesummary"></a>`summary` | [`String!`](#string) | Summary of time spent. |
| <a id="mutationtimelogcreatetimespent"></a>`timeSpent` | [`String!`](#string) | Amount of time spent. |

View File

@ -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. |

View File

@ -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.

View File

@ -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. |

View File

@ -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",

View File

@ -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

View File

@ -122,7 +122,7 @@ To import your GitHub repository using a GitHub Personal Access Token:
1. Go to <https://github.com/settings/tokens/new>.
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 <https://github.com/settings/tokens/new>.
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.

View File

@ -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**.

View File

@ -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

View File

@ -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.

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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(
`<div class="js-main-target-form">
<textarea class="js-vue-comment-form"></textarea>
<textarea class="js-gfm-input"></textarea>
</div>`,
);
document.querySelector('.js-new-note-form').classList.add('js-main-target-form');

View File

@ -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.',
);
});

View File

@ -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,
);
});
});
});
});

View File

@ -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 = `
<nav>
<ul class="js-breadcrumbs-list">
<li>
<a href="/group-name" data-testid="existing-crumb">Group name</a>
</li>
<li>
<a href="/group-name/project-name/-/subpage" data-testid="last-crumb">Subpage</a>
</li>
</ul>
</nav>
`;
const emptyBreadcrumbsHTML = `
<nav>
<ul class="js-breadcrumbs-list" data-testid="breadcumbs-list">
</ul>
</nav>
`;
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 = `<nav></nav>`;
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 = `
<div id="js-vue-page-breadcrumbs-wrapper">
<nav id="js-vue-page-breadcrumbs" class="gl-breadcrumbs"></nav>
<div id="js-injected-page-breadcrumbs"></div>
</div>
`;
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 = `
<nav>
<ul class="js-breadcrumbs-list">
<li>
<a href="/group-name" data-testid="existing-crumb">Group name</a>
</li>
<li>
<a href="/group-name/project-name/-/subpage" data-testid="last-crumb">Subpage</a>
</li>
</ul>
</nav>
`;
const emptyBreadcrumbsHTML = `
<nav>
<ul class="js-breadcrumbs-list" data-testid="breadcumbs-list">
</ul>
</nav>
`;
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);
});
});
});
});

View File

@ -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

View File

@ -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' }