{{ __('Time tracking') }}
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index d126be24ec0..1659a04f5e8 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -297,9 +297,11 @@ export const WORK_ITEM_TO_ISSUE_MAP = {
[WIDGET_TYPE_LABELS]: 'labels',
[WIDGET_TYPE_MILESTONE]: 'milestone',
[WIDGET_TYPE_WEIGHT]: 'weight',
+ [WIDGET_TYPE_ITERATION]: 'iteration',
[WIDGET_TYPE_START_AND_DUE_DATE]: 'dueDate',
[WIDGET_TYPE_HEALTH_STATUS]: 'healthStatus',
[WIDGET_TYPE_AWARD_EMOJI]: 'awardEmoji',
+ [WIDGET_TYPE_TIME_TRACKING]: 'timeEstimate',
};
export const LINKED_CATEGORIES_MAP = {
diff --git a/app/assets/javascripts/work_items/list/components/work_items_list_app.vue b/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
index 8cbd6940404..0899d4073d8 100644
--- a/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
+++ b/app/assets/javascripts/work_items/list/components/work_items_list_app.vue
@@ -5,7 +5,15 @@ import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_st
import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
import { TYPENAME_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
-import { STATUS_ALL, STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants';
+import {
+ STATUS_ALL,
+ STATUS_CLOSED,
+ STATUS_OPEN,
+ WORKSPACE_GROUP,
+ WORKSPACE_PROJECT,
+} from '~/issues/constants';
+import { defaultTypeTokenOptions } from '~/issues/list/constants';
+import searchLabelsQuery from '~/issues/list/queries/search_labels.query.graphql';
import setSortPreferenceMutation from '~/issues/list/queries/set_sort_preference.mutation.graphql';
import {
convertToApiParams,
@@ -13,22 +21,37 @@ import {
deriveSortKey,
getInitialPageParams,
} from '~/issues/list/utils';
+import { fetchPolicies } from '~/lib/graphql';
import { scrollUp } from '~/lib/utils/scroll_utils';
import { __, s__ } from '~/locale';
import {
OPERATORS_IS,
+ OPERATORS_IS_NOT_OR,
+ TOKEN_TITLE_ASSIGNEE,
TOKEN_TITLE_AUTHOR,
+ TOKEN_TITLE_LABEL,
+ TOKEN_TITLE_MILESTONE,
TOKEN_TITLE_SEARCH_WITHIN,
+ TOKEN_TITLE_TYPE,
+ TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
TOKEN_TYPE_SEARCH_WITHIN,
+ TOKEN_TYPE_TYPE,
} from '~/vue_shared/components/filtered_search_bar/constants';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { DEFAULT_PAGE_SIZE, issuableListTabs } from '~/vue_shared/issuable/list/constants';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { STATE_CLOSED } from '../../constants';
import { sortOptions, urlSortParams } from '../constants';
import getWorkItemsQuery from '../queries/get_work_items.query.graphql';
const UserToken = () => import('~/vue_shared/components/filtered_search_bar/tokens/user_token.vue');
+const LabelToken = () =>
+ import('~/vue_shared/components/filtered_search_bar/tokens/label_token.vue');
+const MilestoneToken = () =>
+ import('~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue');
export default {
issuableListTabs,
@@ -39,7 +62,8 @@ export default {
IssueCardStatistics,
IssueCardTimeInfo,
},
- inject: ['fullPath', 'initialSort', 'isSignedIn', 'workItemType'],
+ mixins: [glFeatureFlagMixin()],
+ inject: ['fullPath', 'initialSort', 'isGroup', 'isSignedIn', 'workItemType'],
props: {
eeCreatedWorkItemsCount: {
type: Number,
@@ -73,7 +97,7 @@ export default {
search: this.searchQuery,
...this.apiFilterParams,
...this.pageParams,
- types: [this.workItemType],
+ types: this.apiFilterParams.types || [this.workItemType],
};
},
update(data) {
@@ -115,6 +139,9 @@ export default {
isOpenTab() {
return this.state === STATUS_OPEN;
},
+ namespace() {
+ return this.isGroup ? WORKSPACE_GROUP : WORKSPACE_PROJECT;
+ },
searchQuery() {
return convertToSearchQuery(this.filterTokens);
},
@@ -130,7 +157,20 @@ export default {
});
}
- return [
+ const tokens = [
+ {
+ type: TOKEN_TYPE_ASSIGNEE,
+ title: TOKEN_TITLE_ASSIGNEE,
+ icon: 'user',
+ token: UserToken,
+ dataType: 'user',
+ operators: OPERATORS_IS_NOT_OR,
+ fullPath: this.fullPath,
+ isProject: !this.isGroup,
+ recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-assignee`,
+ preloadedUsers,
+ multiSelect: this.glFeatures.groupMultiSelectTokens,
+ },
{
type: TOKEN_TYPE_AUTHOR,
title: TOKEN_TITLE_AUTHOR,
@@ -138,11 +178,33 @@ export default {
token: UserToken,
dataType: 'user',
defaultUsers: [],
- operators: OPERATORS_IS,
+ operators: OPERATORS_IS_NOT_OR,
fullPath: this.fullPath,
- isProject: false,
+ isProject: !this.isGroup,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-author`,
preloadedUsers,
+ multiSelect: this.glFeatures.groupMultiSelectTokens,
+ },
+ {
+ type: TOKEN_TYPE_LABEL,
+ title: TOKEN_TITLE_LABEL,
+ icon: 'labels',
+ token: LabelToken,
+ operators: OPERATORS_IS_NOT_OR,
+ fetchLabels: this.fetchLabels,
+ fetchLatestLabels: this.glFeatures.frontendCaching ? this.fetchLatestLabels : null,
+ recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-label`,
+ multiSelect: this.glFeatures.groupMultiSelectTokens,
+ },
+ {
+ type: TOKEN_TYPE_MILESTONE,
+ title: TOKEN_TITLE_MILESTONE,
+ icon: 'milestone',
+ token: MilestoneToken,
+ recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-milestone`,
+ shouldSkipSort: true,
+ fullPath: this.fullPath,
+ isProject: !this.isGroup,
},
{
type: TOKEN_TYPE_SEARCH_WITHIN,
@@ -157,6 +219,21 @@ export default {
],
},
];
+
+ if (!this.workItemType) {
+ tokens.push({
+ type: TOKEN_TYPE_TYPE,
+ title: TOKEN_TITLE_TYPE,
+ icon: 'issues',
+ token: GlFilteredSearchToken,
+ operators: OPERATORS_IS,
+ options: defaultTypeTokenOptions,
+ });
+ }
+
+ tokens.sort((a, b) => a.title.localeCompare(b.title));
+
+ return tokens;
},
showPaginationControls() {
return !this.isLoading && (this.pageInfo.hasNextPage || this.pageInfo.hasPreviousPage);
@@ -175,6 +252,26 @@ export default {
},
},
methods: {
+ fetchLabelsWithFetchPolicy(search, fetchPolicy = fetchPolicies.CACHE_FIRST) {
+ return this.$apollo
+ .query({
+ query: searchLabelsQuery,
+ variables: { fullPath: this.fullPath, search, isProject: !this.isGroup },
+ fetchPolicy,
+ })
+ .then(({ data }) => {
+ // TODO remove once we can search by title-only on the backend
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/346353
+ const labels = data[this.namespace]?.labels.nodes;
+ return labels.filter((label) => label.title.toLowerCase().includes(search.toLowerCase()));
+ });
+ },
+ fetchLabels(search) {
+ return this.fetchLabelsWithFetchPolicy(search);
+ },
+ fetchLatestLabels(search) {
+ return this.fetchLabelsWithFetchPolicy(search, fetchPolicies.NETWORK_ONLY);
+ },
getStatus(issue) {
return issue.state === STATE_CLOSED ? __('Closed') : undefined;
},
@@ -256,6 +353,7 @@ export default {
namespace="work-items"
recent-searches-storage-key="issues"
:search-tokens="searchTokens"
+ show-filtered-search-friendly-text
:show-page-size-selector="showPageSizeSelector"
:show-pagination-controls="showPaginationControls"
show-work-item-type-icon
diff --git a/app/assets/javascripts/work_items/list/index.js b/app/assets/javascripts/work_items/list/index.js
index dc146667a60..0648347d6a1 100644
--- a/app/assets/javascripts/work_items/list/index.js
+++ b/app/assets/javascripts/work_items/list/index.js
@@ -18,6 +18,7 @@ export const mountWorkItemsListApp = () => {
hasEpicsFeature,
hasIssuableHealthStatusFeature,
hasIssueWeightsFeature,
+ hasScopedLabelsFeature,
initialSort,
isSignedIn,
showNewIssueLink,
@@ -37,6 +38,7 @@ export const mountWorkItemsListApp = () => {
hasEpicsFeature: parseBoolean(hasEpicsFeature),
hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
+ hasScopedLabelsFeature: parseBoolean(hasScopedLabelsFeature),
initialSort,
isSignedIn: parseBoolean(isSignedIn),
isGroup: true,
diff --git a/app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql b/app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql
index 3d08e9a9216..2ac0762315c 100644
--- a/app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql
+++ b/app/assets/javascripts/work_items/list/queries/get_work_items.query.graphql
@@ -6,17 +6,37 @@ query getWorkItems(
$search: String
$sort: WorkItemSort
$state: IssuableState
+ $assigneeWildcardId: AssigneeWildcardId
+ $assigneeUsernames: [String!]
$authorUsername: String
+ $labelName: [String!]
+ $milestoneTitle: [String!]
+ $milestoneWildcardId: MilestoneWildcardId
+ $types: [IssueType!]
$in: [IssuableSearchableField!]
+ $not: NegatedWorkItemFilterInput
+ $or: UnionedWorkItemFilterInput
$afterCursor: String
$beforeCursor: String
$firstPageSize: Int
$lastPageSize: Int
- $types: [IssueType!] = null
) {
group(fullPath: $fullPath) {
id
- workItemStateCounts(includeDescendants: true, sort: $sort, state: $state, types: $types) {
+ workItemStateCounts(
+ includeDescendants: true
+ sort: $sort
+ state: $state
+ assigneeUsernames: $assigneeUsernames
+ assigneeWildcardId: $assigneeWildcardId
+ authorUsername: $authorUsername
+ labelName: $labelName
+ milestoneTitle: $milestoneTitle
+ milestoneWildcardId: $milestoneWildcardId
+ types: $types
+ not: $not
+ or: $or
+ ) {
all
closed
opened
@@ -26,13 +46,20 @@ query getWorkItems(
search: $search
sort: $sort
state: $state
+ assigneeUsernames: $assigneeUsernames
+ assigneeWildcardId: $assigneeWildcardId
authorUsername: $authorUsername
+ labelName: $labelName
+ milestoneTitle: $milestoneTitle
+ milestoneWildcardId: $milestoneWildcardId
+ types: $types
in: $in
+ not: $not
+ or: $or
after: $afterCursor
before: $beforeCursor
first: $firstPageSize
last: $lastPageSize
- types: $types
) {
pageInfo {
...PageInfo
diff --git a/app/assets/stylesheets/bootstrap_migration_components.scss b/app/assets/stylesheets/bootstrap_migration_components.scss
index 24078c97b4d..5dff277368f 100644
--- a/app/assets/stylesheets/bootstrap_migration_components.scss
+++ b/app/assets/stylesheets/bootstrap_migration_components.scss
@@ -79,6 +79,7 @@ input[type='file'] {
// Add to .label so that old system notes that are saved to the db
// will still receive the correct styling
+// stylelint-disable-next-line gitlab/no-gl-class
.badge:not(.gl-badge),
.label {
padding: 4px 5px;
diff --git a/app/assets/stylesheets/components/action_card_component.scss b/app/assets/stylesheets/components/action_card_component.scss
index 37297fb6fd1..1db8f35b670 100644
--- a/app/assets/stylesheets/components/action_card_component.scss
+++ b/app/assets/stylesheets/components/action_card_component.scss
@@ -3,17 +3,17 @@
display: block;
padding: $gl-spacing-scale-5;
border-radius: $gl-border-radius-base;
-
+
&-default {
border: 1px solid $gray-100;
background-color: $white;
}
-
+
&-success {
border: 1px solid $green-100;
background-color: $green-50;
}
-
+
&-promo {
border: 1px solid $purple-100;
background-color: $purple-50;
@@ -25,11 +25,13 @@
gap: $gl-spacing-scale-2;
font-weight: $gl-font-weight-bold;
text-wrap: balance;
-
+
+ // stylelint-disable-next-line gitlab/no-gl-class
&:is(.gl-link):not(:hover) {
color: $gl-text-color;
}
+ // stylelint-disable-next-line gitlab/no-gl-class
&:is(.gl-link) {
transition: color .2s cubic-bezier(0.22, 0.61, 0.36, 1);
diff --git a/app/assets/stylesheets/components/avatar.scss b/app/assets/stylesheets/components/avatar.scss
index 08396c48e65..110ec90043f 100644
--- a/app/assets/stylesheets/components/avatar.scss
+++ b/app/assets/stylesheets/components/avatar.scss
@@ -195,10 +195,12 @@ $avatar-sizes: (
// Max width of popover container is set by gl-max-w-48
// so we need to ensure that name/username/status container doesn't overflow
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-avatar-labeled-labels {
max-width: px-to-rem(290px);
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-avatar-labeled-label,
.gl-avatar-labeled-sublabel {
@include gl-text-truncate;
diff --git a/app/assets/stylesheets/components/content_editor.scss b/app/assets/stylesheets/components/content_editor.scss
index 18043c503f7..7f939ca58a4 100644
--- a/app/assets/stylesheets/components/content_editor.scss
+++ b/app/assets/stylesheets/components/content_editor.scss
@@ -283,9 +283,11 @@
}
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-new-dropdown-inner li {
margin-left: 0 !important;
+ // stylelint-disable-next-line gitlab/no-gl-class
&.gl-new-dropdown-item {
padding-left: $gl-spacing-scale-2;
padding-right: $gl-spacing-scale-2;
@@ -358,15 +360,18 @@
}
}
+// stylelint-disable-next-line gitlab/no-gl-class
.content-editor-table-dropdown .gl-new-dropdown-panel {
min-width: auto;
}
.content-editor-suggestions-dropdown {
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-new-dropdown-panel {
width: max-content;
}
+ // stylelint-disable-next-line gitlab/no-gl-class
li.focused div.gl-new-dropdown-item-content {
@include gl-focus($inset: true);
background-color: $gray-50;
diff --git a/app/assets/stylesheets/components/ref_selector.scss b/app/assets/stylesheets/components/ref_selector.scss
index f7a9367499e..36a56df4e73 100644
--- a/app/assets/stylesheets/components/ref_selector.scss
+++ b/app/assets/stylesheets/components/ref_selector.scss
@@ -5,6 +5,7 @@
overflow: hidden;
width: 20rem;
+ // stylelint-disable-next-line gitlab/no-gl-class
&,
.gl-dropdown-inner {
max-height: $dropdown-max-height-lg;
diff --git a/app/assets/stylesheets/components/related_items_list.scss b/app/assets/stylesheets/components/related_items_list.scss
index 1cbd0faa900..247820d0b1b 100644
--- a/app/assets/stylesheets/components/related_items_list.scss
+++ b/app/assets/stylesheets/components/related_items_list.scss
@@ -16,6 +16,7 @@ $item-remove-button-space: 42px;
.related-items-tree {
.card-header {
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-label {
line-height: $gl-line-height;
}
diff --git a/app/assets/stylesheets/components/whats_new.scss b/app/assets/stylesheets/components/whats_new.scss
index 04166eab32e..17726bd67ef 100644
--- a/app/assets/stylesheets/components/whats_new.scss
+++ b/app/assets/stylesheets/components/whats_new.scss
@@ -3,14 +3,17 @@
overflow-y: hidden;
width: 500px;
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-infinite-scroll-legend {
display: none;
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-tabs {
overflow-y: auto;
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-tabs-nav {
flex-wrap: nowrap;
overflow-x: scroll;
@@ -26,6 +29,7 @@
}
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-spinner-container {
width: 100%;
position: absolute;
diff --git a/app/assets/stylesheets/mailers/notify.scss b/app/assets/stylesheets/mailers/notify.scss
index 2d501781119..dd1d5560ed4 100644
--- a/app/assets/stylesheets/mailers/notify.scss
+++ b/app/assets/stylesheets/mailers/notify.scss
@@ -1,5 +1,6 @@
@import 'notify_base';
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-label-scoped {
border: 2px solid currentColor;
box-sizing: border-box;
@@ -8,18 +9,22 @@
line-height: 14px;
}
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-label-text {
padding: 0 5px;
}
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-label-text-light {
color: $white;
}
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-label-text-dark {
color: $gl-text-color;
}
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-label-text-scoped {
padding: 0 5px;
color: $gl-text-color;
diff --git a/app/assets/stylesheets/mailers/notify_enhanced.scss b/app/assets/stylesheets/mailers/notify_enhanced.scss
index 4310d3ff261..5cb7d739a98 100644
--- a/app/assets/stylesheets/mailers/notify_enhanced.scss
+++ b/app/assets/stylesheets/mailers/notify_enhanced.scss
@@ -36,10 +36,12 @@ pre {
font-size: 14px;
}
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-mb-5 {
margin-bottom: $gl-spacing-scale-5;
}
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-mt-5 {
margin-top: $gl-spacing-scale-5;
}
diff --git a/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss b/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss
index b5ca59fc2d6..cf69fd8b9a5 100644
--- a/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss
+++ b/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss
@@ -39,6 +39,7 @@
background-color: var(--gray-50, $gray-50);
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-spinner,
svg {
width: $ci-action-dropdown-svg-size;
diff --git a/app/assets/stylesheets/page_bundles/_system_note_styles.scss b/app/assets/stylesheets/page_bundles/_system_note_styles.scss
index ec885966a8e..04fc8a3dfb2 100644
--- a/app/assets/stylesheets/page_bundles/_system_note_styles.scss
+++ b/app/assets/stylesheets/page_bundles/_system_note_styles.scss
@@ -8,30 +8,33 @@ Shared styles for system note dot and icon styles used for MR, Issue, Work Item
margin-left: 12px;
margin-right: 8px;
border: 2px solid var(--gray-50, $gray-50);
-
+
.gl-dark .modal-body & {
border-color: var(--gray-100, $gray-100);
}
}
-
+
.system-note-icon {
width: 20px;
height: 20px;
margin-left: 6px;
-
+
+ // stylelint-disable-next-line gitlab/no-gl-class
&.gl-bg-green-100 {
--bg-color: var(--green-100, #{$green-100});
}
-
+
+ // stylelint-disable-next-line gitlab/no-gl-class
&.gl-bg-red-100 {
--bg-color: var(--red-100, #{$red-100});
}
-
+
+ // stylelint-disable-next-line gitlab/no-gl-class
&.gl-bg-blue-100 {
--bg-color: var(--blue-100, #{$blue-100});
}
}
-
+
.system-note-icon:not(.mr-system-note-empty)::before {
content: '';
display: block;
@@ -41,12 +44,12 @@ Shared styles for system note dot and icon styles used for MR, Issue, Work Item
width: 2px;
height: 20px;
background: linear-gradient(to bottom, transparent, var(--bg-color));
-
+
.system-note:first-child & {
display: none;
}
}
-
+
.system-note-icon:not(.mr-system-note-empty)::after {
content: '';
display: block;
@@ -56,8 +59,8 @@ Shared styles for system note dot and icon styles used for MR, Issue, Work Item
width: 2px;
height: 20px;
background: linear-gradient(to bottom, var(--bg-color), transparent);
-
+
.system-note:last-child & {
display: none;
}
- }
\ No newline at end of file
+ }
diff --git a/app/assets/stylesheets/page_bundles/alert_management_details.scss b/app/assets/stylesheets/page_bundles/alert_management_details.scss
index e0025499aeb..83e9c6d3cb3 100644
--- a/app/assets/stylesheets/page_bundles/alert_management_details.scss
+++ b/app/assets/stylesheets/page_bundles/alert_management_details.scss
@@ -28,6 +28,7 @@
padding-top: $gl-spacing-scale-8;
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-dropdown-item-text-wrapper {
padding-top: 0;
padding-bottom: 0;
diff --git a/app/assets/stylesheets/page_bundles/clusters.scss b/app/assets/stylesheets/page_bundles/clusters.scss
index 09611098bed..bf8a1294b05 100644
--- a/app/assets/stylesheets/page_bundles/clusters.scss
+++ b/app/assets/stylesheets/page_bundles/clusters.scss
@@ -6,6 +6,7 @@
width: 100%;
order: -1;
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-dropdown,
.split-content-button {
width: 100%;
@@ -26,6 +27,7 @@
}
.select-agent-dropdown {
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-button-text {
flex-grow: 1;
}
diff --git a/app/assets/stylesheets/page_bundles/commits.scss b/app/assets/stylesheets/page_bundles/commits.scss
index 03b7bcd1a4e..a0621ba8c9e 100644
--- a/app/assets/stylesheets/page_bundles/commits.scss
+++ b/app/assets/stylesheets/page_bundles/commits.scss
@@ -7,6 +7,7 @@
}
.add-review-item {
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-tab-nav-item {
height: 100%;
}
@@ -22,6 +23,7 @@
color: var(--gl-text-color-subtle);
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-filtered-search-suggestion-list.dropdown-menu {
width: $gl-max-dropdown-max-height;
}
diff --git a/app/assets/stylesheets/page_bundles/environments.scss b/app/assets/stylesheets/page_bundles/environments.scss
index 2e94ae057c1..1528bd779a3 100644
--- a/app/assets/stylesheets/page_bundles/environments.scss
+++ b/app/assets/stylesheets/page_bundles/environments.scss
@@ -48,6 +48,7 @@
}
}
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-responsive-table-row {
.branch-commit {
max-width: 100%;
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index 0cb2c643728..382fb0db134 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -97,6 +97,7 @@ $ide-commit-header-height: 48px;
border-right: 1px solid var(--ide-border-color, $border-color);
border-bottom: 1px solid var(--ide-border-color, $border-color);
+ // stylelint-disable-next-line gitlab/no-gl-class
&.active,
.gl-tab-nav-item-active {
background-color: var(--ide-highlight-background, $white);
@@ -116,10 +117,12 @@ $ide-commit-header-height: 48px;
}
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-tab-content {
padding: 0;
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-tabs-nav {
border-width: 0;
@@ -139,6 +142,7 @@ $ide-commit-header-height: 48px;
border-right: 1px solid var(--ide-border-color, $border-color);
border-bottom: 1px solid var(--ide-border-color, $border-color);
+ // stylelint-disable-next-line gitlab/no-gl-class
&.gl-tab-nav-item-active {
background-color: var(--ide-highlight-background, $white);
border-color: var(--ide-border-color, $border-color);
@@ -562,6 +566,7 @@ $ide-commit-header-height: 48px;
}
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-form-radio,
.gl-form-checkbox {
color: var(--ide-text-color, $gl-text-color);
@@ -644,6 +649,7 @@ $ide-commit-header-height: 48px;
height: 100%;
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.nav-links,
.gl-tabs-nav {
height: 30px;
@@ -925,6 +931,7 @@ $ide-commit-header-height: 48px;
--svg-status-bg: var(--ide-background, #{$white});
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-empty-state {
p {
margin: $grid-size 0;
@@ -938,6 +945,7 @@ $ide-commit-header-height: 48px;
}
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-tab-content {
color: var(--ide-text-color, $gl-text-color);
}
@@ -980,6 +988,7 @@ $ide-commit-header-height: 48px;
}
.ide-nav-form {
+ // stylelint-disable-next-line gitlab/no-gl-class
.nav-links li,
.gl-tabs-nav li {
width: 50%;
@@ -991,11 +1000,13 @@ $ide-commit-header-height: 48px;
font-size: 14px;
line-height: 30px;
+ // stylelint-disable-next-line gitlab/no-gl-class
&:not(.active),
&:not(.gl-tab-nav-item-active) {
background-color: var(--ide-dropdown-background, $gray-10);
}
+ // stylelint-disable-next-line gitlab/no-gl-class
&.gl-tab-nav-item-active {
font-weight: bold;
}
diff --git a/app/assets/stylesheets/page_bundles/incidents.scss b/app/assets/stylesheets/page_bundles/incidents.scss
index 7a9287b1ba0..6eb343ecf89 100644
--- a/app/assets/stylesheets/page_bundles/incidents.scss
+++ b/app/assets/stylesheets/page_bundles/incidents.scss
@@ -24,6 +24,7 @@
.create-timeline-event,
.edit-timeline-event {
.md-area {
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-form-textarea {
@include gl-shadow-none;
}
diff --git a/app/assets/stylesheets/page_bundles/issuable.scss b/app/assets/stylesheets/page_bundles/issuable.scss
index 78225eb75f3..526c5bc86f2 100644
--- a/app/assets/stylesheets/page_bundles/issuable.scss
+++ b/app/assets/stylesheets/page_bundles/issuable.scss
@@ -97,10 +97,12 @@
}
.merge-request-notification-toggle {
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-toggle {
margin-left: auto;
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-toggle-label {
font-weight: $gl-font-weight-normal;
}
@@ -109,6 +111,7 @@
.comment-templates-modal {
padding: 3rem 0.5rem 0;
+ // stylelint-disable-next-line gitlab/no-gl-class
&.gl-modal .modal-dialog {
align-items: flex-start;
}
@@ -121,6 +124,7 @@
padding-left: 0;
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.comment-templates-options .gl-new-dropdown-item {
padding-left: 0;
padding-right: 0;
diff --git a/app/assets/stylesheets/page_bundles/issuable_list.scss b/app/assets/stylesheets/page_bundles/issuable_list.scss
index c2955a8a256..8c9268dfa07 100644
--- a/app/assets/stylesheets/page_bundles/issuable_list.scss
+++ b/app/assets/stylesheets/page_bundles/issuable_list.scss
@@ -59,12 +59,14 @@
color: var(--gl-text-color, $gl-text-color);
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-label-link {
color: inherit;
&:hover {
text-decoration: none;
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-label-text:last-of-type {
text-decoration: underline;
}
diff --git a/app/assets/stylesheets/page_bundles/issues_list.scss b/app/assets/stylesheets/page_bundles/issues_list.scss
index 7c96887d657..f75d08df203 100644
--- a/app/assets/stylesheets/page_bundles/issues_list.scss
+++ b/app/assets/stylesheets/page_bundles/issues_list.scss
@@ -36,10 +36,12 @@
}
.work-item-labels {
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-token {
padding-left: $gl-spacing-scale-1;
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-token-close {
display: none;
}
diff --git a/app/assets/stylesheets/page_bundles/labels.scss b/app/assets/stylesheets/page_bundles/labels.scss
index d2cd67cc408..aad2ca6cbfd 100644
--- a/app/assets/stylesheets/page_bundles/labels.scss
+++ b/app/assets/stylesheets/page_bundles/labels.scss
@@ -94,6 +94,7 @@
.label-name {
width: 200px;
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-label {
line-height: $gl-line-height;
}
diff --git a/app/assets/stylesheets/page_bundles/log_viewer.scss b/app/assets/stylesheets/page_bundles/log_viewer.scss
index af3a653f268..b80285f87e8 100644
--- a/app/assets/stylesheets/page_bundles/log_viewer.scss
+++ b/app/assets/stylesheets/page_bundles/log_viewer.scss
@@ -30,6 +30,7 @@
margin-right: $gl-spacing-scale-3;
margin-left: $gl-spacing-scale-3;
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-icon {
position: absolute;
left: 0;
diff --git a/app/assets/stylesheets/page_bundles/members.scss b/app/assets/stylesheets/page_bundles/members.scss
index aad42ff4ee4..ff9ee6e31ca 100644
--- a/app/assets/stylesheets/page_bundles/members.scss
+++ b/app/assets/stylesheets/page_bundles/members.scss
@@ -84,6 +84,7 @@
}
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-datepicker-input {
width: px-to-rem(165px);
max-width: 100%;
diff --git a/app/assets/stylesheets/page_bundles/merge_request.scss b/app/assets/stylesheets/page_bundles/merge_request.scss
index 657369c1535..a27d26b4825 100644
--- a/app/assets/stylesheets/page_bundles/merge_request.scss
+++ b/app/assets/stylesheets/page_bundles/merge_request.scss
@@ -121,6 +121,7 @@ $comparison-empty-state-height: 62px;
margin-bottom: 0;
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-dropdown-custom-toggle {
width: 100%;
}
@@ -139,6 +140,7 @@ $comparison-empty-state-height: 62px;
}
}
+// stylelint-disable-next-line gitlab/no-gl-class
.issuable-form-label-select-holder .gl-dropdown-toggle {
@include media-breakpoint-up(md) {
width: 250px;
@@ -304,6 +306,7 @@ $comparison-empty-state-height: 62px;
.mr-compare-dropdown {
width: 100%;
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-button-text {
width: 100%;
}
diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss
index e960cbecbf4..4bfc82262f0 100644
--- a/app/assets/stylesheets/page_bundles/merge_requests.scss
+++ b/app/assets/stylesheets/page_bundles/merge_requests.scss
@@ -360,6 +360,7 @@
margin-bottom: $gl-padding-8;
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-button {
margin-left: 0;
}
@@ -470,6 +471,7 @@
display: none;
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-skeleton-loader {
display: block;
}
@@ -817,6 +819,7 @@
z-index: 199;
white-space: nowrap;
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-dropdown-toggle {
width: auto;
max-width: 170px;
@@ -838,10 +841,12 @@
top: 1px;
margin: 0 $gl-spacing-scale-1;
+ // stylelint-disable-next-line gitlab/no-gl-class
.dropdown-toggle.gl-button {
padding: $gl-spacing-scale-2 2px $gl-spacing-scale-2 $gl-spacing-scale-2;
font-weight: $gl-font-weight-bold;
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-button-icon {
margin-left: $gl-spacing-scale-1;
}
@@ -887,6 +892,7 @@
}
}
+// stylelint-disable-next-line gitlab/no-gl-class
.submit-review-dropdown .gl-new-dropdown-panel {
max-width: none;
}
@@ -947,10 +953,12 @@
}
.merge-request-notification-toggle {
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-toggle {
margin-left: auto;
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-toggle-label {
font-weight: $gl-font-weight-normal;
}
@@ -1075,6 +1083,7 @@
vertical-align: middle;
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-skeleton-loader {
max-width: 334px;
}
diff --git a/app/assets/stylesheets/page_bundles/milestone.scss b/app/assets/stylesheets/page_bundles/milestone.scss
index 3b000e3e11e..2baa9583554 100644
--- a/app/assets/stylesheets/page_bundles/milestone.scss
+++ b/app/assets/stylesheets/page_bundles/milestone.scss
@@ -25,6 +25,7 @@
word-wrap: break-word;
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-label-link {
color: inherit;
}
diff --git a/app/assets/stylesheets/page_bundles/oncall_schedules.scss b/app/assets/stylesheets/page_bundles/oncall_schedules.scss
index 1e22cbe4ff9..15fa69a842c 100644
--- a/app/assets/stylesheets/page_bundles/oncall_schedules.scss
+++ b/app/assets/stylesheets/page_bundles/oncall_schedules.scss
@@ -9,6 +9,7 @@
}
.invalid-dropdown {
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-button.gl-dropdown-toggle {
@include inset-border-1-red-500;
@@ -19,10 +20,12 @@
}
.rotations-modal {
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-card {
min-width: 75%;
}
+ // stylelint-disable-next-line gitlab/no-gl-class
&.gl-modal .modal-md {
max-width: 640px;
}
diff --git a/app/assets/stylesheets/page_bundles/paginated_table.scss b/app/assets/stylesheets/page_bundles/paginated_table.scss
index 709a5cdf446..1f8054fb2e7 100644
--- a/app/assets/stylesheets/page_bundles/paginated_table.scss
+++ b/app/assets/stylesheets/page_bundles/paginated_table.scss
@@ -5,12 +5,15 @@
background-color: var(--green-50, $green-50);
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-tabs-nav {
border-bottom-width: 0;
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-tab-nav-item {
color: var(--gray-500, $gray-500);
+ // stylelint-disable-next-line gitlab/no-gl-class
> .gl-tab-counter-badge {
color: inherit;
font-size: $gl-font-size-sm;
diff --git a/app/assets/stylesheets/page_bundles/pipeline.scss b/app/assets/stylesheets/page_bundles/pipeline.scss
index 588305b94c2..066344e8d6c 100644
--- a/app/assets/stylesheets/page_bundles/pipeline.scss
+++ b/app/assets/stylesheets/page_bundles/pipeline.scss
@@ -124,10 +124,12 @@
// These are single-value classes to use with utility-class style CSS.
// They are here to still access a variable or because they use magic values.
// scoped to the graph. Do not add other styles.
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-pipeline-min-h {
min-height: calc(#{$dropdown-max-height-lg} + #{$gl-spacing-scale-6});
}
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-pipeline-job-width {
width: 100%;
max-width: 400px;
@@ -137,6 +139,7 @@
}
}
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-downstream-pipeline-job-width {
width: 8rem;
@@ -149,10 +152,12 @@
}
}
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-linked-pipeline-padding {
padding-right: 120px;
}
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-ci-action-icon-container {
position: absolute;
right: 5px;
@@ -170,6 +175,7 @@
}
}
+// stylelint-disable-next-line gitlab/no-gl-class
.stage-column-title .gl-ci-action-icon-container {
right: 11px;
}
@@ -315,10 +321,12 @@
}
.stage-column .ci-job-group-dropdown {
+ // stylelint-disable-next-line gitlab/no-gl-class
&,
.gl-new-dropdown-custom-toggle {
width: 100%;
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-badge.badge-muted {
.gl-dark & {
@apply gl-bg-gray-100;
@@ -328,6 +336,7 @@
// Reset padding, as inner element will
// define padding
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-new-dropdown-item-content,
.gl-new-dropdown-item-text-wrapper {
padding: 0;
@@ -335,6 +344,7 @@
// Set artificial focus on the menu-item to keep
// it consistent with the original dropdown items
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-new-dropdown-item:focus,
.gl-new-dropdown-item-content:focus {
outline: none;
diff --git a/app/assets/stylesheets/page_bundles/pipeline_editor.scss b/app/assets/stylesheets/page_bundles/pipeline_editor.scss
index 9e09980200f..63b9bac8904 100644
--- a/app/assets/stylesheets/page_bundles/pipeline_editor.scss
+++ b/app/assets/stylesheets/page_bundles/pipeline_editor.scss
@@ -8,6 +8,7 @@
}
}
+// stylelint-disable-next-line gitlab/no-gl-class
.file-tree-container > div.gl-overflow-y-auto {
max-height: 220px;
diff --git a/app/assets/stylesheets/page_bundles/profile.scss b/app/assets/stylesheets/page_bundles/profile.scss
index 7b21a240c62..394c81bb02c 100644
--- a/app/assets/stylesheets/page_bundles/profile.scss
+++ b/app/assets/stylesheets/page_bundles/profile.scss
@@ -96,6 +96,7 @@
padding-left: 40px;
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-label-scoped {
--label-inset-border: inset 0 0 0 1px currentColor;
}
diff --git a/app/assets/stylesheets/page_bundles/projects.scss b/app/assets/stylesheets/page_bundles/projects.scss
index 4f9746b599d..5c253686aa8 100644
--- a/app/assets/stylesheets/page_bundles/projects.scss
+++ b/app/assets/stylesheets/page_bundles/projects.scss
@@ -462,6 +462,7 @@
}
@media (min-width: $breakpoint-lg) {
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-card {
width: calc(50% - 15px);
}
diff --git a/app/assets/stylesheets/page_bundles/search.scss b/app/assets/stylesheets/page_bundles/search.scss
index fc1dc414896..f0c58bb1d60 100644
--- a/app/assets/stylesheets/page_bundles/search.scss
+++ b/app/assets/stylesheets/page_bundles/search.scss
@@ -273,6 +273,7 @@ $language-filter-max-height: 20rem;
display: flex;
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.btn-search,
.btn-success,
.dropdown-menu-toggle,
@@ -301,6 +302,7 @@ $language-filter-max-height: 20rem;
width: 100%;
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.dropdown-menu-toggle,
.gl-dropdown {
@include media-breakpoint-up(lg) {
diff --git a/app/assets/stylesheets/page_bundles/settings.scss b/app/assets/stylesheets/page_bundles/settings.scss
index f003ab6e513..a360a68d450 100644
--- a/app/assets/stylesheets/page_bundles/settings.scss
+++ b/app/assets/stylesheets/page_bundles/settings.scss
@@ -138,6 +138,7 @@
}
.instance-runners-info {
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-alert-body {
p:last-child {
margin-bottom: 0;
@@ -166,12 +167,14 @@
}
.prometheus-metrics-monitoring {
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-card {
.badge.badge-pill {
font-size: 12px;
line-height: 12px;
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-card-header .label-count {
color: var(--white, $white);
background: var(--gray-800, $gray-800);
diff --git a/app/assets/stylesheets/page_bundles/terms.scss b/app/assets/stylesheets/page_bundles/terms.scss
index 727cdcf0627..ae16618fe70 100644
--- a/app/assets/stylesheets/page_bundles/terms.scss
+++ b/app/assets/stylesheets/page_bundles/terms.scss
@@ -19,7 +19,9 @@
padding-top: $gl-padding;
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-card {
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-card-header {
display: flex;
align-items: center;
diff --git a/app/assets/stylesheets/page_bundles/todos.scss b/app/assets/stylesheets/page_bundles/todos.scss
index 0cf29182777..3371e45dfc7 100644
--- a/app/assets/stylesheets/page_bundles/todos.scss
+++ b/app/assets/stylesheets/page_bundles/todos.scss
@@ -89,6 +89,7 @@
color: var(--gl-text-color, $gl-text-color);
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-label-scoped {
--label-inset-border: inset 0 0 0 1px currentColor;
}
diff --git a/app/assets/stylesheets/page_bundles/wiki.scss b/app/assets/stylesheets/page_bundles/wiki.scss
index 309ee921e63..acf938553bf 100644
--- a/app/assets/stylesheets/page_bundles/wiki.scss
+++ b/app/assets/stylesheets/page_bundles/wiki.scss
@@ -162,6 +162,7 @@ ul.wiki-pages-list.content-list {
max-width: 100%;
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-disclosure-dropdown {
display: none !important;
}
diff --git a/app/assets/stylesheets/page_bundles/work_items.scss b/app/assets/stylesheets/page_bundles/work_items.scss
index 2b43a5ad7d7..4071fe7dcc1 100644
--- a/app/assets/stylesheets/page_bundles/work_items.scss
+++ b/app/assets/stylesheets/page_bundles/work_items.scss
@@ -6,6 +6,7 @@ $work-item-overview-right-sidebar-width: 20rem;
$work-item-sticky-header-height: 52px;
$work-item-overview-gap-width: 2rem;
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-token-selector-token-container {
display: flex;
align-items: center;
@@ -32,12 +33,14 @@ $work-item-overview-gap-width: 2rem;
}
.work-item-due-date {
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-datepicker-input.gl-form-input.form-control {
width: 10rem;
&:not(:focus, :hover) {
box-shadow: none;
+ // stylelint-disable-next-line gitlab/no-gl-class
~ .gl-datepicker-actions {
display: none;
}
@@ -47,12 +50,14 @@ $work-item-overview-gap-width: 2rem;
background-color: var(--white, $white);
box-shadow: none;
+ // stylelint-disable-next-line gitlab/no-gl-class
~ .gl-datepicker-actions {
display: none;
}
}
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-datepicker-actions:focus,
.gl-datepicker-actions:hover {
display: flex !important;
@@ -60,10 +65,12 @@ $work-item-overview-gap-width: 2rem;
}
.work-item-labels {
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-token {
padding-left: $gl-spacing-scale-1;
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-token-close {
display: none;
}
@@ -78,15 +85,18 @@ $work-item-overview-gap-width: 2rem;
.work-item-notifications-form {
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-toggle {
margin-left: auto;
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-toggle-label {
font-weight: $gl-font-weight-normal;
}
}
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-modal .work-item-view,
.work-item-drawer .work-item-view:not(:has(.design-detail)) {
container-name: work-item-view;
@@ -172,6 +182,7 @@ $work-item-overview-gap-width: 2rem;
max-width: 65%;
}
+ // stylelint-disable-next-line gitlab/no-gl-class
&.gl-form-select {
&:hover,
&:focus {
@@ -194,10 +205,12 @@ $work-item-overview-gap-width: 2rem;
}
.work-item-notification-toggle {
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-toggle {
margin-left: auto;
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-toggle-label {
font-weight: normal;
}
@@ -345,6 +358,7 @@ $disclosure-hierarchy-chevron-dimension: 1.2rem;
}
}
+// stylelint-disable-next-line gitlab/no-gl-class
.work-item-sidebar-dropdown .gl-new-dropdown-panel {
width: 100% !important;
max-width: 19rem !important;
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 09d18446a57..e76edb4e0db 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -80,6 +80,7 @@
color: $gl-text-color;
}
+ // stylelint-disable-next-line gitlab/no-gl-class
&.gl-inline-flex {
gap: 0.5ch;
}
@@ -119,6 +120,7 @@
.commit-sha-group {
display: inline-flex;
+ // stylelint-disable-next-line gitlab/no-gl-class
.label,
.btn:not(.gl-button) {
padding: $gl-vert-padding $gl-btn-padding;
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 26a19ecaa37..0ff994af59b 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -277,10 +277,12 @@ ul.related-merge-requests > li gl-emoji {
top: $calc-application-header-height;
}
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-drawer .md-header {
top: 0;
}
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-modal .md-header {
top: -$gl-padding-8;
}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 7d5752513c0..9f6ed745aa8 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -124,13 +124,14 @@
flex-flow: row wrap;
width: 100%;
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-float-right {
// Flexbox quirk to make sure right-aligned items stay right-aligned.
margin-left: auto;
}
}
-
+// stylelint-disable-next-line gitlab/no-gl-class
.md-header .gl-tabs-nav {
border-bottom: 0;
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 0d315dcf24b..06c4b53dc8c 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -81,6 +81,7 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
height: 2rem;
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-avatar {
border-color: var(--gray-50, $gray-50);
}
@@ -378,6 +379,7 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
text-transform: lowercase;
}
+ // stylelint-disable-next-line gitlab/no-gl-class
a:not(.gl-link) {
color: $blue-600;
}
@@ -823,10 +825,12 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
text-decoration: underline;
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-label-link:hover {
text-decoration: none;
color: inherit;
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-label-text:last-of-type {
text-decoration: underline;
}
@@ -1005,6 +1009,7 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
padding: 0 8px !important;
box-shadow: none !important;
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-button-loading-indicator {
margin-right: 0 !important;
}
diff --git a/app/assets/stylesheets/pages/registry.scss b/app/assets/stylesheets/pages/registry.scss
index 36b86771295..79fa50c4e62 100644
--- a/app/assets/stylesheets/pages/registry.scss
+++ b/app/assets/stylesheets/pages/registry.scss
@@ -2,6 +2,7 @@
// until this gitlab-ui issue is resolved: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1079
//
// See app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue when this is changed.
+// stylelint-disable-next-line gitlab/no-gl-class
.breadcrumbs .gl-breadcrumbs {
padding: 0;
box-shadow: none;
diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss
index 490ac15241b..56743f1beaa 100644
--- a/app/assets/stylesheets/performance_bar.scss
+++ b/app/assets/stylesheets/performance_bar.scss
@@ -36,6 +36,7 @@
color: initial;
}
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-link,
.gl-button {
color: $white;
diff --git a/app/assets/stylesheets/snippets.scss b/app/assets/stylesheets/snippets.scss
index 3eaa56a73ee..dae8355b107 100644
--- a/app/assets/stylesheets/snippets.scss
+++ b/app/assets/stylesheets/snippets.scss
@@ -13,6 +13,7 @@
margin: 20px;
font-weight: $gl-font-weight-normal;
+ // stylelint-disable-next-line gitlab/no-gl-class
.gl-snippet-icon {
display: inline-block;
background: url('ext_snippet_icons/ext_snippet_icons.png') no-repeat;
@@ -22,9 +23,13 @@
height: 16px;
background-size: cover;
+ // stylelint-disable-next-line gitlab/no-gl-class
&.gl-snippet-icon-doc-code { background-position: 0 0; }
+ // stylelint-disable-next-line gitlab/no-gl-class
&.gl-snippet-icon-doc-text { background-position: 0 -16px; }
+ // stylelint-disable-next-line gitlab/no-gl-class
&.gl-snippet-icon-download { background-position: 0 -32px; }
+ // stylelint-disable-next-line gitlab/no-gl-class
&.gl-snippet-icon-copy-to-clipboard { background-position: 0 -48px; }
}
@@ -136,6 +141,7 @@
}
}
+ // stylelint-disable-next-line gitlab/no-gl-class
img,
.gl-snippet-icon {
display: inline-block;
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index bc6c6e1ddf5..664e65c8602 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -45,16 +45,19 @@
top: $calc-system-headers-height;
}
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-top-app-header {
top: $calc-application-header-height;
}
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-children-ml-sm-3 > * {
@include media-breakpoint-up(sm) {
margin-left: $gl-spacing-scale-3;
}
}
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-first-child-ml-sm-0 > a:first-child,
.gl-first-child-ml-sm-0 > button:first-child {
@include media-breakpoint-up(sm) {
@@ -69,22 +72,29 @@
min-width: 0;
}
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-w-16 { width: px-to-rem($grid-size * 2); }
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-w-64 { width: px-to-rem($grid-size * 8); }
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-h-32 { height: px-to-rem($grid-size * 4); }
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-h-64 { height: px-to-rem($grid-size * 8); }
// Migrate this to Gitlab UI when FF is removed
// https://gitlab.com/groups/gitlab-org/-/epics/2882
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-h-200\! { height: px-to-rem($grid-size * 25) !important; }
// This utility is used to force the z-index to match that of dropdown menu's
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-z-dropdown-menu\! {
z-index: $zindex-dropdown-menu !important;
}
// This is used to help prevent issues with margin collapsing.
// See https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Box_Model/Mastering_margin_collapsing.
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-force-block-formatting-context::after {
content: '';
display: flex;
@@ -102,6 +112,7 @@
See: https://developer.mozilla.org/en-US/docs/Web/CSS/::-webkit-scrollbar
**/
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-webkit-scrollbar-display-none {
&::-webkit-scrollbar {
display: none;
@@ -109,39 +120,47 @@
}
// Will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1465
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-focus-ring-border-1-gray-900\! {
@include gl-focus($gl-border-size-1, $gray-900, true);
}
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-sm-mb-5 {
@include gl-media-breakpoint-down(md) {
margin-bottom: $gl-spacing-scale-5;
}
}
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-fill-orange-500 {
fill: $orange-500;
}
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-fill-red-500 {
fill: $red-500;
}
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-last-of-type-border-b-0:last-of-type {
border-bottom-width: 0;
}
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-md-h-9 {
@include gl-media-breakpoint-up(md) {
height: $gl-spacing-scale-9;
}
}
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-pl-12 {
padding-left: $gl-spacing-scale-12;
}
+// stylelint-disable-next-line gitlab/no-gl-class
.gl-min-w-12 {
min-width: $gl-spacing-scale-12;
}
diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb
index 589dd9b324d..71cd56cfa84 100644
--- a/app/controllers/admin/hooks_controller.rb
+++ b/app/controllers/admin/hooks_controller.rb
@@ -5,6 +5,8 @@ class Admin::HooksController < Admin::ApplicationController
urgency :low, [:test]
+ before_action :not_found, unless: -> { system_hooks? }
+
def test
result = TestHooks::SystemService.new(hook, current_user, params[:trigger]).execute
@@ -30,4 +32,8 @@ class Admin::HooksController < Admin::ApplicationController
def trigger_values
SystemHook.triggers.values
end
+
+ def system_hooks?
+ !Gitlab.com? # rubocop:disable Gitlab/AvoidGitlabInstanceChecks -- Not related to SaaS offerings
+ end
end
diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb
index 98588a02801..adf4f1d0d71 100644
--- a/app/controllers/projects/boards_controller.rb
+++ b/app/controllers/projects/boards_controller.rb
@@ -7,6 +7,7 @@ class Projects::BoardsController < Projects::ApplicationController
before_action :check_issues_available!
before_action do
push_frontend_feature_flag(:board_multi_select, project)
+ push_frontend_feature_flag(:issues_list_drawer, project)
end
feature_category :team_planning
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
index db6cf27566f..1b340d88a41 100644
--- a/app/helpers/boards_helper.rb
+++ b/app/helpers/boards_helper.rb
@@ -24,10 +24,15 @@ module BoardsHelper
board_type: board.to_type,
has_missing_boards: has_missing_boards?.to_s,
multiple_boards_available: multiple_boards_available?.to_s,
- board_base_url: board_base_url
+ board_base_url: board_base_url,
+ wi: work_items_show_data(board_namespace)
}
end
+ def board_namespace
+ board.group_board? ? @group : @project
+ end
+
def group_id
return @group.id if board.group_board?
diff --git a/app/presenters/key_presenter.rb b/app/presenters/key_presenter.rb
index e3eb5feedbf..0397f639add 100644
--- a/app/presenters/key_presenter.rb
+++ b/app/presenters/key_presenter.rb
@@ -7,7 +7,7 @@ class KeyPresenter < Gitlab::View::Presenter::Delegated # rubocop:disable Gitlab
if !key_object.public_key.valid?
help_link = help_page_link(_('supported SSH public key.'), 'user/ssh', 'supported-ssh-key-types')
- _('%{type} must be a %{help_link}').html_safe % { type: type.to_s.titleize, help_link: help_link }
+ _('%{type} must be a %{help_link}').html_safe % { type: type.to_s.humanize, help_link: help_link }
else
key_object.errors.full_messages.join(', ').html_safe
end
diff --git a/app/views/admin/gitaly_servers/index.html.haml b/app/views/admin/gitaly_servers/index.html.haml
index 41f8dea13bb..ad461270e9c 100644
--- a/app/views/admin/gitaly_servers/index.html.haml
+++ b/app/views/admin/gitaly_servers/index.html.haml
@@ -36,4 +36,4 @@
- else
.empty-state
.text-center
- %h4= _("No connection could be made to a Gitaly Server, please check your logs!")
+ %h4= _("No connection could be made to a Gitaly server, please check your logs!")
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index fb5186683b6..623c7c02d65 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -38,7 +38,7 @@
%span.cgray= n_('parent', 'parents', @commit.parents.count)
- @commit.parents.each do |parent|
= link_to parent.short_id, project_commit_path(@project, parent), class: "commit-sha"
- #js-commit-branches-and-tags{ data: { full_path: @project.full_path, commit_sha: @commit.short_id } }
+ #js-commit-branches-and-tags{ data: { full_path: @project.full_path, commit_sha: @commit.id } }
.well-segment.merge-request-info
.icon-container
diff --git a/app/views/projects/deploy_keys/edit.html.haml b/app/views/projects/deploy_keys/edit.html.haml
index 982ab64ac55..97aa46630c6 100644
--- a/app/views/projects/deploy_keys/edit.html.haml
+++ b/app/views/projects/deploy_keys/edit.html.haml
@@ -1,5 +1,5 @@
-- page_title _('Edit Deploy Key')
-%h1.page-title.gl-font-size-h-display= _('Edit Deploy Key')
+- page_title _('Edit deploy key')
+%h1.page-title.gl-font-size-h-display= _('Edit deploy key')
= gitlab_ui_form_for [@project, @deploy_key], include_id: false, html: { class: 'js-requires-input' } do |f|
= render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key }
diff --git a/app/workers/concurrency_limit/resume_worker.rb b/app/workers/concurrency_limit/resume_worker.rb
index e88277dc581..90af4b7a898 100644
--- a/app/workers/concurrency_limit/resume_worker.rb
+++ b/app/workers/concurrency_limit/resume_worker.rb
@@ -18,12 +18,14 @@ module ConcurrencyLimit
reschedule_job = false
workers.each do |worker|
- next unless jobs_in_the_queue?(worker)
+ limit = ::Gitlab::SidekiqMiddleware::ConcurrencyLimit::WorkersMap.limit_for(worker: worker)&.call
+ queue_size = queue_size(worker)
+ report_prometheus_metrics(worker, queue_size, limit)
+
+ next unless queue_size > 0
reschedule_job = true
- limit = ::Gitlab::SidekiqMiddleware::ConcurrencyLimit::WorkersMap.limit_for(worker: worker)&.call
-
processing_limit = if limit
current = current_concurrency(worker: worker)
limit - current
@@ -49,8 +51,8 @@ module ConcurrencyLimit
@current_concurrency[worker.name].to_i
end
- def jobs_in_the_queue?(worker)
- Gitlab::SidekiqMiddleware::ConcurrencyLimit::ConcurrencyLimitService.has_jobs_in_queue?(worker.name)
+ def queue_size(worker)
+ Gitlab::SidekiqMiddleware::ConcurrencyLimit::ConcurrencyLimitService.queue_size(worker.name)
end
def resume_processing!(worker, limit:)
@@ -60,5 +62,18 @@ module ConcurrencyLimit
def workers
Gitlab::SidekiqMiddleware::ConcurrencyLimit::WorkersMap.workers
end
+
+ def report_prometheus_metrics(worker, queue_size, limit)
+ queue_size_metric = Gitlab::Metrics.gauge(:sidekiq_concurrency_limit_queue_jobs,
+ 'Number of jobs queued by the concurrency limit middleware.',
+ {},
+ :max)
+ queue_size_metric.set({ worker: worker.name }, queue_size)
+
+ limit_metric = Gitlab::Metrics.gauge(:sidekiq_concurrency_limit_max_concurrent_jobs,
+ 'Max number of concurrent running jobs.',
+ {})
+ limit_metric.set({ worker: worker.name }, limit || DEFAULT_LIMIT)
+ end
end
end
diff --git a/config/feature_flags/wip/synced_epic_work_item_editable.yml b/config/feature_flags/wip/synced_epic_work_item_editable.yml
deleted file mode 100644
index 8ee11993d9d..00000000000
--- a/config/feature_flags/wip/synced_epic_work_item_editable.yml
+++ /dev/null
@@ -1,9 +0,0 @@
----
-name: synced_epic_work_item_editable
-feature_issue_url:
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156648
-rollout_issue_url:
-milestone: '17.2'
-group: group::product planning
-type: wip
-default_enabled: false
diff --git a/doc/administration/admin_area.md b/doc/administration/admin_area.md
index d3d0fdd197d..7f5b050896f 100644
--- a/doc/administration/admin_area.md
+++ b/doc/administration/admin_area.md
@@ -323,13 +323,13 @@ To merge topics:
## Administering Gitaly servers
-You can list all Gitaly servers in the GitLab instance from the Admin area's **Gitaly Servers**
+You can list all Gitaly servers in the GitLab instance from the Admin area's **Gitaly servers**
page. For more details, see [Gitaly](gitaly/index.md).
-To access the **Gitaly Servers** page:
+To access the **Gitaly servers** page:
1. On the left sidebar, at the bottom, select **Admin area**.
-1. Select **Overview > Gitaly Servers**.
+1. Select **Overview > Gitaly servers**.
For each Gitaly server, the following details are listed:
@@ -431,7 +431,7 @@ For each job, the following details are listed:
The following topics document the **Monitoring** section of the Admin area.
-### System Information
+### System information
> - Support for relative time [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/341248) in GitLab 15.2. "Uptime" statistic was renamed to "System started".
diff --git a/doc/administration/gitaly/troubleshooting.md b/doc/administration/gitaly/troubleshooting.md
index 1a27ace535c..8563c66df16 100644
--- a/doc/administration/gitaly/troubleshooting.md
+++ b/doc/administration/gitaly/troubleshooting.md
@@ -24,7 +24,7 @@ When using standalone Gitaly servers, you must make sure they are the same versi
as GitLab to ensure full compatibility:
1. On the left sidebar, at the bottom, select **Admin area**.
-1. Select **Overview > Gitaly Servers**.
+1. Select **Overview > Gitaly servers**.
1. Confirm all Gitaly servers indicate that they are up to date.
## Find storage resource details
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index a284806124f..5ed8ab3f4d1 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -232,6 +232,8 @@ configuration option in `gitlab.yml`. These metrics are served from the
| `sidekiq_running_jobs` | Gauge | 12.2 | Number of Sidekiq jobs running | `queue`, `boundary`, `external_dependencies`, `feature_category`, `urgency` |
| `sidekiq_concurrency` | Gauge | 12.5 | Maximum number of Sidekiq jobs | |
| `sidekiq_mem_total_bytes` | Gauge | 15.3 | Number of bytes allocated for both objects consuming an object slot and objects that required a malloc'| |
+| `sidekiq_concurrency_limit_queue_jobs` | Gauge | 17.3 | Number of Sidekiq jobs waiting in the concurrency limit queue| `worker` |
+| `sidekiq_concurrency_limit_max_concurrent_jobs` | Gauge | 17.3 | Max number of concurrent running Sidekiq jobs | `worker` |
| `geo_db_replication_lag_seconds` | Gauge | 10.2 | Database replication lag (seconds) | `url` |
| `geo_repositories` | Gauge | 10.2 | Total number of repositories available on primary | `url` |
| `geo_lfs_objects` | Gauge | 10.2 | Number of LFS objects on primary | `url` |
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 288a4fc9a39..84602a7c022 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -385,6 +385,20 @@ four standard [pagination arguments](#pagination-arguments):
| ---- | ---- | ----------- |
| `workflowId` | [`AiDuoWorkflowsWorkflowID!`](#aiduoworkflowsworkflowid) | Array of request IDs to fetch. |
+### `Query.duoWorkflowWorkflows`
+
+List the workflows owned by the current user.
+
+DETAILS:
+**Introduced** in GitLab 17.2.
+**Status**: Experiment.
+
+Returns [`DuoWorkflowConnection!`](#duoworkflowconnection).
+
+This field returns a [connection](#connections). It accepts the
+four standard [pagination arguments](#pagination-arguments):
+`before: String`, `after: String`, `first: Int`, and `last: Int`.
+
### `Query.echo`
Testing endpoint to validate the API with.
@@ -12554,6 +12568,29 @@ The edge type for [`DoraPerformanceScoreCount`](#doraperformancescorecount).
| `cursor` | [`String!`](#string) | A cursor for use in pagination. |
| `node` | [`DoraPerformanceScoreCount`](#doraperformancescorecount) | The item at the end of the edge. |
+#### `DuoWorkflowConnection`
+
+The connection type for [`DuoWorkflow`](#duoworkflow).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `edges` | [`[DuoWorkflowEdge]`](#duoworkflowedge) | A list of edges. |
+| `nodes` | [`[DuoWorkflow]`](#duoworkflow) | A list of nodes. |
+| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+#### `DuoWorkflowEdge`
+
+The edge type for [`DuoWorkflow`](#duoworkflow).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| `node` | [`DuoWorkflow`](#duoworkflow) | The item at the end of the edge. |
+
#### `DuoWorkflowEventConnection`
The connection type for [`DuoWorkflowEvent`](#duoworkflowevent).
@@ -20766,6 +20803,21 @@ Aggregated DORA score counts for projects for the last complete month.
| `metricName` | [`String!`](#string) | Name of the DORA metric. |
| `noDataProjectsCount` | [`Int`](#int) | Number of projects with no data for the metric. |
+### `DuoWorkflow`
+
+A Duo Workflow.
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| `createdAt` | [`Time!`](#time) | Timestamp of when the workflow was created. |
+| `humanStatus` | [`String!`](#string) | Human-readable status of the workflow. |
+| `id` | [`ID!`](#id) | ID of the workflow. |
+| `projectId` | [`ProjectID!`](#projectid) | ID of the project. |
+| `updatedAt` | [`Time!`](#time) | Timestamp of when the workflow was last updated. |
+| `userId` | [`UserID!`](#userid) | ID of the user. |
+
### `DuoWorkflowEvent`
Events that describe the history and progress of a Duo Workflow.
diff --git a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
index 53787b5b772..83300f42cdb 100644
--- a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
+++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
@@ -129,7 +129,7 @@ We'll use this variable in the `.gitlab-ci.yml` later, to easily connect to our

-We also need to add the public key to **Project** > **Settings** > **Repository** as a [Deploy Key](../../../user/project/deploy_keys/index.md), which gives us the ability to access our repository from the server through the SSH protocol.
+We also need to add the public key to **Project** > **Settings** > **Repository** as a [deploy key](../../../user/project/deploy_keys/index.md), which gives us the ability to access our repository from the server through the SSH protocol.
```shell
# As the deployer user on the server
diff --git a/doc/user/analytics/value_streams_dashboard.md b/doc/user/analytics/value_streams_dashboard.md
index 61114510b42..48fee668eaf 100644
--- a/doc/user/analytics/value_streams_dashboard.md
+++ b/doc/user/analytics/value_streams_dashboard.md
@@ -158,6 +158,7 @@ If multiple topics are provided, all topics must match for the project to be inc
### AI Impact analytics
DETAILS:
+**Tier:** For a limited time, Ultimate. In the future, GitLab Duo Enterprise.
**Offering:** GitLab.com, Self-managed
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/443696) in GitLab 16.11 [with a flag](../../administration/feature_flags.md) named `ai_impact_analytics_dashboard`. Disabled by default.
diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md
index c04f5439aeb..f40776852bc 100644
--- a/doc/user/application_security/container_scanning/index.md
+++ b/doc/user/application_security/container_scanning/index.md
@@ -709,7 +709,7 @@ Unlike regular container scanning, the scan results do not include a security re
When security findings are identified, GitLab populates the [Vulnerability Report](../vulnerability_report/index.md) with these findings. Vulnerabilities can be viewed under the **Container registry vulnerabilities** tab of the Vulnerability Report page.
NOTE:
-Container Scanning for Registry only populates the Vulnerability Report when a new advisory is published to the [GitLab Advisory Database](../gitlab_advisory_database/index.md). Future iterations will populate the Vulnerability Report with all present advisory data (instead of only newely detected data). For more details, see [epic 8026](https://gitlab.com/groups/gitlab-org/-/epics/8026).
+Container Scanning for Registry populates the Vulnerability Report only when a new advisory is published to the [GitLab Advisory Database](../gitlab_advisory_database/index.md). Support for populating the Vulnerability Report with all present advisory data, instead of only newly-detected data, is proposed in [epic 8026](https://gitlab.com/groups/gitlab-org/-/epics/8026).
### Prerequisites
diff --git a/doc/user/compliance/audit_event_schema.md b/doc/user/compliance/audit_event_schema.md
index 7fd8fba29bd..8bdba3f6a7c 100644
--- a/doc/user/compliance/audit_event_schema.md
+++ b/doc/user/compliance/audit_event_schema.md
@@ -96,7 +96,7 @@ Streaming audit events can be sent when authenticated users push, pull, or clone
Audit events are not captured for users that are not signed in. For example, when downloading a public project.
-### Example: audit event payloads for Git over SSH events with Deploy Key
+### Example: audit event payloads for Git over SSH events with deploy key
Fetch:
diff --git a/doc/user/group/epics/epic_work_items.md b/doc/user/group/epics/epic_work_items.md
index 23e94257da9..10d28c9b176 100644
--- a/doc/user/group/epics/epic_work_items.md
+++ b/doc/user/group/epics/epic_work_items.md
@@ -50,7 +50,6 @@ Because this is an experimental feature,
| Flag | Description | Actor | Status | Milestone |
| --------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ----- | ------ | ------ |
| `work_item_epics` | Consolidated flag that contains all the changes needed to get epic work items to work for a given group. | Group | **Required** | 17.2 |
-| `synced_epic_work_item_editable` | Allows editing epic work items when they have a legacy epic. | Group | **Required** | 17.2 |
| `work_items_rolledup_dates` | Calculates the start and due dates in a hierarchy for work items. | Group | **Required** | 17.2 |
| `epic_and_work_item_associations_unification` | Delegates other epic and work item associations. | Group | **Required** | 17.2 |
| `work_item_epics_rollout` | Feature flag per user to enable or disable the new work item view as the default experience. | User | **Required** | 17.3 |
diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb
index c238300af74..c6397a79cdf 100644
--- a/lib/api/deploy_keys.rb
+++ b/lib/api/deploy_keys.rb
@@ -204,7 +204,7 @@ module API
if key
present key, with: Entities::DeployKey
else
- not_found!('Deploy Key')
+ not_found!('Deploy key')
end
end
@@ -222,7 +222,7 @@ module API
# rubocop: disable CodeReuse/ActiveRecord
delete ":id/deploy_keys/:key_id" do
deploy_key_project = user_project.deploy_keys_projects.find_by(deploy_key_id: params[:key_id])
- not_found!('Deploy Key') unless deploy_key_project
+ not_found!('Deploy key') unless deploy_key_project
destroy_conditionally!(deploy_key_project)
end
diff --git a/lib/gitlab/audit/deploy_key_author.rb b/lib/gitlab/audit/deploy_key_author.rb
index 53029e9cc1c..886b6818b77 100644
--- a/lib/gitlab/audit/deploy_key_author.rb
+++ b/lib/gitlab/audit/deploy_key_author.rb
@@ -8,7 +8,7 @@ module Gitlab
end
def name
- @name || _('Deploy Key')
+ @name || _('Deploy key')
end
end
end
diff --git a/lib/sidebars/admin/panel.rb b/lib/sidebars/admin/panel.rb
index 92985c636c1..791d97db8f0 100644
--- a/lib/sidebars/admin/panel.rb
+++ b/lib/sidebars/admin/panel.rb
@@ -25,7 +25,7 @@ module Sidebars
add_menu(Sidebars::Admin::Menus::AnalyticsMenu.new(context))
add_menu(Sidebars::Admin::Menus::MonitoringMenu.new(context))
add_menu(Sidebars::Admin::Menus::MessagesMenu.new(context))
- add_menu(Sidebars::Admin::Menus::SystemHooksMenu.new(context))
+ add_menu(Sidebars::Admin::Menus::SystemHooksMenu.new(context)) if system_hooks?
add_menu(Sidebars::Admin::Menus::ApplicationsMenu.new(context))
add_menu(Sidebars::Admin::Menus::AbuseReportsMenu.new(context))
add_menu(Sidebars::Admin::Menus::KubernetesMenu.new(context))
@@ -34,6 +34,12 @@ module Sidebars
add_menu(Sidebars::Admin::Menus::LabelsMenu.new(context))
add_menu(Sidebars::Admin::Menus::AdminSettingsMenu.new(context))
end
+
+ private
+
+ def system_hooks?
+ !Gitlab.com? # rubocop:disable Gitlab/AvoidGitlabInstanceChecks -- Not related to SaaS offerings
+ end
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 4d45fe2e227..ed737dbc384 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -17903,9 +17903,6 @@ msgid_plural "Deploys"
msgstr[0] ""
msgstr[1] ""
-msgid "Deploy Key"
-msgstr ""
-
msgid "Deploy Token"
msgstr ""
@@ -17915,6 +17912,9 @@ msgstr ""
msgid "Deploy freezes"
msgstr ""
+msgid "Deploy key"
+msgstr ""
+
msgid "Deploy key was successfully updated."
msgstr ""
@@ -19679,9 +19679,6 @@ msgstr ""
msgid "Edit Comment"
msgstr ""
-msgid "Edit Deploy Key"
-msgstr ""
-
msgid "Edit Geo Site"
msgstr ""
@@ -34878,7 +34875,7 @@ msgstr ""
msgid "No confirmation email received? Check your spam folder or %{request_link_start}request new confirmation email%{request_link_end}."
msgstr ""
-msgid "No connection could be made to a Gitaly Server, please check your logs!"
+msgid "No connection could be made to a Gitaly server, please check your logs!"
msgstr ""
msgid "No contributions"
diff --git a/scripts/internal_events/cli.rb b/scripts/internal_events/cli.rb
index 50aceea93c5..e2c231a444f 100755
--- a/scripts/internal_events/cli.rb
+++ b/scripts/internal_events/cli.rb
@@ -113,7 +113,7 @@ class Cli
def proceed_to_event_definition
new_page!
- cli.say format_info("Okay! The next step is adding a new event! (~5 min)\n")
+ cli.say format_info("Okay! The next step is adding a new event! (~5-10 min)\n")
return not_ready_error('New Event') unless cli.yes?(format_prompt('Ready to start?'))
diff --git a/spec/controllers/admin/hooks_controller_spec.rb b/spec/controllers/admin/hooks_controller_spec.rb
index 06fb3811a79..03bd4e84ce9 100644
--- a/spec/controllers/admin/hooks_controller_spec.rb
+++ b/spec/controllers/admin/hooks_controller_spec.rb
@@ -9,12 +9,42 @@ RSpec.describe Admin::HooksController, feature_category: :webhooks do
sign_in(admin)
end
+ shared_examples 'disabled on GitLab.com' do
+ let(:gitlab_com?) { false }
+
+ before do
+ allow(::Gitlab).to receive(:com?) { gitlab_com? }
+ end
+
+ context 'when on GitLab.com' do
+ let(:gitlab_com?) { true }
+
+ it 'responds with a not_found status' do
+ subject
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when not on GitLab.com' do
+ it 'does not respond with a not_found status' do
+ subject
+ expect(response).not_to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ describe 'GET #index' do
+ subject(:get_index) { get :index }
+
+ it_behaves_like 'disabled on GitLab.com'
+ end
+
describe 'POST #create' do
- it 'sets all parameters' do
- hook_params = {
+ let_it_be(:hook_params) do
+ {
enable_ssl_verification: true,
- token: "TEST TOKEN",
- url: "http://example.com",
+ token: 'TEST TOKEN',
+ url: 'http://example.com',
push_events: true,
tag_push_events: false,
@@ -22,39 +52,27 @@ RSpec.describe Admin::HooksController, feature_category: :webhooks do
merge_requests_events: false,
url_variables: [{ key: 'token', value: 'some secret value' }]
}
+ end
- post :create, params: { hook: hook_params }
+ subject(:post_create) { post :create, params: { hook: hook_params } }
+
+ it 'sets all parameters' do
+ post_create
expect(response).to have_gitlab_http_status(:found)
expect(SystemHook.all.size).to eq(1)
expect(SystemHook.first).to have_attributes(hook_params.except(:url_variables))
expect(SystemHook.first).to have_attributes(url_variables: { 'token' => 'some secret value' })
end
+
+ it_behaves_like 'disabled on GitLab.com'
end
describe 'POST #update' do
let_it_be_with_reload(:hook) { create(:system_hook) }
- context 'with an existing token' do
- hook_params = {
- token: WebHook::SECRET_MASK,
- url: "http://example.com"
- }
-
- it 'does not change a token' do
- expect do
- post :update, params: { id: hook.id, hook: hook_params }
- end.not_to change { hook.reload.token }
-
- expect(response).to have_gitlab_http_status(:found)
- expect(flash[:alert]).to be_blank
- end
- end
-
- it 'sets all parameters' do
- hook.update!(url_variables: { 'foo' => 'bar', 'baz' => 'woo' })
-
- hook_params = {
+ let_it_be(:hook_params) do
+ {
url: 'http://example.com/{bar}?token={token}',
enable_ssl_verification: false,
url_variables: [
@@ -64,8 +82,30 @@ RSpec.describe Admin::HooksController, feature_category: :webhooks do
{ key: 'bar', value: 'qux' }
]
}
+ end
- put :update, params: { id: hook.id, hook: hook_params }
+ subject(:put_update) { put :update, params: { id: hook.id, hook: hook_params } }
+
+ context 'with an existing token' do
+ let_it_be(:hook_params) do
+ {
+ token: WebHook::SECRET_MASK,
+ url: 'http://example.com'
+ }
+ end
+
+ it 'does not change a token' do
+ expect { put_update }.not_to change { hook.reload.token }
+
+ expect(response).to have_gitlab_http_status(:found)
+ expect(flash[:alert]).to be_blank
+ end
+ end
+
+ it 'sets all parameters' do
+ hook.update!(url_variables: { 'foo' => 'bar', 'baz' => 'woo' })
+
+ put_update
hook.reload
@@ -76,6 +116,8 @@ RSpec.describe Admin::HooksController, feature_category: :webhooks do
url_variables: { 'token' => 'some secret value', 'bar' => 'qux' }
)
end
+
+ it_behaves_like 'disabled on GitLab.com'
end
describe 'DELETE #destroy' do
@@ -84,5 +126,9 @@ RSpec.describe Admin::HooksController, feature_category: :webhooks do
let(:params) { { id: hook } }
it_behaves_like 'Web hook destroyer'
+
+ it_behaves_like 'disabled on GitLab.com' do
+ subject { delete :destroy, params: params }
+ end
end
end
diff --git a/spec/controllers/projects/deploy_keys_controller_spec.rb b/spec/controllers/projects/deploy_keys_controller_spec.rb
index c203effff17..4e90669e100 100644
--- a/spec/controllers/projects/deploy_keys_controller_spec.rb
+++ b/spec/controllers/projects/deploy_keys_controller_spec.rb
@@ -147,7 +147,7 @@ RSpec.describe Projects::DeployKeysController, feature_category: :continuous_del
post :create, params: create_params
expect(assigns(:key).errors.count).to be > 1
- expect(flash[:alert]).to eq('Deploy Key must be a supported SSH public key.')
end
end
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
index f87d7893436..b55bd2c4780 100644
--- a/spec/features/boards/new_issue_spec.rb
+++ b/spec/features/boards/new_issue_spec.rb
@@ -13,220 +13,226 @@ RSpec.describe 'Issue Boards new issue', :js, feature_category: :portfolio_manag
let(:board_list_header) { first('[data-testid="board-list-header"]') }
let(:project_select_dropdown) { find_by_testid('project-select-dropdown') }
- context 'authorized user' do
+ context 'when issues drawer is disabled' do
before do
- project.add_maintainer(user)
-
- sign_in(user)
-
- visit project_board_path(project, board)
-
- wait_for_requests
-
- expect(page).to have_selector('.board', count: 3)
+ stub_feature_flags(issues_list_drawer: false)
end
- it 'displays new issue button' do
- expect(first('.board')).to have_button('Create new issue', count: 1)
- end
+ context 'authorized user' do
+ before do
+ project.add_maintainer(user)
- it 'does not display new issue button in closed list' do
- page.within('.board:nth-child(3)') do
- expect(page).not_to have_button('Create new issue')
- end
- end
+ sign_in(user)
- it 'shows form when clicking button' do
- page.within(first('.board')) do
- click_button 'Create new issue'
-
- expect(page).to have_selector('.board-new-issue-form')
- end
- end
-
- it 'hides form when clicking cancel' do
- page.within(first('.board')) do
- click_button 'Create new issue'
-
- expect(page).to have_selector('.board-new-issue-form')
-
- click_button 'Cancel'
-
- expect(page).not_to have_selector('.board-new-issue-form')
- end
- end
-
- it 'creates new issue, places it on top of the list, and opens sidebar' do
- page.within(first('.board')) do
- click_button 'Create new issue'
- end
-
- page.within(first('.board-new-issue-form')) do
- find('.form-control').set('bug')
- click_button 'Create issue'
- end
-
- wait_for_requests
-
- page.within(first('.board [data-testid="issue-count-badge"]')) do
- expect(page).to have_content('2')
- end
-
- page.within(first('.board-card')) do
- issue = project.issues.find_by_title('bug')
-
- expect(issue.relative_position).to be < existing_issue.relative_position
-
- expect(page).to have_content(issue.to_reference)
- expect(page).to have_link(issue.title, href: /#{issue_path(issue)}/)
- end
-
- expect(page).to have_selector('[data-testid="issue-boards-sidebar"]')
- end
-
- it 'successfully loads labels to be added to newly created issue' do
- page.within(first('.board')) do
- click_button 'Create new issue'
- end
-
- page.within(first('.board-new-issue-form')) do
- find('.form-control').set('new issue')
- click_button 'Create issue'
- end
-
- wait_for_requests
-
- within_testid('sidebar-labels') do
- click_button 'Edit'
+ visit project_board_path(project, board)
wait_for_requests
- expect(page).to have_content 'Label 1'
+ expect(page).to have_selector('.board', count: 3)
end
- end
- it 'allows creating an issue in newly created list' do
- click_button 'New list'
- wait_for_all_requests
+ it 'displays new issue button' do
+ expect(first('.board')).to have_button('Create new issue', count: 1)
+ end
- click_button 'Select a label'
- find('label', text: label.title).click
- click_button 'Add to board'
+ it 'does not display new issue button in closed list' do
+ page.within('.board:nth-child(3)') do
+ expect(page).not_to have_button('Create new issue')
+ end
+ end
- wait_for_all_requests
+ it 'shows form when clicking button' do
+ page.within(first('.board')) do
+ click_button 'Create new issue'
- page.within('.board:nth-child(2)') do
- click_button('Create new issue')
+ expect(page).to have_selector('.board-new-issue-form')
+ end
+ end
+
+ it 'hides form when clicking cancel' do
+ page.within(first('.board')) do
+ click_button 'Create new issue'
+
+ expect(page).to have_selector('.board-new-issue-form')
+
+ click_button 'Cancel'
+
+ expect(page).not_to have_selector('.board-new-issue-form')
+ end
+ end
+
+ it 'creates new issue, places it on top of the list, and opens sidebar' do
+ page.within(first('.board')) do
+ click_button 'Create new issue'
+ end
+
+ page.within(first('.board-new-issue-form')) do
+ find('.form-control').set('bug')
+ click_button 'Create issue'
+ end
+
+ wait_for_requests
+
+ page.within(first('.board [data-testid="issue-count-badge"]')) do
+ expect(page).to have_content('2')
+ end
+
+ page.within(first('.board-card')) do
+ issue = project.issues.find_by_title('bug')
+
+ expect(issue.relative_position).to be < existing_issue.relative_position
+
+ expect(page).to have_content(issue.to_reference)
+ expect(page).to have_link(issue.title, href: /#{issue_path(issue)}/)
+ end
+
+ expect(page).to have_selector('[data-testid="issue-boards-sidebar"]')
+ end
+
+ it 'successfully loads labels to be added to newly created issue' do
+ page.within(first('.board')) do
+ click_button 'Create new issue'
+ end
page.within(first('.board-new-issue-form')) do
find('.form-control').set('new issue')
click_button 'Create issue'
end
+ wait_for_requests
+
+ within_testid('sidebar-labels') do
+ click_button 'Edit'
+
+ wait_for_requests
+
+ expect(page).to have_content 'Label 1'
+ end
+ end
+
+ it 'allows creating an issue in newly created list' do
+ click_button 'New list'
wait_for_all_requests
- page.within('.board-card') do
- expect(page).to have_content 'new issue'
+ click_button 'Select a label'
+ find('label', text: label.title).click
+ click_button 'Add to board'
+
+ wait_for_all_requests
+
+ page.within('.board:nth-child(2)') do
+ click_button('Create new issue')
+
+ page.within(first('.board-new-issue-form')) do
+ find('.form-control').set('new issue')
+ click_button 'Create issue'
+ end
+
+ wait_for_all_requests
+
+ page.within('.board-card') do
+ expect(page).to have_content 'new issue'
+ end
end
end
end
- end
- context 'unauthorized user' do
- before do
- visit project_board_path(project, board)
- wait_for_requests
- end
-
- it 'does not display new issue button in open list' do
- expect(first('.board')).not_to have_button('Create new issue')
- end
-
- it 'does not display new issue button in label list' do
- page.within('.board:nth-child(2)') do
- expect(page).not_to have_button('Create new issue')
- end
- end
- end
-
- context 'group boards' do
- let_it_be(:group) { create(:group, :public) }
- let_it_be(:project) { create(:project, namespace: group, name: "root project") }
- let_it_be(:subgroup) { create(:group, parent: group) }
- let_it_be(:subproject1) { create(:project, group: subgroup, name: "sub project1") }
- let_it_be(:subproject2) { create(:project, group: subgroup, name: "sub project2") }
- let_it_be(:group_board) { create(:board, group: group) }
- let_it_be(:project_label) { create(:label, project: project, name: 'label') }
- let_it_be(:list) { create(:list, board: group_board, label: project_label, position: 0) }
-
- context 'for unauthorized users' do
+ context 'unauthorized user' do
before do
- visit group_board_path(group, group_board)
+ visit project_board_path(project, board)
wait_for_requests
end
- context 'when backlog does not exist' do
- it 'does not display new issue button in label list' do
- page.within('.board.is-draggable') do
- expect(page).not_to have_button('Create new issue')
- end
- end
+ it 'does not display new issue button in open list' do
+ expect(first('.board')).not_to have_button('Create new issue')
end
- context 'when backlog list already exists' do
- it 'does not display new issue button in open list' do
- expect(first('.board')).not_to have_button('Create new issue')
- end
-
- it 'does not display new issue button in label list' do
- page.within('.board.is-draggable') do
- expect(page).not_to have_button('Create new issue')
- end
+ it 'does not display new issue button in label list' do
+ page.within('.board:nth-child(2)') do
+ expect(page).not_to have_button('Create new issue')
end
end
end
- context 'for authorized users' do
- before do
- project.add_reporter(user)
- subproject1.add_reporter(user)
+ context 'group boards' do
+ let_it_be(:group) { create(:group, :public) }
+ let_it_be(:project) { create(:project, namespace: group, name: "root project") }
+ let_it_be(:subgroup) { create(:group, parent: group) }
+ let_it_be(:subproject1) { create(:project, group: subgroup, name: "sub project1") }
+ let_it_be(:subproject2) { create(:project, group: subgroup, name: "sub project2") }
+ let_it_be(:group_board) { create(:board, group: group) }
+ let_it_be(:project_label) { create(:label, project: project, name: 'label') }
+ let_it_be(:list) { create(:list, board: group_board, label: project_label, position: 0) }
- sign_in(user)
- visit group_board_path(group, group_board)
- wait_for_requests
- end
-
- context 'when backlog does not exist' do
+ context 'for unauthorized users' do
before do
- group_board.lists.backlog.delete_all
- end
-
- it 'display new issue button in label list' do
- expect(board_list_header).to have_button('Create new issue')
- end
- end
-
- context 'project select dropdown' do
- before do
- page.within(board_list_header) do
- click_button 'Create new issue'
- end
-
- project_select_dropdown.click
-
+ visit group_board_path(group, group_board)
wait_for_requests
end
- it 'lists a project which is a direct descendant of the top-level group' do
- expect(project_select_dropdown).to have_selector("li", text: "root project")
+ context 'when backlog does not exist' do
+ it 'does not display new issue button in label list' do
+ page.within('.board.is-draggable') do
+ expect(page).not_to have_button('Create new issue')
+ end
+ end
end
- it 'lists a project that belongs to a subgroup' do
- expect(project_select_dropdown).to have_selector("li", text: "sub project1")
+ context 'when backlog list already exists' do
+ it 'does not display new issue button in open list' do
+ expect(first('.board')).not_to have_button('Create new issue')
+ end
+
+ it 'does not display new issue button in label list' do
+ page.within('.board.is-draggable') do
+ expect(page).not_to have_button('Create new issue')
+ end
+ end
+ end
+ end
+
+ context 'for authorized users' do
+ before do
+ project.add_reporter(user)
+ subproject1.add_reporter(user)
+
+ sign_in(user)
+ visit group_board_path(group, group_board)
+ wait_for_requests
end
- it "does not list projects to which user doesn't have access" do
- expect(project_select_dropdown).not_to have_selector("li", text: "sub project2")
+ context 'when backlog does not exist' do
+ before do
+ group_board.lists.backlog.delete_all
+ end
+
+ it 'display new issue button in label list' do
+ expect(board_list_header).to have_button('Create new issue')
+ end
+ end
+
+ context 'project select dropdown' do
+ before do
+ page.within(board_list_header) do
+ click_button 'Create new issue'
+ end
+
+ project_select_dropdown.click
+
+ wait_for_requests
+ end
+
+ it 'lists a project which is a direct descendant of the top-level group' do
+ expect(project_select_dropdown).to have_selector("li", text: "root project")
+ end
+
+ it 'lists a project that belongs to a subgroup' do
+ expect(project_select_dropdown).to have_selector("li", text: "sub project1")
+ end
+
+ it "does not list projects to which user doesn't have access" do
+ expect(project_select_dropdown).not_to have_selector("li", text: "sub project2")
+ end
end
end
end
diff --git a/spec/features/boards/sidebar_labels_spec.rb b/spec/features/boards/sidebar_labels_spec.rb
index 79dbb999295..676cad4aefa 100644
--- a/spec/features/boards/sidebar_labels_spec.rb
+++ b/spec/features/boards/sidebar_labels_spec.rb
@@ -27,14 +27,17 @@ RSpec.describe 'Project issue boards sidebar labels', :js, feature_category: :po
before do
project.add_maintainer(user)
-
- sign_in(user)
-
- visit project_board_path(project, board)
- wait_for_requests
end
- context 'labels' do
+ context 'when issues drawer is disabled' do
+ before do
+ stub_feature_flags(issues_list_drawer: false)
+ sign_in(user)
+
+ visit project_board_path(project, board)
+ wait_for_requests
+ end
+
it 'shows current labels when editing' do
click_card(card)
@@ -222,4 +225,196 @@ RSpec.describe 'Project issue boards sidebar labels', :js, feature_category: :po
expect(page).to have_selector('.board', count: 4)
end
end
+
+ context 'when issues drawer is enabled' do
+ let(:labels_widget) { find_by_testid('work-item-labels') }
+
+ before do
+ sign_in(user)
+
+ visit project_board_path(project, board)
+ wait_for_requests
+ end
+
+ it 'shows current labels when editing' do
+ click_card(card)
+
+ page.within(labels_widget) do
+ click_button 'Edit'
+
+ wait_for_requests
+
+ expect(page).to have_selector('.gl-new-dropdown-item-check-icon', count: 2)
+ expect(page).to have_content(development.title)
+ expect(page).to have_content(stretch.title)
+ end
+ end
+
+ it 'adds a single label' do
+ click_card(card)
+
+ page.within(labels_widget) do
+ click_button 'Edit'
+
+ wait_for_requests
+
+ find_label(bug.title).click
+ click_button 'Apply'
+
+ wait_for_requests
+
+ expect(page).to have_selector('.gl-label-text', count: 3)
+ expect(page).to have_content(bug.title)
+ end
+
+ find_by_testid('close-icon').click
+
+ wait_for_requests
+
+ # 'Development' label does not show since the card is in a 'Development' list label
+ expect(card).to have_selector('.gl-label', count: 2)
+ expect(card).to have_content(bug.title)
+
+ # Card is duplicated in the 'Bug' list
+ page.within(bug_list) do
+ expect(page).to have_selector('.board-card', count: 1)
+ expect(page).to have_content(issue2.title)
+ expect(find('.board-card')).to have_content(development.title)
+ end
+ end
+
+ it 'adds a multiple labels' do
+ click_card(card)
+
+ page.within(labels_widget) do
+ click_button 'Edit'
+
+ wait_for_requests
+
+ find_label(bug.title).click
+ find_label(regression.title).click
+
+ click_button 'Apply'
+
+ wait_for_requests
+
+ expect(page).to have_selector('.gl-label-text', count: 4)
+ expect(page).to have_content(bug.title)
+ expect(page).to have_content(regression.title)
+ end
+
+ # 'Development' label does not show since the card is in a 'Development' list label
+ expect(card).to have_selector('.gl-label', count: 3)
+ expect(card).to have_content(bug.title)
+ expect(card).to have_content(regression.title)
+ end
+
+ it 'removes a label and moves card to backlog' do
+ click_card(card)
+
+ page.within(labels_widget) do
+ click_button 'Edit'
+
+ wait_for_requests
+
+ find_label(development.title).click
+
+ click_button 'Apply'
+
+ wait_for_requests
+ end
+
+ find_by_testid('close-icon').click
+
+ wait_for_requests
+
+ # Card is moved to the 'Backlog' list
+ page.within(backlog_list) do
+ expect(page).to have_selector('.board-card', count: 2)
+ expect(page).to have_content(issue2.title)
+ end
+
+ # Card is moved away from the 'Development' list
+ page.within(development_list) do
+ expect(page).to have_selector('.board-card', count: 1)
+ expect(page).not_to have_content(issue2.title)
+ end
+ end
+
+ it 'adds a label to backlog card and moves the card to the list' do
+ click_card(backlog_card)
+
+ page.within(labels_widget) do
+ click_button 'Edit'
+
+ wait_for_requests
+
+ find_label(development.title).click
+
+ click_button 'Apply'
+
+ wait_for_requests
+ end
+
+ find_by_testid('close-icon').click
+
+ wait_for_requests
+
+ # Card is removed from backlog
+ page.within(backlog_list) do
+ expect(page).to have_selector('.board-card', count: 0)
+ end
+
+ # Card is shown in the 'Development' list
+ page.within(development_list) do
+ expect(page).to have_selector('.board-card', count: 3)
+ expect(page).to have_content(issue3.title)
+ end
+ end
+
+ it 'removes a label' do
+ click_card(card)
+
+ page.within(labels_widget) do
+ click_button 'Edit'
+
+ wait_for_requests
+
+ find_label(stretch.title).click
+
+ click_button 'Apply'
+
+ wait_for_requests
+
+ expect(page).to have_selector('.gl-label-text', count: 1)
+ expect(page).not_to have_content(stretch.title)
+ end
+
+ # 'Development' label does not show since the card is in a 'Development' list label
+ expect(card).to have_selector('.gl-label-text', count: 0)
+ expect(card).not_to have_content(stretch.title)
+ end
+
+ it 'creates project label' do
+ click_card(card)
+
+ page.within(labels_widget) do
+ click_button 'Edit'
+ wait_for_requests
+
+ click_on 'Create project label'
+ fill_in 'Label name', with: 'test label'
+ first('.suggested-colors a').click
+ click_button 'Create'
+ wait_for_requests
+
+ expect(page).to have_content('test label')
+ end
+ expect(page).to have_selector('.board', count: 4)
+ end
+ end
+
+ def find_label(title)
+ find('li', text: title, match: :prefer_exact)
+ end
end
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index ad73661415e..2a567ca1f73 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -16,15 +16,32 @@ RSpec.describe 'Project issue boards sidebar', :js, feature_category: :portfolio
before do
project.add_maintainer(user)
-
- sign_in(user)
-
- visit project_board_path(project, board)
-
- wait_for_requests
end
- it_behaves_like 'issue boards sidebar'
+ context 'when issues drawer is disabled' do
+ before do
+ stub_feature_flags(issues_list_drawer: false)
+ sign_in(user)
+
+ visit project_board_path(project, board)
+
+ wait_for_requests
+ end
+
+ it_behaves_like 'issue boards sidebar'
+ end
+
+ context 'when issues drawer is enabled' do
+ before do
+ sign_in(user)
+
+ visit project_board_path(project, board)
+
+ wait_for_requests
+ end
+
+ it_behaves_like 'work item drawer'
+ end
def first_card
find('.board:nth-child(1)').first("[data-testid='board-card']")
diff --git a/spec/features/projects/work_items/work_items_list_filters_spec.rb b/spec/features/projects/work_items/work_items_list_filters_spec.rb
new file mode 100644
index 00000000000..64b409cc9e9
--- /dev/null
+++ b/spec/features/projects/work_items/work_items_list_filters_spec.rb
@@ -0,0 +1,208 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Work items list filters', :js, feature_category: :team_planning do
+ include FilteredSearchHelpers
+
+ let_it_be(:user1) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, :public, group: group, developers: [user1, user2]) }
+
+ let_it_be(:label1) { create(:label, project: project) }
+ let_it_be(:label2) { create(:label, project: project) }
+
+ let_it_be(:milestone1) { create(:milestone, group: group, start_date: 5.days.ago, due_date: 13.days.from_now) }
+ let_it_be(:milestone2) { create(:milestone, group: group, start_date: 2.days.from_now, due_date: 9.days.from_now) }
+
+ let_it_be(:incident) do
+ create(:incident, project: project, assignees: [user1], author: user1, labels: [label1], description: 'aaa')
+ end
+
+ let_it_be(:issue) do
+ create(:issue, project: project, author: user1, labels: [label1, label2], milestone: milestone1, title: 'eee')
+ end
+
+ let_it_be(:task) do
+ create(:work_item, :task, project: project, assignees: [user2], author: user2, milestone: milestone2)
+ end
+
+ context 'for signed in user' do
+ before do
+ sign_in(user1)
+ visit group_work_items_path(group)
+ end
+
+ describe 'assignees' do
+ it 'filters', :aggregate_failures do
+ select_tokens 'Assignee', '=', user1.username, submit: true
+
+ expect(page).to have_css('.issue', count: 1)
+ expect(page).to have_link(incident.title)
+
+ click_button 'Clear'
+
+ select_tokens 'Assignee', '!=', user1.username, submit: true
+
+ expect(page).to have_css('.issue', count: 2)
+ expect(page).to have_link(issue.title)
+ expect(page).to have_link(task.title)
+
+ click_button 'Clear'
+
+ select_tokens 'Assignee', '||', user1.username, 'Assignee', '||', user2.username, submit: true
+
+ expect(page).to have_css('.issue', count: 2)
+ expect(page).to have_link(incident.title)
+ expect(page).to have_link(task.title)
+
+ click_button 'Clear'
+
+ select_tokens 'Assignee', '=', 'None', submit: true
+
+ expect(page).to have_css('.issue', count: 1)
+ expect(page).to have_link(issue.title)
+
+ click_button 'Clear'
+
+ select_tokens 'Assignee', '=', 'Any', submit: true
+
+ expect(page).to have_css('.issue', count: 2)
+ expect(page).to have_link(incident.title)
+ expect(page).to have_link(task.title)
+ end
+ end
+
+ describe 'author' do
+ it 'filters', :aggregate_failures do
+ select_tokens 'Author', '=', user1.username, submit: true
+
+ expect(page).to have_css('.issue', count: 2)
+ expect(page).to have_link(incident.title)
+ expect(page).to have_link(issue.title)
+
+ click_button 'Clear'
+
+ select_tokens 'Author', '!=', user1.username, submit: true
+
+ expect(page).to have_css('.issue', count: 1)
+ expect(page).to have_link(task.title)
+
+ click_button 'Clear'
+
+ select_tokens 'Author', '||', user1.username, 'Author', '||', user2.username, submit: true
+
+ expect(page).to have_css('.issue', count: 3)
+ expect(page).to have_link(incident.title)
+ expect(page).to have_link(issue.title)
+ expect(page).to have_link(task.title)
+ end
+ end
+
+ describe 'labels' do
+ it 'filters', :aggregate_failures do
+ select_tokens 'Label', '=', label1.title, submit: true
+
+ expect(page).to have_css('.issue', count: 2)
+ expect(page).to have_link(incident.title)
+ expect(page).to have_link(issue.title)
+
+ click_button 'Clear'
+
+ select_tokens 'Label', '!=', label1.title, submit: true
+
+ expect(page).to have_css('.issue', count: 1)
+ expect(page).to have_link(task.title)
+
+ click_button 'Clear'
+
+ select_tokens 'Label', '||', label1.title, 'Label', '||', label2.title, submit: true
+
+ expect(page).to have_css('.issue', count: 2)
+ expect(page).to have_link(incident.title)
+ expect(page).to have_link(issue.title)
+
+ click_button 'Clear'
+
+ select_tokens 'Label', '=', 'None', submit: true
+
+ expect(page).to have_css('.issue', count: 1)
+ expect(page).to have_link(task.title)
+
+ click_button 'Clear'
+
+ select_tokens 'Label', '=', 'Any', submit: true
+
+ expect(page).to have_css('.issue', count: 2)
+ expect(page).to have_link(incident.title)
+ expect(page).to have_link(issue.title)
+ end
+ end
+
+ describe 'milestones' do
+ it 'filters', :aggregate_failures do
+ select_tokens 'Milestone', '=', milestone1.title, submit: true
+
+ expect(page).to have_css('.issue', count: 1)
+ expect(page).to have_link(issue.title)
+
+ click_button 'Clear'
+
+ select_tokens 'Milestone', '!=', milestone1.title, submit: true
+
+ expect(page).to have_css('.issue', count: 2)
+ expect(page).to have_link(incident.title)
+ expect(page).to have_link(task.title)
+
+ click_button 'Clear'
+
+ select_tokens 'Milestone', '=', 'None', submit: true
+
+ expect(page).to have_css('.issue', count: 1)
+ expect(page).to have_link(incident.title)
+
+ click_button 'Clear'
+
+ select_tokens 'Milestone', '=', 'Any', submit: true
+
+ expect(page).to have_css('.issue', count: 2)
+ expect(page).to have_link(issue.title)
+ expect(page).to have_link(task.title)
+
+ click_button 'Clear'
+
+ select_tokens 'Milestone', '=', 'Upcoming', submit: true
+
+ expect(page).to have_css('.issue', count: 1)
+ expect(page).to have_link(task.title)
+
+ click_button 'Clear'
+
+ select_tokens 'Milestone', '=', 'Started', submit: true
+
+ expect(page).to have_css('.issue', count: 1)
+ expect(page).to have_link(issue.title)
+ end
+ end
+
+ describe 'search within' do
+ it 'filters', :aggregate_failures do
+ select_tokens 'Search Within', 'Titles'
+ send_keys 'eee', :enter, :enter
+
+ expect(page).to have_css('.issue', count: 1)
+ expect(page).to have_link(issue.title)
+
+ click_button 'Clear'
+
+ select_tokens 'Search Within', 'Descriptions'
+ send_keys 'aaa', :enter, :enter
+
+ expect(page).to have_css('.issue', count: 1)
+ expect(page).to have_link(incident.title)
+ end
+ end
+ end
+end
diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js
index 3d85f69e1bf..4c451be492b 100644
--- a/spec/frontend/boards/components/board_content_spec.js
+++ b/spec/frontend/boards/components/board_content_spec.js
@@ -15,6 +15,8 @@ import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
import updateBoardListMutation from '~/boards/graphql/board_list_update.mutation.graphql';
import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue';
import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
+import BoardDrawerWrapper from '~/boards/components/board_drawer_wrapper.vue';
+import WorkItemDrawer from '~/work_items/components/work_item_drawer.vue';
import { DraggableItemTypes } from 'ee_else_ce/boards/constants';
import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql';
import {
@@ -33,6 +35,7 @@ describe('BoardContent', () => {
const updateListHandler = jest.fn().mockResolvedValue(updateBoardListResponse);
const errorMessage = 'Failed to update list';
const updateListHandlerFailure = jest.fn().mockRejectedValue(new Error(errorMessage));
+ const mockUpdateCache = jest.fn();
const createComponent = ({
props = {},
@@ -41,8 +44,10 @@ describe('BoardContent', () => {
isIssueBoard = true,
isEpicBoard = false,
handler = updateListHandler,
+ workItemDrawerEnabled = false,
} = {}) => {
mockApollo = createMockApollo([[updateBoardListMutation, handler]]);
+ mockApollo.clients.defaultClient.cache.updateQuery = mockUpdateCache;
const listQueryVariables = { isProject: true };
mockApollo.clients.defaultClient.writeQuery({
@@ -70,11 +75,26 @@ describe('BoardContent', () => {
isEpicBoard,
isGroupBoard: true,
disabled: false,
+ fullPath: 'project-path',
+ glFeatures: {
+ issuesListDrawer: workItemDrawerEnabled,
+ },
},
stubs: {
BoardContentSidebar: stubComponent(BoardContentSidebar, {
template: '
',
}),
+ BoardDrawerWrapper: stubComponent(BoardDrawerWrapper, {
+ template: `
+
+
+
`,
+ }),
},
});
};
@@ -83,6 +103,8 @@ describe('BoardContent', () => {
const findBoardAddNewColumn = () => wrapper.findComponent(BoardAddNewColumn);
const findDraggable = () => wrapper.findComponent(Draggable);
const findError = () => wrapper.findComponent(GlAlert);
+ const findDrawerWrapper = () => wrapper.findComponent(BoardDrawerWrapper);
+ const findWorkItemDrawer = () => wrapper.findComponent(WorkItemDrawer);
const moveList = () => {
const movableListsOrder = [mockLists[0].id, mockLists[1].id];
@@ -114,6 +136,10 @@ describe('BoardContent', () => {
expect(wrapper.findComponent(BoardContentSidebar).exists()).toBe(true);
});
+ it('does not render board drawer wrapper', () => {
+ expect(findDrawerWrapper().exists()).toBe(false);
+ });
+
it('does not display EpicsSwimlanes component', () => {
expect(wrapper.findComponent(EpicsSwimlanes).exists()).toBe(false);
expect(findError().exists()).toBe(false);
@@ -173,6 +199,10 @@ describe('BoardContent', () => {
it('does not render BoardContentSidebar', () => {
expect(wrapper.findComponent(BoardContentSidebar).exists()).toBe(false);
});
+
+ it('does not render board drawer wrapper', () => {
+ expect(findDrawerWrapper().exists()).toBe(false);
+ });
});
describe('can admin list', () => {
@@ -217,4 +247,20 @@ describe('BoardContent', () => {
});
});
});
+
+ describe('when work item drawer is enabled', () => {
+ beforeEach(() => {
+ createComponent({ workItemDrawerEnabled: true });
+ });
+
+ it('renders board drawer wrapper', () => {
+ expect(findDrawerWrapper().exists()).toBe(true);
+ });
+
+ it('updates Apollo cache when work item in the drawer is updated', () => {
+ findWorkItemDrawer().vm.$emit('work-item-updated', { iid: '1' });
+
+ expect(mockUpdateCache).toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/boards/components/board_drawer_wrapper_spec.js b/spec/frontend/boards/components/board_drawer_wrapper_spec.js
new file mode 100644
index 00000000000..622c063022a
--- /dev/null
+++ b/spec/frontend/boards/components/board_drawer_wrapper_spec.js
@@ -0,0 +1,104 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import BoardDrawerWrapper from '~/boards/components/board_drawer_wrapper.vue';
+import activeBoardItemQuery from 'ee_else_ce/boards/graphql/client/active_board_item.query.graphql';
+import { resolvers } from '~/graphql_shared/issuable_client';
+import { rawIssue } from '../mock_data';
+
+Vue.use(VueApollo);
+
+const mockRefetchQueries = jest.fn();
+
+describe('BoardDrawerWrapper', () => {
+ let wrapper;
+
+ const findActiveIssuable = () => wrapper.findByTestId('active-issuable');
+ const findCloseButton = () => wrapper.findByTestId('close-button');
+ const findUpdateAttributeButton = () => wrapper.findByTestId('update-attribute-button');
+
+ const createComponent = (propsData = {}) => {
+ const mockApollo = createMockApollo([], resolvers);
+ mockApollo.clients.defaultClient.cache.writeQuery({
+ query: activeBoardItemQuery,
+ data: {
+ activeBoardItem: { ...rawIssue, listId: 'gid://gitlab/List/3' },
+ },
+ });
+ mockApollo.clients.defaultClient.refetchQueries = mockRefetchQueries;
+
+ wrapper = shallowMountExtended(BoardDrawerWrapper, {
+ propsData: {
+ backlogListId: 'gid://gitlab/List/1',
+ closedListId: 'gid://gitlab/List/2',
+ ...propsData,
+ },
+ apolloProvider: mockApollo,
+ scopedSlots: {
+ default: `
+
+
+
+
+
+
+ `,
+ },
+ });
+ };
+
+ it('renders active issuable', () => {
+ createComponent();
+
+ expect(findActiveIssuable().exists()).toBe(true);
+ });
+
+ it('hides active issuable on drawer close', async () => {
+ createComponent();
+
+ findCloseButton().trigger('click');
+ await waitForPromises();
+
+ expect(findActiveIssuable().exists()).toBe(false);
+ });
+
+ it('does not refetch lists if there were no changes to attributes', async () => {
+ createComponent();
+
+ findCloseButton().trigger('click');
+ await waitForPromises();
+
+ expect(mockRefetchQueries).not.toHaveBeenCalled();
+ });
+
+ it('does not refetch lists if active issuable was on the closed list', async () => {
+ createComponent({ closedListId: 'gid://gitlab/List/3' });
+
+ findUpdateAttributeButton().trigger('click');
+ findCloseButton().trigger('click');
+ await waitForPromises();
+
+ expect(mockRefetchQueries).not.toHaveBeenCalled();
+ });
+
+ it('refetches lists if active issuable was not on the closed list', async () => {
+ createComponent();
+
+ findUpdateAttributeButton().trigger('click');
+ findCloseButton().trigger('click');
+ await waitForPromises();
+
+ expect(mockRefetchQueries).toHaveBeenCalledTimes(2);
+ });
+
+ it('refetches lists on issuable delete', async () => {
+ createComponent();
+
+ wrapper.findByTestId('delete-issuable').trigger('click');
+ await waitForPromises();
+
+ expect(mockRefetchQueries).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js
index 628fd78b4e6..f8525e22aa9 100644
--- a/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -76,7 +76,6 @@ import {
TOKEN_TYPE_CREATED,
TOKEN_TYPE_CLOSED,
} from '~/vue_shared/components/filtered_search_bar/constants';
-import deleteWorkItemMutation from '~/work_items/graphql/delete_work_item.mutation.graphql';
import {
workItemResponseFactory,
workItemByIidResponseFactory,
@@ -149,10 +148,6 @@ describe('CE IssuesListApp component', () => {
const mockIssuesQueryResponse = jest.fn().mockResolvedValue(defaultQueryResponse);
const mockIssuesCountsQueryResponse = jest.fn().mockResolvedValue(getIssuesCountsQueryResponse);
- const deleteWorkItemMutationHandler = jest
- .fn()
- .mockResolvedValue({ data: { workItemDelete: { errors: [] } } });
-
const findCsvImportExportButtons = () => wrapper.findComponent(CsvImportExportButtons);
const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const findIssuableByEmail = () => wrapper.findComponent(IssuableByEmail);
@@ -182,13 +177,11 @@ describe('CE IssuesListApp component', () => {
sortPreferenceMutationResponse = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse),
stubs = {},
mountFn = shallowMount,
- deleteMutationHandler = deleteWorkItemMutationHandler,
} = {}) => {
const requestHandlers = [
[getIssuesQuery, issuesQueryResponse],
[getIssuesCountsQuery, issuesCountsQueryResponse],
[setSortPreferenceMutation, sortPreferenceMutationResponse],
- [deleteWorkItemMutation, deleteMutationHandler],
];
router = new VueRouter({ mode: 'history' });
@@ -1258,13 +1251,8 @@ describe('CE IssuesListApp component', () => {
});
describe('when deleting an issuable from the drawer', () => {
- beforeEach(async () => {
- const {
- data: { workItem },
- } = workItemResponseFactory({ iid: '789' });
- findWorkItemDrawer().vm.$emit('deleteWorkItem', workItem);
-
- await waitForPromises();
+ beforeEach(() => {
+ findWorkItemDrawer().vm.$emit('workItemDeleted');
});
it('should refetch issues and issues count', () => {
@@ -1280,18 +1268,12 @@ describe('CE IssuesListApp component', () => {
});
it('shows an error when deleting from the drawer fails', async () => {
- const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
- const {
- data: { workItem },
- } = workItemResponseFactory({ iid: '789' });
-
wrapper = mountComponent({
provide: {
glFeatures: {
issuesListDrawer: true,
},
},
- deleteMutationHandler: errorHandler,
});
findIssuableList().vm.$emit(
@@ -1300,10 +1282,9 @@ describe('CE IssuesListApp component', () => {
);
await nextTick();
- findWorkItemDrawer().vm.$emit('deleteWorkItem', workItem);
- await waitForPromises();
+ findWorkItemDrawer().vm.$emit('deleteWorkItemError');
+ await nextTick();
- expect(Sentry.captureException).toHaveBeenCalled();
expect(findIssuableList().props('error')).toBe('An error occurred while deleting an issuable.');
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
index 0c634055af4..20c9b49b2a6 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -29,6 +29,7 @@ const commitMessageWithDescription =
readyToMergeResponse.data.project.mergeRequest.defaultMergeCommitMessageWithDescription;
const createTestMr = (customConfig) => {
const mr = {
+ iid: 1,
isPipelineActive: false,
pipeline: null,
isPipelineFailed: false,
@@ -64,6 +65,7 @@ const createTestMr = (customConfig) => {
removeSourceBranch: true,
canMerge: true,
},
+ targetProjectId: 1,
};
Object.assign(mr, customConfig.mr);
@@ -820,11 +822,15 @@ describe('ReadyToMerge', () => {
});
it('should display confirmation modal when merge button is clicked', async () => {
- expect(findPipelineFailedConfirmModal().props()).toEqual({ visible: false });
+ expect(findPipelineFailedConfirmModal().props()).toEqual(
+ expect.objectContaining({ visible: false }),
+ );
await findMergeButton().vm.$emit('click');
- expect(findPipelineFailedConfirmModal().props()).toEqual({ visible: true });
+ expect(findPipelineFailedConfirmModal().props()).toEqual(
+ expect.objectContaining({ visible: true }),
+ );
});
});
@@ -836,11 +842,15 @@ describe('ReadyToMerge', () => {
});
it('should display confirmation modal when merge button is clicked', async () => {
- expect(findPipelineFailedConfirmModal().props()).toEqual({ visible: false });
+ expect(findPipelineFailedConfirmModal().props()).toEqual(
+ expect.objectContaining({ visible: false }),
+ );
await findMergeButton().vm.$emit('click');
- expect(findPipelineFailedConfirmModal().props()).toEqual({ visible: true });
+ expect(findPipelineFailedConfirmModal().props()).toEqual(
+ expect.objectContaining({ visible: true }),
+ );
});
});
diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js
index 9b3e7c3227f..9a729dc95fc 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -112,6 +112,7 @@ describe('WorkItemDetail component', () => {
workItemsBeta = false,
namespaceLevelWorkItems = true,
hasSubepicsFeature = true,
+ router = true,
} = {}) => {
wrapper = shallowMountExtended(WorkItemDetail, {
apolloProvider: createMockApollo([
@@ -161,6 +162,9 @@ describe('WorkItemDetail component', () => {
},
}),
},
+ mocks: {
+ $router: router,
+ },
});
};
@@ -753,6 +757,13 @@ describe('WorkItemDetail component', () => {
});
describe('design widget', () => {
+ it('does not render if application has no router', async () => {
+ createComponent({ router: false });
+ await waitForPromises();
+
+ expect(findWorkItemDesigns().exists()).toBe(false);
+ });
+
it('renders if work item has design widget', async () => {
createComponent();
await waitForPromises();
diff --git a/spec/frontend/work_items/components/work_item_drawer_spec.js b/spec/frontend/work_items/components/work_item_drawer_spec.js
index 86509d40ff8..fa953461865 100644
--- a/spec/frontend/work_items/components/work_item_drawer_spec.js
+++ b/spec/frontend/work_items/components/work_item_drawer_spec.js
@@ -1,7 +1,20 @@
import { GlDrawer, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+
import WorkItemDrawer from '~/work_items/components/work_item_drawer.vue';
import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
+import deleteWorkItemMutation from '~/work_items/graphql/delete_work_item.mutation.graphql';
+
+Vue.use(VueApollo);
+
+const deleteWorkItemMutationHandler = jest
+ .fn()
+ .mockResolvedValue({ data: { workItemDelete: { errors: [] } } });
describe('WorkItemDrawer', () => {
let wrapper;
@@ -23,6 +36,8 @@ describe('WorkItemDrawer', () => {
listeners: {
customEvent: mockListener,
},
+ stubs: { workItemDetail: true },
+ apolloProvider: createMockApollo([[deleteWorkItemMutation, deleteWorkItemMutationHandler]]),
});
};
@@ -54,4 +69,35 @@ describe('WorkItemDrawer', () => {
expect(mockListener).toHaveBeenCalledWith(mockPayload);
});
+
+ describe('when deleting work item', () => {
+ it('calls deleteWorkItemMutation', () => {
+ createComponent({ open: true });
+ findWorkItem().vm.$emit('deleteWorkItem', { workItemId: '1' });
+
+ expect(deleteWorkItemMutationHandler).toHaveBeenCalledWith({
+ input: { id: '1' },
+ });
+ });
+
+ it('emits `workItemDeleted` event when on successful mutation', async () => {
+ createComponent({ open: true });
+ findWorkItem().vm.$emit('deleteWorkItem', { workItemId: '1' });
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('workItemDeleted')).toHaveLength(1);
+ });
+
+ it('emits `deleteWorkItemError` event when mutation failed', async () => {
+ deleteWorkItemMutationHandler.mockRejectedValue('Houston, we have a problem');
+
+ createComponent({ open: true });
+ findWorkItem().vm.$emit('deleteWorkItem', { workItemId: '1' });
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('deleteWorkItemError')).toHaveLength(1);
+ });
+ });
});
diff --git a/spec/frontend/work_items/list/components/work_items_list_app_spec.js b/spec/frontend/work_items/list/components/work_items_list_app_spec.js
index 740cc378083..43c175ab104 100644
--- a/spec/frontend/work_items/list/components/work_items_list_app_spec.js
+++ b/spec/frontend/work_items/list/components/work_items_list_app_spec.js
@@ -21,8 +21,12 @@ import { scrollUp } from '~/lib/utils/scroll_utils';
import {
FILTERED_SEARCH_TERM,
OPERATOR_IS,
+ TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
TOKEN_TYPE_SEARCH_WITHIN,
+ TOKEN_TYPE_TYPE,
} from '~/vue_shared/components/filtered_search_bar/constants';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import WorkItemsListApp from '~/work_items/list/components/work_items_list_app.vue';
@@ -58,6 +62,7 @@ describe('WorkItemsListApp component', () => {
provide: {
fullPath: 'full/path',
initialSort: CREATED_DESC,
+ isGroup: true,
isSignedIn: true,
workItemType: null,
...provide,
@@ -200,28 +205,47 @@ describe('WorkItemsListApp component', () => {
username: 'root',
avatar_url: 'avatar/url',
};
+ const preloadedUsers = [
+ { ...mockCurrentUser, id: convertToGraphQLId(TYPENAME_USER, mockCurrentUser.id) },
+ ];
- beforeEach(async () => {
+ beforeEach(() => {
window.gon = {
current_user_id: mockCurrentUser.id,
current_user_fullname: mockCurrentUser.name,
current_username: mockCurrentUser.username,
current_user_avatar_url: mockCurrentUser.avatar_url,
};
- mountComponent();
- await waitForPromises();
});
- it('renders all tokens', () => {
- const preloadedUsers = [
- { ...mockCurrentUser, id: convertToGraphQLId(TYPENAME_USER, mockCurrentUser.id) },
- ];
+ it('renders all tokens', async () => {
+ mountComponent();
+ await waitForPromises();
expect(findIssuableList().props('searchTokens')).toMatchObject([
+ { type: TOKEN_TYPE_ASSIGNEE, preloadedUsers },
{ type: TOKEN_TYPE_AUTHOR, preloadedUsers },
+ { type: TOKEN_TYPE_LABEL },
+ { type: TOKEN_TYPE_MILESTONE },
{ type: TOKEN_TYPE_SEARCH_WITHIN },
+ { type: TOKEN_TYPE_TYPE },
]);
});
+
+ describe('when workItemType is defined', () => {
+ it('renders all tokens except "Type"', async () => {
+ mountComponent({ provide: { workItemType: 'EPIC' } });
+ await waitForPromises();
+
+ expect(findIssuableList().props('searchTokens')).toMatchObject([
+ { type: TOKEN_TYPE_ASSIGNEE, preloadedUsers },
+ { type: TOKEN_TYPE_AUTHOR, preloadedUsers },
+ { type: TOKEN_TYPE_LABEL },
+ { type: TOKEN_TYPE_MILESTONE },
+ { type: TOKEN_TYPE_SEARCH_WITHIN },
+ ]);
+ });
+ });
});
describe('events', () => {
diff --git a/spec/helpers/boards_helper_spec.rb b/spec/helpers/boards_helper_spec.rb
index 27b7bac5a88..af1ac8a9b36 100644
--- a/spec/helpers/boards_helper_spec.rb
+++ b/spec/helpers/boards_helper_spec.rb
@@ -103,6 +103,8 @@ RSpec.describe BoardsHelper do
allow(helper).to receive(:can?).with(user, :admin_issue, project_board).and_return(true)
allow(helper).to receive(:can?).with(user, :admin_issue_board_list, project).and_return(false)
allow(helper).to receive(:can?).with(user, :admin_issue_board, project).and_return(false)
+ allow(helper).to receive(:can?).with(user, :admin_label, project).and_return(false)
+ allow(helper).to receive(:can?).with(user, :create_saved_replies, project.group).and_return(false)
end
it 'returns board type as parent' do
@@ -157,6 +159,8 @@ RSpec.describe BoardsHelper do
allow(helper).to receive(:can?).with(user, :admin_issue, group_board).and_return(true)
allow(helper).to receive(:can?).with(user, :admin_issue_board_list, base_group).and_return(false)
allow(helper).to receive(:can?).with(user, :admin_issue_board, base_group).and_return(false)
+ allow(helper).to receive(:can?).with(user, :admin_label, base_group).and_return(false)
+ allow(helper).to receive(:can?).with(user, :create_saved_replies, base_group).and_return(false)
end
it 'returns correct path for base group' do
diff --git a/spec/lib/gitlab/audit/deploy_key_author_spec.rb b/spec/lib/gitlab/audit/deploy_key_author_spec.rb
index 72444f77c91..c4f13670712 100644
--- a/spec/lib/gitlab/audit/deploy_key_author_spec.rb
+++ b/spec/lib/gitlab/audit/deploy_key_author_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe Gitlab::Audit::DeployKeyAuthor do
it 'sets default name when it is not provided' do
expect(described_class.new)
- .to have_attributes(id: -3, name: 'Deploy Key')
+ .to have_attributes(id: -3, name: 'Deploy key')
end
end
end
diff --git a/spec/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder_spec.rb b/spec/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder_spec.rb
index 511a4797292..12a018b942e 100644
--- a/spec/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder_spec.rb
+++ b/spec/lib/gitlab/pagination/keyset/in_operator_optimization/query_builder_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder do
+RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder, feature_category: :database do
let_it_be(:two_weeks_ago) { 2.weeks.ago }
let_it_be(:three_weeks_ago) { 3.weeks.ago }
let_it_be(:four_weeks_ago) { 4.weeks.ago }
@@ -44,6 +44,7 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder
end
let(:scope_model) { Issue }
+ let(:sql_type) { ->(model, column = 'id') { model.columns_hash[column].sql_type } }
let(:created_records) { issues }
let(:iterator) do
Gitlab::Pagination::Keyset::Iterator.new(
@@ -344,7 +345,7 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'id_multiplied_by_ten',
order_expression: Arel.sql('(id * 10)').asc,
- sql_type: 'integer'
+ sql_type: sql_type.call(Issue)
),
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: :id,
@@ -411,7 +412,7 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'projects_id',
order_expression: Issue.arel_table[:projects_id].asc,
- sql_type: 'integer',
+ sql_type: sql_type.call(Project),
nullable: :not_nullable
),
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
@@ -442,7 +443,7 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'projects_id',
order_expression: Issue.arel_table[:projects_id].desc,
- sql_type: 'integer',
+ sql_type: sql_type.call(Project),
nullable: :not_nullable
),
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
@@ -473,13 +474,13 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'projects_name',
order_expression: Issue.arel_table[:projects_name].asc,
- sql_type: 'character varying',
+ sql_type: sql_type.call(Project, 'name'),
nullable: :not_nullable
),
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'projects_id',
order_expression: Issue.arel_table[:projects_id].asc,
- sql_type: 'integer',
+ sql_type: sql_type.call(Project),
nullable: :not_nullable
),
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
@@ -509,7 +510,7 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'projects_name',
order_expression: Issue.arel_table[:projects_name].asc,
- sql_type: 'character varying',
+ sql_type: sql_type.call(Project, 'name'),
nullable: :nulls_last
),
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
diff --git a/spec/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/order_values_loader_strategy_spec.rb b/spec/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/order_values_loader_strategy_spec.rb
index 2073142f077..5b7a9f0fce4 100644
--- a/spec/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/order_values_loader_strategy_spec.rb
+++ b/spec/lib/gitlab/pagination/keyset/in_operator_optimization/strategies/order_values_loader_strategy_spec.rb
@@ -21,6 +21,8 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::Strategies::O
Gitlab::Pagination::Keyset::InOperatorOptimization::OrderByColumns.new(keyset_order.column_definitions, model.arel_table)
end
+ let(:id_type) { model.columns_hash['id'].sql_type }
+
subject(:strategy) { described_class.new(model, order_by_columns) }
describe '#initializer_columns' do
@@ -28,7 +30,7 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::Strategies::O
expect(strategy.initializer_columns).to eq(
[
'NULL::timestamp without time zone AS created_at',
- 'NULL::integer AS id'
+ "NULL::#{id_type} AS id"
])
end
end
@@ -59,7 +61,7 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::Strategies::O
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'id_times_ten',
order_expression: Arel.sql('id * 10').asc,
- sql_type: 'integer'
+ sql_type: id_type
)
])
end
@@ -67,7 +69,7 @@ RSpec.describe Gitlab::Pagination::Keyset::InOperatorOptimization::Strategies::O
let(:keyset_scope) { Project.order(order) }
it 'returns the initializer columns' do
- expect(strategy.initializer_columns).to eq(['NULL::integer AS id_times_ten'])
+ expect(strategy.initializer_columns).to eq(["NULL::#{id_type} AS id_times_ten"])
end
end
end
diff --git a/spec/lib/sidebars/admin/panel_spec.rb b/spec/lib/sidebars/admin/panel_spec.rb
index 8404dc2e768..a9b480e2123 100644
--- a/spec/lib/sidebars/admin/panel_spec.rb
+++ b/spec/lib/sidebars/admin/panel_spec.rb
@@ -6,9 +6,8 @@ RSpec.describe Sidebars::Admin::Panel, feature_category: :navigation do
let_it_be(:user) { build(:admin) }
let(:context) { Sidebars::Context.new(current_user: user, container: nil) }
- let(:panel) { described_class.new(context) }
- subject { described_class.new(context) }
+ subject(:panel) { described_class.new(context) }
describe '#aria_label' do
it 'returns the correct aria label' do
@@ -24,4 +23,28 @@ RSpec.describe Sidebars::Admin::Panel, feature_category: :navigation do
it_behaves_like 'a panel with uniquely identifiable menu items'
it_behaves_like 'a panel instantiable by the anonymous user'
+
+ describe 'system hooks disabled on GitLab.com' do
+ let(:gitlab_com?) { false }
+
+ before do
+ allow(::Gitlab).to receive(:com?) { gitlab_com? }
+ end
+
+ context 'when on GitLab.com' do
+ let(:gitlab_com?) { true }
+
+ it 'does not include the SystemHooksMenu' do
+ expect(panel.instance_variable_get(:@menus).map(&:class))
+ .not_to include(Sidebars::Admin::Menus::SystemHooksMenu)
+ end
+ end
+
+ context 'when not on GitLab.com' do
+ it 'includes the SystemHooksMenu' do
+ expect(panel.instance_variable_get(:@menus).map(&:class))
+ .to include(Sidebars::Admin::Menus::SystemHooksMenu)
+ end
+ end
+ end
end
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index a9d2552d7b7..b51f8a5979f 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -29,44 +29,41 @@ RSpec.describe PagesDomain, feature_category: :pages do
end
end
- describe 'validate domain' do
+ describe 'domain validations' do
subject(:pages_domain) { build(:pages_domain, domain: domain) }
- context 'is unique' do
+ context 'when the domain is unique' do
let(:domain) { 'my.domain.com' }
it { is_expected.to validate_uniqueness_of(:domain).case_insensitive }
end
- describe "hostname" do
- {
- 'my.domain.com' => true,
- '123.456.789' => true,
- '0x12345.com' => true,
- '0123123' => true,
- 'a-reserved.com' => true,
- 'a.b-reserved.com' => true,
- 'reserved.com' => true,
- '_foo.com' => false,
- 'a.reserved.com' => false,
- 'a.b.reserved.com' => false,
- nil => false
- }.each do |value, validity|
- context "domain #{value.inspect} validity" do
- before do
- allow(Settings.pages).to receive(:host).and_return('reserved.com')
- end
+ context "with different domain names" do
+ before do
+ allow(Settings.pages).to receive(:host).and_return('reserved.com')
+ end
- let(:domain) { value }
+ where(:domain, :expected) do
+ 'my.domain.com' | true
+ '123.456.789' | true
+ '0x12345.com' | true
+ '0123123' | true
+ 'a-reserved.com' | true
+ 'a.b-reserved.com' | true
+ 'reserved.com' | true
- it { expect(pages_domain.valid?).to eq(validity) }
- end
+ '_foo.com' | false
+ 'a.reserved.com' | false
+ 'a.b.reserved.com' | false
+ nil | false
+ end
+
+ with_them do
+ it { is_expected.to have_attributes(valid?: expected) }
end
end
describe "HTTPS-only" do
- using RSpec::Parameterized::TableSyntax
-
let(:domain) { 'my.domain.com' }
let(:project) do
@@ -124,7 +121,7 @@ RSpec.describe PagesDomain, feature_category: :pages do
describe 'validate certificate' do
subject { domain }
- context 'serverless domain' do
+ context 'for serverless domain' do
it 'requires certificate and key to be present' do
expect(build(:pages_domain, :without_certificate, :without_key, usage: :serverless)).not_to be_valid
expect(build(:pages_domain, :without_certificate, usage: :serverless)).not_to be_valid
@@ -157,9 +154,7 @@ RSpec.describe PagesDomain, feature_category: :pages do
end
context 'when certificate is expired' do
- let(:domain) do
- build(:pages_domain, :with_trusted_expired_chain)
- end
+ let(:domain) { build(:pages_domain, :with_trusted_expired_chain) }
context 'when certificate is being changed' do
it "adds error to certificate" do
@@ -181,19 +176,17 @@ RSpec.describe PagesDomain, feature_category: :pages do
end
context 'with ecdsa certificate' do
- it "is valid" do
- domain = build(:pages_domain, :ecdsa)
+ let(:domain) { build(:pages_domain, :ecdsa) }
- expect(domain).to be_valid
- end
+ it { is_expected.to be_valid }
context 'when curve is set explicitly by parameters' do
+ let(:domain) { build(:pages_domain, :explicit_ecdsa) }
+
it 'adds errors to private key' do
- domain = build(:pages_domain, :explicit_ecdsa)
+ is_expected.to be_invalid
- expect(domain).to be_invalid
-
- expect(domain.errors[:key]).not_to be_empty
+ expect(domain.errors[:key]).to be_present
end
end
end
@@ -230,20 +223,13 @@ RSpec.describe PagesDomain, feature_category: :pages do
end
describe 'default values' do
- it 'defaults wildcard to false' do
- expect(subject.wildcard).to eq(false)
- end
-
- it 'defaults auto_ssl_enabled to false' do
- expect(subject.auto_ssl_enabled).to eq(false)
- end
-
- it 'defaults scope to project' do
- expect(subject.scope).to eq('project')
- end
-
- it 'defaults usage to pages' do
- expect(subject.usage).to eq('pages')
+ it do
+ is_expected.to have_attributes(
+ wildcard: false,
+ auto_ssl_enabled: false,
+ scope: 'project',
+ usage: 'pages'
+ )
end
end
@@ -251,7 +237,7 @@ RSpec.describe PagesDomain, feature_category: :pages do
subject { pages_domain.verification_code }
it 'is set automatically with 128 bits of SecureRandom data' do
- expect(SecureRandom).to receive(:hex).with(16) { 'verification code' }
+ expect(SecureRandom).to receive(:hex).with(16).and_return('verification code')
is_expected.to eq('verification code')
end
@@ -390,17 +376,13 @@ RSpec.describe PagesDomain, feature_category: :pages do
context 'when certificate is provided by user' do
let(:domain) { create(:pages_domain) }
- it 'returns key' do
- is_expected.to eq(domain.key)
- end
+ it { is_expected.to eq domain.key }
end
context 'when certificate is provided by gitlab' do
let(:domain) { create(:pages_domain, :letsencrypt) }
- it 'returns nil' do
- is_expected.to be_nil
- end
+ it { is_expected.to be_nil }
end
end
@@ -410,24 +392,18 @@ RSpec.describe PagesDomain, feature_category: :pages do
context 'when certificate is provided by user' do
let(:domain) { create(:pages_domain) }
- it 'returns key' do
- is_expected.to eq(domain.certificate)
- end
+ it { is_expected.to eq domain.certificate }
end
context 'when certificate is provided by gitlab' do
let(:domain) { create(:pages_domain, :letsencrypt) }
- it 'returns nil' do
- is_expected.to be_nil
- end
+ it { is_expected.to be_nil }
end
end
shared_examples 'certificate setter' do |attribute, setter_name, old_certificate_source, new_certificate_source|
- let(:domain) do
- create(:pages_domain, certificate_source: old_certificate_source)
- end
+ let(:domain) { create(:pages_domain, certificate_source: old_certificate_source) }
let(:old_value) { domain.public_send(attribute) }
@@ -437,15 +413,13 @@ RSpec.describe PagesDomain, feature_category: :pages do
let(:new_value) { 'new_value' }
it "assignes new value to #{attribute}" do
- expect do
- subject
- end.to change { domain.public_send(attribute) }.from(old_value).to('new_value')
+ expect { subject }
+ .to change { domain.public_send(attribute) }.from(old_value).to('new_value')
end
it 'changes certificate source' do
- expect do
- subject
- end.to change { domain.certificate_source }.from(old_certificate_source).to(new_certificate_source)
+ expect { subject }
+ .to change { domain.certificate_source }.from(old_certificate_source).to(new_certificate_source)
end
end
@@ -489,15 +463,11 @@ RSpec.describe PagesDomain, feature_category: :pages do
let(:domain) { create(:pages_domain, auto_ssl_enabled: true, auto_ssl_failed: true) }
it 'clears failure if auto ssl is disabled' do
- expect do
- domain.update!(auto_ssl_enabled: false)
- end.to change { domain.auto_ssl_failed }.from(true).to(false)
+ expect { domain.update!(auto_ssl_enabled: false) }.to change { domain.auto_ssl_failed }.from(true).to(false)
end
it 'does not clear failure on unrelated updates' do
- expect do
- domain.update!(verified_at: Time.current)
- end.not_to change { domain.auto_ssl_failed }.from(true)
+ expect { domain.update!(verified_at: Time.current) }.not_to change { domain.auto_ssl_failed }.from(true)
end
end
end
@@ -531,24 +501,16 @@ RSpec.describe PagesDomain, feature_category: :pages do
end
describe '.instance_serverless' do
+ let_it_be(:domain_1) { create(:pages_domain, wildcard: true) }
+ let_it_be(:domain_2) { create(:pages_domain, :instance_serverless) }
+ let_it_be(:domain_3) { create(:pages_domain, scope: :instance) }
+ let_it_be(:domain_4) { create(:pages_domain, :instance_serverless) }
+ let_it_be(:domain_5) { create(:pages_domain, usage: :serverless) }
+
subject { described_class.instance_serverless }
- before do
- create(:pages_domain, wildcard: true)
- create(:pages_domain, :instance_serverless)
- create(:pages_domain, scope: :instance)
- create(:pages_domain, :instance_serverless)
- create(:pages_domain, usage: :serverless)
- end
-
it 'returns domains that are wildcard, instance-level, and serverless' do
- expect(subject.length).to eq(2)
-
- subject.each do |domain|
- expect(domain.wildcard).to eq(true)
- expect(domain.usage).to eq('serverless')
- expect(domain.scope).to eq('instance')
- end
+ is_expected.to match_array [domain_2, domain_4]
end
end
@@ -610,10 +572,10 @@ RSpec.describe PagesDomain, feature_category: :pages do
end
describe '.find_by_domain_case_insensitive' do
- it 'lookup is case-insensitive' do
- pages_domain = create(:pages_domain, domain: "Pages.IO")
+ let_it_be(:pages_domain) { create(:pages_domain, domain: "Pages.IO") }
- expect(described_class.find_by_domain_case_insensitive('pages.io')).to eq(pages_domain)
+ it 'lookup is case-insensitive' do
+ expect(described_class.find_by_domain_case_insensitive('pages.io')).to eq pages_domain
end
end
end
diff --git a/spec/presenters/deploy_key_presenter_spec.rb b/spec/presenters/deploy_key_presenter_spec.rb
index 9e50da12395..71f1de4172f 100644
--- a/spec/presenters/deploy_key_presenter_spec.rb
+++ b/spec/presenters/deploy_key_presenter_spec.rb
@@ -16,7 +16,7 @@ RSpec.describe DeployKeyPresenter do
let(:deploy_key) { build(:deploy_key, key: 'a') }
it 'returns the custom error message' do
- expect(subject).to eq('Deploy Key must be a supported SSH public key.')
end
end
diff --git a/spec/scripts/internal_events/cli_spec.rb b/spec/scripts/internal_events/cli_spec.rb
index 76cdf213cdb..afc075cec8d 100644
--- a/spec/scripts/internal_events/cli_spec.rb
+++ b/spec/scripts/internal_events/cli_spec.rb
@@ -1153,7 +1153,7 @@ RSpec.describe Cli, feature_category: :service_ping do
with_cli_thread do
expect { plain_last_lines(30) }
- .to eventually_include_cli_text("Okay! The next step is adding a new event! (~5 min)")
+ .to eventually_include_cli_text("Okay! The next step is adding a new event! (~5-10 min)")
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 5ae7d31d8eb..89f0b53ddb5 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -339,9 +339,6 @@ RSpec.configure do |config|
# Experimental merge request dashboard
stub_feature_flags(merge_request_dashboard: false)
- # We want this this FF disabled by default
- stub_feature_flags(synced_epic_work_item_editable: false)
-
# Since we are very early in the Vue migration, there isn't much value in testing when the feature flag is enabled
# Please see https://gitlab.com/gitlab-org/gitlab/-/issues/466081 for tracking revisiting this.
stub_feature_flags(your_work_projects_vue: false)
diff --git a/spec/support/shared_examples/features/work_item_drawer_shared_examples.rb b/spec/support/shared_examples/features/work_item_drawer_shared_examples.rb
new file mode 100644
index 00000000000..a4dbdac2943
--- /dev/null
+++ b/spec/support/shared_examples/features/work_item_drawer_shared_examples.rb
@@ -0,0 +1,178 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'work item drawer' do
+ include MobileHelpers
+
+ before do
+ first_card.click
+ wait_for_requests
+ end
+
+ it 'shows drawer when clicking issue' do
+ expect(page).to have_selector('[data-testid="work-item-drawer"]')
+ end
+
+ it 'closes drawer when clicking issue' do
+ expect(page).to have_selector('[data-testid="work-item-drawer"]')
+
+ first_card.click
+
+ expect(page).not_to have_selector('[data-testid="work-item-drawer"]')
+ end
+
+ it 'shows issue details when drawer is open', :aggregate_failures do
+ within_testid('work-item-drawer') do
+ expect(page).to have_content(issue.title)
+ end
+ end
+
+ context 'when clicking close button' do
+ before do
+ find('[data-testid="work-item-drawer"] .gl-drawer-close-button').click
+ end
+
+ it 'unhighlights the active issue card' do
+ expect(first_card[:class]).not_to include('is-active')
+ expect(first_card[:class]).not_to include('multi-select')
+ end
+
+ it 'closes drawer when clicking close button' do
+ expect(page).not_to have_selector('[data-testid="work-item-drawer"]')
+ end
+ end
+
+ context 'when editing issue title' do
+ it 'edits issue title' do
+ within_testid('work-item-drawer') do
+ find_by_testid('work-item-edit-form-button').click
+
+ find_by_testid('work-item-title-input').set('Test title')
+
+ click_button 'Save changes'
+
+ wait_for_requests
+
+ expect(page).to have_content('Test title')
+ end
+
+ expect(first_card).to have_content('Test title')
+ end
+ end
+
+ context 'when in notifications subscription' do
+ before do
+ within_testid('work-item-drawer') do
+ find_by_testid('work-item-actions-dropdown').click
+ end
+ end
+
+ it 'displays notifications toggle', :aggregate_failures do
+ within_testid('work-item-drawer') do
+ expect(page).to have_selector('[data-testid="notifications-toggle-form"]')
+ expect(page).to have_content('Notifications')
+ expect(page).not_to have_content('Disabled by project owner')
+ end
+ end
+
+ it 'shows toggle as on then as off as user toggles to subscribe and unsubscribe', :aggregate_failures do
+ within_testid('notifications-toggle-form') do
+ subscription_button = find('[data-testid="toggle-wrapper"] button')
+
+ expect(page).not_to have_css("button.is-checked")
+
+ subscription_button.click
+
+ wait_for_requests
+
+ expect(page).to have_css("button.is-checked")
+
+ subscription_button.click
+
+ wait_for_requests
+
+ expect(page).not_to have_css("button.is-checked")
+ end
+ end
+ end
+
+ context 'when editing confidentiality' do
+ before do
+ within_testid('work-item-drawer') do
+ find_by_testid('work-item-actions-dropdown').click
+ end
+ end
+
+ it 'make issue confidential' do
+ within_testid('work-item-drawer') do
+ expect(page).not_to have_content('Confidential')
+
+ find_by_testid('confidentiality-toggle-action').click
+
+ wait_for_requests
+
+ expect(page).to have_content('Confidential')
+ end
+ end
+ end
+
+ context 'in time tracking' do
+ it 'displays time tracking feature with default message' do
+ within_testid('work-item-time-tracking') do
+ expect(page).to have_content('Time tracking')
+ expect(page).to have_content('Add an estimate or time spent')
+ end
+ end
+
+ context 'when only spent time is recorded' do
+ before do
+ issue.timelogs.create!(time_spent: 3600, user: user)
+
+ refresh_and_click_first_card
+ end
+
+ it 'shows the total time spent only' do
+ within_testid('work-item-time-tracking') do
+ expect(page).to have_content('Spent 1h')
+ expect(page).not_to have_content('Estimated')
+ end
+ end
+ end
+
+ context 'when only estimated time is recorded' do
+ before do
+ issue.update!(time_estimate: 3600)
+
+ refresh_and_click_first_card
+ end
+
+ it 'shows the estimated time only', :aggregate_failures do
+ within_testid('work-item-time-tracking') do
+ expect(page).to have_content('Estimate 1h')
+ expect(page).to have_content('Spent 0h')
+ end
+ end
+ end
+
+ context 'when estimated and spent times are available' do
+ before do
+ issue.timelogs.create!(time_spent: 1800, user: user)
+ issue.update!(time_estimate: 3600)
+
+ refresh_and_click_first_card
+ end
+
+ it 'shows time tracking progress bar' do
+ within_testid('work-item-time-tracking') do
+ expect(page).to have_selector('.progress-bar')
+ end
+ end
+
+ it 'shows both estimated and spent time text', :aggregate_failures do
+ within_testid('work-item-time-tracking') do
+ expect(page).to have_content('Spent 30m')
+ expect(page).to have_content('Estimate 1h')
+ end
+ end
+ end
+ end
+end
diff --git a/tooling/eslint-config/conditionally_ignore.js b/tooling/eslint-config/conditionally_ignore.js
index 6132c1f52f4..71e382dc786 100644
--- a/tooling/eslint-config/conditionally_ignore.js
+++ b/tooling/eslint-config/conditionally_ignore.js
@@ -1,5 +1,3 @@
-/* eslint-disable import/no-commonjs */
-
const IS_EE = require('../../config/helpers/is_ee_env');
const IS_JH = require('../../config/helpers/is_jh_env');
diff --git a/tooling/stylelint/gitlab_no_gl_class.plugin.js b/tooling/stylelint/gitlab_no_gl_class.plugin.js
new file mode 100644
index 00000000000..4f8cde8d26e
--- /dev/null
+++ b/tooling/stylelint/gitlab_no_gl_class.plugin.js
@@ -0,0 +1,44 @@
+const stylelint = require('stylelint');
+
+const {
+ createPlugin,
+ utils: { report, ruleMessages, validateOptions },
+} = stylelint;
+
+const ruleName = 'gitlab/no-gl-class';
+
+const messages = ruleMessages(ruleName, {
+ rejected: () => '"gl-" class selectors are disallowed',
+});
+
+const meta = {
+ url: 'https://docs.gitlab.com/ee/development/fe_guide/style/scss.html#selectors-with-util-css-classes',
+};
+
+/** @type {import('stylelint').Rule} */
+const ruleFunction = (primary) => {
+ return (root, result) => {
+ const validOptions = validateOptions(result, ruleName, {
+ actual: primary,
+ possible: [true],
+ });
+
+ if (!validOptions) return;
+
+ root.walkRules(/\.gl-(?!dark)/, (ruleNode) => {
+ report({
+ result,
+ ruleName,
+ message: messages.rejected(),
+ node: ruleNode,
+ word: ruleNode.selector,
+ });
+ });
+ };
+};
+
+ruleFunction.ruleName = ruleName;
+ruleFunction.messages = messages;
+ruleFunction.meta = meta;
+
+module.exports = createPlugin(ruleName, ruleFunction);