Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/work_items/components/work_item_ancestors/disclosure_hierarchy.vue127
-rw-r--r--app/assets/javascripts/work_items/components/work_item_ancestors/disclosure_hierarchy_item.vue61
-rw-r--r--app/assets/javascripts/work_items/components/work_item_ancestors/work_item_ancestors.vue95
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue50
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_ancestors.query.graphql33
-rw-r--r--app/assets/javascripts/work_items/utils.js10
-rw-r--r--app/assets/stylesheets/components/content_editor.scss2
-rw-r--r--app/assets/stylesheets/components/detail_page.scss15
-rw-r--r--app/assets/stylesheets/framework/buttons.scss2
-rw-r--r--app/assets/stylesheets/framework/common.scss2
-rw-r--r--app/assets/stylesheets/framework/contextual_sidebar.scss2
-rw-r--r--app/assets/stylesheets/framework/diffs.scss4
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss2
-rw-r--r--app/assets/stylesheets/framework/files.scss2
-rw-r--r--app/assets/stylesheets/framework/filters.scss4
-rw-r--r--app/assets/stylesheets/framework/gfm.scss2
-rw-r--r--app/assets/stylesheets/framework/lists.scss6
-rw-r--r--app/assets/stylesheets/framework/modal.scss2
-rw-r--r--app/assets/stylesheets/framework/responsive_tables.scss4
-rw-r--r--app/assets/stylesheets/framework/super_sidebar.scss2
-rw-r--r--app/assets/stylesheets/framework/variables.scss8
-rw-r--r--app/assets/stylesheets/highlight/common.scss2
-rw-r--r--app/assets/stylesheets/highlight/themes/none.scss20
-rw-r--r--app/assets/stylesheets/highlight/themes/solarized-light.scss2
-rw-r--r--app/assets/stylesheets/highlight/white_base.scss2
-rw-r--r--app/assets/stylesheets/mailers/highlighted_diff_email.scss6
-rw-r--r--app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss6
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss10
-rw-r--r--app/assets/stylesheets/page_bundles/merge_request.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/pipeline.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/pipelines.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/work_items.scss127
-rw-r--r--app/assets/stylesheets/pages/commits.scss2
-rw-r--r--app/assets/stylesheets/pages/events.scss2
-rw-r--r--app/assets/stylesheets/pages/groups.scss2
-rw-r--r--app/assets/stylesheets/pages/note_form.scss2
-rw-r--r--app/assets/stylesheets/snippets.scss6
-rw-r--r--app/assets/stylesheets/themes/_dark.scss1
-rw-r--r--app/assets/stylesheets/utilities.scss2
-rw-r--r--app/models/ci/build.rb4
-rw-r--r--app/models/ci/job_artifact.rb2
-rw-r--r--app/models/ci/pipeline.rb4
-rw-r--r--app/models/concerns/ci/metadatable.rb4
-rw-r--r--app/serializers/ci/downloadable_artifact_entity.rb5
-rw-r--r--app/serializers/merge_requests/pipeline_entity.rb5
-rw-r--r--app/services/ci/job_artifacts/create_service.rb2
-rw-r--r--app/views/dashboard/projects/_blank_state_admin_welcome.html.haml2
-rw-r--r--app/views/dashboard/projects/_blank_state_welcome.html.haml2
-rw-r--r--app/workers/all_queues.yml2
-rw-r--r--app/workers/bulk_imports/relation_batch_export_worker.rb1
-rw-r--r--config/feature_flags/development/telesign_intelligence.yml8
-rw-r--r--doc/tutorials/scan_result_policy/index.md1
-rw-r--r--locale/gitlab.pot9
-rw-r--r--qa/Gemfile2
-rw-r--r--qa/Gemfile.lock10
-rw-r--r--spec/commands/sidekiq_cluster/cli_spec.rb4
-rw-r--r--spec/factories/users/phone_number_validations.rb4
-rw-r--r--spec/frontend/work_items/components/work_item_ancestors/disclosure_hierarchy_item_spec.js53
-rw-r--r--spec/frontend/work_items/components/work_item_ancestors/disclosure_hierarchy_spec.js99
-rw-r--r--spec/frontend/work_items/components/work_item_ancestors/mock_data.js197
-rw-r--r--spec/frontend/work_items/components/work_item_ancestors/work_item_ancestors_spec.js117
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js58
-rw-r--r--spec/models/ci/build_spec.rb2
64 files changed, 1048 insertions, 186 deletions
diff --git a/app/assets/javascripts/work_items/components/work_item_ancestors/disclosure_hierarchy.vue b/app/assets/javascripts/work_items/components/work_item_ancestors/disclosure_hierarchy.vue
new file mode 100644
index 00000000000..708121ee210
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_ancestors/disclosure_hierarchy.vue
@@ -0,0 +1,127 @@
+<script>
+import uniqueId from 'lodash/uniqueId';
+import { GlIcon, GlTooltip, GlDisclosureDropdown } from '@gitlab/ui';
+import DisclosureHierarchyItem from './disclosure_hierarchy_item.vue';
+
+export default {
+ components: {
+ GlDisclosureDropdown,
+ GlIcon,
+ GlTooltip,
+ DisclosureHierarchyItem,
+ },
+ props: {
+ /**
+ * A list of items in the form:
+ * ```
+ * {
+ * title: String, required
+ * icon: String, optional
+ * }
+ * ```
+ */
+ items: {
+ type: Array,
+ required: false,
+ default: () => [],
+ validator: (items) => {
+ return items.every((item) => Object.keys(item).includes('title'));
+ },
+ },
+ /**
+ * When set, displays only first and last item, and groups the rest under an ellipsis button
+ */
+ withEllipsis: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ /**
+ * When set, a tooltip displays when hovering middle ellipsis button
+ */
+ ellipsisTooltipLabel: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ itemUuid: uniqueId('disclosure-hierarchy-'),
+ };
+ },
+ computed: {
+ middleItems() {
+ return this.items.slice(1, -1).map((item) => ({ ...item, text: item.title }));
+ },
+ firstItem() {
+ return this.items[0];
+ },
+ lastItemIndex() {
+ return this.items.length - 1;
+ },
+ lastItem() {
+ return this.items[this.lastItemIndex];
+ },
+ },
+ methods: {
+ itemId(index) {
+ return `${this.itemUuid}-item-${index}`;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-relative gl-display-flex">
+ <ul class="gl-p-0 gl-m-0 gl-relative gl-list-style-none gl-display-inline-flex gl-w-85p">
+ <template v-if="withEllipsis">
+ <disclosure-hierarchy-item :item="firstItem" :item-id="itemId(0)">
+ <slot :item="firstItem" :item-id="itemId(0)"></slot>
+ </disclosure-hierarchy-item>
+ <li class="disclosure-hierarchy-item">
+ <gl-disclosure-dropdown :items="middleItems">
+ <template #toggle>
+ <button
+ id="disclosure-hierarchy-ellipsis-button"
+ class="disclosure-hierarchy-button"
+ :aria-label="ellipsisTooltipLabel"
+ >
+ <gl-icon name="ellipsis_h" class="gl-ml-3 gl-text-gray-600 gl-z-index-200" />
+ </button>
+ </template>
+ <template #list-item="{ item }">
+ <span class="gl-display-flex">
+ <gl-icon
+ v-if="item.icon"
+ :name="item.icon"
+ class="gl-mr-3 gl-vertical-align-middle gl-text-gray-600 gl-flex-shrink-0"
+ />
+ {{ item.title }}
+ </span>
+ </template>
+ </gl-disclosure-dropdown>
+ </li>
+ <gl-tooltip
+ v-if="ellipsisTooltipLabel"
+ target="disclosure-hierarchy-ellipsis-button"
+ triggers="hover"
+ >
+ {{ ellipsisTooltipLabel }}
+ </gl-tooltip>
+ <disclosure-hierarchy-item :item="lastItem" :item-id="itemId(lastItemIndex)">
+ <slot :item="lastItem" :item-id="itemId(lastItemIndex)"></slot>
+ </disclosure-hierarchy-item>
+ </template>
+ <disclosure-hierarchy-item
+ v-for="(item, index) in items"
+ v-else
+ :key="index"
+ :item="item"
+ :item-id="itemId(index)"
+ >
+ <slot :item="item" :item-id="itemId(index)"></slot>
+ </disclosure-hierarchy-item>
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_ancestors/disclosure_hierarchy_item.vue b/app/assets/javascripts/work_items/components/work_item_ancestors/disclosure_hierarchy_item.vue
new file mode 100644
index 00000000000..8347583f3c5
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_ancestors/disclosure_hierarchy_item.vue
@@ -0,0 +1,61 @@
+<!-- eslint-disable vue/multi-word-component-names -->
+<script>
+import iconSpriteInfo from '@gitlab/svgs/dist/icons.json';
+import { GlIcon, GlLink } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlIcon,
+ GlLink,
+ },
+ props: {
+ /**
+ * Path item in the form:
+ * ```
+ * {
+ * title: String, required
+ * icon: String, optional
+ * }
+ * ```
+ */
+ item: {
+ type: Object,
+ required: false,
+ default: () => {},
+ },
+ itemId: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ methods: {
+ shouldDisplayIcon(icon) {
+ return icon && iconSpriteInfo.icons.includes(icon);
+ },
+ },
+};
+</script>
+
+<template>
+ <li class="disclosure-hierarchy-item">
+ <gl-link
+ :id="itemId"
+ :href="item.webUrl"
+ class="disclosure-hierarchy-button gl-text-gray-900 gl-hover-text-decoration-none gl-active-text-decoration-none!"
+ >
+ <gl-icon
+ v-if="shouldDisplayIcon(item.icon)"
+ :name="item.icon"
+ class="gl-mx-2 gl-text-gray-600 gl-flex-shrink-0"
+ />
+ <span class="gl-z-index-200 gl-text-truncate">{{ item.title }}</span>
+ </gl-link>
+ <!--
+ @slot Additional content to be displayed in an item.
+ @binding {Object} item The item being rendered.
+ @binding {String} itemId The rendered item's ID.
+ -->
+ <slot :item="item" :item-id="itemId"></slot>
+ </li>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_ancestors/work_item_ancestors.vue b/app/assets/javascripts/work_items/components/work_item_ancestors/work_item_ancestors.vue
new file mode 100644
index 00000000000..bebe5d64761
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_ancestors/work_item_ancestors.vue
@@ -0,0 +1,95 @@
+<script>
+import { GlIcon, GlPopover, GlBadge, GlSprintf } from '@gitlab/ui';
+
+import { createAlert } from '~/alert';
+import { s__ } from '~/locale';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+
+import { formatAncestors } from '../../utils';
+import workItemAncestorsQuery from '../../graphql/work_item_ancestors.query.graphql';
+import WorkItemStateBadge from '../work_item_state_badge.vue';
+import DisclosureHierarchy from './disclosure_hierarchy.vue';
+
+export default {
+ i18n: {
+ ancestorLabel: s__('WorkItem|Ancestor'),
+ ancestorsTooltipLabel: s__('WorkItem|Show all ancestors'),
+ },
+ components: {
+ GlIcon,
+ GlPopover,
+ GlBadge,
+ GlSprintf,
+ TimeAgoTooltip,
+ WorkItemStateBadge,
+ DisclosureHierarchy,
+ },
+ props: {
+ workItem: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ ancestors: [],
+ };
+ },
+ apollo: {
+ ancestors: {
+ query: workItemAncestorsQuery,
+ variables() {
+ return {
+ id: this.workItem.id,
+ };
+ },
+ update(data) {
+ return formatAncestors(data.workItem);
+ },
+ skip() {
+ return !this.workItem.id;
+ },
+ error(error) {
+ createAlert({
+ message: s__('Hierarchy|Something went wrong while fetching ancestors.'),
+ captureError: true,
+ error,
+ });
+ },
+ },
+ },
+};
+</script>
+
+<template>
+ <disclosure-hierarchy
+ v-if="ancestors.length > 0"
+ class="gl-mr-auto"
+ :items="ancestors"
+ :with-ellipsis="ancestors.length > 2"
+ :ellipsis-tooltip-label="$options.i18n.ancestorsTooltipLabel"
+ >
+ <template #default="{ item, itemId }">
+ <gl-popover triggers="hover focus" placement="bottom" :target="itemId">
+ <template #title>
+ <gl-badge variant="muted" size="sm">{{ $options.i18n.ancestorLabel }}</gl-badge>
+ <div class="gl-pt-3">
+ {{ item.title }}
+ </div>
+ </template>
+ <div class="gl-pb-3 gl-text-gray-500">
+ <gl-icon v-if="item.icon" :name="item.icon" />
+ <span>{{ item.reference }}</span>
+ </div>
+ <work-item-state-badge v-if="item.state" :work-item-state="item.state" />
+ <span class="gl-text-gray-500">
+ <gl-sprintf v-if="item.createdAt" :message="__('Created %{timeAgo}')">
+ <template #timeAgo>
+ <time-ago-tooltip :time="item.createdAt" />
+ </template>
+ </gl-sprintf>
+ </span>
+ </gl-popover>
+ </template>
+ </disclosure-hierarchy>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index 2440fc7d433..5d8c41176d0 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -1,13 +1,6 @@
<script>
import { isEmpty } from 'lodash';
-import {
- GlAlert,
- GlSkeletonLoader,
- GlIcon,
- GlButton,
- GlTooltipDirective,
- GlEmptyState,
-} from '@gitlab/ui';
+import { GlAlert, GlSkeletonLoader, GlButton, GlTooltipDirective, GlEmptyState } from '@gitlab/ui';
import noAccessSvg from '@gitlab/svgs/dist/illustrations/analytics/no-access.svg?raw';
import { s__ } from '~/locale';
import { getParameterByName, updateHistory, setUrlParams } from '~/lib/utils/url_utility';
@@ -48,8 +41,8 @@ import WorkItemNotes from './work_item_notes.vue';
import WorkItemDetailModal from './work_item_detail_modal.vue';
import WorkItemAwardEmoji from './work_item_award_emoji.vue';
import WorkItemRelationships from './work_item_relationships/work_item_relationships.vue';
-import WorkItemTypeIcon from './work_item_type_icon.vue';
import WorkItemStickyHeader from './work_item_sticky_header.vue';
+import WorkItemAncestors from './work_item_ancestors/work_item_ancestors.vue';
export default {
i18n,
@@ -61,7 +54,6 @@ export default {
GlAlert,
GlButton,
GlSkeletonLoader,
- GlIcon,
GlEmptyState,
WorkItemActions,
WorkItemTodos,
@@ -70,13 +62,13 @@ export default {
WorkItemAwardEmoji,
WorkItemTitle,
WorkItemAttributesWrapper,
- WorkItemTypeIcon,
WorkItemTree,
WorkItemNotes,
WorkItemDetailModal,
AbuseCategorySelector,
WorkItemRelationships,
WorkItemStickyHeader,
+ WorkItemAncestors,
},
mixins: [glFeatureFlagMixin()],
inject: ['fullPath', 'isGroup', 'reportAbusePath'],
@@ -427,37 +419,9 @@ export default {
/>
</div>
<div
- class="gl-display-block gl-sm-display-flex! gl-align-items-flex-start gl-flex-direction-column gl-sm-flex-direction-row gl-gap-3 gl-pt-3"
+ class="gl-display-block gl-md-display-flex! gl-align-items-flex-start gl-flex-direction-column gl-sm-flex-direction-row flex-wrap gl-gap-3 gl-pt-3"
>
- <ul
- v-if="parentWorkItem"
- class="list-unstyled gl-display-flex gl-min-w-0 gl-mr-auto gl-mb-0 gl-z-index-0"
- data-testid="work-item-parent"
- >
- <li class="gl-ml-n4 gl-display-flex gl-align-items-center gl-min-w-0">
- <gl-button
- v-gl-tooltip.hover
- class="gl-text-truncate"
- :icon="parentWorkItemIconName"
- category="tertiary"
- :href="parentUrl"
- :title="parentWorkItemReference"
- @click="openInModal({ event: $event, modalWorkItem: parentWorkItem })"
- >{{ parentWorkItemReference }}</gl-button
- >
- <gl-icon name="chevron-right" :size="16" class="gl-flex-shrink-0" />
- </li>
- <li
- class="gl-px-4 gl-py-3 gl-line-height-0 gl-display-flex gl-align-items-center gl-overflow-hidden gl-flex-shrink-0"
- >
- <work-item-type-icon
- :work-item-icon-name="workItemIconName"
- :work-item-type="workItemType"
- show-text
- />
- {{ workItemBreadcrumbReference }}
- </li>
- </ul>
+ <work-item-ancestors v-if="parentWorkItem" :work-item="workItem" class="gl-mb-1" />
<div
v-if="!error && !workItemLoading"
:class="titleClassHeader"
@@ -475,7 +439,9 @@ export default {
@error="updateError = $event"
/>
</div>
- <div class="detail-page-header-actions gl-display-flex gl-align-self-start gl-gap-3">
+ <div
+ class="detail-page-header-actions gl-display-flex gl-align-self-start gl-ml-auto gl-gap-3"
+ >
<work-item-todos
v-if="showWorkItemCurrentUserTodos"
:work-item-id="workItem.id"
diff --git a/app/assets/javascripts/work_items/graphql/work_item_ancestors.query.graphql b/app/assets/javascripts/work_items/graphql/work_item_ancestors.query.graphql
new file mode 100644
index 00000000000..bfcac11f51f
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/work_item_ancestors.query.graphql
@@ -0,0 +1,33 @@
+query workItemAncestorsQuery($id: WorkItemID!) {
+ workItem(id: $id) {
+ id
+ title
+ widgets {
+ type
+ ... on WorkItemWidgetHierarchy {
+ type
+ parent {
+ id
+ }
+ ancestors {
+ nodes {
+ id
+ iid
+ confidential
+ workItemType {
+ id
+ name
+ iconName
+ }
+ title
+ state
+ reference(full: true)
+ createdAt
+ closedAt
+ webUrl
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/utils.js b/app/assets/javascripts/work_items/utils.js
index ac5d8b32fad..221131f6854 100644
--- a/app/assets/javascripts/work_items/utils.js
+++ b/app/assets/javascripts/work_items/utils.js
@@ -26,6 +26,16 @@ export const findHierarchyWidgets = (widgets) =>
export const findHierarchyWidgetChildren = (workItem) =>
findHierarchyWidgets(workItem?.widgets)?.children?.nodes || [];
+export const findHierarchyWidgetAncestors = (workItem) =>
+ findHierarchyWidgets(workItem?.widgets)?.ancestors?.nodes || [];
+
+export const formatAncestors = (workItem) =>
+ findHierarchyWidgetAncestors(workItem).map((ancestor) => ({
+ ...ancestor,
+ icon: ancestor.workItemType?.iconName,
+ href: ancestor.webUrl,
+ }));
+
const autocompleteSourcesPath = (autocompleteType, fullPath, workItemIid) => {
return `${
gon.relative_url_root || ''
diff --git a/app/assets/stylesheets/components/content_editor.scss b/app/assets/stylesheets/components/content_editor.scss
index 2030f2c7095..97f2add4e77 100644
--- a/app/assets/stylesheets/components/content_editor.scss
+++ b/app/assets/stylesheets/components/content_editor.scss
@@ -335,7 +335,7 @@
border-radius: 3px;
margin-left: 4px;
margin-top: -2px;
- border: 1px solid $black-transparent;
+ border: 1px solid $t-gray-a-24;
background-color: var(--gl-color-chip-color);
}
diff --git a/app/assets/stylesheets/components/detail_page.scss b/app/assets/stylesheets/components/detail_page.scss
index a5fd57f6c57..56214040fdd 100644
--- a/app/assets/stylesheets/components/detail_page.scss
+++ b/app/assets/stylesheets/components/detail_page.scss
@@ -38,18 +38,9 @@
.detail-page-header-actions {
flex: 0 0 auto;
- &:not(.is-merge-request) {
- @include media-breakpoint-down(xs) {
- width: 100%;
- margin-top: 10px;
- }
- }
-
- &.is-merge-request {
- @include media-breakpoint-down(sm) {
- width: 100%;
- margin-top: 10px;
- }
+ @include media-breakpoint-down(sm) {
+ width: 100%;
+ margin-top: 10px;
}
}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 88509dbc4a1..07539b59574 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -118,7 +118,7 @@
}
@mixin btn-white {
- @include btn-color($white, $border-color, $white-normal, $border-white-normal, $white-dark, $border-white-normal, $gl-text-color);
+ @include btn-color($white, $border-color, $gray-50, $border-white-normal, $white-dark, $border-white-normal, $gl-text-color);
}
@mixin btn-purple {
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index aa5bd4cf098..e7d158de7e7 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -495,7 +495,7 @@ li.note {
width: 4px;
&:hover {
- background-color: $white-normal;
+ background-color: $gray-50;
}
&.is-dragging {
diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss
index fb9816d1402..defe67b68ad 100644
--- a/app/assets/stylesheets/framework/contextual_sidebar.scss
+++ b/app/assets/stylesheets/framework/contextual_sidebar.scss
@@ -446,7 +446,7 @@
&.mobile-nav-open {
display: block;
position: fixed;
- background-color: $black-transparent;
+ background-color: $t-gray-a-24;
height: 100%;
width: 100%;
z-index: $zindex-dropdown-menu;
diff --git a/app/assets/stylesheets/framework/diffs.scss b/app/assets/stylesheets/framework/diffs.scss
index fff42c0973c..b948a57ea33 100644
--- a/app/assets/stylesheets/framework/diffs.scss
+++ b/app/assets/stylesheets/framework/diffs.scss
@@ -689,7 +689,7 @@ table.code {
.note-container {
background-color: $gray-light;
- border-top: 1px solid $white-normal;
+ border-top: 1px solid $gray-50;
// double jagged line divider
.discussion-notes + .discussion-notes::before,
@@ -750,7 +750,7 @@ table.code {
.diff-file .note-container > .new-note,
.note-container .discussion-notes.diff-discussions {
margin-left: 100px;
- border-left: 1px solid $white-normal;
+ border-left: 1px solid $gray-50;
}
.notes.active {
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index a467d9e8c8a..b21504a6e31 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -846,7 +846,7 @@
}
.loading-animation {
- color: $almost-black;
+ color: $gray-950;
}
.frequent-items-dropdown-content {
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index c99e985e18c..9cb264c992b 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -376,7 +376,7 @@ span.idiff {
border-bottom: 1px $gray-darkest dashed;
&:hover {
- border-bottom-color: $almost-black;
+ border-bottom-color: $gray-950;
}
}
}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 67e96f08cb0..5949a1b2809 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -110,7 +110,7 @@
}
.operator {
- background-color: $white-normal;
+ background-color: $gray-50;
color: $gl-text-color;
margin-right: 1px;
}
@@ -118,7 +118,7 @@
.value-container {
display: flex;
align-items: center;
- background-color: $white-normal;
+ background-color: $gray-50;
color: $gl-text-color;
border-radius: 0 2px 2px 0;
margin-right: 5px;
diff --git a/app/assets/stylesheets/framework/gfm.scss b/app/assets/stylesheets/framework/gfm.scss
index 66d163f608a..b87a7f15c1c 100644
--- a/app/assets/stylesheets/framework/gfm.scss
+++ b/app/assets/stylesheets/framework/gfm.scss
@@ -45,6 +45,6 @@
height: 100%;
margin-bottom: 2px;
border-radius: 3px;
- border: 1px solid $black-transparent;
+ border: 1px solid $t-gray-a-24;
}
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index e9a507ebb6b..832b2297673 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -53,7 +53,7 @@
p {
padding-top: 1px;
margin: 0;
- color: $white-normal;
+ color: $gray-50;
img {
position: relative;
@@ -104,7 +104,7 @@ ul.content-list {
padding: 0;
li {
- border-color: $white-normal;
+ border-color: $gray-50;
font-size: $gl-font-size;
color: $gl-text-color;
word-break: break-word;
@@ -165,7 +165,7 @@ ul.content-list {
&.list-placeholder {
background-color: $gray-light;
- border: dotted 1px $white-normal;
+ border: dotted 1px $gray-50;
margin: 1px 0;
min-height: 52px;
}
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index f76a9cf0373..0265820bfe1 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -85,7 +85,7 @@ body.modal-open {
}
.modal {
- background-color: $black-transparent;
+ background-color: $t-gray-a-24;
.modal-content {
border-radius: $modal-border-radius;
diff --git a/app/assets/stylesheets/framework/responsive_tables.scss b/app/assets/stylesheets/framework/responsive_tables.scss
index f57d906e73c..5a86a96a96e 100644
--- a/app/assets/stylesheets/framework/responsive_tables.scss
+++ b/app/assets/stylesheets/framework/responsive_tables.scss
@@ -38,7 +38,7 @@
border: 0;
&:not(:last-child) {
- border-bottom: 1px solid $white-normal;
+ border-bottom: 1px solid $gray-50;
}
}
}
@@ -69,7 +69,7 @@
min-height: 62px;
&:not(:first-child) {
- border-top: 1px solid $white-normal;
+ border-top: 1px solid $gray-50;
}
}
diff --git a/app/assets/stylesheets/framework/super_sidebar.scss b/app/assets/stylesheets/framework/super_sidebar.scss
index c8bf2877b5a..93b7768eb99 100644
--- a/app/assets/stylesheets/framework/super_sidebar.scss
+++ b/app/assets/stylesheets/framework/super_sidebar.scss
@@ -245,7 +245,7 @@ $super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4;
bottom: 0;
left: 0;
right: 0;
- background-color: $black-transparent;
+ background-color: $t-gray-a-24;
z-index: $super-sidebar-z-index - 1;
@include media-breakpoint-up(md) {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 1964487c5e0..18c60009537 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -108,17 +108,13 @@ $t-gray-a-08: rgba($gray-950, 0.08) !default;
$t-gray-a-16: rgba($gray-950, 0.16) !default;
$t-gray-a-24: rgba($gray-950, 0.24) !default;
-$white-normal: $gray-50 !default;
$white-dark: darken($gray-50, 2) !default;
-$black-transparent: $t-gray-a-24 !default;
-$almost-black: $gray-950 !default;
-
// To do this variant right for darkmode, we need to create a variable for it.
$indigo-900-alpha-008: rgba($theme-indigo-900, 0.08);
$border-white-light: darken($white, $darken-border-factor) !default;
-$border-white-normal: darken($white-normal, $darken-border-factor) !default;
+$border-white-normal: darken($gray-50, $darken-border-factor) !default;
$border-gray-light: darken($gray-light, $darken-border-factor);
$border-gray-normal: darken($gray-normal, $darken-border-factor);
@@ -315,7 +311,7 @@ $dark-diff-match-color: rgba($white, 0.1);
$diff-image-info-color: #808080;
$diff-view-modes-color: #808080;
$diff-view-modes-border: #c1c1c1;
-$diff-jagged-border-gradient-color: darken($white-normal, 8%);
+$diff-jagged-border-gradient-color: darken($gray-50, 8%);
/*
* Fonts
diff --git a/app/assets/stylesheets/highlight/common.scss b/app/assets/stylesheets/highlight/common.scss
index 085e25a0cdc..23fa1326881 100644
--- a/app/assets/stylesheets/highlight/common.scss
+++ b/app/assets/stylesheets/highlight/common.scss
@@ -132,7 +132,7 @@
}
}
-@mixin line-hover-bg($color: $white-normal) {
+@mixin line-hover-bg($color: $gray-50) {
&:hover,
&:focus-within {
background-color: darken($color, 10);
diff --git a/app/assets/stylesheets/highlight/themes/none.scss b/app/assets/stylesheets/highlight/themes/none.scss
index f36eaa663e5..c2bc35ec91a 100644
--- a/app/assets/stylesheets/highlight/themes/none.scss
+++ b/app/assets/stylesheets/highlight/themes/none.scss
@@ -5,8 +5,8 @@
@import '../common';
@mixin match-line {
- color: $black-transparent;
- background-color: $white-normal;
+ color: $t-gray-a-24;
+ background-color: $gray-50;
}
:root {
@@ -40,13 +40,13 @@
.diff-line-num,
.diff-line-num a {
- color: $black-transparent;
+ color: $t-gray-a-24;
}
// Code itself
pre.code,
.diff-line-num {
- border-color: $white-normal;
+ border-color: $gray-50;
}
&,
@@ -86,7 +86,7 @@
&.new,
&.new-nomappinginraw,
&.old-nomappinginraw {
- background-color: $white-normal;
+ background-color: $gray-50;
}
}
@@ -137,27 +137,27 @@
.line_content {
&.old, &.old-nomappinginraw {
- background-color: $white-normal;
+ background-color: $gray-50;
&::before {
color: $gl-text-color;
}
span.idiff {
- background-color: $white-normal;
+ background-color: $gray-50;
text-decoration: underline;
}
}
&.new:not(.hll), &.new-nomappinginraw:not(.hll) {
- background-color: $white-normal;
+ background-color: $gray-50;
&::before {
color: $gl-text-color;
}
span.idiff {
- background-color: $white-normal;
+ background-color: $gray-50;
text-decoration: underline;
}
}
@@ -170,7 +170,7 @@
// Search result highlight
span.highlight_word {
- background-color: $white-normal;
+ background-color: $gray-50;
}
// Links to URLs, emails, or dependencies
diff --git a/app/assets/stylesheets/highlight/themes/solarized-light.scss b/app/assets/stylesheets/highlight/themes/solarized-light.scss
index b3aa10c3ace..c902d9357e8 100644
--- a/app/assets/stylesheets/highlight/themes/solarized-light.scss
+++ b/app/assets/stylesheets/highlight/themes/solarized-light.scss
@@ -101,7 +101,7 @@ $solarized-light-il: #2aa198;
}
@mixin match-line {
- color: $black-transparent;
+ color: $t-gray-a-24;
background: $solarized-light-matchline-bg;
}
diff --git a/app/assets/stylesheets/highlight/white_base.scss b/app/assets/stylesheets/highlight/white_base.scss
index 2631055706f..89d6d93614f 100644
--- a/app/assets/stylesheets/highlight/white_base.scss
+++ b/app/assets/stylesheets/highlight/white_base.scss
@@ -73,7 +73,7 @@ $white-gc-bg: #eaf2f5;
@mixin match-line {
- color: $black-transparent;
+ color: $t-gray-a-24;
background-color: $gray-light;
}
diff --git a/app/assets/stylesheets/mailers/highlighted_diff_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
index fd212d14e30..db8c3d163c0 100644
--- a/app/assets/stylesheets/mailers/highlighted_diff_email.scss
+++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
@@ -93,8 +93,8 @@ $highlighted-gc-bg: #eaf2f5;
text-align: right;
width: 35px;
background-color: $gray-light;
- color: $black-transparent;
- border-right: 1px solid $white-normal;
+ color: $t-gray-a-24;
+ border-right: 1px solid $gray-50;
&.old {
background-color: $line-number-old;
@@ -130,7 +130,7 @@ $highlighted-gc-bg: #eaf2f5;
}
&.match {
- color: $black-transparent;
+ color: $t-gray-a-24;
background-color: $gray-light;
}
}
diff --git a/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss b/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss
index d93b4f75d77..0fb1b3c9c92 100644
--- a/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss
+++ b/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss
@@ -92,7 +92,7 @@
}
.line-numbers {
- color: $black-transparent;
+ color: $t-gray-a-24;
}
.view-overlays {
diff --git a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss
index c584bbaac09..b6caa845cfa 100644
--- a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss
+++ b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss
@@ -67,7 +67,7 @@
}
.drag-handle:hover {
- background-color: var(--ide-dropdown-hover-background, $white-normal);
+ background-color: var(--ide-dropdown-hover-background, $gray-50);
}
.card-header {
@@ -221,7 +221,7 @@
.filtered-search-token .value-container,
.filtered-search-term .value-container {
- background-color: var(--ide-dropdown-hover-background, $white-normal);
+ background-color: var(--ide-dropdown-hover-background, $gray-50);
color: var(--ide-text-color, $gl-text-color);
&:hover {
@@ -292,7 +292,7 @@
&:hover,
&:focus {
border-color: var(--ide-btn-default-hover-border, $border-white-normal) !important;
- background-color: var(--ide-btn-default-background, $white-normal) !important;
+ background-color: var(--ide-btn-default-background, $gray-50) !important;
}
&:active,
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index 7f8068e5d56..45270ab3d30 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -436,7 +436,7 @@ $ide-commit-header-height: 48px;
}
&.is-active {
- background-color: var(--ide-background, $white-normal);
+ background-color: var(--ide-background, $gray-50);
}
svg {
@@ -545,7 +545,7 @@ $ide-commit-header-height: 48px;
width: $ide-commit-row-height;
height: $ide-commit-row-height;
color: inherit;
- background-color: var(--ide-background, $white-normal);
+ background-color: var(--ide-background, $gray-50);
}
.ide-commit-options {
@@ -740,7 +740,7 @@ $ide-commit-header-height: 48px;
background-color: var(--ide-input-background, transparent);
&:hover {
- background-color: var(--ide-dropdown-btn-hover-background, $white-normal);
+ background-color: var(--ide-dropdown-btn-hover-background, $gray-50);
}
svg {
@@ -1063,7 +1063,7 @@ $ide-commit-header-height: 48px;
&:active,
&:focus {
- color: $white-normal;
+ color: $gray-50;
background-color: var(--ide-link-color, $blue-500);
outline: 0;
}
@@ -1077,7 +1077,7 @@ $ide-commit-header-height: 48px;
}
.dropdown.show .ide-entry-dropdown-toggle {
- color: $white-normal;
+ color: $gray-50;
background-color: var(--ide-link-color, $blue-500);
}
}
diff --git a/app/assets/stylesheets/page_bundles/merge_request.scss b/app/assets/stylesheets/page_bundles/merge_request.scss
index 71a7b3a7af0..70aeedb10bf 100644
--- a/app/assets/stylesheets/page_bundles/merge_request.scss
+++ b/app/assets/stylesheets/page_bundles/merge_request.scss
@@ -304,7 +304,7 @@ $comparison-empty-state-height: 62px;
.merge-request-details .file-finder-overlay.diff-file-finder {
position: fixed;
z-index: 99999;
- background: $black-transparent;
+ background: $t-gray-a-24;
}
.mr-compare {
diff --git a/app/assets/stylesheets/page_bundles/pipeline.scss b/app/assets/stylesheets/page_bundles/pipeline.scss
index 6b68fa75001..9bab5d65b59 100644
--- a/app/assets/stylesheets/page_bundles/pipeline.scss
+++ b/app/assets/stylesheets/page_bundles/pipeline.scss
@@ -218,7 +218,7 @@
min-width: 195px;
left: 100%;
top: -10px;
- box-shadow: 0 1px 5px $black-transparent;
+ box-shadow: 0 1px 5px $t-gray-a-24;
}
.codequality-report {
diff --git a/app/assets/stylesheets/page_bundles/pipelines.scss b/app/assets/stylesheets/page_bundles/pipelines.scss
index bcc0ad112ac..2bd8984c2ea 100644
--- a/app/assets/stylesheets/page_bundles/pipelines.scss
+++ b/app/assets/stylesheets/page_bundles/pipelines.scss
@@ -24,7 +24,7 @@
.btn.btn-retry:hover,
.btn.btn-retry:focus {
border-color: $dropdown-toggle-active-border-color;
- background-color: $white-normal;
+ background-color: $gray-50;
}
svg path {
@@ -42,7 +42,7 @@
}
.btn-group.open .btn-default {
- background-color: $white-normal;
+ background-color: $gray-50;
border-color: $border-white-normal;
}
diff --git a/app/assets/stylesheets/page_bundles/work_items.scss b/app/assets/stylesheets/page_bundles/work_items.scss
index ec73f27ed09..b9ab2450ff9 100644
--- a/app/assets/stylesheets/page_bundles/work_items.scss
+++ b/app/assets/stylesheets/page_bundles/work_items.scss
@@ -215,3 +215,130 @@ $work-item-sticky-header-height: 52px;
font-weight: normal;
}
}
+
+// Disclosure hierarchy component, used for Ancestors widget
+
+$disclosure-hierarchy-chevron-dimension: 1.2rem;
+
+@mixin hierarchy-active-item-color {
+ background-color: var(--gray-50, $gray-50);
+
+ &::after {
+ background-color: var(--gray-50, $gray-50);
+ }
+}
+
+@mixin hierarchy-path-chevron {
+ content: '';
+ @include gl-absolute;
+ @include gl-reset-bg;
+ top: 0.39rem;
+ right: px-to-rem(-9px);
+ width: $disclosure-hierarchy-chevron-dimension;
+ height: $disclosure-hierarchy-chevron-dimension;
+ transform: rotate(45deg) skew(14deg, 14deg);
+}
+
+.disclosure-hierarchy-button {
+ @include gl-pl-4;
+ @include gl-py-3;
+ @include gl-display-flex;
+ @include gl-relative;
+ @include gl-font-sm;
+ border: 1px solid var(--gray-100, $gray-100);
+ @include gl-border-r-none;
+ @include gl-border-l-none;
+ @include gl-line-height-normal;
+ padding-right: $grid-size;
+ max-width: $gl-spacing-scale-20;
+ background: var(--gray-10, $white);
+
+ @include media-breakpoint-up(sm) {
+ max-width: $gl-spacing-scale-48;
+ }
+
+ &::before,
+ &::after {
+ @include hierarchy-path-chevron;
+ border: 1px solid var(--gray-100, $gray-100);
+ border-color: inherit;
+ @include gl-border-b-transparent;
+ @include gl-border-l-transparent;
+ @include gl-reset-bg;
+ @include gl-rounded-top-left-small;
+ @include gl-rounded-bottom-right-small;
+ }
+
+ &::before {
+ background: var(--gray-10, $white);
+ left: -10px;
+ z-index: 1;
+ }
+
+ &::after {
+ z-index: 0;
+ }
+
+ .disclosure-hierarchy-item:first-child & {
+ @include gl-pl-3;
+ border-left: 1px solid var(--gray-100, $gray-100);
+ @include gl-rounded-top-left-base;
+ @include gl-rounded-bottom-left-base;
+
+ &::before {
+ @include gl-display-none;
+ }
+
+ &:active,
+ &:focus,
+ &:focus:active {
+ // Custom focus
+ box-shadow: 1px 1px 0 1px $blue-400, 2px -1px 0 1px $blue-400, -1px 1px 0 1px $blue-400, -1px -1px 0 1px $blue-400 !important;
+ }
+ }
+
+ .disclosure-hierarchy-item:last-child & {
+ @include gl-pr-4;
+ border-right: 1px solid var(--gray-100, $gray-100);
+ @include gl-rounded-top-right-base;
+ @include gl-rounded-bottom-right-base;
+
+ &::after {
+ display: none;
+ }
+ }
+
+ &[disabled] {
+ color: $gl-text-color-disabled;
+ @include gl-cursor-not-allowed;
+ }
+
+ &:not([disabled]):hover {
+ @include gl-border-gray-400;
+ @include hierarchy-active-item-color;
+ color: var(--gray-900, $gray-900);
+
+ &::after {
+ border-left: 1px solid var(--gray-50, $gray-50);
+ border-bottom: 1px solid var(--gray-50, $gray-50);
+ z-index: 3;
+ }
+ }
+
+ &:active,
+ &:focus,
+ &:focus:active {
+ // Custom focus
+ box-shadow: 1px 1px 0 1px $blue-400, 2px -1px 0 1px $blue-400 !important;
+ outline: none;
+ border-top: 1px solid var(--gray-400, $gray-400);
+ border-bottom: 1px solid var(--gray-400, $gray-400);
+ @include hierarchy-active-item-color;
+ z-index: 2;
+ @include gl-rounded-small;
+
+ &::before, &::after {
+ box-shadow: 2px -2px 0 1px $blue-400;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 72ea586979f..8511bc22725 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -79,7 +79,7 @@
.commits-row {
+ .commits-row {
- border-top: 1px solid $white-normal;
+ border-top: 1px solid $gray-50;
}
+ .commits-empty {
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 269b3078495..cfb964e6227 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -5,7 +5,7 @@
.event-item {
font-size: $gl-font-size;
padding: $gl-padding 0 $gl-padding $gl-spacing-scale-8;
- border-bottom: 1px solid $white-normal;
+ border-bottom: 1px solid $gray-50;
color: $gl-text-color-secondary;
position: relative;
line-height: $gl-line-height-20;
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 2e1bb9b9eac..a8f557270ba 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -179,7 +179,7 @@ table.pipeline-project-metrics tr td {
}
&:first-child {
- border-top: 1px solid $white-normal;
+ border-top: 1px solid $gray-50;
}
}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 38686d5e713..e5808c71a6d 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -56,7 +56,7 @@
&.is-dropzone-hover {
border-color: $green-500;
- box-shadow: 0 0 2px $black-transparent,
+ box-shadow: 0 0 2px $t-gray-a-24,
0 0 4px $green-500-focus;
.comment-toolbar,
diff --git a/app/assets/stylesheets/snippets.scss b/app/assets/stylesheets/snippets.scss
index e249ecbd10b..f5787799fce 100644
--- a/app/assets/stylesheets/snippets.scss
+++ b/app/assets/stylesheets/snippets.scss
@@ -82,12 +82,12 @@
font-size: $code-font-size;
line-height: $code-line-height;
white-space: nowrap;
- color: $black-transparent;
+ color: $t-gray-a-24;
min-width: 30px;
}
.diff-line-num:hover {
- color: $almost-black;
+ color: $gray-950;
cursor: pointer;
}
}
@@ -158,7 +158,7 @@
border-right: 0;
&:hover {
- background-color: $white-normal;
+ background-color: $gray-50;
border-color: $border-white-normal;
text-decoration: none;
}
diff --git a/app/assets/stylesheets/themes/_dark.scss b/app/assets/stylesheets/themes/_dark.scss
index 303ea9dd005..0fce87faa29 100644
--- a/app/assets/stylesheets/themes/_dark.scss
+++ b/app/assets/stylesheets/themes/_dark.scss
@@ -28,7 +28,6 @@ $t-gray-a-16: rgba($gray-10, 0.16);
$t-gray-a-24: rgba($gray-10, 0.24);
$black-normal: $gray-900;
-$white-normal: $gray-50;
$white-dark: $gray-100;
$theme-indigo-50: #1a1a40;
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index 347b8e20ab4..79ea8d3cc70 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -37,7 +37,7 @@
.border-color-default { border-color: $border-color; }
.border-radius-default { border-radius: $border-radius-default; }
.border-radius-small { border-radius: $border-radius-small; }
-.box-shadow-default { box-shadow: 0 2px 4px 0 $black-transparent; }
+.box-shadow-default { box-shadow: 0 2px 4px 0 $t-gray-a-24; }
// Override Bootstrap class with offset for system-header and
// performance bar when present
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 6d8b2711734..fb73cb55ec0 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -709,7 +709,7 @@ module Ci
end
def artifacts_public?
- return true if Feature.disabled?(:non_public_artifacts, type: :development)
+ return true if Feature.disabled?(:non_public_artifacts, project, type: :development)
return true if job_artifacts_archive.nil? # To backward compatibility return true if no artifacts found
@@ -717,7 +717,7 @@ module Ci
end
def artifact_is_public_in_config?
- return true if Feature.disabled?(:non_public_artifacts, type: :development)
+ return true if Feature.disabled?(:non_public_artifacts, project, type: :development)
artifacts_public = options.dig(:artifacts, :public)
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index d3705265d91..0c6fcc02344 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -362,7 +362,7 @@ module Ci
end
def public_access?
- return true unless Feature.enabled?(:non_public_artifacts, type: :development)
+ return true unless Feature.enabled?(:non_public_artifacts, project, type: :development)
public_accessibility?
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index f771c264651..f132214ab7c 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -97,11 +97,11 @@ module Ci
has_many :build_trace_chunks, class_name: 'Ci::BuildTraceChunk', through: :builds, source: :trace_chunks
has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id, inverse_of: :pipeline # rubocop:disable Cop/ActiveRecordDependent
has_many :variables, class_name: 'Ci::PipelineVariable'
- has_many :latest_builds, ->(pipeline) { in_partition(pipeline).latest.with_project_and_metadata }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Build'
+ has_many :latest_builds, ->(pipeline) { in_partition(pipeline).latest.with_project_and_metadata(pipeline.project) }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Build'
has_many :downloadable_artifacts, -> do
not_expired.or(where_exists(Ci::Pipeline.artifacts_locked.where("#{Ci::Pipeline.quoted_table_name}.id = #{Ci::Build.quoted_table_name}.commit_id"))).downloadable.with_job
end, through: :latest_builds, source: :job_artifacts
- has_many :latest_successful_jobs, ->(pipeline) { in_partition(pipeline).latest.success.with_project_and_metadata }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Processable'
+ has_many :latest_successful_jobs, ->(pipeline) { in_partition(pipeline).latest.success.with_project_and_metadata(pipeline.project) }, foreign_key: :commit_id, inverse_of: :pipeline, class_name: 'Ci::Processable'
has_many :messages, class_name: 'Ci::PipelineMessage', inverse_of: :pipeline
diff --git a/app/models/concerns/ci/metadatable.rb b/app/models/concerns/ci/metadatable.rb
index 5dcae7d594d..c0830d237c5 100644
--- a/app/models/concerns/ci/metadatable.rb
+++ b/app/models/concerns/ci/metadatable.rb
@@ -27,8 +27,8 @@ module Ci
before_validation :ensure_metadata, on: :create
- scope :with_project_and_metadata, -> do
- if Feature.enabled?(:non_public_artifacts, type: :development)
+ scope :with_project_and_metadata, ->(project) do
+ if Feature.enabled?(:non_public_artifacts, project, type: :development)
joins(:metadata).includes(:metadata).preload(:project)
end
end
diff --git a/app/serializers/ci/downloadable_artifact_entity.rb b/app/serializers/ci/downloadable_artifact_entity.rb
index 2e8aafcee43..c2caa371614 100644
--- a/app/serializers/ci/downloadable_artifact_entity.rb
+++ b/app/serializers/ci/downloadable_artifact_entity.rb
@@ -6,12 +6,13 @@ module Ci
expose :artifacts do |pipeline, options|
artifacts = pipeline.downloadable_artifacts
+ project = pipeline.project
- if Feature.enabled?(:non_public_artifacts)
+ if Feature.enabled?(:non_public_artifacts, project)
artifacts = artifacts.select { |artifact| can?(request.current_user, :read_job_artifacts, artifact) }
end
- BuildArtifactEntity.represent(artifacts, options.merge(project: pipeline.project))
+ BuildArtifactEntity.represent(artifacts, options.merge(project: project))
end
end
end
diff --git a/app/serializers/merge_requests/pipeline_entity.rb b/app/serializers/merge_requests/pipeline_entity.rb
index 83f168682db..ef4debbd89f 100644
--- a/app/serializers/merge_requests/pipeline_entity.rb
+++ b/app/serializers/merge_requests/pipeline_entity.rb
@@ -25,12 +25,13 @@ class MergeRequests::PipelineEntity < Grape::Entity
expose :artifacts do |pipeline, options|
rel = pipeline.downloadable_artifacts
+ project = pipeline.project
- if Feature.enabled?(:non_public_artifacts, type: :development)
+ if Feature.enabled?(:non_public_artifacts, project, type: :development)
rel = rel.select { |artifact| can?(request.current_user, :read_job_artifacts, artifact) }
end
- BuildArtifactEntity.represent(rel, options.merge(project: pipeline.project))
+ BuildArtifactEntity.represent(rel, options.merge(project: project))
end
expose :detailed_status, as: :status, with: DetailedStatusEntity do |pipeline|
diff --git a/app/services/ci/job_artifacts/create_service.rb b/app/services/ci/job_artifacts/create_service.rb
index b04e73c69ee..5927e9602b9 100644
--- a/app/services/ci/job_artifacts/create_service.rb
+++ b/app/services/ci/job_artifacts/create_service.rb
@@ -128,7 +128,7 @@ module Ci
def accessibility(params)
accessibility = params[:accessibility]
- return :public if Feature.disabled?(:non_public_artifacts, type: :development)
+ return :public if Feature.disabled?(:non_public_artifacts, project, type: :development)
return accessibility if accessibility.present?
diff --git a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
index 1d2e6e1e332..c3d8da0f9a0 100644
--- a/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
+++ b/app/views/dashboard/projects/_blank_state_admin_welcome.html.haml
@@ -1,6 +1,8 @@
- link_classes = "blank-state blank-state-link gl-text-body gl-display-flex gl-align-items-center gl-border-1 gl-border-solid gl-border-gray-100 gl-rounded-base gl-mb-5"
.gl-display-flex.gl-flex-wrap.gl-justify-content-space-between
+ = render_if_exists "dashboard/projects/blank_state_extra_info"
+
- if has_start_trial?
= render_if_exists "dashboard/projects/blank_state_ee_trial"
diff --git a/app/views/dashboard/projects/_blank_state_welcome.html.haml b/app/views/dashboard/projects/_blank_state_welcome.html.haml
index 032c5206d99..15048d29146 100644
--- a/app/views/dashboard/projects/_blank_state_welcome.html.haml
+++ b/app/views/dashboard/projects/_blank_state_welcome.html.haml
@@ -1,6 +1,8 @@
- link_classes = "blank-state blank-state-link gl-text-body gl-display-flex gl-align-items-center gl-border-1 gl-border-solid gl-border-gray-100 gl-rounded-base gl-mb-5"
.gl-display-flex.gl-flex-wrap.gl-justify-content-space-between
+ = render_if_exists "dashboard/projects/blank_state_extra_info"
+
- if current_user.can_create_project?
= link_to new_project_path, class: link_classes, data: { testid: 'new-project-button' } do
.blank-state-icon
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index d30ff5ce4e6..d8e78e28b55 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -2636,7 +2636,7 @@
:feature_category: :importers
:has_external_dependencies: false
:urgency: :low
- :resource_boundary: :unknown
+ :resource_boundary: :memory
:weight: 1
:idempotent: true
:tags: []
diff --git a/app/workers/bulk_imports/relation_batch_export_worker.rb b/app/workers/bulk_imports/relation_batch_export_worker.rb
index a9ffb2b252d..08c5fb81460 100644
--- a/app/workers/bulk_imports/relation_batch_export_worker.rb
+++ b/app/workers/bulk_imports/relation_batch_export_worker.rb
@@ -8,6 +8,7 @@ module BulkImports
data_consistency :always # rubocop:disable SidekiqLoadBalancing/WorkerDataConsistency
feature_category :importers
sidekiq_options status_expiration: StuckExportJobsWorker::EXPORT_JOBS_EXPIRATION, retry: 6
+ worker_resource_boundary :memory
sidekiq_retries_exhausted do |job, exception|
batch = BulkImports::ExportBatch.find(job['args'][1])
diff --git a/config/feature_flags/development/telesign_intelligence.yml b/config/feature_flags/development/telesign_intelligence.yml
new file mode 100644
index 00000000000..5d824f4025b
--- /dev/null
+++ b/config/feature_flags/development/telesign_intelligence.yml
@@ -0,0 +1,8 @@
+---
+name: telesign_intelligence
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/137739
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/432757
+milestone: '16.7'
+type: development
+group: group::anti-abuse
+default_enabled: false
diff --git a/doc/tutorials/scan_result_policy/index.md b/doc/tutorials/scan_result_policy/index.md
index 46005bd8e56..a6ca0c36135 100644
--- a/doc/tutorials/scan_result_policy/index.md
+++ b/doc/tutorials/scan_result_policy/index.md
@@ -30,6 +30,7 @@ The namespace used for this tutorial must:
- **Project name**: `sast-scan-result-policy`.
- Select the **Enable Static Application Security Testing (SAST)** checkbox.
1. Select **Create project**.
+1. Go to the newly created project and create [protected branches](../../user/project/protected_branches.md).
## Add a scan result policy
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index b852bda50ba..06db6608955 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -24047,6 +24047,9 @@ msgstr ""
msgid "Hierarchy|Planning hierarchy"
msgstr ""
+msgid "Hierarchy|Something went wrong while fetching ancestors."
+msgstr ""
+
msgid "Hierarchy|Something went wrong while fetching children."
msgstr ""
@@ -54721,6 +54724,9 @@ msgstr ""
msgid "WorkItem|All activity"
msgstr ""
+msgid "WorkItem|Ancestor"
+msgstr ""
+
msgid "WorkItem|Are you sure you want to cancel editing?"
msgstr ""
@@ -54894,6 +54900,9 @@ msgstr ""
msgid "WorkItem|Select type"
msgstr ""
+msgid "WorkItem|Show all ancestors"
+msgstr ""
+
msgid "WorkItem|Show labels"
msgstr ""
diff --git a/qa/Gemfile b/qa/Gemfile
index 399211493db..ef1faad7601 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -6,7 +6,7 @@ gem 'gitlab-qa', '~> 12', '>= 12.5.0', require: 'gitlab/qa'
gem 'gitlab_quality-test_tooling', '~> 1.5.3', require: false
gem 'gitlab-utils', path: '../gems/gitlab-utils'
gem 'activesupport', '~> 7.0.8' # This should stay in sync with the root's Gemfile
-gem 'allure-rspec', '~> 2.20.0'
+gem 'allure-rspec', '~> 2.23.0'
gem 'capybara', '~> 3.39.2'
gem 'capybara-screenshot', '~> 1.0.26'
gem 'rake', '~> 13', '>= 13.1.0'
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index 733f3c1dd09..4df8b2b7c55 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -30,12 +30,11 @@ GEM
rack-test (>= 1.1.0, < 2.0)
rest-client (>= 2.0.2, < 3.0)
rspec (~> 3.8)
- allure-rspec (2.20.0)
- allure-ruby-commons (= 2.20.0)
+ allure-rspec (2.23.0)
+ allure-ruby-commons (= 2.23.0)
rspec-core (>= 3.8, < 4)
- allure-ruby-commons (2.20.0)
+ allure-ruby-commons (2.23.0)
mime-types (>= 3.3, < 4)
- oj (>= 3.10, < 4)
require_all (>= 2, < 4)
rspec-expectations (~> 3.12)
uuid (>= 2.3, < 3)
@@ -224,7 +223,6 @@ GEM
octokit (8.0.0)
faraday (>= 1, < 3)
sawyer (~> 0.9)
- oj (3.13.23)
os (1.1.4)
parallel (1.23.0)
parallel_tests (4.3.0)
@@ -353,7 +351,7 @@ PLATFORMS
DEPENDENCIES
activesupport (~> 7.0.8)
airborne (~> 0.3.7)
- allure-rspec (~> 2.20.0)
+ allure-rspec (~> 2.23.0)
capybara (~> 3.39.2)
capybara-screenshot (~> 1.0.26)
chemlab (~> 0.11, >= 0.11.1)
diff --git a/spec/commands/sidekiq_cluster/cli_spec.rb b/spec/commands/sidekiq_cluster/cli_spec.rb
index 5fdeb1fa900..ceee61fb302 100644
--- a/spec/commands/sidekiq_cluster/cli_spec.rb
+++ b/spec/commands/sidekiq_cluster/cli_spec.rb
@@ -248,13 +248,13 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, feature_category: :gitlab_cli, stub_
if Gitlab.ee?
[
%w[incident_management_close_incident status_page_publish] + described_class::DEFAULT_QUEUES,
- %w[bulk_imports_pipeline bulk_imports_pipeline_batch bulk_imports_relation_export project_export projects_import_export_parallel_project_export projects_import_export_relation_export repository_import project_template_export] +
+ %w[bulk_imports_pipeline bulk_imports_pipeline_batch bulk_imports_relation_batch_export bulk_imports_relation_export project_export projects_import_export_parallel_project_export projects_import_export_relation_export repository_import project_template_export] +
described_class::DEFAULT_QUEUES
]
else
[
%w[incident_management_close_incident] + described_class::DEFAULT_QUEUES,
- %w[bulk_imports_pipeline bulk_imports_pipeline_batch bulk_imports_relation_export project_export projects_import_export_parallel_project_export projects_import_export_relation_export repository_import] +
+ %w[bulk_imports_pipeline bulk_imports_pipeline_batch bulk_imports_relation_batch_export bulk_imports_relation_export project_export projects_import_export_parallel_project_export projects_import_export_relation_export repository_import] +
described_class::DEFAULT_QUEUES
]
end
diff --git a/spec/factories/users/phone_number_validations.rb b/spec/factories/users/phone_number_validations.rb
index b7e6e819127..f4e373dd9bd 100644
--- a/spec/factories/users/phone_number_validations.rb
+++ b/spec/factories/users/phone_number_validations.rb
@@ -7,5 +7,9 @@ FactoryBot.define do
international_dial_code { 1 }
phone_number { '555' }
telesign_reference_xid { FFaker::Guid.guid }
+
+ trait(:validated) do
+ validated_at { Time.zone.now }
+ end
end
end
diff --git a/spec/frontend/work_items/components/work_item_ancestors/disclosure_hierarchy_item_spec.js b/spec/frontend/work_items/components/work_item_ancestors/disclosure_hierarchy_item_spec.js
new file mode 100644
index 00000000000..2cfe61654ad
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_ancestors/disclosure_hierarchy_item_spec.js
@@ -0,0 +1,53 @@
+import { GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+
+import DisclosureHierarchyItem from '~/work_items/components/work_item_ancestors/disclosure_hierarchy_item.vue';
+import { mockDisclosureHierarchyItems } from './mock_data';
+
+describe('DisclosurePathItem', () => {
+ let wrapper;
+
+ const findIcon = () => wrapper.findComponent(GlIcon);
+
+ const createComponent = (props = {}, options = {}) => {
+ return shallowMount(DisclosureHierarchyItem, {
+ propsData: {
+ item: mockDisclosureHierarchyItems[0],
+ ...props,
+ },
+ ...options,
+ });
+ };
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ describe('renders the item', () => {
+ it('renders the inline icon', () => {
+ expect(findIcon().exists()).toBe(true);
+ expect(findIcon().props('name')).toBe(mockDisclosureHierarchyItems[0].icon);
+ });
+ });
+
+ describe('item slot', () => {
+ beforeEach(() => {
+ wrapper = createComponent(null, {
+ scopedSlots: {
+ default: `
+ <div
+ data-testid="item-slot-content">
+ {{ props.item.title }}
+ </div>
+ `,
+ },
+ });
+ });
+
+ it('contains all elements passed into the additional slot', () => {
+ const item = wrapper.find('[data-testid="item-slot-content"]');
+
+ expect(item.text()).toBe(mockDisclosureHierarchyItems[0].title);
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_ancestors/disclosure_hierarchy_spec.js b/spec/frontend/work_items/components/work_item_ancestors/disclosure_hierarchy_spec.js
new file mode 100644
index 00000000000..b808c13c3e7
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_ancestors/disclosure_hierarchy_spec.js
@@ -0,0 +1,99 @@
+import { shallowMount } from '@vue/test-utils';
+
+import { GlDisclosureDropdown, GlTooltip } from '@gitlab/ui';
+import DisclosureHierarchy from '~/work_items/components/work_item_ancestors//disclosure_hierarchy.vue';
+import DisclosureHierarchyItem from '~/work_items/components/work_item_ancestors/disclosure_hierarchy_item.vue';
+import { mockDisclosureHierarchyItems } from './mock_data';
+
+describe('DisclosurePath', () => {
+ let wrapper;
+
+ const createComponent = (props = {}, options = {}) => {
+ return shallowMount(DisclosureHierarchy, {
+ propsData: {
+ items: mockDisclosureHierarchyItems,
+ ...props,
+ },
+ ...options,
+ });
+ };
+
+ const listItems = () => wrapper.findAllComponents(DisclosureHierarchyItem);
+ const itemAt = (index) => listItems().at(index);
+ const itemTextAt = (index) => itemAt(index).props('item').title;
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ describe('renders the list of items', () => {
+ it('renders the correct number of items', () => {
+ expect(listItems().length).toBe(mockDisclosureHierarchyItems.length);
+ });
+
+ it('renders the items in the correct order', () => {
+ expect(itemTextAt(0)).toContain(mockDisclosureHierarchyItems[0].title);
+ expect(itemTextAt(4)).toContain(mockDisclosureHierarchyItems[4].title);
+ expect(itemTextAt(9)).toContain(mockDisclosureHierarchyItems[9].title);
+ });
+ });
+
+ describe('slots', () => {
+ beforeEach(() => {
+ wrapper = createComponent(null, {
+ scopedSlots: {
+ default: `
+ <div
+ :data-itemid="props.itemId"
+ data-testid="item-slot-content">
+ {{ props.item.title }}
+ </div>
+ `,
+ },
+ });
+ });
+
+ it('contains all elements passed into the default slot', () => {
+ mockDisclosureHierarchyItems.forEach((item, index) => {
+ const disclosureItem = wrapper.findAll('[data-testid="item-slot-content"]').at(index);
+
+ expect(disclosureItem.text()).toBe(item.title);
+ expect(disclosureItem.attributes('data-itemid')).toContain('disclosure-');
+ });
+ });
+ });
+
+ describe('with ellipsis', () => {
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findTooltip = () => wrapper.findComponent(GlTooltip);
+ const findTooltipText = () => findTooltip().text();
+ const tooltipText = 'Display more items';
+
+ beforeEach(() => {
+ wrapper = createComponent({ withEllipsis: true, ellipsisTooltipLabel: tooltipText });
+ });
+
+ describe('renders items and dropdown', () => {
+ it('renders 2 items', () => {
+ expect(listItems().length).toBe(2);
+ });
+
+ it('renders first and last items', () => {
+ expect(itemTextAt(0)).toContain(mockDisclosureHierarchyItems[0].title);
+ expect(itemTextAt(1)).toContain(
+ mockDisclosureHierarchyItems[mockDisclosureHierarchyItems.length - 1].title,
+ );
+ });
+
+ it('renders dropdown with the rest of the items passed down', () => {
+ expect(findDropdown().exists()).toBe(true);
+ expect(findDropdown().props('items').length).toBe(mockDisclosureHierarchyItems.length - 2);
+ });
+
+ it('renders tooltip with text passed as prop', () => {
+ expect(findTooltip().exists()).toBe(true);
+ expect(findTooltipText()).toBe(tooltipText);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_ancestors/mock_data.js b/spec/frontend/work_items/components/work_item_ancestors/mock_data.js
new file mode 100644
index 00000000000..8e7f99658de
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_ancestors/mock_data.js
@@ -0,0 +1,197 @@
+export const mockDisclosureHierarchyItems = [
+ {
+ title: 'First',
+ icon: 'epic',
+ href: '#',
+ },
+ {
+ title: 'Second',
+ icon: 'epic',
+ href: '#',
+ },
+ {
+ title: 'Third',
+ icon: 'epic',
+ href: '#',
+ },
+ {
+ title: 'Fourth',
+ icon: 'epic',
+ href: '#',
+ },
+ {
+ title: 'Fifth',
+ icon: 'issues',
+ href: '#',
+ },
+ {
+ title: 'Sixth',
+ icon: 'issues',
+ href: '#',
+ },
+ {
+ title: 'Seventh',
+ icon: 'issues',
+ href: '#',
+ },
+ {
+ title: 'Eighth',
+ icon: 'issue-type-task',
+ href: '#',
+ disabled: true,
+ },
+ {
+ title: 'Ninth',
+ icon: 'issue-type-task',
+ href: '#',
+ },
+ {
+ title: 'Tenth',
+ icon: 'issue-type-task',
+ href: '#',
+ },
+];
+
+export const workItemAncestorsQueryResponse = {
+ data: {
+ workItem: {
+ __typename: 'WorkItem',
+ id: 'gid://gitlab/WorkItem/1',
+ title: 'Test',
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetHierarchy',
+ type: 'HIERARCHY',
+ parent: {
+ id: 'gid://gitlab/Issue/1',
+ },
+ ancestors: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WorkItem/444',
+ iid: '4',
+ reference: '#40',
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
+ confidential: false,
+ title: '123',
+ state: 'OPEN',
+ webUrl: '/gitlab-org/gitlab-test/-/work_items/4',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/2',
+ name: 'Issue',
+ iconName: 'issue-type-issue',
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+};
+
+export const workItemThreeAncestorsQueryResponse = {
+ data: {
+ workItem: {
+ __typename: 'WorkItem',
+ id: 'gid://gitlab/WorkItem/1',
+ title: 'Test',
+ workItemType: {
+ __typename: 'WorkItemType',
+ id: 'gid://gitlab/WorkItems::Type/5',
+ name: 'Task',
+ iconName: 'issue-type-task',
+ },
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetHierarchy',
+ type: 'HIERARCHY',
+ parent: {
+ id: 'gid://gitlab/Issue/1',
+ },
+ ancestors: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WorkItem/444',
+ iid: '4',
+ reference: '#40',
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
+ confidential: false,
+ title: '123',
+ state: 'OPEN',
+ webUrl: '/gitlab-org/gitlab-test/-/work_items/4',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/2',
+ name: 'Issue',
+ iconName: 'issue-type-issue',
+ },
+ },
+ {
+ id: 'gid://gitlab/WorkItem/445',
+ iid: '5',
+ reference: '#41',
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
+ confidential: false,
+ title: '1234',
+ state: 'OPEN',
+ webUrl: '/gitlab-org/gitlab-test/-/work_items/5',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/2',
+ name: 'Issue',
+ iconName: 'issue-type-issue',
+ },
+ },
+ {
+ id: 'gid://gitlab/WorkItem/446',
+ iid: '6',
+ reference: '#42',
+ createdAt: '2022-08-03T12:41:54Z',
+ closedAt: null,
+ confidential: false,
+ title: '12345',
+ state: 'OPEN',
+ webUrl: '/gitlab-org/gitlab-test/-/work_items/6',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/2',
+ name: 'Issue',
+ iconName: 'issue-type-issue',
+ },
+ },
+ ],
+ },
+ },
+ ],
+ },
+ },
+};
+
+export const workItemEmptyAncestorsQueryResponse = {
+ data: {
+ workItem: {
+ __typename: 'WorkItem',
+ id: 'gid://gitlab/WorkItem/1',
+ title: 'Test',
+ workItemType: {
+ __typename: 'WorkItemType',
+ id: 'gid://gitlab/WorkItems::Type/5',
+ name: 'Task',
+ iconName: 'issue-type-task',
+ },
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetHierarchy',
+ type: 'HIERARCHY',
+ parent: {
+ id: null,
+ },
+ ancestors: {
+ nodes: [],
+ },
+ },
+ ],
+ },
+ },
+};
diff --git a/spec/frontend/work_items/components/work_item_ancestors/work_item_ancestors_spec.js b/spec/frontend/work_items/components/work_item_ancestors/work_item_ancestors_spec.js
new file mode 100644
index 00000000000..a9f66b20f06
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_ancestors/work_item_ancestors_spec.js
@@ -0,0 +1,117 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlPopover } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+
+import { createAlert } from '~/alert';
+import DisclosureHierarchy from '~/work_items/components/work_item_ancestors/disclosure_hierarchy.vue';
+import WorkItemAncestors from '~/work_items/components/work_item_ancestors/work_item_ancestors.vue';
+import workItemAncestorsQuery from '~/work_items/graphql/work_item_ancestors.query.graphql';
+import { formatAncestors } from '~/work_items/utils';
+
+import { workItemTask } from '../../mock_data';
+import {
+ workItemAncestorsQueryResponse,
+ workItemEmptyAncestorsQueryResponse,
+ workItemThreeAncestorsQueryResponse,
+} from './mock_data';
+
+Vue.use(VueApollo);
+jest.mock('~/alert');
+
+describe('WorkItemAncestors', () => {
+ let wrapper;
+ let mockApollo;
+
+ const workItemAncestorsQueryHandler = jest.fn().mockResolvedValue(workItemAncestorsQueryResponse);
+ const workItemEmptyAncestorsQueryHandler = jest
+ .fn()
+ .mockResolvedValue(workItemEmptyAncestorsQueryResponse);
+ const workItemThreeAncestorsQueryHandler = jest
+ .fn()
+ .mockResolvedValue(workItemThreeAncestorsQueryResponse);
+ const workItemAncestorsFailureHandler = jest.fn().mockRejectedValue(new Error());
+
+ const findDisclosureHierarchy = () => wrapper.findComponent(DisclosureHierarchy);
+ const findPopover = () => wrapper.findComponent(GlPopover);
+
+ const createComponent = ({
+ props = {},
+ options = {},
+ ancestorsQueryHandler = workItemAncestorsQueryHandler,
+ } = {}) => {
+ mockApollo = createMockApollo([[workItemAncestorsQuery, ancestorsQueryHandler]]);
+ return mountExtended(WorkItemAncestors, {
+ apolloProvider: mockApollo,
+ propsData: {
+ workItem: workItemTask,
+ ...props,
+ },
+ ...options,
+ });
+ };
+
+ beforeEach(async () => {
+ createAlert.mockClear();
+ wrapper = createComponent();
+ await waitForPromises();
+ });
+
+ it('fetches work item ancestors', () => {
+ expect(workItemAncestorsQueryHandler).toHaveBeenCalled();
+ });
+
+ it('displays DisclosureHierarchy component with ancestors when work item has at least one ancestor', () => {
+ expect(findDisclosureHierarchy().exists()).toBe(true);
+ expect(findDisclosureHierarchy().props('items')).toEqual(
+ expect.objectContaining(formatAncestors(workItemAncestorsQueryResponse.data.workItem)),
+ );
+ });
+
+ it('does not display DisclosureHierarchy component when work item has no ancestor', async () => {
+ wrapper = createComponent({ ancestorsQueryHandler: workItemEmptyAncestorsQueryHandler });
+ await waitForPromises();
+
+ expect(findDisclosureHierarchy().exists()).toBe(false);
+ });
+
+ it('displays work item info in popover on hover and focus', () => {
+ expect(findPopover().exists()).toBe(true);
+ expect(findPopover().props('triggers')).toBe('hover focus');
+
+ const ancestor = findDisclosureHierarchy().props('items')[0];
+
+ expect(findPopover().text()).toContain(ancestor.title);
+ expect(findPopover().text()).toContain(ancestor.reference);
+ });
+
+ describe('when work item has less than 3 ancestors', () => {
+ it('does not activate ellipsis option for DisclosureHierarchy component', () => {
+ expect(findDisclosureHierarchy().props('withEllipsis')).toBe(false);
+ });
+ });
+
+ describe('when work item has at least 3 ancestors', () => {
+ beforeEach(async () => {
+ wrapper = createComponent({ ancestorsQueryHandler: workItemThreeAncestorsQueryHandler });
+ await waitForPromises();
+ });
+
+ it('activates ellipsis option for DisclosureHierarchy component', () => {
+ expect(findDisclosureHierarchy().props('withEllipsis')).toBe(true);
+ });
+ });
+
+ it('creates alert when the query fails', async () => {
+ createComponent({ ancestorsQueryHandler: workItemAncestorsFailureHandler });
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ captureError: true,
+ error: expect.any(Object),
+ message: 'Something went wrong while fetching ancestors.',
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js
index d0fc732afe7..b88396f89d4 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert, GlSkeletonLoader, GlButton, GlEmptyState } from '@gitlab/ui';
+import { GlAlert, GlSkeletonLoader, GlEmptyState } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -9,6 +9,7 @@ import setWindowLocation from 'helpers/set_window_location_helper';
import { stubComponent } from 'helpers/stub_component';
import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
import WorkItemActions from '~/work_items/components/work_item_actions.vue';
+import WorkItemAncestors from '~/work_items/components/work_item_ancestors/work_item_ancestors.vue';
import WorkItemDescription from '~/work_items/components/work_item_description.vue';
import WorkItemCreatedUpdated from '~/work_items/components/work_item_created_updated.vue';
import WorkItemAttributesWrapper from '~/work_items/components/work_item_attributes_wrapper.vue';
@@ -69,8 +70,7 @@ describe('WorkItemDetail component', () => {
const findCreatedUpdated = () => wrapper.findComponent(WorkItemCreatedUpdated);
const findWorkItemDescription = () => wrapper.findComponent(WorkItemDescription);
const findWorkItemAttributesWrapper = () => wrapper.findComponent(WorkItemAttributesWrapper);
- const findParent = () => wrapper.findByTestId('work-item-parent');
- const findParentButton = () => findParent().findComponent(GlButton);
+ const findAncestors = () => wrapper.findComponent(WorkItemAncestors);
const findCloseButton = () => wrapper.findByTestId('work-item-close');
const findWorkItemType = () => wrapper.findByTestId('work-item-type');
const findHierarchyTree = () => wrapper.findComponent(WorkItemTree);
@@ -127,6 +127,7 @@ describe('WorkItemDetail component', () => {
reportAbusePath: '/report/abuse/path',
},
stubs: {
+ WorkItemAncestors: true,
WorkItemWeight: true,
WorkItemIteration: true,
WorkItemHealthStatus: true,
@@ -359,19 +360,19 @@ describe('WorkItemDetail component', () => {
});
});
- describe('secondary breadcrumbs', () => {
- it('does not show secondary breadcrumbs by default', () => {
+ describe('ancestors widget', () => {
+ it('does not show ancestors widget by default', () => {
createComponent();
- expect(findParent().exists()).toBe(false);
+ expect(findAncestors().exists()).toBe(false);
});
- it('does not show secondary breadcrumbs if there is not a parent', async () => {
+ it('does not show ancestors widget if there is not a parent', async () => {
createComponent({ handler: jest.fn().mockResolvedValue(workItemQueryResponseWithoutParent) });
await waitForPromises();
- expect(findParent().exists()).toBe(false);
+ expect(findAncestors().exists()).toBe(false);
});
it('shows title in the header when there is no parent', async () => {
@@ -389,45 +390,8 @@ describe('WorkItemDetail component', () => {
return waitForPromises();
});
- it('shows secondary breadcrumbs if there is a parent', () => {
- expect(findParent().exists()).toBe(true);
- });
-
- it('shows parent breadcrumb icon', () => {
- expect(findParentButton().props('icon')).toBe(mockParent.parent.workItemType.iconName);
- });
-
- it('shows parent title and iid', () => {
- expect(findParentButton().text()).toBe(
- `${mockParent.parent.title} #${mockParent.parent.iid}`,
- );
- });
-
- it('sets the parent breadcrumb URL pointing to issue page when parent type is `Issue`', () => {
- expect(findParentButton().attributes().href).toBe('../../-/issues/5');
- });
-
- it('sets the parent breadcrumb URL based on parent webUrl when parent type is not `Issue`', async () => {
- const mockParentObjective = {
- parent: {
- ...mockParent.parent,
- workItemType: {
- id: mockParent.parent.workItemType.id,
- name: 'Objective',
- iconName: 'issue-type-objective',
- },
- },
- };
- const parentResponse = workItemByIidResponseFactory(mockParentObjective);
- createComponent({ handler: jest.fn().mockResolvedValue(parentResponse) });
- await waitForPromises();
-
- expect(findParentButton().attributes().href).toBe(mockParentObjective.parent.webUrl);
- });
-
- it('shows work item type and iid', () => {
- const { iid } = workItemQueryResponse.data.workspace.workItems.nodes[0];
- expect(findParent().text()).toContain(`#${iid}`);
+ it('shows ancestors widget if there is a parent', () => {
+ expect(findAncestors().exists()).toBe(true);
});
it('does not show title in the header when parent exists', () => {
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 07052113d5d..4a71b34391c 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -5270,7 +5270,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
describe '.with_project_and_metadata' do
it 'does not join across databases' do
with_cross_joins_prevented do
- ::Ci::Build.with_project_and_metadata.to_a
+ ::Ci::Build.with_project_and_metadata(project).to_a
end
end
end