Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-07-17 15:24:19 +00:00
parent 3088254c32
commit a4cfb7653b
118 changed files with 1990 additions and 468 deletions

View File

@ -244,3 +244,11 @@ overrides:
- 'ee/app/assets/javascripts/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql'
rules:
'@graphql-eslint/require-id-when-available': off
- files:
- '{,spec/}tooling/**/*'
rules:
'no-undef': off
'import/no-commonjs': off
'import/no-extraneous-dependencies': off
'no-restricted-syntax': off
'@gitlab/require-i18n-strings': off

View File

@ -2078,7 +2078,6 @@ RSpec/ContextWording:
- 'spec/models/packages/dependency_spec.rb'
- 'spec/models/packages/package_file_spec.rb'
- 'spec/models/packages/package_spec.rb'
- 'spec/models/pages_domain_spec.rb'
- 'spec/models/personal_access_token_spec.rb'
- 'spec/models/plan_limits_spec.rb'
- 'spec/models/preloaders/labels_preloader_spec.rb'

View File

@ -2648,7 +2648,6 @@ RSpec/NamedSubject:
- 'spec/models/packages/package_file_spec.rb'
- 'spec/models/packages/package_spec.rb'
- 'spec/models/packages/rpm/repository_file_spec.rb'
- 'spec/models/pages_domain_spec.rb'
- 'spec/models/personal_access_token_spec.rb'
- 'spec/models/plan_limits_spec.rb'
- 'spec/models/project_authorization_spec.rb'

View File

@ -143,7 +143,6 @@ RSpec/ReturnFromStub:
- 'spec/models/internal_id_spec.rb'
- 'spec/models/issue_spec.rb'
- 'spec/models/merge_request_spec.rb'
- 'spec/models/pages_domain_spec.rb'
- 'spec/models/project_spec.rb'
- 'spec/models/project_statistics_spec.rb'
- 'spec/models/snippet_statistics_spec.rb'

View File

@ -1,18 +1,32 @@
{
"extends": ["@gitlab/stylelint-config"],
"ignoreFiles": [
"app/assets/stylesheets/pages/emojis.scss",
"app/assets/stylesheets/startup/startup-*.scss",
"ee/app/assets/stylesheets/startup/startup-*.scss",
"app/assets/stylesheets/highlight/themes/*.scss",
"app/assets/stylesheets/lazy_bundles/cropper.css"
],
"overrides": [
"extends": ["@gitlab/stylelint-config"],
"plugins": ["./tooling/stylelint/gitlab_no_gl_class.plugin.js"],
"rules": {
"gitlab/no-gl-class": true
},
"ignoreFiles": [
"app/assets/stylesheets/pages/emojis.scss",
"app/assets/stylesheets/startup/startup-*.scss",
"ee/app/assets/stylesheets/startup/startup-*.scss",
"app/assets/stylesheets/highlight/themes/*.scss",
"app/assets/stylesheets/lazy_bundles/cropper.css"
],
"overrides": [
{
"files": ["app/assets/stylesheets/mailers/mailer.scss"],
"rules": {
"color-hex-length": "long"
}
},
{
"files": [
"app/assets/stylesheets/framework/**/*.scss",
"app/assets/stylesheets/themes/dark_mode_overrides.scss",
"app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss"
],
"rules": {
"gitlab/no-gl-class": null
}
}
]
]
}

View File

@ -5,18 +5,24 @@ import produce from 'immer';
import Draggable from 'vuedraggable';
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 glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import WorkItemDrawer from '~/work_items/components/work_item_drawer.vue';
import { s__ } from '~/locale';
import { defaultSortableOptions, DRAG_DELAY } from '~/sortable/constants';
import { mapWorkItemWidgetsToIssueFields } from '~/issues/list/utils';
import {
DraggableItemTypes,
flashAnimationDuration,
listsQuery,
updateListQueries,
ListType,
listIssuablesQueries,
DEFAULT_BOARD_LIST_ITEMS_SIZE,
} from 'ee_else_ce/boards/constants';
import { calculateNewPosition } from 'ee_else_ce/boards/boards_util';
import { setError } from '../graphql/cache_updates';
import BoardColumn from './board_column.vue';
import BoardDrawerWrapper from './board_drawer_wrapper.vue';
export default {
draggableItemTypes: DraggableItemTypes,
@ -24,13 +30,25 @@ export default {
BoardAddNewColumn,
BoardAddNewColumnTrigger,
BoardColumn,
BoardDrawerWrapper,
BoardContentSidebar: () => import('~/boards/components/board_content_sidebar.vue'),
EpicBoardContentSidebar: () =>
import('ee_component/boards/components/epic_board_content_sidebar.vue'),
EpicsSwimlanes: () => import('ee_component/boards/components/epics_swimlanes.vue'),
GlAlert,
WorkItemDrawer,
},
inject: ['boardType', 'canAdminList', 'isIssueBoard', 'isEpicBoard', 'disabled', 'issuableType'],
mixins: [glFeatureFlagsMixin()],
inject: [
'boardType',
'canAdminList',
'isIssueBoard',
'isEpicBoard',
'disabled',
'issuableType',
'isGroupBoard',
'fullPath',
],
props: {
boardId: {
type: String,
@ -108,6 +126,9 @@ export default {
const closedList = this.boardListsToUse.find((list) => list.listType === ListType.closed);
return closedList?.id || '';
},
issuesDrawerEnabled() {
return this.glFeatures.issuesListDrawer;
},
},
methods: {
afterFormEnters() {
@ -193,6 +214,24 @@ export default {
});
}
},
updateBoardCard(workItem, activeCard) {
const { cache } = this.$apollo.provider.clients.defaultClient;
const variables = {
id: activeCard.listId,
filters: this.filterParams,
fullPath: this.fullPath,
boardId: this.boardId,
isGroup: this.isGroupBoard,
isProject: !this.isGroupBoard,
first: DEFAULT_BOARD_LIST_ITEMS_SIZE,
};
cache.updateQuery(
{ query: listIssuablesQueries[this.issuableType].query, variables },
(boardList) => mapWorkItemWidgetsToIssueFields(boardList, workItem, true),
);
},
},
};
</script>
@ -283,9 +322,34 @@ export default {
/>
</div>
</epics-swimlanes>
<board-drawer-wrapper
v-if="issuesDrawerEnabled"
:backlog-list-id="backlogListId"
:closed-list-id="closedListId"
>
<template
#default="{
activeIssuable,
onDrawerClosed,
onAttributeUpdated,
onIssuableDeleted,
onStateUpdated,
}"
>
<work-item-drawer
:open="Boolean(activeIssuable && activeIssuable.iid)"
:active-item="activeIssuable"
@close="onDrawerClosed"
@work-item-updated="updateBoardCard($event, activeIssuable)"
@workItemDeleted="onIssuableDeleted(activeIssuable)"
@attributesUpdated="onAttributeUpdated"
@workItemStateUpdated="onStateUpdated"
/>
</template>
</board-drawer-wrapper>
<board-content-sidebar
v-if="isIssueBoard"
v-if="isIssueBoard && !issuesDrawerEnabled"
:backlog-list-id="backlogListId"
:closed-list-id="closedListId"
data-testid="issue-boards-sidebar"

View File

@ -0,0 +1,134 @@
<script>
import { union } from 'lodash';
import activeBoardItemQuery from 'ee_else_ce/boards/graphql/client/active_board_item.query.graphql';
import setActiveBoardItemMutation from 'ee_else_ce/boards/graphql/client/set_active_board_item.mutation.graphql';
import { TYPE_ISSUE } from '~/issues/constants';
import { ListType } from 'ee_else_ce/boards/constants';
import { identifyAffectedLists } from '../graphql/cache_updates';
export default {
name: 'BoardDrawerWrapper',
inject: {
issuableType: {
default: TYPE_ISSUE,
},
},
props: {
backlogListId: {
type: String,
required: true,
},
closedListId: {
type: String,
required: true,
},
},
data() {
return {
affectedListTypes: [],
updatedAttributeIds: [],
};
},
apollo: {
activeBoardItem: {
query: activeBoardItemQuery,
variables: {
isIssue: true,
},
},
},
computed: {
apolloClient() {
return this.$apollo.getClient();
},
},
methods: {
async onDrawerClosed() {
const item = this.activeBoardItem;
await this.$apollo.mutate({
mutation: setActiveBoardItemMutation,
variables: {
boardItem: null,
listId: null,
},
});
if (item.listId !== this.closedListId || this.affectedListTypes.includes(ListType.closed)) {
await this.refetchAffectedLists(item);
}
this.affectedListTypes = [];
this.updatedAttributeIds = [];
},
onAttributeUpdated({ ids, type }) {
if (!this.affectedListTypes.includes(type)) {
this.affectedListTypes.push(type);
}
this.updatedAttributeIds = union(this.updatedAttributeIds, ids);
},
refetchAffectedLists(item) {
if (!this.affectedListTypes.length) {
return;
}
const affectedLists = identifyAffectedLists({
client: this.apolloClient,
item,
issuableType: TYPE_ISSUE,
affectedListTypes: this.affectedListTypes,
updatedAttributeIds: this.updatedAttributeIds,
});
if (this.backlogListId && !affectedLists.includes(this.backlogListId)) {
affectedLists.push(this.backlogListId);
}
if (this.closedListId && this.affectedListTypes.includes(ListType.closed)) {
affectedLists.push(this.closedListId);
}
this.refetchActiveIssuableLists(item);
this.apolloClient.refetchQueries({
updateCache(cache) {
affectedLists.forEach((listId) => {
cache.evict({
id: cache.identify({
__typename: 'BoardList',
id: listId,
}),
fieldName: 'issues',
});
cache.evict({
id: cache.identify({
__typename: 'BoardList',
id: listId,
}),
fieldName: 'issuesCount',
});
});
},
});
},
refetchActiveIssuableLists(item) {
this.apolloClient.refetchQueries({
updateCache(cache) {
cache.evict({ id: cache.identify(item) });
},
});
},
onStateUpdated() {
this.affectedListTypes.push(ListType.closed);
},
},
render() {
return this.$scopedSlots.default({
activeIssuable: this.activeBoardItem,
onDrawerClosed: this.onDrawerClosed,
onAttributeUpdated: this.onAttributeUpdated,
onIssuableDeleted: this.refetchActiveIssuableLists,
onStateUpdated: this.onStateUpdated,
});
},
};
</script>

View File

@ -22,7 +22,15 @@ const apolloProvider = new VueApollo({
});
function mountBoardApp(el) {
const { boardId, groupId, fullPath, rootPath, hasScopedLabelsFeature } = el.dataset;
const {
boardId,
groupId,
fullPath,
rootPath,
hasScopedLabelsFeature,
wiGroupPath,
wiCanAdminLabel,
} = el.dataset;
const rawFilterParams = queryToObject(window.location.search, { gatherArrays: true });
@ -43,9 +51,11 @@ function mountBoardApp(el) {
groupId: Number(groupId),
rootPath,
fullPath,
groupPath: wiGroupPath,
initialFilterParams,
boardBaseUrl: el.dataset.boardBaseUrl,
boardType,
isGroup: boardType === WORKSPACE_GROUP,
isGroupBoard: boardType === WORKSPACE_GROUP,
isProjectBoard: boardType === WORKSPACE_PROJECT,
currentUserId: gon.current_user_id || null,
@ -60,6 +70,8 @@ function mountBoardApp(el) {
weights: el.dataset.weights ? JSON.parse(el.dataset.weights) : [],
isIssueBoard: true,
isEpicBoard: false,
reportAbusePath: el.dataset.wiReportAbusePath,
issuesListPath: el.dataset.wiIssuesListPath,
// Permissions
canUpdate: parseBoolean(el.dataset.canUpdate),
canAdminList: parseBoolean(el.dataset.canAdminList),
@ -67,6 +79,7 @@ function mountBoardApp(el) {
allowLabelCreate: parseBoolean(el.dataset.canUpdate),
allowLabelEdit: parseBoolean(el.dataset.canUpdate),
isSignedIn: isLoggedIn(),
canAdminLabel: parseBoolean(wiCanAdminLabel),
// Features
multipleAssigneesFeatureAvailable: parseBoolean(el.dataset.multipleAssigneesFeatureAvailable),
epicFeatureAvailable: parseBoolean(el.dataset.epicFeatureAvailable),
@ -83,6 +96,10 @@ function mountBoardApp(el) {
scopedIssueBoardFeatureEnabled: parseBoolean(el.dataset.scopedIssueBoardFeatureEnabled),
allowSubEpics: false,
hasScopedLabelsFeature: parseBoolean(hasScopedLabelsFeature),
hasIterationsFeature: parseBoolean(el.dataset.iterationFeatureAvailable),
hasIssueWeightsFeature: parseBoolean(el.dataset.weightFeatureAvailable),
hasIssuableHealthStatusFeature: parseBoolean(el.dataset.healthStatusFeatureAvailable),
hasSubepicsFeature: parseBoolean(el.dataset.subEpicsFeatureAvailable),
},
render: (createComponent) => createComponent(BoardApp),
});

View File

@ -70,7 +70,6 @@ import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_ro
import { DEFAULT_PAGE_SIZE, issuableListTabs } from '~/vue_shared/issuable/list/constants';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import NewResourceDropdown from '~/vue_shared/components/new_resource_dropdown/new_resource_dropdown.vue';
import deleteWorkItemMutation from '~/work_items/graphql/delete_work_item.mutation.graphql';
import { WORK_ITEM_TYPE_ENUM_OBJECTIVE } from '~/work_items/constants';
import WorkItemDrawer from '~/work_items/components/work_item_drawer.vue';
import {
@ -864,23 +863,9 @@ export default {
this.$apollo.queries.issues.refetch();
this.$apollo.queries.issuesCounts.refetch();
},
deleteIssuable({ workItemId }) {
this.$apollo
.mutate({
mutation: deleteWorkItemMutation,
variables: { input: { id: workItemId } },
})
.then(({ data }) => {
if (data.workItemDelete.errors?.length) {
throw new Error(data.workItemDelete.errors[0]);
}
this.activeIssuable = null;
this.refetchIssuables();
})
.catch((error) => {
this.issuesError = __('An error occurred while deleting an issuable.');
Sentry.captureException(error);
});
deleteIssuable() {
this.activeIssuable = null;
this.refetchIssuables();
},
updateIssuableEmojis(workItem) {
const client = this.$apollo.provider.clients.defaultClient;
@ -907,7 +892,8 @@ export default {
@work-item-updated="updateIssuablesCache"
@work-item-emoji-updated="updateIssuableEmojis"
@addChild="refetchIssuables"
@deleteWorkItem="deleteIssuable"
@deleteWorkItemError="issuesError = __('An error occurred while deleting an issuable.')"
@workItemDeleted="deleteIssuable"
@promotedToObjective="promoteToObjective"
/>
<issuable-list

View File

@ -429,9 +429,13 @@ export function findWidget(type, workItem) {
return workItem?.widgets?.find((widget) => widget.type === type);
}
export function mapWorkItemWidgetsToIssueFields(issuesList, workItem) {
export function mapWorkItemWidgetsToIssueFields(issuesList, workItem, isBoard = false) {
return produce(issuesList, (draftData) => {
const activeItem = draftData.project.issues.nodes.find((issue) => issue.iid === workItem.iid);
const activeList = isBoard
? draftData.project.board.lists.nodes[0].issues.nodes
: draftData.project.issues.nodes;
const activeItem = activeList.find((issue) => issue.iid === workItem.iid);
Object.keys(WORK_ITEM_TO_ISSUE_MAP).forEach((type) => {
const currentWidget = findWidget(type, workItem);

View File

@ -86,7 +86,8 @@ export default {
},
workItemState: {
type: String,
required: true,
required: false,
default: null,
},
workItemId: {
type: String,
@ -403,6 +404,7 @@ export default {
:full-path="fullPath"
show-as-dropdown-item
@error="emitStateToggleError"
@workItemStateUpdated="$emit('workItemStateUpdated')"
/>
<gl-disclosure-dropdown-item

View File

@ -274,6 +274,7 @@ export default {
return;
}
this.track('updated_assignees');
this.$emit('assigneesUpdated', localAssigneeIds);
} catch {
this.throwUpdateError();
} finally {

View File

@ -1,6 +1,7 @@
<script>
import Participants from '~/sidebar/components/participants/participants.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { ListType } from '~/boards/constants';
import {
WIDGET_TYPE_ASSIGNEES,
WIDGET_TYPE_HEALTH_STATUS,
@ -27,6 +28,7 @@ import WorkItemTimeTracking from './work_item_time_tracking.vue';
import WorkItemDevelopment from './work_item_development/work_item_development.vue';
export default {
ListType,
components: {
Participants,
WorkItemLabels,
@ -159,6 +161,9 @@ export default {
:work-item-type="workItemType"
:can-invite-members="workItemAssignees.canInviteMembers"
@error="$emit('error', $event)"
@assigneesUpdated="
$emit('attributesUpdated', { type: $options.ListType.assignee, ids: $event })
"
/>
</template>
<template v-if="workItemLabels">
@ -170,6 +175,7 @@ export default {
:work-item-iid="workItem.iid"
:work-item-type="workItemType"
@error="$emit('error', $event)"
@labelsUpdated="$emit('attributesUpdated', { type: $options.ListType.label, ids: $event })"
/>
</template>
<template v-if="workItemWeight">
@ -207,6 +213,9 @@ export default {
:work-item-type="workItemType"
:can-update="canUpdate"
@error="$emit('error', $event)"
@milestoneUpdated="
$emit('attributesUpdated', { type: $options.ListType.milestone, ids: [$event] })
"
/>
</template>
<template v-if="workItemIteration">
@ -219,6 +228,9 @@ export default {
:work-item-iid="workItem.iid"
:work-item-type="workItemType"
@error="$emit('error', $event)"
@iterationUpdated="
$emit('attributesUpdated', { type: $options.ListType.iteration, ids: [$event] })
"
/>
</template>
<template v-if="workItemDueDate && !showRolledupDates">

View File

@ -53,6 +53,7 @@ import WorkItemLoading from './work_item_loading.vue';
import DesignWidget from './design_management/design_management_widget.vue';
export default {
name: 'WorkItemDetail',
i18n,
directives: {
GlTooltip: GlTooltipDirective,
@ -257,7 +258,7 @@ export default {
return this.isWidgetPresent(WIDGET_TYPE_DESCRIPTION);
},
hasDesignWidget() {
return this.isWidgetPresent(WIDGET_TYPE_DESIGNS);
return this.isWidgetPresent(WIDGET_TYPE_DESIGNS) && this.$router;
},
workItemNotificationsSubscribed() {
return Boolean(this.isWidgetPresent(WIDGET_TYPE_NOTIFICATIONS)?.subscribed);
@ -495,6 +496,7 @@ export default {
@error="updateError = $event"
@promotedToObjective="$emit('promotedToObjective', workItemIid)"
@toggleEditMode="enableEditMode"
@workItemStateUpdated="$emit('workItemStateUpdated')"
/>
<section class="work-item-view">
<section v-if="updateError" class="flash-container flash-container-page sticky">
@ -585,6 +587,7 @@ export default {
@toggleWorkItemConfidentiality="toggleConfidentiality"
@error="updateError = $event"
@promotedToObjective="$emit('promotedToObjective', workItemIid)"
@workItemStateUpdated="$emit('workItemStateUpdated')"
/>
</div>
<gl-button
@ -650,6 +653,7 @@ export default {
:work-item="workItem"
:group-path="groupPath"
@error="updateError = $event"
@attributesUpdated="$emit('attributesUpdated', $event)"
/>
</aside>

View File

@ -1,13 +1,14 @@
<script>
import { GlLink, GlDrawer } from '@gitlab/ui';
import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
import deleteWorkItemMutation from '~/work_items/graphql/delete_work_item.mutation.graphql';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
export default {
name: 'WorkItemDrawer',
components: {
GlLink,
GlDrawer,
WorkItemDetail,
WorkItemDetail: () => import('~/work_items/components/work_item_detail.vue'),
},
inheritAttrs: false,
props: {
@ -21,12 +22,30 @@ export default {
default: () => ({}),
},
},
methods: {
async deleteWorkItem({ workItemId }) {
try {
const { data } = await this.$apollo.mutate({
mutation: deleteWorkItemMutation,
variables: { input: { id: workItemId } },
});
if (data.workItemDelete.errors?.length) {
throw new Error(data.workItemDelete.errors[0]);
}
this.$emit('workItemDeleted');
} catch (error) {
this.$emit('deleteWorkItemError');
Sentry.captureException(error);
}
},
},
};
</script>
<template>
<gl-drawer
:open="open"
data-testid="work-item-drawer"
header-height="calc(var(--top-bar-height) + var(--performance-bar-height))"
class="gl-w-full gl-sm-w-40p gl-leading-reset"
@close="$emit('close')"
@ -42,6 +61,7 @@ export default {
:work-item-iid="activeItem.iid"
is-drawer
class="gl-pt-0! work-item-drawer"
@deleteWorkItem="deleteWorkItem"
v-on="$listeners"
/>
</template>

View File

@ -260,6 +260,7 @@ export default {
}
this.track('updated_labels');
this.$emit('labelsUpdated', [...this.addLabelIds, ...this.removeLabelIds]);
} catch {
this.$emit('error', i18n.updateError);
} finally {

View File

@ -163,6 +163,7 @@ export default {
if (data.workItemUpdate.errors.length) {
throw new Error(data.workItemUpdate.errors.join('\n'));
}
this.$emit('milestoneUpdated', selectedMilestoneId);
})
.catch((error) => {
this.localMilestone = this.workItemMilestone;

View File

@ -193,6 +193,7 @@ export default {
if (this.hasComment) {
this.$emit('submit-comment');
}
this.$emit('workItemStateUpdated');
this.updateInProgress = false;
},

View File

@ -159,6 +159,7 @@ export default {
"
@error="$emit('error')"
@promotedToObjective="$emit('promotedToObjective')"
@workItemStateUpdated="$emit('workItemStateUpdated')"
/>
</div>
</div>

View File

@ -108,7 +108,7 @@ export default {
</script>
<template>
<div>
<div data-testid="work-item-time-tracking">
<div class="gl-display-flex gl-align-items-center gl-justify-content-space-between">
<h3 class="gl-heading-5 gl-mb-2!">
{{ __('Time tracking') }}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -48,6 +48,7 @@
}
}
// stylelint-disable-next-line gitlab/no-gl-class
.gl-responsive-table-row {
.branch-commit {
max-width: 100%;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -94,6 +94,7 @@
.label-name {
width: 200px;
// stylelint-disable-next-line gitlab/no-gl-class
.gl-label {
line-height: $gl-line-height;
}

View File

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

View File

@ -84,6 +84,7 @@
}
}
// stylelint-disable-next-line gitlab/no-gl-class
.gl-datepicker-input {
width: px-to-rem(165px);
max-width: 100%;

View File

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

View File

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

View File

@ -25,6 +25,7 @@
word-wrap: break-word;
}
// stylelint-disable-next-line gitlab/no-gl-class
.gl-label-link {
color: inherit;
}

View File

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

View File

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

View File

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

View File

@ -8,6 +8,7 @@
}
}
// stylelint-disable-next-line gitlab/no-gl-class
.file-tree-container > div.gl-overflow-y-auto {
max-height: 220px;

View File

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

View File

@ -462,6 +462,7 @@
}
@media (min-width: $breakpoint-lg) {
// stylelint-disable-next-line gitlab/no-gl-class
.gl-card {
width: calc(50% - 15px);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -36,6 +36,7 @@
color: initial;
}
// stylelint-disable-next-line gitlab/no-gl-class
.gl-link,
.gl-button {
color: $white;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -385,6 +385,20 @@ four standard [pagination arguments](#pagination-arguments):
| ---- | ---- | ----------- |
| <a id="queryduoworkfloweventsworkflowid"></a>`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).
| <a id="doraperformancescorecountedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="doraperformancescorecountedgenode"></a>`node` | [`DoraPerformanceScoreCount`](#doraperformancescorecount) | The item at the end of the edge. |
#### `DuoWorkflowConnection`
The connection type for [`DuoWorkflow`](#duoworkflow).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="duoworkflowconnectionedges"></a>`edges` | [`[DuoWorkflowEdge]`](#duoworkflowedge) | A list of edges. |
| <a id="duoworkflowconnectionnodes"></a>`nodes` | [`[DuoWorkflow]`](#duoworkflow) | A list of nodes. |
| <a id="duoworkflowconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
#### `DuoWorkflowEdge`
The edge type for [`DuoWorkflow`](#duoworkflow).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="duoworkflowedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="duoworkflowedgenode"></a>`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.
| <a id="doraperformancescorecountmetricname"></a>`metricName` | [`String!`](#string) | Name of the DORA metric. |
| <a id="doraperformancescorecountnodataprojectscount"></a>`noDataProjectsCount` | [`Int`](#int) | Number of projects with no data for the metric. |
### `DuoWorkflow`
A Duo Workflow.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="duoworkflowcreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp of when the workflow was created. |
| <a id="duoworkflowhumanstatus"></a>`humanStatus` | [`String!`](#string) | Human-readable status of the workflow. |
| <a id="duoworkflowid"></a>`id` | [`ID!`](#id) | ID of the workflow. |
| <a id="duoworkflowprojectid"></a>`projectId` | [`ProjectID!`](#projectid) | ID of the project. |
| <a id="duoworkflowupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp of when the workflow was last updated. |
| <a id="duoworkflowuserid"></a>`userId` | [`UserID!`](#userid) | ID of the user. |
### `DuoWorkflowEvent`
Events that describe the history and progress of a Duo Workflow.

View File

@ -129,7 +129,7 @@ We'll use this variable in the `.gitlab-ci.yml` later, to easily connect to our
![variables page](img/variables_page.png)
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@ module Gitlab
end
def name
@name || _('Deploy Key')
@name || _('Deploy key')
end
end
end

View File

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

View File

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

View File

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

View File

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

View File

@ -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 <a target="_blank" rel="noopener noreferrer" ' \
expect(flash[:alert]).to eq('Deploy key must be a <a target="_blank" rel="noopener noreferrer" ' \
'href="/help/user/ssh#supported-ssh-key-types">supported SSH public key.</a>')
end
end

View File

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

View File

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

View File

@ -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']")

View File

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

View File

@ -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: '<div></div>',
}),
BoardDrawerWrapper: stubComponent(BoardDrawerWrapper, {
template: `
<div>
<slot
:active-issuable="{ listId: 1 }"
:onDrawerClosed="() => {}"
:onIssuableDeleted="() => {}"
:onAttributeUpdated="() => {}"
:onStateUpdated="() => {}"/>
</div>`,
}),
},
});
};
@ -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();
});
});
});

Some files were not shown because too many files have changed in this diff Show More