Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
f6b95a66bc
commit
4e7abe540d
|
|
@ -229,14 +229,6 @@
|
|||
- *node-modules-cache # We don't push this cache as it's already rebuilt by `update-assets-compile-*-cache`
|
||||
- *storybook-node-modules-cache-push
|
||||
|
||||
.use-pg11:
|
||||
services:
|
||||
- name: postgres:11.6
|
||||
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
|
||||
- name: redis:5.0-alpine
|
||||
variables:
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
PG_VERSION: "11"
|
||||
|
||||
.use-pg12:
|
||||
services:
|
||||
|
|
@ -256,21 +248,6 @@
|
|||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
PG_VERSION: "13"
|
||||
|
||||
.use-pg11-es7-ee:
|
||||
services:
|
||||
- name: postgres:11.6
|
||||
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
|
||||
- name: redis:5.0-alpine
|
||||
- name: elasticsearch:7.17.6
|
||||
command: ["elasticsearch", "-E", "discovery.type=single-node", "-E", "xpack.security.enabled=false"]
|
||||
- name: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images:zoekt-ci-image-1.0
|
||||
alias: zoekt-ci-image
|
||||
variables:
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
PG_VERSION: "11"
|
||||
ZOEKT_INDEX_BASE_URL: http://zoekt-ci-image:6060
|
||||
ZOEKT_SEARCH_BASE_URL: http://zoekt-ci-image:6070
|
||||
|
||||
.use-pg12-es7-ee:
|
||||
services:
|
||||
- name: postgres:12
|
||||
|
|
|
|||
|
|
@ -191,16 +191,6 @@ rspec system pg12 praefect:
|
|||
- .praefect-with-db
|
||||
- .rails:rules:praefect-with-db
|
||||
|
||||
# Dedicated job to test DB library code against PG11.
|
||||
# Note that these are already tested against PG12 in the `rspec unit pg12` / `rspec-ee unit pg12` jobs.
|
||||
rspec db-library-code pg11:
|
||||
extends:
|
||||
- .rspec-base-pg11
|
||||
- .rails:rules:ee-and-foss-db-library-code
|
||||
script:
|
||||
- !reference [.base-script, script]
|
||||
- rspec_db_library_code
|
||||
|
||||
rspec fast_spec_helper:
|
||||
extends:
|
||||
- .rspec-base-pg12
|
||||
|
|
@ -616,39 +606,6 @@ rspec-ee system pg12 single-db:
|
|||
##########################################
|
||||
# EE/FOSS: default branch nightly scheduled jobs #
|
||||
|
||||
# PG11
|
||||
rspec migration pg11:
|
||||
extends:
|
||||
- .rspec-base-pg11
|
||||
- .rspec-base-migration
|
||||
- .rails:rules:rspec-on-pg11
|
||||
- .rspec-migration-parallel
|
||||
|
||||
rspec background_migration pg11:
|
||||
extends:
|
||||
- .rspec-base-pg11
|
||||
- .rspec-base-migration
|
||||
- .rails:rules:rspec-on-pg11
|
||||
- .rspec-background-migration-parallel
|
||||
|
||||
rspec unit pg11:
|
||||
extends:
|
||||
- .rspec-base-pg11
|
||||
- .rails:rules:rspec-on-pg11
|
||||
- .rspec-unit-parallel
|
||||
|
||||
rspec integration pg11:
|
||||
extends:
|
||||
- .rspec-base-pg11
|
||||
- .rails:rules:rspec-on-pg11
|
||||
- .rspec-integration-parallel
|
||||
|
||||
rspec system pg11:
|
||||
extends:
|
||||
- .rspec-base-pg11
|
||||
- .rails:rules:rspec-on-pg11
|
||||
- .rspec-system-parallel
|
||||
|
||||
# PG13
|
||||
rspec migration pg13:
|
||||
extends:
|
||||
|
|
@ -687,39 +644,6 @@ rspec system pg13:
|
|||
#####################################
|
||||
# EE: default branch nightly scheduled jobs #
|
||||
|
||||
# PG11
|
||||
rspec-ee migration pg11:
|
||||
extends:
|
||||
- .rspec-ee-base-pg11
|
||||
- .rspec-base-migration
|
||||
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
|
||||
- .rspec-ee-migration-parallel
|
||||
|
||||
rspec-ee background_migration pg11:
|
||||
extends:
|
||||
- .rspec-ee-base-pg11
|
||||
- .rspec-base-migration
|
||||
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
|
||||
- .rspec-ee-background-migration-parallel
|
||||
|
||||
rspec-ee unit pg11:
|
||||
extends:
|
||||
- .rspec-ee-base-pg11
|
||||
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
|
||||
- .rspec-ee-unit-parallel
|
||||
|
||||
rspec-ee integration pg11:
|
||||
extends:
|
||||
- .rspec-ee-base-pg11
|
||||
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
|
||||
- .rspec-ee-integration-parallel
|
||||
|
||||
rspec-ee system pg11:
|
||||
extends:
|
||||
- .rspec-ee-base-pg11
|
||||
- .rails:rules:default-branch-schedule-nightly--code-backstage-ee-only
|
||||
- .rspec-ee-system-parallel
|
||||
|
||||
# PG12
|
||||
rspec-ee unit pg12 opensearch1:
|
||||
extends:
|
||||
|
|
|
|||
|
|
@ -92,11 +92,6 @@ include:
|
|||
- !reference [.base-script, script]
|
||||
- rspec_paralellized_job "--tag ~quarantine --tag ~zoekt"
|
||||
|
||||
.rspec-base-pg11:
|
||||
extends:
|
||||
- .rspec-base
|
||||
- .use-pg11
|
||||
|
||||
.rspec-base-pg12:
|
||||
extends:
|
||||
- .rspec-base
|
||||
|
|
@ -119,11 +114,6 @@ include:
|
|||
- .rspec-base
|
||||
- .use-pg13
|
||||
|
||||
.rspec-ee-base-pg11:
|
||||
extends:
|
||||
- .rspec-base
|
||||
- .use-pg11-es7-ee
|
||||
|
||||
.rspec-ee-base-pg12:
|
||||
extends:
|
||||
- .rspec-base
|
||||
|
|
|
|||
|
|
@ -85,9 +85,6 @@
|
|||
.if-merge-request-labels-run-review-app: &if-merge-request-labels-run-review-app
|
||||
if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:run-review-app/'
|
||||
|
||||
.if-merge-request-labels-run-on-pg11: &if-merge-request-labels-run-on-pg11
|
||||
if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:run-on-pg11/'
|
||||
|
||||
.if-merge-request-labels-skip-undercoverage: &if-merge-request-labels-skip-undercoverage
|
||||
if: '$CI_MERGE_REQUEST_LABELS =~ /pipeline:skip-undercoverage/'
|
||||
|
||||
|
|
@ -1605,7 +1602,6 @@
|
|||
- <<: *if-default-refs
|
||||
changes: *db-library-patterns
|
||||
- <<: *if-merge-request-labels-run-all-rspec
|
||||
- <<: *if-merge-request-labels-run-on-pg11
|
||||
|
||||
.rails:rules:ee-mr-and-default-branch-only:
|
||||
rules:
|
||||
|
|
@ -1695,11 +1691,6 @@
|
|||
- <<: *if-merge-request
|
||||
changes: *backend-patterns
|
||||
|
||||
.rails:rules:rspec-on-pg11:
|
||||
rules:
|
||||
- <<: *if-merge-request-labels-run-on-pg11
|
||||
- !reference [".rails:rules:default-branch-schedule-nightly--code-backstage-default-rules", rules]
|
||||
|
||||
.rails:rules:default-branch-schedule-nightly--code-backstage-default-rules:
|
||||
rules:
|
||||
- <<: *if-default-branch-schedule-nightly
|
||||
|
|
|
|||
|
|
@ -2,6 +2,13 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
entry.
|
||||
|
||||
## 15.9.1 (2023-02-23)
|
||||
|
||||
### Fixed (2 changes)
|
||||
|
||||
- [Fix Broadcast messages not showing in admin console](gitlab-org/gitlab@f50dfdfe43231b4bb52378eaaa515ee76c918d03) ([merge request](gitlab-org/gitlab!112831))
|
||||
- [Fix dependency check in license approval policies](gitlab-org/gitlab@ff5a77036fdb74c4b410fbb954428dbf8736ffd8) ([merge request](gitlab-org/gitlab!112831)) **GitLab Enterprise Edition**
|
||||
|
||||
## 15.9.0 (2023-02-21)
|
||||
|
||||
### Added (223 changes)
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ export default {
|
|||
eventHub.$on('skip-beforeunload', this.handleSkipBeforeUnload);
|
||||
|
||||
if (this.themeName)
|
||||
document.querySelector('.navbar-gitlab').classList.add(`theme-${this.themeName}`);
|
||||
document.querySelector('.navbar-gitlab')?.classList.add(`theme-${this.themeName}`);
|
||||
},
|
||||
destroyed() {
|
||||
eventHub.$off('skip-beforeunload', this.handleSkipBeforeUnload);
|
||||
|
|
|
|||
|
|
@ -71,14 +71,14 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
clearBtnSharedClasses: ['gl-flex-grow-0 gl-flex-basis-0'],
|
||||
clearBtnSharedClasses: ['gl-flex-grow-0 gl-flex-basis-0 gl-m-0! gl-ml-3!'],
|
||||
inputTypes: {
|
||||
key: 'key',
|
||||
value: 'value',
|
||||
},
|
||||
i18n: {
|
||||
cancel: s__('CiVariables|Cancel'),
|
||||
clearInputs: s__('CiVariables|Clear inputs'),
|
||||
removeInputs: s__('CiVariables|Remove inputs'),
|
||||
formHelpText: s__(
|
||||
'CiVariables|Specify variable values to be used in this run. The variables specified in the configuration file and %{linkStart}CI/CD settings%{linkEnd} are used by default.',
|
||||
),
|
||||
|
|
@ -209,7 +209,7 @@ export default {
|
|||
<div
|
||||
v-for="(variable, index) in variables"
|
||||
:key="variable.id"
|
||||
class="gl-display-flex gl-align-items-center gl-mb-4"
|
||||
class="gl-display-flex gl-align-items-center gl-mb-5"
|
||||
data-testid="ci-variable-row"
|
||||
>
|
||||
<gl-form-input-group class="gl-mr-4 gl-flex-grow-1">
|
||||
|
|
@ -244,12 +244,11 @@ export default {
|
|||
<gl-button
|
||||
v-if="canRemove(index)"
|
||||
v-gl-tooltip
|
||||
:aria-label="$options.i18n.clearInputs"
|
||||
:title="$options.i18n.clearInputs"
|
||||
:aria-label="$options.i18n.removeInputs"
|
||||
:title="$options.i18n.removeInputs"
|
||||
:class="$options.clearBtnSharedClasses"
|
||||
category="tertiary"
|
||||
variant="danger"
|
||||
icon="clear"
|
||||
icon="remove"
|
||||
data-testid="delete-variable-btn"
|
||||
@click="deleteVariable(variable.id)"
|
||||
/>
|
||||
|
|
@ -260,8 +259,7 @@ export default {
|
|||
:class="$options.clearBtnSharedClasses"
|
||||
data-testid="delete-variable-btn-placeholder"
|
||||
category="tertiary"
|
||||
variant="danger"
|
||||
icon="clear"
|
||||
icon="remove"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import $ from 'jquery';
|
||||
import Shortcuts from '~/behaviors/shortcuts/shortcuts';
|
||||
import { insertText } from '~/lib/utils/common_utils';
|
||||
import { ENTER_KEY } from '~/lib/utils/keys';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
|
||||
const LINK_TAG_PATTERN = '[{text}](url)';
|
||||
|
|
@ -520,7 +521,7 @@ function continueOlText(listLineMatch, nextLineMatch) {
|
|||
|
||||
function handleContinueList(e, textArea) {
|
||||
if (!gon.markdown_automatic_lists) return;
|
||||
if (!(e.key === 'Enter')) return;
|
||||
if (!(e.key === ENTER_KEY)) return;
|
||||
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
|
||||
if (textArea.selectionStart !== textArea.selectionEnd) return;
|
||||
|
||||
|
|
@ -577,6 +578,25 @@ function handleContinueList(e, textArea) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a Markdown hard break when `Shift+Enter` is pressed
|
||||
*
|
||||
* @param {Object} e - the event
|
||||
* @param {Object} textArea - the targeted text area
|
||||
*/
|
||||
function handleHardBreak(e, textArea) {
|
||||
if (!(e.key === ENTER_KEY)) return;
|
||||
if (!e.shiftKey) return;
|
||||
if (e.altKey || e.ctrlKey || e.metaKey) return;
|
||||
|
||||
// prevent unintended line breaks inserted using Japanese IME on MacOS
|
||||
if (compositioningNoteText) return;
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
insertText(textArea, '\\\n');
|
||||
}
|
||||
|
||||
export function keypressNoteText(e) {
|
||||
const textArea = this;
|
||||
|
||||
|
|
@ -584,6 +604,7 @@ export function keypressNoteText(e) {
|
|||
|
||||
handleContinueList(e, textArea);
|
||||
handleSurroundSelectedText(e, textArea);
|
||||
handleHardBreak(e, textArea);
|
||||
}
|
||||
|
||||
export function compositionStartNoteText() {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,9 @@ import { initReportAbuse } from '~/projects/report_abuse';
|
|||
|
||||
const hasPerfBar = document.querySelector('.with-performance-bar');
|
||||
const performanceHeight = hasPerfBar ? 35 : 0;
|
||||
initDiffStatsDropdown(document.querySelector('.navbar-gitlab').offsetHeight + performanceHeight);
|
||||
initDiffStatsDropdown(
|
||||
(document.querySelector('.navbar-gitlab')?.offsetHeight ?? 0) + performanceHeight,
|
||||
);
|
||||
new ZenMode();
|
||||
new ShortcutsNavigation();
|
||||
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ export default {
|
|||
return this.approvalDetails.length;
|
||||
},
|
||||
detailsPath() {
|
||||
return `${this.branchRulesPath}?branch=${this.name}`;
|
||||
return `${this.branchRulesPath}?branch=${encodeURIComponent(this.name)}`;
|
||||
},
|
||||
statusChecksText() {
|
||||
return sprintf(this.$options.i18n.statusChecks, {
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ export default {
|
|||
>
|
||||
<gl-icon name="users" />
|
||||
<gl-loading-icon v-if="loading" size="sm" />
|
||||
<span v-else data-testid="collapsed-count" class="gl-pt-2 gl-px-3 gl-font-sm">
|
||||
<span v-else class="gl-pt-2 gl-px-3 gl-font-sm">
|
||||
{{ participantCount }}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -133,7 +133,6 @@ export default {
|
|||
<gl-button
|
||||
variant="link"
|
||||
button-text-classes="gl-text-secondary"
|
||||
data-testid="more-participants"
|
||||
@click="toggleMoreParticipants"
|
||||
>{{ toggleLabel }}</gl-button
|
||||
>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
<script>
|
||||
import { MountingPortal } from 'portal-vue';
|
||||
import { SIDEBAR_PORTAL_ID, portalState } from '../constants';
|
||||
|
||||
/**
|
||||
* Use this component to render content into the sidebar.
|
||||
*
|
||||
* Arbitrary content is allowed, but nav items should be added using a Ruby
|
||||
* Sidebars::Panel subclass instead.
|
||||
*
|
||||
* Only one instance of this component on a given page is supported. This is to
|
||||
* avoid ordering issues and cluttering the sidebar.
|
||||
*/
|
||||
export default {
|
||||
components: {
|
||||
MountingPortal,
|
||||
},
|
||||
data() {
|
||||
// This is shared state, by design. Do not mutate this state here.
|
||||
return portalState;
|
||||
},
|
||||
mountSelector: `#${SIDEBAR_PORTAL_ID}`,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<mounting-portal v-if="ready" :mount-to="$options.mountSelector" append>
|
||||
<slot></slot>
|
||||
</mounting-portal>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<script>
|
||||
import { SIDEBAR_PORTAL_ID, portalState } from '../constants';
|
||||
|
||||
export default {
|
||||
mounted() {
|
||||
portalState.ready = true;
|
||||
},
|
||||
beforeDestroy() {
|
||||
portalState.ready = false;
|
||||
},
|
||||
mountId: SIDEBAR_PORTAL_ID,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-once :id="$options.mountId"></div>
|
||||
</template>
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
<script>
|
||||
import { GlCollapse } from '@gitlab/ui';
|
||||
import UserBar from './user_bar.vue';
|
||||
import SidebarPortalTarget from './sidebar_portal_target.vue';
|
||||
import ContextSwitcherToggle from './context_switcher_toggle.vue';
|
||||
import ContextSwitcher from './context_switcher.vue';
|
||||
import HelpCenter from './help_center.vue';
|
||||
|
|
@ -14,6 +15,7 @@ export default {
|
|||
ContextSwitcher,
|
||||
HelpCenter,
|
||||
SidebarMenu,
|
||||
SidebarPortalTarget,
|
||||
},
|
||||
props: {
|
||||
sidebarData: {
|
||||
|
|
@ -53,6 +55,7 @@ export default {
|
|||
</gl-collapse>
|
||||
<gl-collapse :visible="!contextSwitcherOpened">
|
||||
<sidebar-menu :items="menuItems" />
|
||||
<sidebar-portal-target />
|
||||
</gl-collapse>
|
||||
</div>
|
||||
<div class="gl-p-3">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
// Note: all constants defined here are considered internal implementation
|
||||
// details for the sidebar. They should not be imported by anything outside of
|
||||
// the super_sidebar directory.
|
||||
|
||||
import Vue from 'vue';
|
||||
|
||||
export const SIDEBAR_PORTAL_ID = 'sidebar-portal-mount';
|
||||
|
||||
export const portalState = Vue.observable({
|
||||
ready: false,
|
||||
});
|
||||
|
|
@ -133,6 +133,7 @@
|
|||
|
||||
.page-with-super-sidebar {
|
||||
padding-left: 0;
|
||||
transition: padding-left $gl-transition-duration-medium;
|
||||
|
||||
@include media-breakpoint-up(xl) {
|
||||
padding-left: $contextual-sidebar-width;
|
||||
|
|
|
|||
|
|
@ -1082,6 +1082,9 @@ $tabs-holder-z-index: 250;
|
|||
.merge-request-sticky-header {
|
||||
z-index: 204;
|
||||
box-shadow: 0 1px 2px $issue-boards-card-shadow;
|
||||
}
|
||||
|
||||
.page-with-contextual-sidebar .merge-request-sticky-header {
|
||||
--width: calc(100% - #{$contextual-sidebar-width});
|
||||
|
||||
@include media-breakpoint-down(lg) {
|
||||
|
|
@ -1093,6 +1096,18 @@ $tabs-holder-z-index: 250;
|
|||
--width: calc(100% - #{$contextual-sidebar-collapsed-width});
|
||||
}
|
||||
|
||||
.page-with-super-sidebar .merge-request-sticky-header {
|
||||
@include media-breakpoint-up(xl) {
|
||||
--width: calc(100% - #{$super-sidebar-width});
|
||||
}
|
||||
}
|
||||
|
||||
.page-with-super-sidebar-collapsed .merge-request-sticky-header {
|
||||
@include media-breakpoint-up(xl) {
|
||||
--width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.merge-request-notification-toggle {
|
||||
.gl-toggle {
|
||||
@include gl-ml-auto;
|
||||
|
|
|
|||
|
|
@ -272,20 +272,30 @@ ul.related-merge-requests > li gl-emoji {
|
|||
|
||||
@include media-breakpoint-up(md) {
|
||||
// collapsed left sidebar + collapsed right sidebar
|
||||
.issue-sticky-header {
|
||||
.page-with-contextual-sidebar .issue-sticky-header {
|
||||
left: $contextual-sidebar-collapsed-width;
|
||||
--width: calc(100% - #{$contextual-sidebar-collapsed-width} - #{$gutter-collapsed-width});
|
||||
}
|
||||
|
||||
// collapsed left sidebar + expanded right sidebar
|
||||
.right-sidebar-expanded .issue-sticky-header {
|
||||
.page-with-contextual-sidebar.right-sidebar-expanded .issue-sticky-header {
|
||||
--width: calc(100% - #{$contextual-sidebar-collapsed-width} - #{$gutter-width});
|
||||
}
|
||||
|
||||
// collapsed super sidebar + collapsed right sidebar
|
||||
.page-with-super-sidebar .issue-sticky-header {
|
||||
--width: calc(100% - #{$gutter-collapsed-width});
|
||||
}
|
||||
|
||||
// collapsed super sidebar + expanded right sidebar
|
||||
.page-with-super-sidebar.right-sidebar-expanded .issue-sticky-header {
|
||||
--width: calc(100% - #{$gutter-width});
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(xl) {
|
||||
// expanded left sidebar + collapsed right sidebar
|
||||
.issue-sticky-header {
|
||||
.page-with-contextual-sidebar .issue-sticky-header {
|
||||
left: $contextual-sidebar-width;
|
||||
--width: calc(100% - #{$contextual-sidebar-width} - #{$gutter-collapsed-width});
|
||||
}
|
||||
|
|
@ -297,14 +307,38 @@ ul.related-merge-requests > li gl-emoji {
|
|||
}
|
||||
|
||||
// expanded left sidebar + expanded right sidebar
|
||||
.right-sidebar-expanded .issue-sticky-header {
|
||||
.page-with-contextual-sidebar.right-sidebar-expanded .issue-sticky-header {
|
||||
--width: calc(100% - #{$contextual-sidebar-width} - #{$gutter-width});
|
||||
}
|
||||
|
||||
// collapsed left sidebar + expanded right sidebar
|
||||
.right-sidebar-expanded.page-with-icon-sidebar .issue-sticky-header {
|
||||
.page-with-contextual-sidebar.right-sidebar-expanded.page-with-icon-sidebar .issue-sticky-header {
|
||||
--width: calc(100% - #{$contextual-sidebar-collapsed-width} - #{$gutter-width});
|
||||
}
|
||||
|
||||
// expanded super sidebar + collapsed right sidebar
|
||||
.page-with-super-sidebar .issue-sticky-header {
|
||||
left: $super-sidebar-width;
|
||||
--width: calc(100% - #{$super-sidebar-width} - #{$gutter-collapsed-width});
|
||||
}
|
||||
|
||||
// collapsed super sidebar + collapsed right sidebar
|
||||
.page-with-super-sidebar-collapsed .issue-sticky-header {
|
||||
left: 0;
|
||||
--width: calc(100% - #{$gutter-collapsed-width});
|
||||
}
|
||||
|
||||
// expanded super sidebar + expanded right sidebar
|
||||
.page-with-super-sidebar.right-sidebar-expanded .issue-sticky-header {
|
||||
left: $super-sidebar-width;
|
||||
--width: calc(100% - #{$super-sidebar-width} - #{$gutter-width});
|
||||
}
|
||||
|
||||
// collapsed super sidebar + expanded right sidebar
|
||||
.page-with-super-sidebar-collapsed.right-sidebar-expanded .issue-sticky-header {
|
||||
left: 0;
|
||||
--width: calc(100% - #{$gutter-width});
|
||||
}
|
||||
}
|
||||
|
||||
.issuable-header-slide-enter-active,
|
||||
|
|
|
|||
|
|
@ -90,6 +90,10 @@ module SortingPreference
|
|||
return false unless sort_order
|
||||
return can_sort_by_issue_weight?(action_name == 'issues') if sort_order.include?('weight')
|
||||
|
||||
if sort_order.include?('merged_at')
|
||||
return can_sort_by_merged_date?(controller_name == 'merge_requests' || action_name == 'merge_requests')
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -22,8 +22,6 @@ module JiraConnect
|
|||
end
|
||||
|
||||
def public_key_storage_enabled?
|
||||
return true if Gitlab.config.jira_connect.enable_public_keys_storage
|
||||
|
||||
Gitlab::CurrentSettings.jira_connect_public_key_storage_enabled?
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ module JiraConnectHelper
|
|||
users_path: current_user ? nil : jira_connect_users_path, # users_path is used to determine if user is signed in
|
||||
gitlab_user_path: current_user ? user_path(current_user) : nil,
|
||||
oauth_metadata: Feature.enabled?(:jira_connect_oauth, current_user) ? jira_connect_oauth_data(installation).to_json : nil,
|
||||
public_key_storage_enabled: Gitlab.config.jira_connect.enable_public_keys_storage || Gitlab::CurrentSettings.jira_connect_public_key_storage_enabled?
|
||||
public_key_storage_enabled: Gitlab::CurrentSettings.jira_connect_public_key_storage_enabled?
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -11,10 +11,15 @@ module NavHelper
|
|||
|
||||
def page_with_sidebar_class
|
||||
class_name = page_gutter_class
|
||||
class_name << 'page-with-contextual-sidebar' if defined?(@left_sidebar) && @left_sidebar
|
||||
class_name << 'page-with-super-sidebar' if show_super_sidebar? && @left_sidebar
|
||||
class_name << 'page-with-super-sidebar-collapsed' if show_super_sidebar? && collapsed_super_sidebar? && @left_sidebar
|
||||
class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @left_sidebar && !show_super_sidebar?
|
||||
|
||||
if show_super_sidebar?
|
||||
class_name << 'page-with-super-sidebar' if defined?(@left_sidebar) && @left_sidebar
|
||||
class_name << 'page-with-super-sidebar-collapsed' if collapsed_super_sidebar? && @left_sidebar
|
||||
else
|
||||
class_name << 'page-with-contextual-sidebar' if defined?(@left_sidebar) && @left_sidebar
|
||||
class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @left_sidebar
|
||||
end
|
||||
|
||||
class_name -= ['right-sidebar-expanded'] if defined?(@right_sidebar) && !@right_sidebar
|
||||
|
||||
class_name
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ module SortingHelper
|
|||
options.concat([due_date_option]) if viewing_issues
|
||||
|
||||
options.concat([popularity_option, label_priority_option])
|
||||
options.concat([merged_option]) if viewing_merge_requests
|
||||
options.concat([merged_option]) if can_sort_by_merged_date?(viewing_merge_requests)
|
||||
options.concat([relative_position_option]) if viewing_issues
|
||||
|
||||
options.concat([title_option])
|
||||
|
|
@ -237,6 +237,10 @@ module SortingHelper
|
|||
false
|
||||
end
|
||||
|
||||
def can_sort_by_merged_date?(viewing_merge_requests)
|
||||
viewing_merge_requests && %w[all merged].include?(params[:state])
|
||||
end
|
||||
|
||||
def due_date_option
|
||||
{ value: sort_value_due_date, text: sort_title_due_date, href: page_filter_path(sort: sort_value_due_date) }
|
||||
end
|
||||
|
|
|
|||
|
|
@ -58,15 +58,15 @@
|
|||
= c.body do
|
||||
= link_to _('Try the troubleshooting steps here.'), help_page_path('user/profile/account/two_factor_authentication.md', anchor: 'troubleshooting'), target: '_blank', rel: 'noopener noreferrer'
|
||||
|
||||
.form-group
|
||||
= label_tag :pin_code, _('Enter verification code'), class: "label-bold"
|
||||
= text_field_tag :pin_code, nil, autocomplete: 'off', inputmode: 'numeric', class: "form-control gl-form-input", required: true, data: { qa_selector: 'pin_code_field' }
|
||||
- if current_password_required?
|
||||
.form-group
|
||||
= label_tag :current_password, _('Current password'), class: 'label-bold'
|
||||
= password_field_tag :current_password, nil, autocomplete: 'current-password', required: true, class: 'form-control gl-form-input', data: { qa_selector: 'current_password_field' }
|
||||
%p.form-text.text-muted
|
||||
= _('Your current password is required to register a two-factor authenticator app.')
|
||||
.form-group
|
||||
= label_tag :pin_code, _('Enter verification code'), class: "label-bold"
|
||||
= text_field_tag :pin_code, nil, autocomplete: 'off', inputmode: 'numeric', class: "form-control gl-form-input", required: true, data: { qa_selector: 'pin_code_field' }
|
||||
.gl-mt-3
|
||||
= submit_tag _('Register with two-factor app'), class: 'gl-button btn btn-confirm', data: { qa_selector: 'register_2fa_app_button' }
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@
|
|||
= c.body do
|
||||
= s_('ExternalIssueIntegration|Only one issue tracker integration can be active at a time. Please disable the active tracker first and try again.')
|
||||
|
||||
- if integration.to_param === 'slack'
|
||||
= render 'shared/integrations/slack_notifications_deprecation_alert'
|
||||
|
||||
%h2.gl-mb-4
|
||||
= integration.title
|
||||
- if integration.operating?
|
||||
|
|
|
|||
|
|
@ -495,7 +495,6 @@ production: &base
|
|||
## To switch to a Jira connect development environment
|
||||
jira_connect:
|
||||
# atlassian_js_url: 'http://localhost:9292/atlassian.js'
|
||||
# enable_public_keys_storage: true
|
||||
# enforce_jira_base_url_https: false
|
||||
# additional_iframe_ancestors: ['localhost:*']
|
||||
|
||||
|
|
|
|||
|
|
@ -449,8 +449,6 @@ Settings.mattermost['host'] = nil unless Settings.mattermost.enabled
|
|||
Settings['jira_connect'] ||= Settingslogic.new({})
|
||||
|
||||
Settings.jira_connect['atlassian_js_url'] ||= 'https://connect-cdn.atl-paas.net/all.js'
|
||||
Settings.jira_connect['enable_public_keys_storage'] ||= false
|
||||
Settings.jira_connect['enable_public_keys_storage'] = true if Gitlab.com?
|
||||
Settings.jira_connect['enforce_jira_base_url_https'] = true if Settings.jira_connect['enforce_jira_base_url_https'].nil?
|
||||
Settings.jira_connect['additional_iframe_ancestors'] ||= []
|
||||
|
||||
|
|
@ -830,7 +828,7 @@ Gitlab.ee do
|
|||
Settings.cron_jobs['abandoned_trial_emails']['cron'] ||= "0 1 * * *"
|
||||
Settings.cron_jobs['abandoned_trial_emails']['job_class'] = 'Emails::AbandonedTrialEmailsCronWorker'
|
||||
Settings.cron_jobs['package_metadata_sync_worker'] ||= Settingslogic.new({})
|
||||
Settings.cron_jobs['package_metadata_sync_worker']['cron'] ||= "0 1 * * *"
|
||||
Settings.cron_jobs['package_metadata_sync_worker']['cron'] ||= "0 * * * *"
|
||||
Settings.cron_jobs['package_metadata_sync_worker']['job_class'] = 'PackageMetadata::SyncWorker'
|
||||
Gitlab.com do
|
||||
Settings.cron_jobs['free_user_cap_backfill_notification_jobs_worker'] ||= Settingslogic.new({})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
table_name: ci_cost_settings
|
||||
classes:
|
||||
- Ci::Minutes::CostSetting
|
||||
feature_categories:
|
||||
- continuous_integration
|
||||
description: A set of cost factors per runner which are applied to ci job duration based on project type.
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111977
|
||||
milestone: '15.10'
|
||||
gitlab_schema: gitlab_ci
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateCiRunnerCostSettings < Gitlab::Database::Migration[2.1]
|
||||
enable_lock_retries!
|
||||
|
||||
def change
|
||||
create_table :ci_cost_settings, id: false do |t|
|
||||
t.timestamps_with_timezone null: false
|
||||
t.references :runner, null: false, primary_key: true, index: false,
|
||||
foreign_key: { to_table: :ci_runners, on_delete: :cascade },
|
||||
type: :bigint, default: nil
|
||||
t.float :standard_factor, null: false, default: 1.00
|
||||
t.float :os_contribution_factor, null: false, default: 0.008
|
||||
t.float :os_plan_factor, null: false, default: 0.5
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateInitialPartitionForCiRunnerMachineBuilds < Gitlab::Database::Migration[2.1]
|
||||
PARTITION_NAME = 'gitlab_partitions_dynamic.ci_runner_machine_builds_100'
|
||||
TABLE_NAME = 'p_ci_runner_machine_builds'
|
||||
FIRST_PARTITION = 100
|
||||
BUILDS_TABLE = 'ci_builds'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
with_lock_retries(**lock_args) do
|
||||
connection.execute(<<~SQL)
|
||||
LOCK TABLE #{BUILDS_TABLE} IN SHARE UPDATE EXCLUSIVE MODE;
|
||||
LOCK TABLE ONLY #{TABLE_NAME} IN ACCESS EXCLUSIVE MODE;
|
||||
SQL
|
||||
|
||||
connection.execute(<<~SQL)
|
||||
CREATE TABLE IF NOT EXISTS #{PARTITION_NAME}
|
||||
PARTITION OF #{TABLE_NAME}
|
||||
FOR VALUES IN (#{FIRST_PARTITION});
|
||||
SQL
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
# no-op
|
||||
#
|
||||
# The migration should not remove the partition table since it might
|
||||
# have been created by 20230215074223_add_ci_runner_machine_builds_partitioned_table.rb.
|
||||
# In that case, the rollback would result in a different state.
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def lock_args
|
||||
{
|
||||
raise_on_exhaustion: true,
|
||||
timing_configuration: lock_timing_configuration
|
||||
}
|
||||
end
|
||||
|
||||
def lock_timing_configuration
|
||||
iterations = Gitlab::Database::WithLockRetries::DEFAULT_TIMING_CONFIGURATION
|
||||
aggressive_iterations = Array.new(5) { [10.seconds, 1.minute] }
|
||||
|
||||
iterations + aggressive_iterations
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
ee00d6aba8a310c236dd16749228a42589657d060bbf1785c4358bf886fd59cc
|
||||
|
|
@ -0,0 +1 @@
|
|||
661fdc00029ab9bae8b4da6a8d92f172db89087aecc13f3ad65b2b3e8ad501d3
|
||||
|
|
@ -543,6 +543,13 @@ CREATE TABLE batched_background_migration_job_transition_logs (
|
|||
)
|
||||
PARTITION BY RANGE (created_at);
|
||||
|
||||
CREATE TABLE p_ci_runner_machine_builds (
|
||||
partition_id bigint NOT NULL,
|
||||
build_id bigint NOT NULL,
|
||||
runner_machine_id bigint NOT NULL
|
||||
)
|
||||
PARTITION BY LIST (partition_id);
|
||||
|
||||
CREATE TABLE incident_management_pending_alert_escalations (
|
||||
id bigint NOT NULL,
|
||||
rule_id bigint NOT NULL,
|
||||
|
|
@ -13060,6 +13067,15 @@ CREATE SEQUENCE ci_builds_runner_session_id_seq
|
|||
|
||||
ALTER SEQUENCE ci_builds_runner_session_id_seq OWNED BY ci_builds_runner_session.id;
|
||||
|
||||
CREATE TABLE ci_cost_settings (
|
||||
created_at timestamp with time zone NOT NULL,
|
||||
updated_at timestamp with time zone NOT NULL,
|
||||
runner_id bigint NOT NULL,
|
||||
standard_factor double precision DEFAULT 1.0 NOT NULL,
|
||||
os_contribution_factor double precision DEFAULT 0.008 NOT NULL,
|
||||
os_plan_factor double precision DEFAULT 0.5 NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE ci_daily_build_group_report_results (
|
||||
id bigint NOT NULL,
|
||||
date date NOT NULL,
|
||||
|
|
@ -19018,13 +19034,6 @@ CREATE SEQUENCE operations_user_lists_id_seq
|
|||
|
||||
ALTER SEQUENCE operations_user_lists_id_seq OWNED BY operations_user_lists.id;
|
||||
|
||||
CREATE TABLE p_ci_runner_machine_builds (
|
||||
partition_id bigint NOT NULL,
|
||||
build_id bigint NOT NULL,
|
||||
runner_machine_id bigint NOT NULL
|
||||
)
|
||||
PARTITION BY LIST (partition_id);
|
||||
|
||||
CREATE TABLE packages_build_infos (
|
||||
id bigint NOT NULL,
|
||||
package_id integer NOT NULL,
|
||||
|
|
@ -26219,6 +26228,9 @@ ALTER TABLE ONLY ci_builds
|
|||
ALTER TABLE ONLY ci_builds_runner_session
|
||||
ADD CONSTRAINT ci_builds_runner_session_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY ci_cost_settings
|
||||
ADD CONSTRAINT ci_cost_settings_pkey PRIMARY KEY (runner_id);
|
||||
|
||||
ALTER TABLE ONLY ci_daily_build_group_report_results
|
||||
ADD CONSTRAINT ci_daily_build_group_report_results_pkey PRIMARY KEY (id);
|
||||
|
||||
|
|
@ -35618,6 +35630,9 @@ ALTER TABLE ONLY geo_hashed_storage_migrated_events
|
|||
ALTER TABLE ONLY plan_limits
|
||||
ADD CONSTRAINT fk_rails_69f8b6184f FOREIGN KEY (plan_id) REFERENCES plans(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY ci_cost_settings
|
||||
ADD CONSTRAINT fk_rails_6a70651f75 FOREIGN KEY (runner_id) REFERENCES ci_runners(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY operations_feature_flags_issues
|
||||
ADD CONSTRAINT fk_rails_6a8856ca4f FOREIGN KEY (feature_flag_id) REFERENCES operations_feature_flags(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
|||
|
|
@ -113,6 +113,8 @@ To disable the inbound job token scope allowlist:
|
|||
1. Toggle **Allow access to this project with a CI_JOB_TOKEN** to disabled.
|
||||
Enabled by default in new projects.
|
||||
|
||||
You can also disable the allowlist [with the API](../../api/graphql/reference/index.md#mutationprojectcicdsettingsupdate).
|
||||
|
||||
### Add a project to the inbound job token scope allowlist
|
||||
|
||||
You can add projects to the inbound allowlist for a project. Projects added to the allowlist
|
||||
|
|
@ -133,6 +135,8 @@ To add a project:
|
|||
1. Under **Allow CI job tokens from the following projects to access this project**,
|
||||
add projects to the allowlist.
|
||||
|
||||
You can also add a target project to the allowlist [with the API](../../api/graphql/reference/index.md#mutationcijobtokenscopeaddproject).
|
||||
|
||||
### Limit your project's job token access
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/328553) in GitLab 14.1. [Deployed behind the `:ci_scoped_job_token` feature flag](../../user/feature_flags.md), disabled by default.
|
||||
|
|
|
|||
|
|
@ -207,6 +207,14 @@ class Boards::ListsController < ApplicationController
|
|||
end
|
||||
```
|
||||
|
||||
A custom RSpec matcher is available to check endpoint's request urgency in the controller specs:
|
||||
|
||||
```ruby
|
||||
specify do
|
||||
expect(get(:index, params: request_params)).to have_request_urgency(:medium)
|
||||
end
|
||||
```
|
||||
|
||||
### Grape endpoints
|
||||
|
||||
To specify the urgency for an entire API class:
|
||||
|
|
@ -240,6 +248,15 @@ get 'client/features', urgency: :low do
|
|||
end
|
||||
```
|
||||
|
||||
A custom RSpec matcher is also compatible with grape endpoints' specs:
|
||||
|
||||
```ruby
|
||||
|
||||
specify do
|
||||
expect(get(api('/avatar'), params: { email: 'public@example.com' })).to have_request_urgency(:medium)
|
||||
end
|
||||
```
|
||||
|
||||
WARNING:
|
||||
We can't specify the urgency at the namespace level. The directive is ignored when doing so.
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
stage: Manage
|
||||
group: Foundations
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Navigation sidebar
|
||||
|
||||
Follow these guidelines when contributing additions or changes to the
|
||||
[redesigned](https://gitlab.com/groups/gitlab-org/-/epics/9044) navigation
|
||||
sidebar.
|
||||
|
||||
These guidelines reflect the current state of the navigation sidebar. However,
|
||||
the sidebar is a work in progress, and so is this documentation.
|
||||
|
||||
## Enable the new navigation sidebar
|
||||
|
||||
To enable the new navigation sidebar:
|
||||
|
||||
- Enable the `super_sidebar_nav` feature flag.
|
||||
- Select your avatar, then turn on the **New navigation** toggle.
|
||||
|
||||
## Adding page-specific Vue content
|
||||
|
||||
Pages can render arbitrary content into the sidebar using the `SidebarPortal`
|
||||
component. Content passed to its default slot is rendered below that
|
||||
page's navigation items in the sidebar.
|
||||
|
||||
NOTE:
|
||||
Only one instance of this component on a given page is supported. This is to
|
||||
avoid ordering issues and cluttering the sidebar.
|
||||
|
||||
NOTE:
|
||||
Arbitrary content is allowed, but nav items should be implemented by
|
||||
subclassing `::Sidebars::Panel`.
|
||||
|
||||
NOTE:
|
||||
Do not use the `SidebarPortalTarget` component. It is internal to the sidebar.
|
||||
|
|
@ -484,19 +484,17 @@ This should let us:
|
|||
Our test suite runs against PG12 as GitLab.com runs on PG12 and
|
||||
[Omnibus defaults to PG12 for new installs and upgrades](../../administration/package_information/postgresql_versions.md).
|
||||
|
||||
We do run our test suite against PG11 and PG13 on nightly scheduled pipelines.
|
||||
|
||||
We also run our test suite against PG11 upon specific database library changes in MRs and `main` pipelines (with the `rspec db-library-code pg11` job).
|
||||
We do run our test suite against PG13 on nightly scheduled pipelines.
|
||||
|
||||
#### Current versions testing
|
||||
|
||||
| Where? | PostgreSQL version | Ruby version |
|
||||
|------------------------------------------------------------------------------------------------|-------------------------------------------------|--------------|
|
||||
| Merge requests | 12 (default version), 11 for DB library changes | 3.0 (default version) |
|
||||
| `master` branch commits | 12 (default version), 11 for DB library changes | 3.0 (default version) |
|
||||
| `maintenance` scheduled pipelines for the `master` branch (every even-numbered hour) | 12 (default version), 11 for DB library changes | 3.0 (default version) |
|
||||
| `maintenance` scheduled pipelines for the `ruby2` branch (every odd-numbered hour), see below. | 12 (default version), 11 for DB library changes | 2.7 |
|
||||
| `nightly` scheduled pipelines for the `master` branch | 12 (default version), 11, 13 | 3.0 (default version) |
|
||||
| Where? | PostgreSQL version | Ruby version |
|
||||
|------------------------------------------------------------------------------------------------|--------------------------|-----------------------|
|
||||
| Merge requests | 12 (default version) | 3.0 (default version) |
|
||||
| `master` branch commits | 12 (default version) | 3.0 (default version) |
|
||||
| `maintenance` scheduled pipelines for the `master` branch (every even-numbered hour) | 12 (default version) | 3.0 (default version) |
|
||||
| `maintenance` scheduled pipelines for the `ruby2` branch (every odd-numbered hour), see below. | 12 (default version) | 2.7 |
|
||||
| `nightly` scheduled pipelines for the `master` branch | 12 (default version), 13 | 3.0 (default version) |
|
||||
|
||||
There are 2 pipeline schedules used for testing Ruby 2.7. One is triggering a
|
||||
pipeline in `ruby2-sync` branch, which updates the `ruby2` branch with latest
|
||||
|
|
@ -518,7 +516,6 @@ We follow the [PostgreSQL versions shipped with Omnibus GitLab](../../administra
|
|||
| PostgreSQL version | 14.1 (July 2021) | 14.2 (August 2021) | 14.3 (September 2021) | 14.4 (October 2021) | 14.5 (November 2021) | 14.6 (December 2021) |
|
||||
| -------------------| ---------------------- | ---------------------- | ---------------------- | ---------------------- | ---------------------- | ---------------------- |
|
||||
| PG12 | MRs/`2-hour`/`nightly` | MRs/`2-hour`/`nightly` | MRs/`2-hour`/`nightly` | MRs/`2-hour`/`nightly` | MRs/`2-hour`/`nightly` | MRs/`2-hour`/`nightly` |
|
||||
| PG11 | `nightly` | `nightly` | `nightly` | `nightly` | `nightly` | `nightly` |
|
||||
| PG13 | `nightly` | `nightly` | `nightly` | `nightly` | `nightly` | `nightly` |
|
||||
|
||||
### Redis versions testing
|
||||
|
|
|
|||
|
|
@ -136,8 +136,6 @@ that are scoped to a single [configuration keyword](../../ci/yaml/index.md#job-k
|
|||
| `.qa-cache` | Allows a job to use a default `cache` definition suitable for QA tasks. |
|
||||
| `.yarn-cache` | Allows a job to use a default `cache` definition suitable for frontend jobs that do a `yarn install`. |
|
||||
| `.assets-compile-cache` | Allows a job to use a default `cache` definition suitable for frontend jobs that compile assets. |
|
||||
| `.use-pg11` | Allows a job to run the `postgres` 11 and `redis` services (see [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/global.gitlab-ci.yml) for the specific versions of the services). |
|
||||
| `.use-pg11-ee` | Same as `.use-pg11` but also use an `elasticsearch` service (see [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/global.gitlab-ci.yml) for the specific version of the service). |
|
||||
| `.use-pg12` | Allows a job to use the `postgres` 12 and `redis` services (see [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/global.gitlab-ci.yml) for the specific versions of the services). |
|
||||
| `.use-pg12-ee` | Same as `.use-pg12` but also use an `elasticsearch` service (see [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/global.gitlab-ci.yml) for the specific version of the service). |
|
||||
| `.use-pg13` | Allows a job to use the `postgres` 13 and `redis` services (see [`.gitlab/ci/global.gitlab-ci.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/ci/global.gitlab-ci.yml) for the specific versions of the services). |
|
||||
|
|
|
|||
|
|
@ -6,220 +6,312 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Publish packages with Yarn
|
||||
|
||||
Publish npm packages in your project's Package Registry using Yarn. Then install the
|
||||
packages whenever you need to use them as a dependency.
|
||||
You can publish packages with [Yarn 1 (Classic)](https://classic.yarnpkg.com) and [Yarn 2+](https://yarnpkg.com).
|
||||
|
||||
Learn how to build a [yarn](../workflows/build_packages.md#yarn) package.
|
||||
To find the Yarn version used in the deployment container, run `yarn --version` in the `script` block of the CI
|
||||
script job block that is responsible for calling `yarn publish`**`. The Yarn version is shown in the pipeline output.
|
||||
|
||||
You can get started with Yarn 2 by following the [Yarn documentation](https://yarnpkg.com/getting-started/install/).
|
||||
Learn how to build a [Yarn](../workflows/build_packages.md#yarn) package.
|
||||
|
||||
You can use the Yarn documentation to get started with
|
||||
[Yarn Classic](https://classic.yarnpkg.com/en/docs/getting-started) and
|
||||
[Yarn 2+](https://yarnpkg.com/getting-started/).
|
||||
|
||||
## Publish to GitLab Package Registry
|
||||
|
||||
You can use Yarn to publish to the GitLab Package Registry.
|
||||
|
||||
### Authentication to the Package Registry
|
||||
|
||||
You need a token to publish a package. Different tokens are available depending on what you're trying to
|
||||
achieve. For more information, review the [guidance on tokens](../../../user/packages/package_registry/index.md#authenticate-with-the-registry).
|
||||
|
||||
- If your organization uses two-factor authentication (2FA), you must use a personal access token with the scope set to `api`.
|
||||
- If you publish a package via CI/CD pipelines, you must use a CI job token.
|
||||
- If your organization uses two-factor authentication (2FA), you must use a
|
||||
personal access token with the scope set to `api`.
|
||||
- If you publish a package via CI/CD pipelines, you can use a CI job token in
|
||||
private runners or you can register a variable for shared runners.
|
||||
|
||||
Create a token and save it to use later in the process.
|
||||
### Publish configuration
|
||||
|
||||
### Naming convention
|
||||
|
||||
Depending on how you install the package, you may need to adhere to the naming convention.
|
||||
|
||||
You can use one of two API endpoints to install packages:
|
||||
|
||||
- **Instance-level**: Use when you have many npm packages in different GitLab groups or in their own namespace.
|
||||
- **Project-level**: Use when you have a few npm packages, and they are not in the same GitLab group.
|
||||
|
||||
If you plan to install a package through the [project level](#install-from-the-project-level), you do not have to
|
||||
adhere to the naming convention.
|
||||
|
||||
If you plan to install a package through the [instance level](#install-from-the-instance-level), then you must name
|
||||
your package with a [scope](https://docs.npmjs.com/misc/scope/). Scoped packages begin with a `@` and have the
|
||||
`@owner/package-name` format. You can set up the scope for your package in the `.yarnrc.yml` file and by using the
|
||||
`publishConfig` option in the `package.json`.
|
||||
|
||||
- The value used for the `@scope` is the root of the project that hosts the packages and not the root
|
||||
of the project with the package's source code. The scope should be lowercase.
|
||||
- The package name can be anything you want
|
||||
|
||||
| Project URL | Package Registry in | Scope | Full package name |
|
||||
| ------------------------------------------------------- | ------------------- | --------- | ---------------------- |
|
||||
| `https://gitlab.com/my-org/engineering-group/analytics` | Analytics | `@my-org` | `@my-org/package-name` |
|
||||
|
||||
### Configuring `.yarnrc.yml` to publish from the project level
|
||||
|
||||
To publish with the project-level npm endpoint, set the following configuration in
|
||||
`.yarnrc.yml`:
|
||||
To publish, set the following configuration in `.yarnrc.yml`. This file should be
|
||||
located in the root directory of your package project source where `package.json` is found.
|
||||
|
||||
```yaml
|
||||
npmScopes:
|
||||
foo:
|
||||
npmRegistryServer: 'https://<your_domain>/api/v4/projects/<your_project_id>/packages/npm/'
|
||||
<my-org>:
|
||||
npmPublishRegistry: 'https://<your_domain>/api/v4/projects/<your_project_id>/packages/npm/'
|
||||
|
||||
npmRegistries:
|
||||
//gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/:
|
||||
npmAlwaysAuth: true
|
||||
npmAuthToken: '<your_token>'
|
||||
```
|
||||
|
||||
In this configuration:
|
||||
|
||||
- Replace `<my-org>` with your organization scope, exclude the `@` symbol.
|
||||
- Replace `<your_domain>` with your domain name.
|
||||
- Replace `<your_project_id>` with your project's ID, which you can find on the project's home page.
|
||||
- Replace `<your_token>` with a deploy token, group access token, project access token, or personal access token.
|
||||
- Replace `<your_token>` with a deployment token, group access token, project access token, or personal access token.
|
||||
|
||||
### Configuring `.yarnrc.yml` to publish from the instance level
|
||||
Scoped registry does not work in Yarn Classic in `package.json` file, based on
|
||||
this [issue](https://github.com/yarnpkg/yarn/pull/7829).
|
||||
Therefore, under `publishConfig` there should be `registry` and not `@scope:registry` for Yarn Classic.
|
||||
You can publish using your command line or a CI/CD pipeline to the GitLab Package Registry.
|
||||
|
||||
For the instance-level npm endpoint, use this Yarn 2 configuration in `.yarnrc.yml`:
|
||||
|
||||
```yaml
|
||||
npmScopes:
|
||||
<scope>:
|
||||
npmRegistryServer: 'https://<your_domain>/api/v4/packages/npm/'
|
||||
|
||||
npmRegistries:
|
||||
//gitlab.example.com/api/v4/packages/npm/:
|
||||
npmAlwaysAuth: true
|
||||
npmAuthToken: '<your_token>'
|
||||
```
|
||||
|
||||
In this configuration:
|
||||
|
||||
- Replace `<your_domain>` with your domain name.
|
||||
- Your scope is `<scope>`, without `@`.
|
||||
- Replace `<your_token>` with a deploy token, group access token, project access token, or personal access token.
|
||||
|
||||
### Publishing a package via the command line
|
||||
|
||||
Publish a package:
|
||||
### Publishing via the command line - Manual Publish
|
||||
|
||||
```shell
|
||||
npm publish
|
||||
# Yarn 1 (Classic)
|
||||
yarn publish
|
||||
|
||||
# Yarn 2+
|
||||
yarn npm publish
|
||||
```
|
||||
|
||||
Your package should now publish to the Package Registry.
|
||||
|
||||
### Publishing via a CI/CD pipeline
|
||||
### Publishing via a CI/CD pipeline - Automated Publish
|
||||
|
||||
In the GitLab project that houses your `yarnrc.yml`, edit or create a `.gitlab-ci.yml` file. For example:
|
||||
You can use pipeline variables when you use this method.
|
||||
|
||||
You can use **Shared Runners** *(Default)* or **Private Runners** (Advanced).
|
||||
|
||||
#### Shared runners
|
||||
|
||||
Third party images such as `node:latest` or `node:current` do not have direct access
|
||||
to the `CI_JOB_TOKEN` when operating in a shared runner. You must configure an
|
||||
authentication token or use a private runner.
|
||||
|
||||
To create a authentication token:
|
||||
|
||||
1. On the top bar, select **Main menu**, and:
|
||||
- For a project, select **Projects** and find your project.
|
||||
- For a group, select **Groups** and find your group.
|
||||
1. On the left sidebar, select **Settings > Repository > Deploy Tokens**.
|
||||
1. Create a deployment token with `read_package_registry` and `write_package_registry` scopes and copy the generated token.
|
||||
1. On the left sidebar, select **Settings > CI/CD > Variables**.
|
||||
1. Select `Add variable` and use the following settings:
|
||||
|
||||
| Field | Value |
|
||||
|--------------------|------------------------------|
|
||||
| key | `NPM_AUTH_TOKEN` |
|
||||
| value | `<DEPLOY-TOKEN-FROM-STEP-3>` |
|
||||
| type | Variable |
|
||||
| Protected variable | `CHECKED` |
|
||||
| Mask variable | `CHECKED` |
|
||||
| Expand variable | `CHECKED` |
|
||||
|
||||
To use any **Protected variable**:
|
||||
|
||||
1. Go to the repository that contains the Yarn package source code.
|
||||
1. On the left sidebar, select **Settings > Repository**.
|
||||
- If you are building from branches with tags, select **Protected Tags** and add `v*` (wildcard) for semantic versioning.
|
||||
- If you are building from branches without tags, select **Protected Branches**.
|
||||
|
||||
Then add the `NPM_AUTH_TOKEN` created above, to the `.yarnrc.yml` configuration
|
||||
in your package project root directory where `package.json` is found:
|
||||
|
||||
```yaml
|
||||
image: node:latest
|
||||
npmScopes:
|
||||
esp-code:
|
||||
npmPublishRegistry: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/npm/"
|
||||
npmAlwaysAuth: true
|
||||
npmAuthToken: "${NPM_AUTH_TOKEN}"
|
||||
```
|
||||
|
||||
#### Private runners
|
||||
|
||||
Add the `CI_JOB_TOKEN` to the `.yarnrc.yml` configuration in your package project
|
||||
root directory where `package.json` is found:
|
||||
|
||||
```yaml
|
||||
npmScopes:
|
||||
esp-code:
|
||||
npmPublishRegistry: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/npm/"
|
||||
npmAlwaysAuth: true
|
||||
npmAuthToken: "${CI_JOB_TOKEN}"
|
||||
```
|
||||
|
||||
To publish the package using CI/CD pipeline, In the GitLab project that houses
|
||||
your `yarnrc.yml`, edit or create a `.gitlab-ci.yml` file. For example to trigger
|
||||
only on any tag push:
|
||||
|
||||
```yaml
|
||||
# Yarn 1
|
||||
image: node:lts
|
||||
|
||||
stages:
|
||||
- deploy
|
||||
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
|
||||
deploy:
|
||||
stage: deploy
|
||||
script:
|
||||
- npm publish
|
||||
- yarn publish
|
||||
```
|
||||
|
||||
```yaml
|
||||
# Yarn 2+
|
||||
image: node:lts
|
||||
|
||||
stages:
|
||||
- deploy
|
||||
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
|
||||
deploy:
|
||||
stage: deploy
|
||||
before_script:
|
||||
- corepack enable
|
||||
- yarn set version stable
|
||||
script:
|
||||
- yarn npm publish
|
||||
```
|
||||
|
||||
Your package should now publish to the Package Registry when the pipeline runs.
|
||||
|
||||
## Install a package
|
||||
|
||||
If multiple packages have the same name and version, the most recently-published package is retrieved when you install a package.
|
||||
NOTE:
|
||||
If multiple packages have the same name and version, the most recently-published
|
||||
package is retrieved when you install a package.
|
||||
|
||||
You can install a package from a GitLab project or instance:
|
||||
You can use one of two API endpoints to install packages:
|
||||
|
||||
- **Instance-level**: Use when you have many npm packages in different GitLab groups or in their own namespace.
|
||||
- **Project-level**: Use when you have a few npm packages, and they are not in the same GitLab group.
|
||||
- **Instance-level**: Best used when working with many packages in an organization scope.
|
||||
|
||||
- If you plan to install a package through the [instance level](#install-from-the-instance-level),
|
||||
then you must name your package with a [scope](https://docs.npmjs.com/misc/scope/).
|
||||
Scoped packages begin with a `@` and have the `@owner/package-name` format. You can set up
|
||||
the scope for your package in the `.yarnrc.yml` file and by using the `publishConfig`
|
||||
option in the `package.json`.
|
||||
|
||||
- The value used for the `@scope` is the organization root (top-level project) `...com/my-org`
|
||||
*(@my-org)* that hosts the packages, not the root of the project with the package's source code.
|
||||
- The scope is always lowercase.
|
||||
- The package name can be anything you want `@my-org/any-name`.
|
||||
|
||||
- **Project-level**: For when you have a one-off package.
|
||||
|
||||
If you plan to install a package through the [project level](#install-from-the-project-level),
|
||||
you do not have to adhere to the naming convention.
|
||||
|
||||
| Project URL | Package Registry | Organization Scope | Full package name |
|
||||
|-------------------------------------------------------------------|----------------------|--------------------|-----------------------------|
|
||||
| `https://gitlab.com/<my-org>/<group-name>/<package-name-example>` | Package Name Example | `@my-org` | `@my-org/package-name` |
|
||||
| `https://gitlab.com/<example-org>/<group-name>/<project-name>` | Project Name | `@example-org` | `@example-org/project-name` |
|
||||
|
||||
You can install from the instance level or from the project level.
|
||||
|
||||
The configurations for `.yarnrc.yml` can be added per package consuming project
|
||||
root where `package.json` is located, or you can use a global
|
||||
configuration located in your system user home directory.
|
||||
|
||||
### Install from the instance level
|
||||
|
||||
WARNING:
|
||||
You must use packages published with the scoped [naming convention](#naming-convention) when you install a package from the instance level.
|
||||
Use these steps for global configuration in the `.yarnrc.yml` file:
|
||||
|
||||
1. Authenticate to the Package Registry
|
||||
1. [Configure organization scope](#configure-organization-scope).
|
||||
1. [Set the registry](#set-the-registry).
|
||||
|
||||
If you install a package from a private project, you must authenticate to the Package Registry. Skip this step if the project is not private.
|
||||
#### Configure organization scope
|
||||
|
||||
```shell
|
||||
npm config set -- //your_domain_name/api/v4/packages/npm/:_authToken=your_token
|
||||
```
|
||||
```yaml
|
||||
npmScopes:
|
||||
<my-org>:
|
||||
npmRegistryServer: "https://<your_domain_name>/api/v4/packages/npm"
|
||||
```
|
||||
|
||||
- Replace `your_domain_name` with your domain name, for example, `gitlab.com`.
|
||||
- Replace `your_token` with a deploy token, group access token, project access token, or personal access token.
|
||||
- Replace `<my-org>` with the root level group of the project you're installing to the package from excluding the `@` symbol.
|
||||
- Replace `<your_domain_name>` with your domain name, for example, `gitlab.com`.
|
||||
|
||||
1. Set the registry
|
||||
#### Set the registry
|
||||
|
||||
```shell
|
||||
npm config set @scope:registry https://your_domain_name.com/api/v4/packages/npm/
|
||||
```
|
||||
Skip this step if your package is public not private.
|
||||
|
||||
- Replace `@scope` with the [root level group](#naming-convention) of the project you're installing to the package from.
|
||||
- Replace `your_domain_name` with your domain name, for example, `gitlab.com`.
|
||||
- Replace `your_token` with a deploy token, group access token, project access token, or personal access token.
|
||||
```yaml
|
||||
npmRegistries:
|
||||
//<your_domain_name>/api/v4/packages/npm:
|
||||
npmAlwaysAuth: true
|
||||
npmAuthToken: "<your_token>"
|
||||
```
|
||||
|
||||
1. Install the package
|
||||
|
||||
```shell
|
||||
yarn add @scope/my-package
|
||||
```
|
||||
- Replace `<your_domain_name>` with your domain name, for example, `gitlab.com`.
|
||||
- Replace `<your_token>` with a deployment token (recommended), group access token, project access token, or personal access token.
|
||||
|
||||
### Install from the project level
|
||||
|
||||
1. Authenticate to the Package Registry
|
||||
Use these steps for each project in the `.yarnrc.yml` file:
|
||||
|
||||
If you install a package from a private project, you must authenticate to the Package Registry. Skip this step if the project is not private.
|
||||
1. [Configure project scope](#configure-project-scope).
|
||||
1. [Set the registry](#set-the-registry-project-level).
|
||||
|
||||
```shell
|
||||
npm config set -- //your_domain_name/api/v4/projects/your_project_id/packages/npm/:_authToken=your_token
|
||||
```
|
||||
#### Configure project scope
|
||||
|
||||
- Replace `your_domain_name` with your domain name, for example, `gitlab.com`.
|
||||
- Replace `your_project_id` is your project ID, found on the project's home page.
|
||||
- Replace `your_token` with a deploy token, group access token, project access token, or personal access token.
|
||||
```yaml
|
||||
npmScopes:
|
||||
<my-org>:
|
||||
npmRegistryServer: "https://<your_domain_name>/api/v4/projects/<your_project_id>/packages/npm"
|
||||
```
|
||||
|
||||
1. Set the registry
|
||||
- Replace `<my-org>` with the root level group of the project you're installing to the package from excluding the `@` symbol.
|
||||
- Replace `<your_domain_name>` with your domain name, for example, `gitlab.com`.
|
||||
- Replace `<your_project_id>` with your project ID, found on the project's home page.
|
||||
|
||||
```shell
|
||||
npm config set @scope:registry=https://your_domain_name/api/v4/projects/your_project_id/packages/npm/
|
||||
```
|
||||
#### Set the registry (project level)
|
||||
|
||||
- Replace `@scope` with the [root level group](#naming-convention) of the project you're installing to the package from.
|
||||
- Replace `your_domain_name` with your domain name, for example, `gitlab.com`.
|
||||
- Replace `your_project_id` is your project ID, found on the project's home page.
|
||||
Skip this step if your package is public not private.
|
||||
|
||||
1. Install the package
|
||||
```yaml
|
||||
npmRegistries:
|
||||
//<your_domain_name>/api/v4/projects/<your_project_id>/packages/npm:
|
||||
npmAlwaysAuth: true
|
||||
npmAuthToken: "<your_token>"
|
||||
```
|
||||
|
||||
```shell
|
||||
yarn add @scope/my-package
|
||||
```
|
||||
- Replace `<your_domain_name>` with your domain name, for example, `gitlab.com`.
|
||||
- Replace `<your_token>` with a deployment token (recommended), group access token, project access token, or personal access token.
|
||||
- Replace `<your_project_id>` with your project ID, found on the project's home page.
|
||||
|
||||
## Helpful hints
|
||||
### Install the package
|
||||
|
||||
For full helpful hints information, refer to the [npm documentation](../npm_registry/index.md#helpful-hints).
|
||||
For Yarn 2+, use `yarn add` either in the command line or in the CI/CD pipelines to install your packages:
|
||||
|
||||
### Supported CLI commands
|
||||
```shell
|
||||
yarn add @scope/my-package
|
||||
```
|
||||
|
||||
The GitLab npm repository supports the following commands for the npm CLI (`npm`) and yarn CLI
|
||||
(`yarn`):
|
||||
#### For Yarn Classic
|
||||
|
||||
- `npm install`: Install npm packages.
|
||||
- `npm publish`: Publish an npm package to the registry.
|
||||
- `npm dist-tag add`: Add a dist-tag to an npm package.
|
||||
- `npm dist-tag ls`: List dist-tags for a package.
|
||||
- `npm dist-tag rm`: Delete a dist-tag.
|
||||
- `npm ci`: Install npm packages directly from your `package-lock.json` file.
|
||||
- `npm view`: Show package metadata.
|
||||
- `yarn add`: Install an npm package.
|
||||
- `yarn update`: Update your dependencies.
|
||||
The Yarn Classic setup, requires both `.npmrc` and `.yarnrc` files as
|
||||
[mentioned in issue](https://github.com/yarnpkg/yarn/issues/4451#issuecomment-753670295):
|
||||
|
||||
- Place credentials in the `.npmrc` file.
|
||||
- Place the scoped registry in the `.yarnrc` file.
|
||||
|
||||
```shell
|
||||
# .npmrc
|
||||
//<your_domain_name>/api/v4/projects/<your_project_id>/packages/npm/:_authToken="<your_token>"
|
||||
|
||||
# .yarnrc
|
||||
"@scope:registry" "https://<your_domain_name>/api/v4/projects/<your_project_id>/packages/npm/"
|
||||
```
|
||||
|
||||
Then you can use `yarn add` to install your packages.
|
||||
|
||||
## Related topics
|
||||
|
||||
- For full helpful hints information, see the
|
||||
[npm documentation](../npm_registry/index.md#helpful-hints).
|
||||
- For Yarn 1 to Yarn 2+ migration information see the
|
||||
[Yarn Migration Guide](https://yarnpkg.com/getting-started/migration).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
For full troubleshooting information, refer to the [npm documentation](../npm_registry/index.md#troubleshooting).
|
||||
|
||||
### Error running Yarn with the Package Registry for the npm registry
|
||||
|
||||
If you are using [Yarn](https://classic.yarnpkg.com/en/) with the npm registry, you may get
|
||||
an error message like:
|
||||
If you are using [Yarn](https://classic.yarnpkg.com/en/) with the npm registry, you may get an error message like:
|
||||
|
||||
```shell
|
||||
yarn install v1.15.2
|
||||
|
|
@ -233,14 +325,7 @@ info If you think this is a bug, please open a bug report with the information p
|
|||
info Visit https://classic.yarnpkg.com/en/docs/cli/install for documentation about this command
|
||||
```
|
||||
|
||||
In this case, try adding this to your `.npmrc` file (and replace `<your_token>`
|
||||
with your personal access token or deploy token):
|
||||
|
||||
```plaintext
|
||||
//gitlab.example.com/api/v4/projects/:_authToken=<your_token>
|
||||
```
|
||||
|
||||
You can also use `yarn config` instead of `npm config` when setting your auth-token dynamically:
|
||||
In this case, the following commands creates a file called `.yarnrc` in the current directory. Make sure to be in either your user home directory for global configuration or your project root for per-project configuration:
|
||||
|
||||
```shell
|
||||
yarn config set '//gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken' "<your_token>"
|
||||
|
|
|
|||
|
|
@ -39,9 +39,9 @@ These shortcuts are available in most areas of GitLab:
|
|||
| <kbd>Shift</kbd> + <kbd>m</kbd> | Go to your [Merge requests](project/merge_requests/index.md) page. |
|
||||
| <kbd>Shift</kbd> + <kbd>r</kbd> | Go to your Review requests page. |
|
||||
| <kbd>Shift</kbd> + <kbd>t</kbd> | Go to your To-Do List page. |
|
||||
| <kbd>p</kbd>, then <kbd>b</kbd> | Show or hide the Performance Bar. |
|
||||
| <kbd>p</kbd>, then <kbd>b</kbd> | Show or hide the Performance Bar. |
|
||||
| <kbd>Escape</kbd> | Hide tooltips or popovers. |
|
||||
| <kbd>g</kbd>, then <kbd>x</kbd> | Toggle between [GitLab](https://gitlab.com/) and [GitLab Next](https://next.gitlab.com/) (GitLab SaaS only). |
|
||||
| <kbd>g</kbd>, then <kbd>x</kbd> | Toggle between [GitLab](https://gitlab.com/) and [GitLab Next](https://next.gitlab.com/) (GitLab SaaS only). |
|
||||
| <kbd>.</kbd> | Open the [Web IDE](project/web_ide/index.md). |
|
||||
|
||||
Additionally, the following shortcuts are available when editing text in text
|
||||
|
|
@ -55,9 +55,10 @@ descriptions):
|
|||
| <kbd>Command</kbd> + <kbd>b</kbd> | <kbd>Control</kbd> + <kbd>b</kbd> | Bold the selected text (surround it with `**`). |
|
||||
| <kbd>Command</kbd> + <kbd>i</kbd> | <kbd>Control</kbd> + <kbd>i</kbd> | Italicize the selected text (surround it with `_`). |
|
||||
| <kbd>Command</kbd> + <kbd>Shift</kbd> + <kbd>x</kbd> | <kbd>Control</kbd> + <kbd>Shift</kbd> + <kbd>x</kbd> | Strike through the selected text (surround it with `~~`). |
|
||||
| <kbd>Command</kbd> + <kbd>k</kbd> | <kbd>Control</kbd> + <kbd>k</kbd> | Add a link (surround the selected text with `[]()`). |
|
||||
| <kbd>Command</kbd> + <kbd>]</kbd> | <kbd>Control</kbd> + <kbd>]</kbd> | Indent list item. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/351924) in GitLab 15.3. |
|
||||
| <kbd>Command</kbd> + <kbd>[</kbd> | <kbd>Control</kbd> + <kbd>[</kbd> | Outdent list item. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/351924) in GitLab 15.3. |
|
||||
| <kbd>Command</kbd> + <kbd>k</kbd> | <kbd>Control</kbd> + <kbd>k</kbd> | Add a link (surround the selected text with `[]()`). |
|
||||
| <kbd>Command</kbd> + <kbd>]</kbd> | <kbd>Control</kbd> + <kbd>]</kbd> | Indent list item. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/351924) in GitLab 15.3. |
|
||||
| <kbd>Command</kbd> + <kbd>[</kbd> | <kbd>Control</kbd> + <kbd>[</kbd> | Outdent list item. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/351924) in GitLab 15.3. |
|
||||
| <kbd>Shift</kbd> + <kbd>Enter</kbd> | <kbd>Shift</kbd> + <kbd>Enter</kbd> | Add a [line break](markdown.md#newlines). [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/21916) in GitLab 15.10. |
|
||||
|
||||
The shortcuts for editing in text fields are always enabled, even if other
|
||||
keyboard shortcuts are disabled.
|
||||
|
|
@ -112,7 +113,7 @@ These shortcuts are available when viewing [merge requests](project/merge_reques
|
|||
| macOS shortcut | Windows shortcut | Description |
|
||||
|-----------------------------------|---------------------|-------------|
|
||||
| <kbd>]</kbd> or <kbd>j</kbd> | | Move to next file. |
|
||||
| <kbd>[</kbd> or <kbd>k</kbd> | | Move to previous file. |
|
||||
| <kbd>[</kbd> or <kbd>k</kbd> | | Move to previous file. |
|
||||
| <kbd>Command</kbd> + <kbd>p</kbd> | <kbd>Control</kbd> + <kbd>p</kbd> | Search for, and then jump to a file for review. |
|
||||
| <kbd>n</kbd> | | Move to next unresolved discussion. |
|
||||
| <kbd>p</kbd> | | Move to previous unresolved discussion. |
|
||||
|
|
@ -277,7 +278,7 @@ These shortcuts are available when editing a file with the
|
|||
| <kbd>Command</kbd> + <kbd>Shift</kbd> + <kbd>h</kbd> | <kbd>Control</kbd> + <kbd>Shift</kbd> + <kbd>h</kbd> | Highlight |
|
||||
| <kbd>Command</kbd> + <kbd>,</kbd> | <kbd>Control</kbd> + <kbd>,</kbd> | Subscript |
|
||||
| <kbd>Command</kbd> + <kbd>.</kbd> | <kbd>Control</kbd> + <kbd>.</kbd> | Superscript |
|
||||
| <kbd>Tab</kbd> | <kbd>Tab</kbd> | Indent list |
|
||||
| <kbd>Tab</kbd> | <kbd>Tab</kbd> | Indent list |
|
||||
| <kbd>Shift</kbd> + <kbd>Tab</kbd> | <kbd>Shift</kbd> + <kbd>Tab</kbd> | Outdent list |
|
||||
|
||||
#### Text selection
|
||||
|
|
|
|||
|
|
@ -45,6 +45,11 @@ module Gitlab
|
|||
return gitlab_schema
|
||||
end
|
||||
|
||||
# Partitions that belong to the CI domain
|
||||
if table_name.start_with?('ci_') && gitlab_schema = views_and_tables_to_schema["p_#{table_name}"]
|
||||
return gitlab_schema
|
||||
end
|
||||
|
||||
# All tables from `information_schema.` are marked as `internal`
|
||||
return :gitlab_internal if schema_name == 'information_schema'
|
||||
|
||||
|
|
|
|||
|
|
@ -8869,9 +8869,6 @@ msgstr ""
|
|||
msgid "CiVariables|Cannot use Masked Variable with current value"
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariables|Clear inputs"
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariables|Environments"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -8899,6 +8896,9 @@ msgstr ""
|
|||
msgid "CiVariables|Protected"
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariables|Remove inputs"
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariables|Remove variable"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -26,11 +26,14 @@ RSpec.describe SortingPreference do
|
|||
|
||||
describe '#set_sort_order' do
|
||||
let(:group) { build(:group) }
|
||||
let(:controller_name) { 'issues' }
|
||||
let(:action_name) { 'issues' }
|
||||
let(:issue_weights_available) { true }
|
||||
|
||||
before do
|
||||
allow(controller).to receive(:default_sort_order).and_return('updated_desc')
|
||||
allow(controller).to receive(:action_name).and_return('issues')
|
||||
allow(controller).to receive(:controller_name).and_return(controller_name)
|
||||
allow(controller).to receive(:action_name).and_return(action_name)
|
||||
allow(controller).to receive(:can_sort_by_issue_weight?).and_return(issue_weights_available)
|
||||
user.user_preference.update!(issues_sort: sorting_field)
|
||||
end
|
||||
|
|
@ -62,6 +65,42 @@ RSpec.describe SortingPreference do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user preference contains merged date sorting' do
|
||||
let(:sorting_field) { 'merged_at_desc' }
|
||||
let(:can_sort_by_merged_date?) { false }
|
||||
|
||||
before do
|
||||
allow(controller)
|
||||
.to receive(:can_sort_by_merged_date?)
|
||||
.with(can_sort_by_merged_date?)
|
||||
.and_return(can_sort_by_merged_date?)
|
||||
end
|
||||
|
||||
it 'sets default sort order' do
|
||||
is_expected.to eq('updated_desc')
|
||||
end
|
||||
|
||||
shared_examples 'user can sort by merged date' do
|
||||
it 'sets sort order from user_preference' do
|
||||
is_expected.to eq('merged_at_desc')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when controller_name is merge_requests' do
|
||||
let(:controller_name) { 'merge_requests' }
|
||||
let(:can_sort_by_merged_date?) { true }
|
||||
|
||||
it_behaves_like 'user can sort by merged date'
|
||||
end
|
||||
|
||||
context 'when action_name is merge_requests' do
|
||||
let(:action_name) { 'merge_requests' }
|
||||
let(:can_sort_by_merged_date?) { true }
|
||||
|
||||
it_behaves_like 'user can sort by merged date'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#set_sort_order_from_user_preference' do
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: :
|
|||
project.add_developer(user)
|
||||
end
|
||||
|
||||
specify { expect(get(:index, params: request_params)).to have_request_urgency(:medium) }
|
||||
|
||||
it 'passes last_fetched_at from headers to NotesFinder and MergeIntoNotesService' do
|
||||
last_fetched_at = Time.zone.at(3.hours.ago.to_i) # remove nanoseconds
|
||||
|
||||
|
|
@ -244,6 +246,8 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: :
|
|||
sign_in(user)
|
||||
end
|
||||
|
||||
specify { expect(create!).to have_request_urgency(:low) }
|
||||
|
||||
describe 'making the creation request' do
|
||||
before do
|
||||
create!
|
||||
|
|
@ -732,19 +736,21 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: :
|
|||
end
|
||||
|
||||
describe 'PUT update' do
|
||||
context "should update the note with a valid issue" do
|
||||
let(:request_params) do
|
||||
{
|
||||
namespace_id: project.namespace,
|
||||
project_id: project,
|
||||
id: note,
|
||||
format: :json,
|
||||
note: {
|
||||
note: "New comment"
|
||||
}
|
||||
let(:request_params) do
|
||||
{
|
||||
namespace_id: project.namespace,
|
||||
project_id: project,
|
||||
id: note,
|
||||
format: :json,
|
||||
note: {
|
||||
note: "New comment"
|
||||
}
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
specify { expect(put(:update, params: request_params)).to have_request_urgency(:low) }
|
||||
|
||||
context "should update the note with a valid issue" do
|
||||
before do
|
||||
sign_in(note.author)
|
||||
project.add_developer(note.author)
|
||||
|
|
@ -790,6 +796,8 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: :
|
|||
}
|
||||
end
|
||||
|
||||
specify { expect(delete(:destroy, params: request_params)).to have_request_urgency(:low) }
|
||||
|
||||
context 'user is the author of a note' do
|
||||
before do
|
||||
sign_in(note.author)
|
||||
|
|
@ -831,6 +839,8 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: :
|
|||
|
||||
let(:emoji_name) { 'thumbsup' }
|
||||
|
||||
it { is_expected.to have_request_urgency(:low) }
|
||||
|
||||
it "toggles the award emoji" do
|
||||
expect do
|
||||
subject
|
||||
|
|
@ -866,6 +876,8 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: :
|
|||
sign_in user
|
||||
end
|
||||
|
||||
specify { expect(post(:resolve, params: request_params)).to have_request_urgency(:low) }
|
||||
|
||||
context "when the user is not authorized to resolve the note" do
|
||||
it "returns status 404" do
|
||||
post :resolve, params: request_params
|
||||
|
|
@ -929,6 +941,8 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: :
|
|||
note.resolve!(user)
|
||||
end
|
||||
|
||||
specify { expect(delete(:unresolve, params: request_params)).to have_request_urgency(:low) }
|
||||
|
||||
context "when the user is not authorized to resolve the note" do
|
||||
it "returns status 404" do
|
||||
delete :unresolve, params: request_params
|
||||
|
|
@ -998,6 +1012,8 @@ RSpec.describe Projects::NotesController, type: :controller, feature_category: :
|
|||
expect(json_response.count).to eq(1)
|
||||
expect(json_response.first).to include({ "line_text" => "Test" })
|
||||
end
|
||||
|
||||
specify { expect(get(:outdated_line_change, params: request_params)).to have_request_urgency(:low) }
|
||||
end
|
||||
|
||||
# Convert a time to an integer number of microseconds
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ RSpec.describe 'Commit', feature_category: :source_code_management do
|
|||
let_it_be(:project) { create(:project, :repository) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
describe "single commit view" do
|
||||
shared_examples "single commit view" do
|
||||
let(:commit) do
|
||||
project.repository.commits(nil, limit: 100).find do |commit|
|
||||
commit.diffs.size > 1
|
||||
|
|
@ -69,4 +69,15 @@ RSpec.describe 'Commit', feature_category: :source_code_management do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like "single commit view"
|
||||
|
||||
context "when super sidebar is enabled" do
|
||||
before do
|
||||
user.update!(use_new_navigation: true)
|
||||
stub_feature_flags(super_sidebar_nav: true)
|
||||
end
|
||||
|
||||
it_behaves_like "single commit view"
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ RSpec.describe 'Merge requests > User lists merge requests', feature_category: :
|
|||
milestone: create(:milestone, project: project, due_date: '2013-12-11'),
|
||||
created_at: 1.minute.ago,
|
||||
updated_at: 1.minute.ago)
|
||||
@fix.metrics.update!(merged_at: 10.seconds.ago, latest_closed_at: 10.seconds.ago)
|
||||
@fix.metrics.update!(merged_at: 10.seconds.ago, latest_closed_at: 20.seconds.ago)
|
||||
|
||||
@markdown = create(:merge_request,
|
||||
title: 'markdown',
|
||||
|
|
@ -33,7 +33,8 @@ RSpec.describe 'Merge requests > User lists merge requests', feature_category: :
|
|||
reviewers: [user, user2, user3, user4],
|
||||
milestone: create(:milestone, project: project, due_date: '2013-12-12'),
|
||||
created_at: 2.minutes.ago,
|
||||
updated_at: 2.minutes.ago)
|
||||
updated_at: 2.minutes.ago,
|
||||
state: 'merged')
|
||||
@markdown.metrics.update!(merged_at: 10.minutes.ago, latest_closed_at: 10.seconds.ago)
|
||||
|
||||
@merge_test = create(:merge_request,
|
||||
|
|
@ -49,7 +50,8 @@ RSpec.describe 'Merge requests > User lists merge requests', feature_category: :
|
|||
source_project: project,
|
||||
source_branch: 'feautre',
|
||||
created_at: 2.minutes.ago,
|
||||
updated_at: 1.minute.ago)
|
||||
updated_at: 1.minute.ago,
|
||||
state: 'merged')
|
||||
@feature.metrics.update!(merged_at: 10.seconds.ago, latest_closed_at: 10.minutes.ago)
|
||||
end
|
||||
|
||||
|
|
@ -79,10 +81,9 @@ RSpec.describe 'Merge requests > User lists merge requests', feature_category: :
|
|||
|
||||
expect(page).to have_current_path(project_merge_requests_path(project), ignore_query: true)
|
||||
expect(page).to have_content 'merge-test'
|
||||
expect(page).to have_content 'feature'
|
||||
expect(page).not_to have_content 'fix'
|
||||
expect(page).not_to have_content 'markdown'
|
||||
expect(count_merge_requests).to eq(2)
|
||||
expect(count_merge_requests).to eq(1)
|
||||
end
|
||||
|
||||
it 'filters on a specific assignee' do
|
||||
|
|
@ -90,8 +91,7 @@ RSpec.describe 'Merge requests > User lists merge requests', feature_category: :
|
|||
|
||||
expect(page).not_to have_content 'merge-test'
|
||||
expect(page).to have_content 'fix'
|
||||
expect(page).to have_content 'markdown'
|
||||
expect(count_merge_requests).to eq(2)
|
||||
expect(count_merge_requests).to eq(1)
|
||||
end
|
||||
|
||||
it 'sorts by newest' do
|
||||
|
|
@ -99,35 +99,35 @@ RSpec.describe 'Merge requests > User lists merge requests', feature_category: :
|
|||
|
||||
expect(first_merge_request).to include('fix')
|
||||
expect(last_merge_request).to include('merge-test')
|
||||
expect(count_merge_requests).to eq(4)
|
||||
expect(count_merge_requests).to eq(2)
|
||||
end
|
||||
|
||||
it 'sorts by last updated' do
|
||||
visit_merge_requests(project, sort: sort_value_recently_updated)
|
||||
|
||||
expect(first_merge_request).to include('merge-test')
|
||||
expect(count_merge_requests).to eq(4)
|
||||
expect(count_merge_requests).to eq(2)
|
||||
end
|
||||
|
||||
it 'sorts by milestone due date' do
|
||||
visit_merge_requests(project, sort: sort_value_milestone)
|
||||
|
||||
expect(first_merge_request).to include('fix')
|
||||
expect(count_merge_requests).to eq(4)
|
||||
expect(count_merge_requests).to eq(2)
|
||||
end
|
||||
|
||||
it 'sorts by merged at' do
|
||||
it 'ignores sorting by merged at' do
|
||||
visit_merge_requests(project, sort: sort_value_merged_date)
|
||||
|
||||
expect(first_merge_request).to include('markdown')
|
||||
expect(count_merge_requests).to eq(4)
|
||||
expect(first_merge_request).to include('fix')
|
||||
expect(count_merge_requests).to eq(2)
|
||||
end
|
||||
|
||||
it 'sorts by closed at' do
|
||||
visit_merge_requests(project, sort: sort_value_closed_date)
|
||||
|
||||
expect(first_merge_request).to include('feature')
|
||||
expect(count_merge_requests).to eq(4)
|
||||
expect(first_merge_request).to include('fix')
|
||||
expect(count_merge_requests).to eq(2)
|
||||
end
|
||||
|
||||
it 'filters on one label and sorts by milestone due date' do
|
||||
|
|
@ -141,6 +141,15 @@ RSpec.describe 'Merge requests > User lists merge requests', feature_category: :
|
|||
expect(count_merge_requests).to eq(1)
|
||||
end
|
||||
|
||||
context 'when viewing merged merge requests' do
|
||||
it 'sorts by merged at' do
|
||||
visit_merge_requests(project, state: 'merged', sort: sort_value_merged_date)
|
||||
|
||||
expect(first_merge_request).to include('markdown')
|
||||
expect(count_merge_requests).to eq(2)
|
||||
end
|
||||
end
|
||||
|
||||
context 'while filtering on two labels' do
|
||||
let(:label) { create(:label, project: project) }
|
||||
let(:label2) { create(:label, project: project) }
|
||||
|
|
|
|||
|
|
@ -1,160 +1,61 @@
|
|||
import { GlIcon } from '@gitlab/ui';
|
||||
import { GlIcon, GlTooltip } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
|
||||
import { mockMilestone } from 'jest/boards/mock_data';
|
||||
import IssueMilestone from '~/issuable/components/issue_milestone.vue';
|
||||
|
||||
const createComponent = (milestone = mockMilestone) => {
|
||||
const Component = Vue.extend(IssueMilestone);
|
||||
|
||||
return shallowMount(Component, {
|
||||
propsData: {
|
||||
milestone,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
describe('IssueMilestoneComponent', () => {
|
||||
describe('IssueMilestone component', () => {
|
||||
let wrapper;
|
||||
let vm;
|
||||
|
||||
beforeEach(async () => {
|
||||
const findTooltip = () => wrapper.findComponent(GlTooltip);
|
||||
|
||||
const createComponent = (milestone = mockMilestone) =>
|
||||
shallowMount(IssueMilestone, { propsData: { milestone } });
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent();
|
||||
|
||||
({ vm } = wrapper);
|
||||
|
||||
await nextTick();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
it('renders milestone icon', () => {
|
||||
expect(wrapper.findComponent(GlIcon).props('name')).toBe('clock');
|
||||
});
|
||||
|
||||
describe('computed', () => {
|
||||
describe('isMilestoneStarted', () => {
|
||||
it('should return `false` when milestoneStart prop is not defined', async () => {
|
||||
wrapper.setProps({
|
||||
milestone: { ...mockMilestone, start_date: '' },
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.vm.isMilestoneStarted).toBe(false);
|
||||
});
|
||||
|
||||
it('should return `true` when milestone start date is past current date', async () => {
|
||||
await wrapper.setProps({
|
||||
milestone: { ...mockMilestone, start_date: '1990-07-22' },
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.vm.isMilestoneStarted).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isMilestonePastDue', () => {
|
||||
it('should return `false` when milestoneDue prop is not defined', async () => {
|
||||
wrapper.setProps({
|
||||
milestone: { ...mockMilestone, due_date: '' },
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.vm.isMilestonePastDue).toBe(false);
|
||||
});
|
||||
|
||||
it('should return `true` when milestone due is past current date', () => {
|
||||
wrapper.setProps({
|
||||
milestone: { ...mockMilestone, due_date: '1990-07-22' },
|
||||
});
|
||||
|
||||
expect(wrapper.vm.isMilestonePastDue).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('milestoneDatesAbsolute', () => {
|
||||
it('returns string containing absolute milestone due date', () => {
|
||||
expect(vm.milestoneDatesAbsolute).toBe('(December 31, 2019)');
|
||||
});
|
||||
|
||||
it('returns string containing absolute milestone start date when due date is not present', async () => {
|
||||
wrapper.setProps({
|
||||
milestone: { ...mockMilestone, due_date: '' },
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.vm.milestoneDatesAbsolute).toBe('(January 1, 2018)');
|
||||
});
|
||||
|
||||
it('returns empty string when both milestone start and due dates are not present', async () => {
|
||||
wrapper.setProps({
|
||||
milestone: { ...mockMilestone, start_date: '', due_date: '' },
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.vm.milestoneDatesAbsolute).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('milestoneDatesHuman', () => {
|
||||
it('returns string containing milestone due date when date is yet to be due', async () => {
|
||||
wrapper.setProps({
|
||||
milestone: { ...mockMilestone, due_date: `${new Date().getFullYear() + 10}-01-01` },
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.vm.milestoneDatesHuman).toContain('years remaining');
|
||||
});
|
||||
|
||||
it('returns string containing milestone start date when date has already started and due date is not present', async () => {
|
||||
wrapper.setProps({
|
||||
milestone: { ...mockMilestone, start_date: '1990-07-22', due_date: '' },
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.vm.milestoneDatesHuman).toContain('Started');
|
||||
});
|
||||
|
||||
it('returns string containing milestone start date when date is yet to start and due date is not present', async () => {
|
||||
wrapper.setProps({
|
||||
milestone: {
|
||||
...mockMilestone,
|
||||
start_date: `${new Date().getFullYear() + 10}-01-01`,
|
||||
due_date: '',
|
||||
},
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.vm.milestoneDatesHuman).toContain('Starts');
|
||||
});
|
||||
|
||||
it('returns empty string when milestone start and due dates are not present', async () => {
|
||||
wrapper.setProps({
|
||||
milestone: { ...mockMilestone, start_date: '', due_date: '' },
|
||||
});
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.vm.milestoneDatesHuman).toBe('');
|
||||
});
|
||||
});
|
||||
it('renders milestone title', () => {
|
||||
expect(wrapper.find('.milestone-title').text()).toBe(mockMilestone.title);
|
||||
});
|
||||
|
||||
describe('template', () => {
|
||||
it('renders component root element with class `issue-milestone-details`', () => {
|
||||
expect(vm.$el.classList.contains('issue-milestone-details')).toBe(true);
|
||||
});
|
||||
|
||||
it('renders milestone icon', () => {
|
||||
expect(wrapper.findComponent(GlIcon).props('name')).toBe('clock');
|
||||
describe('tooltip', () => {
|
||||
it('renders `Milestone`', () => {
|
||||
expect(findTooltip().text()).toContain('Milestone');
|
||||
});
|
||||
|
||||
it('renders milestone title', () => {
|
||||
expect(vm.$el.querySelector('.milestone-title').innerText.trim()).toBe(mockMilestone.title);
|
||||
expect(findTooltip().text()).toContain(mockMilestone.title);
|
||||
});
|
||||
|
||||
it('renders milestone tooltip', () => {
|
||||
expect(vm.$el.querySelector('.js-item-milestone').innerText.trim()).toContain(
|
||||
mockMilestone.title,
|
||||
);
|
||||
describe('humanized dates', () => {
|
||||
it('renders `Expired` when there is a due date in the past', () => {
|
||||
wrapper = createComponent({ ...mockMilestone, due_date: '2019-12-31', start_date: '' });
|
||||
|
||||
expect(findTooltip().text()).toContain('Expired 6 months ago(December 31, 2019)');
|
||||
});
|
||||
|
||||
it('renders `remaining` when there is a due date in the future', () => {
|
||||
wrapper = createComponent({ ...mockMilestone, due_date: '2020-12-31', start_date: '' });
|
||||
|
||||
expect(findTooltip().text()).toContain('5 months remaining(December 31, 2020)');
|
||||
});
|
||||
|
||||
it('renders `Started` when there is a start date in the past', () => {
|
||||
wrapper = createComponent({ ...mockMilestone, due_date: '', start_date: '2019-12-31' });
|
||||
|
||||
expect(findTooltip().text()).toContain('Started 6 months ago(December 31, 2019)');
|
||||
});
|
||||
|
||||
it('renders `Starts` when there is a start date in the future', () => {
|
||||
wrapper = createComponent({ ...mockMilestone, due_date: '', start_date: '2020-12-31' });
|
||||
|
||||
expect(findTooltip().text()).toContain('Starts in 5 months(December 31, 2020)');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
} from '~/lib/utils/text_markdown';
|
||||
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
|
||||
import '~/lib/utils/jquery_at_who';
|
||||
import { ENTER_KEY } from '~/lib/utils/keys';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
|
||||
|
||||
|
|
@ -208,7 +209,7 @@ describe('init markdown', () => {
|
|||
let enterEvent;
|
||||
|
||||
beforeEach(() => {
|
||||
enterEvent = new KeyboardEvent('keydown', { key: 'Enter', cancelable: true });
|
||||
enterEvent = new KeyboardEvent('keydown', { key: ENTER_KEY, cancelable: true });
|
||||
textArea.addEventListener('keydown', keypressNoteText);
|
||||
textArea.addEventListener('compositionstart', compositionStartNoteText);
|
||||
textArea.addEventListener('compositionend', compositionEndNoteText);
|
||||
|
|
@ -492,6 +493,53 @@ describe('init markdown', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('adding a hard break using Shift+Enter', () => {
|
||||
let enterEvent;
|
||||
|
||||
beforeEach(() => {
|
||||
enterEvent = new KeyboardEvent('keydown', { key: ENTER_KEY, shiftKey: true });
|
||||
textArea.addEventListener('keydown', keypressNoteText);
|
||||
textArea.addEventListener('compositionstart', compositionStartNoteText);
|
||||
textArea.addEventListener('compositionend', compositionEndNoteText);
|
||||
});
|
||||
|
||||
it.each`
|
||||
selectionStart | selectionEnd | expected | expectedSelectionStart
|
||||
${0} | ${0} | ${'\\\n0123456789'} | ${2}
|
||||
${3} | ${3} | ${'012\\\n3456789'} | ${5}
|
||||
${3} | ${6} | ${'012\\\n6789'} | ${5}
|
||||
`(
|
||||
'adds a hard break',
|
||||
({ selectionStart, selectionEnd, expected, expectedSelectionStart }) => {
|
||||
const text = '0123456789';
|
||||
textArea.value = text;
|
||||
textArea.setSelectionRange(selectionStart, selectionEnd);
|
||||
|
||||
textArea.dispatchEvent(enterEvent);
|
||||
|
||||
expect(textArea.value).toEqual(expected);
|
||||
expect(textArea.selectionStart).toEqual(expectedSelectionStart);
|
||||
expect(textArea.selectionEnd).toEqual(expectedSelectionStart);
|
||||
},
|
||||
);
|
||||
|
||||
it.each`
|
||||
keyEvent
|
||||
${new KeyboardEvent('keydown', { key: ENTER_KEY, shiftKey: false })}
|
||||
${new KeyboardEvent('keydown', { key: ENTER_KEY, shiftKey: true, metaKey: true })}
|
||||
${new KeyboardEvent('keydown', { key: ENTER_KEY, shiftKey: true, altKey: true })}
|
||||
${new KeyboardEvent('keydown', { key: ENTER_KEY, shiftKey: true, ctrlKey: true })}
|
||||
`('does not add when shift is pressed with other keys', ({ keyEvent }) => {
|
||||
const text = '0123456789';
|
||||
textArea.value = text;
|
||||
textArea.setSelectionRange(0, 0);
|
||||
|
||||
textArea.dispatchEvent(keyEvent);
|
||||
|
||||
expect(textArea.value).toEqual(text);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with selection', () => {
|
||||
let text = 'initial selected value';
|
||||
let selected = 'selected';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { GlModal } from '@gitlab/ui';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { nextTick } from 'vue';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { createAlert } from '~/flash';
|
||||
|
|
@ -43,6 +44,7 @@ describe('UpdateUsername component', () => {
|
|||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
axiosMock.restore();
|
||||
Vue.config.errorHandler = null;
|
||||
});
|
||||
|
||||
const findElements = () => {
|
||||
|
|
@ -58,6 +60,13 @@ describe('UpdateUsername component', () => {
|
|||
};
|
||||
};
|
||||
|
||||
const clickModalWithErrorResponse = () => {
|
||||
Vue.config.errorHandler = jest.fn(); // silence thrown error
|
||||
const { modal } = findElements();
|
||||
modal.vm.$emit('primary');
|
||||
return waitForPromises();
|
||||
};
|
||||
|
||||
it('has a disabled button if the username was not changed', async () => {
|
||||
const { openModalBtn } = findElements();
|
||||
|
||||
|
|
@ -98,14 +107,15 @@ describe('UpdateUsername component', () => {
|
|||
axiosMock.onPut(actionUrl).replyOnce(() => [HTTP_STATUS_OK, { message: 'Username changed' }]);
|
||||
jest.spyOn(axios, 'put');
|
||||
|
||||
await wrapper.vm.onConfirm();
|
||||
await nextTick();
|
||||
const { modal } = findElements();
|
||||
modal.vm.$emit('primary');
|
||||
await waitForPromises();
|
||||
|
||||
expect(axios.put).toHaveBeenCalledWith(actionUrl, { user: { username: newUsername } });
|
||||
});
|
||||
|
||||
it('sets the username after a successful update', async () => {
|
||||
const { input, openModalBtn } = findElements();
|
||||
const { input, openModalBtn, modal } = findElements();
|
||||
|
||||
axiosMock.onPut(actionUrl).replyOnce(() => {
|
||||
expect(input.attributes('disabled')).toBe('disabled');
|
||||
|
|
@ -115,8 +125,8 @@ describe('UpdateUsername component', () => {
|
|||
return [HTTP_STATUS_OK, { message: 'Username changed' }];
|
||||
});
|
||||
|
||||
await wrapper.vm.onConfirm();
|
||||
await nextTick();
|
||||
modal.vm.$emit('primary');
|
||||
await waitForPromises();
|
||||
|
||||
expect(input.attributes('disabled')).toBe(undefined);
|
||||
expect(openModalBtn.props('disabled')).toBe(true);
|
||||
|
|
@ -134,7 +144,8 @@ describe('UpdateUsername component', () => {
|
|||
return [HTTP_STATUS_BAD_REQUEST, { message: 'Invalid username' }];
|
||||
});
|
||||
|
||||
await expect(wrapper.vm.onConfirm()).rejects.toThrow();
|
||||
await clickModalWithErrorResponse();
|
||||
|
||||
expect(input.attributes('disabled')).toBe(undefined);
|
||||
expect(openModalBtn.props('disabled')).toBe(false);
|
||||
expect(openModalBtn.props('loading')).toBe(false);
|
||||
|
|
@ -145,7 +156,7 @@ describe('UpdateUsername component', () => {
|
|||
return [HTTP_STATUS_BAD_REQUEST, { message: 'Invalid username' }];
|
||||
});
|
||||
|
||||
await expect(wrapper.vm.onConfirm()).rejects.toThrow();
|
||||
await clickModalWithErrorResponse();
|
||||
|
||||
expect(createAlert).toHaveBeenCalledWith({
|
||||
message: 'Invalid username',
|
||||
|
|
@ -157,7 +168,7 @@ describe('UpdateUsername component', () => {
|
|||
return [HTTP_STATUS_BAD_REQUEST];
|
||||
});
|
||||
|
||||
await expect(wrapper.vm.onConfirm()).rejects.toThrow();
|
||||
await clickModalWithErrorResponse();
|
||||
|
||||
expect(createAlert).toHaveBeenCalledWith({
|
||||
message: 'An error occurred while updating your username, please try again.',
|
||||
|
|
|
|||
|
|
@ -71,8 +71,10 @@ describe('Branch rule', () => {
|
|||
});
|
||||
|
||||
it('renders a detail button with the correct href', () => {
|
||||
const encodedBranchName = encodeURIComponent(branchRulePropsMock.name);
|
||||
|
||||
expect(findDetailsButton().attributes('href')).toBe(
|
||||
`${branchRuleProvideMock.branchRulesPath}?branch=${branchRulePropsMock.name}`,
|
||||
`${branchRuleProvideMock.branchRulesPath}?branch=${encodedBranchName}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ export const branchRuleProvideMock = {
|
|||
};
|
||||
|
||||
export const branchRulePropsMock = {
|
||||
name: 'main',
|
||||
name: 'branch-with-$speci@l-#-chars',
|
||||
isDefault: true,
|
||||
matchingBranchesCount: 1,
|
||||
branchProtection: {
|
||||
|
|
|
|||
|
|
@ -1,203 +1,114 @@
|
|||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import Participants from '~/sidebar/components/participants/participants.vue';
|
||||
|
||||
const PARTICIPANT = {
|
||||
id: 1,
|
||||
state: 'active',
|
||||
username: 'marcene',
|
||||
name: 'Allie Will',
|
||||
web_url: 'foo.com',
|
||||
avatar_url: 'gravatar.com/avatar/xxx',
|
||||
};
|
||||
|
||||
const PARTICIPANT_LIST = [PARTICIPANT, { ...PARTICIPANT, id: 2 }, { ...PARTICIPANT, id: 3 }];
|
||||
|
||||
describe('Participants', () => {
|
||||
describe('Participants component', () => {
|
||||
let wrapper;
|
||||
|
||||
const getMoreParticipantsButton = () => wrapper.find('[data-testid="more-participants"]');
|
||||
const getCollapsedParticipantsCount = () => wrapper.find('[data-testid="collapsed-count"]');
|
||||
const participant = {
|
||||
id: 1,
|
||||
state: 'active',
|
||||
username: 'marcene',
|
||||
name: 'Allie Will',
|
||||
web_url: 'foo.com',
|
||||
avatar_url: 'gravatar.com/avatar/xxx',
|
||||
};
|
||||
|
||||
const mountComponent = (propsData) =>
|
||||
shallowMount(Participants, {
|
||||
propsData,
|
||||
});
|
||||
const participants = [participant, { ...participant, id: 2 }, { ...participant, id: 3 }];
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
|
||||
const findMoreParticipantsButton = () => wrapper.findComponent(GlButton);
|
||||
const findCollapsedIcon = () => wrapper.find('.sidebar-collapsed-icon');
|
||||
const findParticipantsAuthor = () => wrapper.findAll('.participants-author');
|
||||
|
||||
const mountComponent = (propsData) => shallowMount(Participants, { propsData });
|
||||
|
||||
describe('collapsed sidebar state', () => {
|
||||
it('shows loading spinner when loading', () => {
|
||||
wrapper = mountComponent({
|
||||
loading: true,
|
||||
});
|
||||
wrapper = mountComponent({ loading: true });
|
||||
|
||||
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
|
||||
expect(findLoadingIcon().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not show loading spinner not loading', () => {
|
||||
wrapper = mountComponent({
|
||||
loading: false,
|
||||
});
|
||||
it('does not show loading spinner when not loading', () => {
|
||||
wrapper = mountComponent({ loading: false });
|
||||
|
||||
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
|
||||
expect(findLoadingIcon().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('shows participant count when given', () => {
|
||||
wrapper = mountComponent({
|
||||
loading: false,
|
||||
participants: PARTICIPANT_LIST,
|
||||
});
|
||||
wrapper = mountComponent({ participants });
|
||||
|
||||
expect(getCollapsedParticipantsCount().text()).toBe(`${PARTICIPANT_LIST.length}`);
|
||||
expect(findCollapsedIcon().text()).toBe(participants.length.toString());
|
||||
});
|
||||
|
||||
it('shows full participant count when there are hidden participants', () => {
|
||||
wrapper = mountComponent({
|
||||
loading: false,
|
||||
participants: PARTICIPANT_LIST,
|
||||
numberOfLessParticipants: 1,
|
||||
});
|
||||
wrapper = mountComponent({ participants, numberOfLessParticipants: 1 });
|
||||
|
||||
expect(getCollapsedParticipantsCount().text()).toBe(`${PARTICIPANT_LIST.length}`);
|
||||
expect(findCollapsedIcon().text()).toBe(participants.length.toString());
|
||||
});
|
||||
});
|
||||
|
||||
describe('expanded sidebar state', () => {
|
||||
it('shows loading spinner when loading', () => {
|
||||
wrapper = mountComponent({
|
||||
loading: true,
|
||||
});
|
||||
wrapper = mountComponent({ loading: true });
|
||||
|
||||
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
|
||||
expect(findLoadingIcon().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('when only showing visible participants, shows an avatar only for each participant under the limit', async () => {
|
||||
it('when only showing visible participants, shows an avatar only for each participant under the limit', () => {
|
||||
const numberOfLessParticipants = 2;
|
||||
wrapper = mountComponent({
|
||||
loading: false,
|
||||
participants: PARTICIPANT_LIST,
|
||||
numberOfLessParticipants,
|
||||
});
|
||||
wrapper = mountComponent({ participants, numberOfLessParticipants });
|
||||
|
||||
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
wrapper.setData({
|
||||
isShowingMoreParticipants: false,
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
expect(wrapper.findAll('.participants-author')).toHaveLength(numberOfLessParticipants);
|
||||
expect(findParticipantsAuthor()).toHaveLength(numberOfLessParticipants);
|
||||
});
|
||||
|
||||
it('when only showing all participants, each has an avatar', async () => {
|
||||
wrapper = mountComponent({
|
||||
loading: false,
|
||||
participants: PARTICIPANT_LIST,
|
||||
numberOfLessParticipants: 2,
|
||||
});
|
||||
wrapper = mountComponent({ participants, numberOfLessParticipants: 2 });
|
||||
|
||||
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
wrapper.setData({
|
||||
isShowingMoreParticipants: true,
|
||||
});
|
||||
await findMoreParticipantsButton().vm.$emit('click');
|
||||
|
||||
await nextTick();
|
||||
expect(wrapper.findAll('.participants-author')).toHaveLength(PARTICIPANT_LIST.length);
|
||||
expect(findParticipantsAuthor()).toHaveLength(participants.length);
|
||||
});
|
||||
|
||||
it('does not have more participants link when they can all be shown', () => {
|
||||
const numberOfLessParticipants = 100;
|
||||
wrapper = mountComponent({
|
||||
loading: false,
|
||||
participants: PARTICIPANT_LIST,
|
||||
numberOfLessParticipants,
|
||||
});
|
||||
wrapper = mountComponent({ participants, numberOfLessParticipants });
|
||||
|
||||
expect(PARTICIPANT_LIST.length).toBeLessThan(numberOfLessParticipants);
|
||||
expect(getMoreParticipantsButton().exists()).toBe(false);
|
||||
expect(participants.length).toBeLessThan(numberOfLessParticipants);
|
||||
expect(findMoreParticipantsButton().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('when too many participants, has more participants link to show more', async () => {
|
||||
wrapper = mountComponent({
|
||||
loading: false,
|
||||
participants: PARTICIPANT_LIST,
|
||||
numberOfLessParticipants: 2,
|
||||
});
|
||||
it('when too many participants, has more participants link to show more', () => {
|
||||
wrapper = mountComponent({ participants, numberOfLessParticipants: 2 });
|
||||
|
||||
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
wrapper.setData({
|
||||
isShowingMoreParticipants: false,
|
||||
});
|
||||
|
||||
await nextTick();
|
||||
expect(getMoreParticipantsButton().text()).toBe('+ 1 more');
|
||||
expect(findMoreParticipantsButton().text()).toBe('+ 1 more');
|
||||
});
|
||||
|
||||
it('when too many participants and already showing them, has more participants link to show less', async () => {
|
||||
wrapper = mountComponent({
|
||||
loading: false,
|
||||
participants: PARTICIPANT_LIST,
|
||||
numberOfLessParticipants: 2,
|
||||
});
|
||||
wrapper = mountComponent({ participants, numberOfLessParticipants: 2 });
|
||||
|
||||
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
wrapper.setData({
|
||||
isShowingMoreParticipants: true,
|
||||
});
|
||||
await findMoreParticipantsButton().vm.$emit('click');
|
||||
|
||||
await nextTick();
|
||||
expect(getMoreParticipantsButton().text()).toBe('- show less');
|
||||
expect(findMoreParticipantsButton().text()).toBe('- show less');
|
||||
});
|
||||
|
||||
it('clicking more participants link emits event', () => {
|
||||
wrapper = mountComponent({
|
||||
loading: false,
|
||||
participants: PARTICIPANT_LIST,
|
||||
numberOfLessParticipants: 2,
|
||||
});
|
||||
it('clicking on participants icon emits `toggleSidebar` event', () => {
|
||||
wrapper = mountComponent({ participants, numberOfLessParticipants: 2 });
|
||||
|
||||
expect(wrapper.vm.isShowingMoreParticipants).toBe(false);
|
||||
findCollapsedIcon().trigger('click');
|
||||
|
||||
getMoreParticipantsButton().vm.$emit('click');
|
||||
|
||||
expect(wrapper.vm.isShowingMoreParticipants).toBe(true);
|
||||
});
|
||||
|
||||
it('clicking on participants icon emits `toggleSidebar` event', async () => {
|
||||
wrapper = mountComponent({
|
||||
loading: false,
|
||||
participants: PARTICIPANT_LIST,
|
||||
numberOfLessParticipants: 2,
|
||||
});
|
||||
|
||||
const spy = jest.spyOn(wrapper.vm, '$emit');
|
||||
|
||||
wrapper.find('.sidebar-collapsed-icon').trigger('click');
|
||||
|
||||
await nextTick();
|
||||
expect(spy).toHaveBeenCalledWith('toggleSidebar');
|
||||
spy.mockRestore();
|
||||
expect(wrapper.emitted('toggleSidebar')).toEqual([[]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when not showing participants label', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = mountComponent({
|
||||
participants: PARTICIPANT_LIST,
|
||||
showParticipantLabel: false,
|
||||
});
|
||||
wrapper = mountComponent({ participants, showParticipantLabel: false });
|
||||
});
|
||||
|
||||
it('does not show sidebar collapsed icon', () => {
|
||||
expect(wrapper.find('.sidebar-collapsed-icon').exists()).toBe(false);
|
||||
expect(findCollapsedIcon().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not show participants label title', () => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
import { nextTick } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import SidebarPortal from '~/super_sidebar/components/sidebar_portal.vue';
|
||||
import SidebarPortalTarget from '~/super_sidebar/components/sidebar_portal_target.vue';
|
||||
|
||||
describe('SidebarPortal', () => {
|
||||
let targetWrapper;
|
||||
|
||||
const Target = {
|
||||
components: { SidebarPortalTarget },
|
||||
props: ['show'],
|
||||
template: '<sidebar-portal-target v-if="show" />',
|
||||
};
|
||||
|
||||
const Source = {
|
||||
components: { SidebarPortal },
|
||||
template: '<sidebar-portal><br data-testid="test"></sidebar-portal>',
|
||||
};
|
||||
|
||||
const mountSource = () => {
|
||||
mount(Source);
|
||||
};
|
||||
|
||||
const mountTarget = ({ show = true } = {}) => {
|
||||
targetWrapper = mount(Target, {
|
||||
propsData: { show },
|
||||
attachTo: document.body,
|
||||
});
|
||||
};
|
||||
|
||||
const findTestContent = () => targetWrapper.find('[data-testid="test"]');
|
||||
|
||||
it('renders content into the target', async () => {
|
||||
mountTarget();
|
||||
await nextTick();
|
||||
|
||||
mountSource();
|
||||
await nextTick();
|
||||
|
||||
expect(findTestContent().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('waits for target to be available before rendering', async () => {
|
||||
mountSource();
|
||||
await nextTick();
|
||||
|
||||
mountTarget();
|
||||
await nextTick();
|
||||
|
||||
expect(findTestContent().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('supports conditional rendering of target', async () => {
|
||||
mountTarget({ show: false });
|
||||
await nextTick();
|
||||
|
||||
mountSource();
|
||||
await nextTick();
|
||||
|
||||
expect(findTestContent().exists()).toBe(false);
|
||||
|
||||
await targetWrapper.setProps({ show: true });
|
||||
expect(findTestContent().exists()).toBe(true);
|
||||
|
||||
await targetWrapper.setProps({ show: false });
|
||||
expect(findTestContent().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -2,6 +2,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
|||
import SuperSidebar from '~/super_sidebar/components/super_sidebar.vue';
|
||||
import HelpCenter from '~/super_sidebar/components/help_center.vue';
|
||||
import UserBar from '~/super_sidebar/components/user_bar.vue';
|
||||
import SidebarPortalTarget from '~/super_sidebar/components/sidebar_portal_target.vue';
|
||||
import { sidebarData } from '../mock_data';
|
||||
|
||||
describe('SuperSidebar component', () => {
|
||||
|
|
@ -9,6 +10,7 @@ describe('SuperSidebar component', () => {
|
|||
|
||||
const findUserBar = () => wrapper.findComponent(UserBar);
|
||||
const findHelpCenter = () => wrapper.findComponent(HelpCenter);
|
||||
const findSidebarPortalTarget = () => wrapper.findComponent(SidebarPortalTarget);
|
||||
|
||||
const createWrapper = (props = {}) => {
|
||||
wrapper = shallowMountExtended(SuperSidebar, {
|
||||
|
|
@ -31,5 +33,9 @@ describe('SuperSidebar component', () => {
|
|||
it('renders HelpCenter with sidebarData', () => {
|
||||
expect(findHelpCenter().props('sidebarData')).toBe(sidebarData);
|
||||
});
|
||||
|
||||
it('renders SidebarPortalTarget', () => {
|
||||
expect(findSidebarPortalTarget().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,8 +9,7 @@ RSpec.describe JiraConnectHelper, feature_category: :integrations do
|
|||
|
||||
let(:user) { create(:user) }
|
||||
let(:client_id) { '123' }
|
||||
let(:enable_public_keys_storage_config) { false }
|
||||
let(:enable_public_keys_storage_setting) { false }
|
||||
let(:enable_public_keys_storage) { false }
|
||||
|
||||
before do
|
||||
stub_application_setting(jira_connect_application_key: client_id)
|
||||
|
|
@ -22,9 +21,7 @@ RSpec.describe JiraConnectHelper, feature_category: :integrations do
|
|||
before do
|
||||
allow(view).to receive(:current_user).and_return(nil)
|
||||
allow(Gitlab.config.gitlab).to receive(:url).and_return('http://test.host')
|
||||
allow(Gitlab.config.jira_connect).to receive(:enable_public_keys_storage)
|
||||
.and_return(enable_public_keys_storage_config)
|
||||
stub_application_setting(jira_connect_public_key_storage_enabled: enable_public_keys_storage_setting)
|
||||
stub_application_setting(jira_connect_public_key_storage_enabled: enable_public_keys_storage)
|
||||
end
|
||||
|
||||
it 'includes Jira Connect app attributes' do
|
||||
|
|
@ -108,16 +105,8 @@ RSpec.describe JiraConnectHelper, feature_category: :integrations do
|
|||
expect(subject[:public_key_storage_enabled]).to eq(false)
|
||||
end
|
||||
|
||||
context 'when public_key_storage is enabled via config' do
|
||||
let(:enable_public_keys_storage_config) { true }
|
||||
|
||||
it 'assignes public_key_storage_enabled to true' do
|
||||
expect(subject[:public_key_storage_enabled]).to eq(true)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when public_key_storage is enabled via setting' do
|
||||
let(:enable_public_keys_storage_setting) { true }
|
||||
context 'when public_key_storage is enabled' do
|
||||
let(:enable_public_keys_storage) { true }
|
||||
|
||||
it 'assignes public_key_storage_enabled to true' do
|
||||
expect(subject[:public_key_storage_enabled]).to eq(true)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,60 @@ RSpec.describe SortingHelper do
|
|||
allow(self).to receive(:request).and_return(double(path: 'http://test.com', query_parameters: { label_name: option }))
|
||||
end
|
||||
|
||||
describe '#issuable_sort_options' do
|
||||
let(:viewing_issues) { false }
|
||||
let(:viewing_merge_requests) { false }
|
||||
let(:params) { {} }
|
||||
|
||||
subject(:options) { helper.issuable_sort_options(viewing_issues, viewing_merge_requests) }
|
||||
|
||||
before do
|
||||
allow(helper).to receive(:params).and_return(params)
|
||||
end
|
||||
|
||||
shared_examples 'with merged date option' do
|
||||
it 'adds merged date option' do
|
||||
expect(options).to include(
|
||||
a_hash_including(
|
||||
value: 'merged_at',
|
||||
text: 'Merged date'
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'without merged date option' do
|
||||
it 'does not set merged date option' do
|
||||
expect(options).not_to include(
|
||||
a_hash_including(
|
||||
value: 'merged_at',
|
||||
text: 'Merged date'
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'without merged date option'
|
||||
|
||||
context 'when viewing_merge_requests is true' do
|
||||
let(:viewing_merge_requests) { true }
|
||||
|
||||
it_behaves_like 'without merged date option'
|
||||
|
||||
context 'when state param is all' do
|
||||
let(:params) { { state: 'all' } }
|
||||
|
||||
it_behaves_like 'with merged date option'
|
||||
end
|
||||
|
||||
context 'when state param is merged' do
|
||||
let(:params) { { state: 'merged' } }
|
||||
|
||||
it_behaves_like 'with merged date option'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#admin_users_sort_options' do
|
||||
it 'returns correct link attributes in array' do
|
||||
options = admin_users_sort_options(filter: 'filter', search_query: 'search')
|
||||
|
|
|
|||
|
|
@ -16,19 +16,21 @@ RSpec.shared_examples 'validate schema data' do |tables_and_views|
|
|||
end
|
||||
end
|
||||
|
||||
RSpec.describe Gitlab::Database::GitlabSchema do
|
||||
RSpec.describe Gitlab::Database::GitlabSchema, feature_category: :database do
|
||||
shared_examples 'maps table name to table schema' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:name, :classification) do
|
||||
'ci_builds' | :gitlab_ci
|
||||
'my_schema.ci_builds' | :gitlab_ci
|
||||
'information_schema.columns' | :gitlab_internal
|
||||
'audit_events_part_5fc467ac26' | :gitlab_main
|
||||
'_test_gitlab_main_table' | :gitlab_main
|
||||
'_test_gitlab_ci_table' | :gitlab_ci
|
||||
'_test_my_table' | :gitlab_shared
|
||||
'pg_attribute' | :gitlab_internal
|
||||
'ci_builds' | :gitlab_ci
|
||||
'my_schema.ci_builds' | :gitlab_ci
|
||||
'my_schema.ci_runner_machine_builds_100' | :gitlab_ci
|
||||
'my_schema._test_gitlab_main_table' | :gitlab_main
|
||||
'information_schema.columns' | :gitlab_internal
|
||||
'audit_events_part_5fc467ac26' | :gitlab_main
|
||||
'_test_gitlab_main_table' | :gitlab_main
|
||||
'_test_gitlab_ci_table' | :gitlab_ci
|
||||
'_test_my_table' | :gitlab_shared
|
||||
'pg_attribute' | :gitlab_internal
|
||||
end
|
||||
|
||||
with_them do
|
||||
|
|
|
|||
|
|
@ -238,23 +238,20 @@ RSpec.describe Gitlab::Database::Partitioning::PartitionManager do
|
|||
expect(pending_drop.drop_after).to eq(Time.current + described_class::RETAIN_DETACHED_PARTITIONS_FOR)
|
||||
end
|
||||
|
||||
# Postgres 11 does not support foreign keys to partitioned tables
|
||||
if ApplicationRecord.database.version.to_f >= 12
|
||||
context 'when the model is the target of a foreign key' do
|
||||
before do
|
||||
connection.execute(<<~SQL)
|
||||
context 'when the model is the target of a foreign key' do
|
||||
before do
|
||||
connection.execute(<<~SQL)
|
||||
create unique index idx_for_fk ON #{partitioned_table_name}(created_at);
|
||||
|
||||
create table _test_gitlab_main_referencing_table (
|
||||
id bigserial primary key not null,
|
||||
referencing_created_at timestamptz references #{partitioned_table_name}(created_at)
|
||||
);
|
||||
SQL
|
||||
end
|
||||
SQL
|
||||
end
|
||||
|
||||
it 'does not detach partitions with a referenced foreign key' do
|
||||
expect { subject }.not_to change { find_partitions(my_model.table_name).size }
|
||||
end
|
||||
it 'does not detach partitions with a referenced foreign key' do
|
||||
expect { subject }.not_to change { find_partitions(my_model.table_name).size }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -203,10 +203,8 @@ RSpec.describe Gitlab::Database::PostgresForeignKey, type: :model, feature_categ
|
|||
end
|
||||
end
|
||||
|
||||
context 'when supporting foreign keys to inherited tables in postgres 12' do
|
||||
context 'when supporting foreign keys to inherited tables' do
|
||||
before do
|
||||
skip('not supported before postgres 12') if ApplicationRecord.database.version.to_f < 12
|
||||
|
||||
ApplicationRecord.connection.execute(<<~SQL)
|
||||
create table #{schema_table_name('parent')} (
|
||||
id bigserial primary key not null
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ RSpec.describe API::Avatar, feature_category: :user_profile do
|
|||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(json_response['avatar_url']).to eql("#{::Settings.gitlab.base_url}#{user.avatar.local_url}")
|
||||
is_expected.to have_request_urgency(:medium)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -5,11 +5,10 @@ require 'spec_helper'
|
|||
RSpec.describe JiraConnect::PublicKeysController, feature_category: :integrations do
|
||||
describe 'GET /-/jira_connect/public_keys/:uuid' do
|
||||
let(:uuid) { non_existing_record_id }
|
||||
let(:public_key_storage_enabled_config) { true }
|
||||
let(:public_key_storage_enabled) { true }
|
||||
|
||||
before do
|
||||
allow(Gitlab.config.jira_connect).to receive(:enable_public_keys_storage)
|
||||
.and_return(public_key_storage_enabled_config)
|
||||
stub_application_setting(jira_connect_public_key_storage_enabled: public_key_storage_enabled)
|
||||
end
|
||||
|
||||
it 'renders 404' do
|
||||
|
|
@ -30,26 +29,14 @@ RSpec.describe JiraConnect::PublicKeysController, feature_category: :integration
|
|||
expect(response.body).to eq(public_key.key)
|
||||
end
|
||||
|
||||
context 'when public key storage config disabled' do
|
||||
let(:public_key_storage_enabled_config) { false }
|
||||
context 'when public key storage setting disabled' do
|
||||
let(:public_key_storage_enabled) { false }
|
||||
|
||||
it 'renders 404' do
|
||||
get jira_connect_public_key_path(id: uuid)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
|
||||
context 'when public key storage setting is enabled' do
|
||||
before do
|
||||
stub_application_setting(jira_connect_public_key_storage_enabled: true)
|
||||
end
|
||||
|
||||
it 'renders 404' do
|
||||
get jira_connect_public_key_path(id: uuid)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -178,6 +178,8 @@ RSpec.configure do |config|
|
|||
config.include RenderedHelpers
|
||||
config.include RSpec::Benchmark::Matchers, type: :benchmark
|
||||
config.include DetailedErrorHelpers
|
||||
config.include RequestUrgencyMatcher, type: :controller
|
||||
config.include RequestUrgencyMatcher, type: :request
|
||||
|
||||
config.include_context 'when rendered has no HTML escapes', type: :view
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
module RequestUrgencyMatcher
|
||||
RSpec::Matchers.define :have_request_urgency do |request_urgency|
|
||||
match do |_actual|
|
||||
if controller_instance = request.env["action_controller.instance"]
|
||||
controller_instance.urgency.name == request_urgency
|
||||
elsif endpoint = request.env['api.endpoint']
|
||||
urgency = endpoint.options[:for].try(:urgency_for_app, endpoint)
|
||||
urgency.name == request_urgency
|
||||
else
|
||||
raise 'neither a controller nor a request spec'
|
||||
end
|
||||
end
|
||||
|
||||
failure_message do |_actual|
|
||||
if controller_instance = request.env["action_controller.instance"]
|
||||
"request urgency #{controller_instance.urgency.name} is set, \
|
||||
but expected to be #{request_urgency}".squish
|
||||
elsif endpoint = request.env['api.endpoint']
|
||||
urgency = endpoint.options[:for].try(:urgency_for_app, endpoint)
|
||||
"request urgency #{urgency.name} is set, \
|
||||
but expected to be #{request_urgency}".squish
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in New Issue