diff options
Diffstat (limited to 'app/assets')
13 files changed, 299 insertions, 57 deletions
diff --git a/app/assets/javascripts/branches/init_new_branch_ref_selector.js b/app/assets/javascripts/branches/init_new_branch_ref_selector.js new file mode 100644 index 00000000000..aad3fbb9982 --- /dev/null +++ b/app/assets/javascripts/branches/init_new_branch_ref_selector.js @@ -0,0 +1,25 @@ +import Vue from 'vue'; +import RefSelector from '~/ref/components/ref_selector.vue'; + +export default function initNewBranchRefSelector() { + const el = document.querySelector('.js-new-branch-ref-selector'); + + if (!el) { + return false; + } + + const { projectId, defaultBranchName, hiddenInputName } = el.dataset; + + return new Vue({ + el, + render(createComponent) { + return createComponent(RefSelector, { + props: { + value: defaultBranchName, + name: hiddenInputName, + projectId, + }, + }); + }, + }); +} diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js index ef36e58374c..a7c2e572037 100644 --- a/app/assets/javascripts/new_branch_form.js +++ b/app/assets/javascripts/new_branch_form.js @@ -1,15 +1,9 @@ /* eslint-disable func-names, no-return-assign, @gitlab/require-i18n-strings */ - -import $ from 'jquery'; -import RefSelectDropdown from './ref_select_dropdown'; - export default class NewBranchForm { - constructor(form, availableRefs) { + constructor(form) { this.validate = this.validate.bind(this); this.branchNameError = form.querySelector('.js-branch-name-error'); this.name = form.querySelector('.js-branch-name'); - this.ref = form.querySelector('#ref'); - new RefSelectDropdown($('.js-branch-select'), availableRefs); // eslint-disable-line no-new this.setupRestrictions(); this.addBinding(); this.init(); diff --git a/app/assets/javascripts/pages/projects/branches/new/index.js b/app/assets/javascripts/pages/projects/branches/new/index.js index dbae89b5ade..f2b03468b0b 100644 --- a/app/assets/javascripts/pages/projects/branches/new/index.js +++ b/app/assets/javascripts/pages/projects/branches/new/index.js @@ -1,7 +1,6 @@ import NewBranchForm from '~/new_branch_form'; +import initNewBranchRefSelector from '~/branches/init_new_branch_ref_selector'; +initNewBranchRefSelector(); // eslint-disable-next-line no-new -new NewBranchForm( - document.querySelector('.js-create-branch-form'), - JSON.parse(document.getElementById('availableRefs').innerHTML), -); +new NewBranchForm(document.querySelector('.js-create-branch-form')); diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue index b04da53bf89..763f2f338a3 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue @@ -1,5 +1,5 @@ <script> -import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui'; +import { GlButton, GlLink, GlIcon, GlTooltipDirective } from '@gitlab/ui'; import { __, s__ } from '~/locale'; import { createAlert } from '~/flash'; @@ -10,18 +10,24 @@ import { STATE_OPEN, TASK_TYPE_NAME, WORK_ITEM_TYPE_VALUE_OBJECTIVE, + WIDGET_TYPE_MILESTONE, WIDGET_TYPE_HIERARCHY, + WIDGET_TYPE_ASSIGNEES, + WIDGET_TYPE_LABELS, WORK_ITEM_NAME_TO_ICON_MAP, } from '../../constants'; import getWorkItemTreeQuery from '../../graphql/work_item_tree.query.graphql'; +import WorkItemLinkChildMetadata from './work_item_link_child_metadata.vue'; import WorkItemLinksMenu from './work_item_links_menu.vue'; import WorkItemTreeChildren from './work_item_tree_children.vue'; export default { components: { + GlLink, GlButton, GlIcon, RichTimestampTooltip, + WorkItemLinkChildMetadata, WorkItemLinksMenu, WorkItemTreeChildren, }, @@ -67,6 +73,9 @@ export default { canHaveChildren() { return this.workItemType === WORK_ITEM_TYPE_VALUE_OBJECTIVE; }, + allowsScopedLabels() { + return this.getWidgetByType(this.childItem, WIDGET_TYPE_LABELS)?.allowsScopedLabels; + }, isItemOpen() { return this.childItem.state === STATE_OPEN; }, @@ -95,7 +104,7 @@ export default { return `/${this.projectPath}/-/work_items/${getIdFromGraphQLId(this.childItem.id)}`; }, hasChildren() { - return this.getWidgetHierarchyForChild(this.childItem)?.hasChildren; + return this.getWidgetByType(this.childItem, WIDGET_TYPE_HIERARCHY)?.hasChildren; }, chevronType() { return this.isExpanded ? 'chevron-down' : 'chevron-right'; @@ -103,6 +112,18 @@ export default { chevronTooltip() { return this.isExpanded ? __('Collapse') : __('Expand'); }, + hasMetadata() { + return this.milestone || this.assignees.length > 0 || this.labels.length > 0; + }, + milestone() { + return this.getWidgetByType(this.childItem, WIDGET_TYPE_MILESTONE)?.milestone; + }, + assignees() { + return this.getWidgetByType(this.childItem, WIDGET_TYPE_ASSIGNEES)?.assignees?.nodes || []; + }, + labels() { + return this.getWidgetByType(this.childItem, WIDGET_TYPE_LABELS)?.labels?.nodes || []; + }, }, methods: { toggleItem() { @@ -111,11 +132,8 @@ export default { this.fetchChildren(); } }, - getWidgetHierarchyForChild(workItem) { - const widgetHierarchy = workItem?.widgets?.find( - (widget) => widget.type === WIDGET_TYPE_HIERARCHY, - ); - return widgetHierarchy || {}; + getWidgetByType(workItem, widgetType) { + return workItem?.widgets?.find((widget) => widget.type === widgetType); }, async fetchChildren() { this.isLoadingChildren = true; @@ -126,7 +144,7 @@ export default { id: this.childItem.id, }, }); - this.children = this.getWidgetHierarchyForChild(data?.workItem).children.nodes; + this.children = this.getWidgetByType(data?.workItem, WIDGET_TYPE_HIERARCHY).children.nodes; } catch (error) { this.isExpanded = !this.isExpanded; createAlert({ @@ -145,7 +163,7 @@ export default { <template> <div> <div - class="gl-display-flex gl-align-items-center gl-mb-3" + class="gl-display-flex gl-align-items-flex-start gl-mb-3" :class="{ 'gl-ml-6': canHaveChildren && !hasChildren && hasIndirectChildren }" > <gl-button @@ -156,7 +174,7 @@ export default { :icon="chevronType" category="tertiary" :loading="isLoadingChildren" - class="gl-px-0! gl-py-4! gl-mr-3" + class="gl-px-0! gl-py-3! gl-mr-3" data-testid="expand-child" @click="toggleItem" /> @@ -164,40 +182,60 @@ export default { class="gl-relative gl-display-flex gl-flex-grow-1 gl-overflow-break-word gl-min-w-0 gl-bg-white gl-py-3 gl-px-4 gl-border gl-border-gray-100 gl-rounded-base gl-line-height-32" data-testid="links-child" > - <div class="gl-overflow-hidden gl-display-flex gl-align-items-center gl-flex-grow-1"> - <span :id="`stateIcon-${childItem.id}`" class="gl-mr-3" data-testid="item-status-icon"> + <span + :id="`stateIcon-${childItem.id}`" + class="gl-mr-3" + :class="{ 'gl-display-flex': hasMetadata }" + data-testid="item-status-icon" + > + <gl-icon + class="gl-text-secondary" + :class="iconClass" + :name="iconName" + :aria-label="stateTimestampTypeText" + /> + </span> + <div + class="gl-display-flex gl-flex-grow-1" + :class="{ + 'gl-flex-direction-column gl-align-items-flex-start': hasMetadata, + 'gl-align-items-center': !hasMetadata, + }" + > + <div class="gl-display-flex"> + <rich-timestamp-tooltip + :target="`stateIcon-${childItem.id}`" + :raw-timestamp="stateTimestamp" + :timestamp-type-text="stateTimestampTypeText" + /> <gl-icon - class="gl-text-secondary" - :class="iconClass" - :name="iconName" - :aria-label="stateTimestampTypeText" + v-if="childItem.confidential" + v-gl-tooltip.top + name="eye-slash" + class="gl-mr-2 gl-text-orange-500" + data-testid="confidential-icon" + :aria-label="__('Confidential')" + :title="__('Confidential')" /> - </span> - <rich-timestamp-tooltip - :target="`stateIcon-${childItem.id}`" - :raw-timestamp="stateTimestamp" - :timestamp-type-text="stateTimestampTypeText" - /> - <gl-icon - v-if="childItem.confidential" - v-gl-tooltip.top - name="eye-slash" - class="gl-mr-2 gl-text-orange-500" - data-testid="confidential-icon" - :aria-label="__('Confidential')" - :title="__('Confidential')" + <gl-link + :href="childPath" + class="gl-overflow-wrap-break gl-line-height-normal gl-text-black-normal! gl-font-weight-bold" + data-testid="item-title" + @click="$emit('click', $event)" + @mouseover="$emit('mouseover')" + @mouseout="$emit('mouseout')" + > + {{ childItem.title }} + </gl-link> + </div> + <work-item-link-child-metadata + v-if="hasMetadata" + :allows-scoped-labels="allowsScopedLabels" + :milestone="milestone" + :assignees="assignees" + :labels="labels" + class="gl-mt-3" /> - <gl-button - :href="childPath" - category="tertiary" - variant="link" - class="gl-text-truncate gl-max-w-80 gl-text-black-normal!" - @click="$emit('click', $event)" - @mouseover="$emit('mouseover')" - @mouseout="$emit('mouseout')" - > - {{ childItem.title }} - </gl-button> </div> <div v-if="canUpdate" diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child_metadata.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child_metadata.vue new file mode 100644 index 00000000000..7be7e1f3496 --- /dev/null +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child_metadata.vue @@ -0,0 +1,123 @@ +<script> +import { GlLabel, GlAvatar, GlAvatarLink, GlAvatarsInline, GlTooltipDirective } from '@gitlab/ui'; + +import { s__, sprintf } from '~/locale'; +import { isScopedLabel } from '~/lib/utils/common_utils'; + +import ItemMilestone from '~/issuable/components/issue_milestone.vue'; + +export default { + components: { + GlLabel, + GlAvatar, + GlAvatarLink, + GlAvatarsInline, + ItemMilestone, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + allowsScopedLabels: { + type: Boolean, + required: false, + default: false, + }, + milestone: { + type: Object, + required: false, + default: null, + }, + assignees: { + type: Array, + required: false, + default: () => [], + }, + labels: { + type: Array, + required: false, + default: () => [], + }, + }, + computed: { + assigneesCollapsedTooltip() { + if (this.assignees.length > 2) { + return sprintf(s__('WorkItem|%{count} more assignees'), { + count: this.assignees.length - 2, + }); + } + return ''; + }, + assigneesContainerClass() { + if (this.assignees.length === 2) { + return 'fixed-width-avatars-2'; + } else if (this.assignees.length > 2) { + return 'fixed-width-avatars-3'; + } + return ''; + }, + labelsContainerClass() { + if (this.milestone || this.assignees.length) { + return 'gl-sm-ml-5'; + } + return ''; + }, + }, + methods: { + showScopedLabel(label) { + return isScopedLabel(label) && this.allowsScopedLabels; + }, + }, +}; +</script> + +<template> + <div class="gl-display-flex gl-flex-wrap gl-align-items-center"> + <item-milestone + v-if="milestone" + :milestone="milestone" + class="gl-display-flex gl-align-items-center gl-mr-5 gl-max-w-15 gl-text-secondary! gl-cursor-help! gl-text-decoration-none!" + /> + <gl-avatars-inline + v-if="assignees.length" + :avatars="assignees" + :collapsed="true" + :max-visible="2" + :avatar-size="24" + badge-tooltip-prop="name" + :badge-sr-only-text="assigneesCollapsedTooltip" + :class="assigneesContainerClass" + > + <template #avatar="{ avatar }"> + <gl-avatar-link v-gl-tooltip target="blank" :href="avatar.webUrl" :title="avatar.name"> + <gl-avatar :src="avatar.avatarUrl" :size="24" /> + </gl-avatar-link> + </template> + </gl-avatars-inline> + <div v-if="labels.length" class="gl-display-flex gl-flex-wrap" :class="labelsContainerClass"> + <gl-label + v-for="label in labels" + :key="label.id" + :title="label.title" + :background-color="label.color" + :description="label.description" + :scoped="showScopedLabel(label)" + class="gl-mt-2 gl-sm-mt-0 gl-mr-2 gl-mb-auto gl-label-sm" + tooltip-placement="top" + /> + </div> + </div> +</template> + +<style scoped> +/** + * These overrides are needed to address https://gitlab.com/gitlab-org/gitlab-ui/-/issues/865 + */ +.fixed-width-avatars-2 { + width: 42px !important; +} + +.fixed-width-avatars-3 { + width: 67px !important; +} +</style> diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue index b4bb8a99452..f06de2ca048 100644 --- a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue +++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue @@ -15,6 +15,7 @@ import { WORK_ITEMS_TREE_TEXT_MAP, WORK_ITEM_TYPE_ENUM_OBJECTIVE, WORK_ITEM_TYPE_ENUM_KEY_RESULT, + WORK_ITEM_TYPE_VALUE_OBJECTIVE, } from '../../constants'; import workItemQuery from '../../graphql/work_item.query.graphql'; import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql'; @@ -148,13 +149,17 @@ export default { }); }, prefetchWorkItem({ id, iid }) { - this.prefetch = setTimeout( - () => this.addWorkItemQuery({ id, iid }), - DEFAULT_DEBOUNCE_AND_THROTTLE_MS, - ); + if (this.workItemType !== WORK_ITEM_TYPE_VALUE_OBJECTIVE) { + this.prefetch = setTimeout( + () => this.addWorkItemQuery({ id, iid }), + DEFAULT_DEBOUNCE_AND_THROTTLE_MS, + ); + } }, clearPrefetching() { - clearTimeout(this.prefetch); + if (this.prefetch) { + clearTimeout(this.prefetch); + } }, }, }; diff --git a/app/assets/javascripts/work_items/components/work_item_type_icon.vue b/app/assets/javascripts/work_items/components/work_item_type_icon.vue index 96a6493357c..32678e29fa4 100644 --- a/app/assets/javascripts/work_items/components/work_item_type_icon.vue +++ b/app/assets/javascripts/work_items/components/work_item_type_icon.vue @@ -33,6 +33,11 @@ export default { }, computed: { iconName() { + // TODO: Remove this once https://gitlab.com/gitlab-org/gitlab-svgs/-/merge_requests/865 + // is merged and updated in GitLab repo. + if (this.workItemIconName === 'issue-type-keyresult') { + return 'issue-type-key-result'; + } return ( this.workItemIconName || WORK_ITEMS_TYPE_MAP[this.workItemType]?.icon || 'issue-type-issue' ); diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js index cedebca8190..7fdb84a77ff 100644 --- a/app/assets/javascripts/work_items/constants.js +++ b/app/assets/javascripts/work_items/constants.js @@ -127,6 +127,12 @@ export const WORK_ITEMS_TREE_TEXT_MAP = { title: s__('WorkItem|Child objectives and key results'), empty: s__('WorkItem|No objectives or key results are currently assigned.'), }, + [WORK_ITEM_TYPE_VALUE_ISSUE]: { + title: s__('WorkItem|Tasks'), + empty: s__( + 'WorkItem|No tasks are currently assigned. Use tasks to break down this issue into smaller parts.', + ), + }, }; export const WORK_ITEM_NAME_TO_ICON_MAP = { diff --git a/app/assets/javascripts/work_items/graphql/milestone.fragment.graphql b/app/assets/javascripts/work_items/graphql/milestone.fragment.graphql index 58140aff89e..5c93370aac9 100644 --- a/app/assets/javascripts/work_items/graphql/milestone.fragment.graphql +++ b/app/assets/javascripts/work_items/graphql/milestone.fragment.graphql @@ -2,4 +2,7 @@ fragment MilestoneFragment on Milestone { expired id title + state + startDate + dueDate } diff --git a/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql new file mode 100644 index 00000000000..baefcdaea93 --- /dev/null +++ b/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql @@ -0,0 +1,29 @@ +#import "~/graphql_shared/fragments/label.fragment.graphql" +#import "~/graphql_shared/fragments/user.fragment.graphql" +#import "~/work_items/graphql/milestone.fragment.graphql" + +fragment WorkItemMetadataWidgets on WorkItemWidget { + ... on WorkItemWidgetMilestone { + type + milestone { + ...MilestoneFragment + } + } + ... on WorkItemWidgetAssignees { + type + assignees { + nodes { + ...User + } + } + } + ... on WorkItemWidgetLabels { + type + allowsScopedLabels + labels { + nodes { + ...Label + } + } + } +} diff --git a/app/assets/javascripts/work_items/graphql/work_item_tree.query.graphql b/app/assets/javascripts/work_items/graphql/work_item_tree.query.graphql index a850d002de8..006ca29e01c 100644 --- a/app/assets/javascripts/work_items/graphql/work_item_tree.query.graphql +++ b/app/assets/javascripts/work_items/graphql/work_item_tree.query.graphql @@ -1,3 +1,7 @@ +#import "~/graphql_shared/fragments/label.fragment.graphql" +#import "~/graphql_shared/fragments/user.fragment.graphql" +#import "./work_item_metadata_widgets.fragment.graphql" + query workItemTreeQuery($id: WorkItemID!) { workItem(id: $id) { id @@ -38,10 +42,12 @@ query workItemTreeQuery($id: WorkItemID!) { type hasChildren } + ...WorkItemMetadataWidgets } } } } + ...WorkItemMetadataWidgets } } } diff --git a/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql index a8d4392c1a5..cf3374e1737 100644 --- a/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql +++ b/app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql @@ -1,6 +1,7 @@ #import "~/graphql_shared/fragments/label.fragment.graphql" #import "~/graphql_shared/fragments/user.fragment.graphql" #import "~/work_items/graphql/milestone.fragment.graphql" +#import "./work_item_metadata_widgets.fragment.graphql" fragment WorkItemWidgets on WorkItemWidget { ... on WorkItemWidgetDescription { @@ -69,6 +70,7 @@ fragment WorkItemWidgets on WorkItemWidget { type hasChildren } + ...WorkItemMetadataWidgets } } } diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss index 0c777ea5d56..714dd932147 100644 --- a/app/assets/stylesheets/utilities.scss +++ b/app/assets/stylesheets/utilities.scss @@ -236,6 +236,13 @@ to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1709 } } +// TODO: Remove once https: //gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/3198 is merged +.gl-sm-ml-5 { + @include gl-media-breakpoint-up(sm) { + @include gl-ml-5; + } +} + /* End gitlab-ui#1709 */ /* |