Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
26ae9cfbfb
commit
c5ac71d93e
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
d5833f9560b9a225a108e766bf052103b899ee76
|
||||
902f1b82628d4df113f87b7cbb87ac108e028209
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}.',
|
||||
)
|
||||
"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
})),
|
||||
);
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -57,5 +57,5 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<gl-breadcrumb :key="isLoaded" :items="allCrumbs" />
|
||||
<gl-breadcrumb :key="isLoaded" :items="allCrumbs" :auto-resize="false" />
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -45,5 +45,5 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<gl-breadcrumb :key="isLoaded" :items="allCrumbs" />
|
||||
<gl-breadcrumb :key="isLoaded" :items="allCrumbs" :auto-resize="false" />
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,)
|
||||
|
|
|
|||
|
|
@ -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`:
|
||||
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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**.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue