diff options
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 |