Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
358bd7fce3
commit
62aae3415c
43 changed files with 939 additions and 38 deletions
|
@ -42,6 +42,9 @@ export default {
|
|||
showLabel: {
|
||||
default: false,
|
||||
},
|
||||
noFlip: {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -127,6 +130,7 @@ export default {
|
|||
:disabled="disabled"
|
||||
:split="isCustomNotification"
|
||||
:text="buttonText"
|
||||
:no-flip="noFlip"
|
||||
@click="openNotificationsModal"
|
||||
>
|
||||
<notifications-dropdown-item
|
||||
|
|
|
@ -21,6 +21,7 @@ export default () => {
|
|||
projectId,
|
||||
groupId,
|
||||
showLabel,
|
||||
noFlip,
|
||||
} = el.dataset;
|
||||
|
||||
return new Vue({
|
||||
|
@ -35,6 +36,7 @@ export default () => {
|
|||
projectId,
|
||||
groupId,
|
||||
showLabel: parseBoolean(showLabel),
|
||||
noFlip: parseBoolean(noFlip),
|
||||
},
|
||||
render(h) {
|
||||
return h(NotificationsDropdown);
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
import { initWorkItemsHierarchy } from '~/work_items_hierarchy/work_items_hierarchy_bundle';
|
||||
|
||||
initWorkItemsHierarchy();
|
|
@ -0,0 +1,95 @@
|
|||
<script>
|
||||
import { GlBanner } from '@gitlab/ui';
|
||||
import Cookies from 'js-cookie';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import RESPONSE from '../static_response';
|
||||
import { WORK_ITEMS_SURVEY_COOKIE_NAME, workItemTypes } from '../constants';
|
||||
import Hierarchy from './hierarchy.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlBanner,
|
||||
Hierarchy,
|
||||
},
|
||||
inject: ['illustrationPath', 'licensePlan'],
|
||||
data() {
|
||||
return {
|
||||
bannerVisible: !parseBoolean(Cookies.get(WORK_ITEMS_SURVEY_COOKIE_NAME)),
|
||||
workItemHierarchy: RESPONSE[this.licensePlan],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasUnavailableStructure() {
|
||||
return this.workItemTypes.unavailable.length > 0;
|
||||
},
|
||||
workItemTypes() {
|
||||
return this.workItemHierarchy.reduce(
|
||||
(itemTypes, item) => {
|
||||
const key = item.available ? 'available' : 'unavailable';
|
||||
itemTypes[key].push({
|
||||
...item,
|
||||
...workItemTypes[item.type],
|
||||
nestedTypes: item.nestedTypes
|
||||
? item.nestedTypes.map((type) => workItemTypes[type])
|
||||
: null,
|
||||
});
|
||||
return itemTypes;
|
||||
},
|
||||
{ available: [], unavailable: [] },
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
Cookies.set(WORK_ITEMS_SURVEY_COOKIE_NAME, 'true', { expires: 365 * 10 });
|
||||
this.bannerVisible = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<gl-banner
|
||||
v-if="bannerVisible"
|
||||
class="gl-mt-4 gl-px-5!"
|
||||
:title="s__('Hierarchy|Help us improve work items in GitLab!')"
|
||||
:button-text="s__('Hierarchy|Take the work items survey')"
|
||||
button-link="https://forms.gle/u1BmRp8rTbwj52iq5"
|
||||
:svg-path="illustrationPath"
|
||||
@close="handleClose"
|
||||
>
|
||||
<p>
|
||||
{{
|
||||
s__(
|
||||
'Hierarchy|Is there a framework or type of work item you wish you had access to in GitLab? Give us your feedback and help us build the experiences valuable to you.',
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</gl-banner>
|
||||
<h3 class="gl-mt-5!">{{ s__('Hierarchy|Planning hierarchy') }}</h3>
|
||||
<p>
|
||||
{{
|
||||
s__(
|
||||
'Hierarchy|Deliver value more efficiently by breaking down necessary work into a hierarchical structure. This structure helps teams understand scope, priorities, and how work cascades up toward larger goals.',
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
|
||||
<div class="gl-font-weight-bold gl-mb-2">{{ s__('Hierarchy|Current structure') }}</div>
|
||||
<p class="gl-mb-3!">{{ s__('Hierarchy|You can start using these items now.') }}</p>
|
||||
<hierarchy :work-item-types="workItemTypes.available" />
|
||||
|
||||
<div
|
||||
v-if="hasUnavailableStructure"
|
||||
data-testid="unavailable-structure"
|
||||
class="gl-font-weight-bold gl-mt-5 gl-mb-2"
|
||||
>
|
||||
{{ s__('Hierarchy|Unavailable structure') }}
|
||||
</div>
|
||||
<p v-if="hasUnavailableStructure" class="gl-mb-3!">
|
||||
{{ s__('Hierarchy|These items are unavailable in the current structure.') }}
|
||||
</p>
|
||||
<hierarchy :work-item-types="workItemTypes.unavailable" />
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,119 @@
|
|||
<script>
|
||||
import { GlIcon, GlBadge } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlIcon,
|
||||
GlBadge,
|
||||
},
|
||||
props: {
|
||||
workItemTypes: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isLastItem(index, workItem) {
|
||||
const hasMoreThanOneItem = workItem.nestedTypes.length > 1;
|
||||
const isLastItemInArray = index === workItem.nestedTypes.length - 1;
|
||||
|
||||
return isLastItemInArray && hasMoreThanOneItem;
|
||||
},
|
||||
nestedWorkItemTypeMargin(index, workItem) {
|
||||
const isLastItemInArray = index === workItem.nestedTypes.length - 1;
|
||||
const hasMoreThanOneItem = workItem.nestedTypes.length > 1;
|
||||
|
||||
if (isLastItemInArray && hasMoreThanOneItem) {
|
||||
return 'gl-ml-0';
|
||||
}
|
||||
|
||||
return 'gl-ml-6';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-for="workItem in workItemTypes"
|
||||
:key="workItem.id"
|
||||
class="gl-mb-3"
|
||||
:class="{ flex: !workItem.available }"
|
||||
>
|
||||
<span
|
||||
class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-line-height-normal"
|
||||
data-testid="work-item-wrapper"
|
||||
>
|
||||
<span
|
||||
:style="{
|
||||
backgroundColor: workItem.backgroundColor,
|
||||
color: workItem.color,
|
||||
}"
|
||||
class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper"
|
||||
>
|
||||
<gl-icon :size="workItem.iconSize || 12" :name="workItem.icon" />
|
||||
</span>
|
||||
|
||||
{{ workItem.title }}
|
||||
</span>
|
||||
|
||||
<gl-badge
|
||||
v-if="!workItem.available"
|
||||
variant="info"
|
||||
icon="license"
|
||||
size="sm"
|
||||
class="gl-ml-3 gl-align-self-center"
|
||||
>{{ workItem.license }}</gl-badge
|
||||
>
|
||||
|
||||
<div v-if="workItem.nestedTypes" :class="{ 'gl-relative': workItem.nestedTypes.length > 1 }">
|
||||
<svg
|
||||
v-if="workItem.nestedTypes.length > 1"
|
||||
class="hierarchy-rounded-arrow-tail gl-text-gray-400"
|
||||
data-testid="hierarchy-rounded-arrow-tail"
|
||||
width="2"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<line
|
||||
x1="0.75"
|
||||
y1="1"
|
||||
x2="0.75"
|
||||
y2="100%"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<template v-for="(nestedWorkItem, index) in workItem.nestedTypes">
|
||||
<div :key="nestedWorkItem.id" class="gl-display-block gl-mt-2 gl-ml-6">
|
||||
<gl-icon name="arrow-down" class="gl-text-gray-400" />
|
||||
</div>
|
||||
<gl-icon
|
||||
v-if="isLastItem(index, workItem)"
|
||||
:key="nestedWorkItem.id"
|
||||
name="level-up"
|
||||
class="gl-text-gray-400 gl-ml-2 hierarchy-rounded-arrow"
|
||||
/>
|
||||
<span
|
||||
:key="nestedWorkItem.id"
|
||||
class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-mt-2 gl-line-height-normal"
|
||||
:class="nestedWorkItemTypeMargin(index, workItem)"
|
||||
>
|
||||
<span
|
||||
:style="{
|
||||
backgroundColor: nestedWorkItem.backgroundColor,
|
||||
color: nestedWorkItem.color,
|
||||
}"
|
||||
class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper"
|
||||
>
|
||||
<gl-icon :size="nestedWorkItem.iconSize || 12" :name="nestedWorkItem.icon" />
|
||||
</span>
|
||||
|
||||
{{ nestedWorkItem.title }}
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
61
app/assets/javascripts/work_items_hierarchy/constants.js
Normal file
61
app/assets/javascripts/work_items_hierarchy/constants.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
import { __ } from '~/locale';
|
||||
|
||||
export const WORK_ITEMS_SURVEY_COOKIE_NAME = 'hide_work_items_hierarchy_survey';
|
||||
|
||||
/**
|
||||
* Hard-coded strings since we're rendering hierarchy
|
||||
* items from mock responses. Remove this when we
|
||||
* have a real hierarchy endpoint.
|
||||
*/
|
||||
export const LICENSE_PLAN = {
|
||||
FREE: 'free',
|
||||
PREMIUM: 'premium',
|
||||
ULTIMATE: 'ultimate',
|
||||
};
|
||||
|
||||
export const workItemTypes = {
|
||||
EPIC: {
|
||||
title: __('Epic'),
|
||||
icon: 'epic',
|
||||
color: '#694CC0',
|
||||
backgroundColor: '#E1D8F9',
|
||||
},
|
||||
ISSUE: {
|
||||
title: __('Issue'),
|
||||
icon: 'issues',
|
||||
color: '#1068BF',
|
||||
backgroundColor: '#CBE2F9',
|
||||
},
|
||||
TASK: {
|
||||
title: __('Task'),
|
||||
icon: 'task-done',
|
||||
color: '#217645',
|
||||
backgroundColor: '#C3E6CD',
|
||||
},
|
||||
INCIDENT: {
|
||||
title: __('Incident'),
|
||||
icon: 'issue-type-incident',
|
||||
backgroundColor: '#db2a0f',
|
||||
color: '#FDD4CD',
|
||||
iconSize: 16,
|
||||
},
|
||||
SUB_EPIC: {
|
||||
title: __('Child epic'),
|
||||
icon: 'epic',
|
||||
color: '#AB6100',
|
||||
backgroundColor: '#F5D9A8',
|
||||
},
|
||||
REQUIREMENT: {
|
||||
title: __('Requirement'),
|
||||
icon: 'requirements',
|
||||
color: '#0068c5',
|
||||
backgroundColor: '#c5e3fb',
|
||||
},
|
||||
TEST_CASE: {
|
||||
title: __('Test case'),
|
||||
icon: 'issue-type-test-case',
|
||||
backgroundColor: '#007a3f',
|
||||
color: '#bae8cb',
|
||||
iconSize: 16,
|
||||
},
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
import { LICENSE_PLAN } from './constants';
|
||||
|
||||
export function inferLicensePlan({ hasSubEpics, hasEpics }) {
|
||||
if (hasSubEpics) {
|
||||
return LICENSE_PLAN.ULTIMATE;
|
||||
} else if (hasEpics) {
|
||||
return LICENSE_PLAN.PREMIUM;
|
||||
}
|
||||
return LICENSE_PLAN.FREE;
|
||||
}
|
142
app/assets/javascripts/work_items_hierarchy/static_response.js
Normal file
142
app/assets/javascripts/work_items_hierarchy/static_response.js
Normal file
|
@ -0,0 +1,142 @@
|
|||
const FREE_TIER = 'free';
|
||||
const ULTIMATE_TIER = 'ultimate';
|
||||
const PREMIUM_TIER = 'premium';
|
||||
|
||||
const RESPONSE = {
|
||||
[FREE_TIER]: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'ISSUE',
|
||||
available: true,
|
||||
license: null,
|
||||
nestedTypes: null,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'TASK',
|
||||
available: true,
|
||||
license: null,
|
||||
nestedTypes: null,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'INCIDENT',
|
||||
available: true,
|
||||
license: null,
|
||||
nestedTypes: null,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
type: 'EPIC',
|
||||
available: false,
|
||||
license: 'Premium', // eslint-disable-line @gitlab/require-i18n-strings
|
||||
nestedTypes: null,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
type: 'SUB_EPIC',
|
||||
available: false,
|
||||
license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
|
||||
nestedTypes: null,
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
type: 'REQUIREMENT',
|
||||
available: false,
|
||||
license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
|
||||
nestedTypes: null,
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
type: 'TEST_CASE',
|
||||
available: false,
|
||||
license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
|
||||
nestedTypes: null,
|
||||
},
|
||||
],
|
||||
|
||||
[PREMIUM_TIER]: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'EPIC',
|
||||
available: true,
|
||||
license: null,
|
||||
nestedTypes: ['ISSUE'],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'TASK',
|
||||
available: true,
|
||||
license: null,
|
||||
nestedTypes: null,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'INCIDENT',
|
||||
available: true,
|
||||
license: null,
|
||||
nestedTypes: null,
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
type: 'SUB_EPIC',
|
||||
available: false,
|
||||
license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
|
||||
nestedTypes: null,
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
type: 'REQUIREMENT',
|
||||
available: false,
|
||||
license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
|
||||
nestedTypes: null,
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
type: 'TEST_CASE',
|
||||
available: false,
|
||||
license: 'Ultimate', // eslint-disable-line @gitlab/require-i18n-strings
|
||||
nestedTypes: null,
|
||||
},
|
||||
],
|
||||
|
||||
[ULTIMATE_TIER]: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'EPIC',
|
||||
available: true,
|
||||
license: null,
|
||||
nestedTypes: ['SUB_EPIC', 'ISSUE'],
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
type: 'TASK',
|
||||
available: true,
|
||||
license: null,
|
||||
nestedTypes: null,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'INCIDENT',
|
||||
available: true,
|
||||
license: null,
|
||||
nestedTypes: null,
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
type: 'REQUIREMENT',
|
||||
available: true,
|
||||
license: null,
|
||||
nestedTypes: null,
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
type: 'TEST_CASE',
|
||||
available: true,
|
||||
license: null,
|
||||
nestedTypes: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default RESPONSE;
|
|
@ -0,0 +1,26 @@
|
|||
import Vue from 'vue';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import App from './components/app.vue';
|
||||
import { inferLicensePlan } from './hierarchy_util';
|
||||
|
||||
export const initWorkItemsHierarchy = () => {
|
||||
const el = document.querySelector('#js-work-items-hierarchy');
|
||||
|
||||
const { illustrationPath, hasEpics, hasSubEpics } = el.dataset;
|
||||
|
||||
const licensePlan = inferLicensePlan({
|
||||
hasEpics: parseBoolean(hasEpics),
|
||||
hasSubEpics: parseBoolean(hasSubEpics),
|
||||
});
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
provide: {
|
||||
illustrationPath,
|
||||
licensePlan,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(App);
|
||||
},
|
||||
});
|
||||
};
|
|
@ -31,3 +31,4 @@
|
|||
@import './pages/storage_quota';
|
||||
@import './pages/tree';
|
||||
@import './pages/users';
|
||||
@import './pages/hierarchy';
|
||||
|
|
|
@ -525,32 +525,26 @@
|
|||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.fa-2x,
|
||||
.admonitionblock td.icon [class^='fa icon-'] {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.fa-exclamation-triangle::before,
|
||||
.admonitionblock td.icon .icon-warning::before {
|
||||
content: '⚠';
|
||||
}
|
||||
|
||||
.fa-exclamation-circle::before,
|
||||
.admonitionblock td.icon .icon-important::before {
|
||||
content: '❗';
|
||||
}
|
||||
|
||||
.fa-lightbulb-o::before,
|
||||
.admonitionblock td.icon .icon-tip::before {
|
||||
content: '💡';
|
||||
}
|
||||
|
||||
.fa-thumb-tack::before,
|
||||
.admonitionblock td.icon .icon-note::before {
|
||||
content: '📌';
|
||||
}
|
||||
|
||||
.fa-fire::before,
|
||||
.admonitionblock td.icon .icon-caution::before {
|
||||
content: '🔥';
|
||||
}
|
||||
|
|
15
app/assets/stylesheets/pages/hierarchy.scss
Normal file
15
app/assets/stylesheets/pages/hierarchy.scss
Normal file
|
@ -0,0 +1,15 @@
|
|||
.hierarchy-rounded-arrow-tail {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 5px;
|
||||
height: calc(100% - 20px);
|
||||
}
|
||||
|
||||
.hierarchy-icon-wrapper {
|
||||
height: $default-icon-size;
|
||||
width: $default-icon-size;
|
||||
}
|
||||
|
||||
.hierarchy-rounded-arrow {
|
||||
transform: scale(1, -1) rotate(90deg);
|
||||
}
|
17
app/controllers/concerns/planning_hierarchy.rb
Normal file
17
app/controllers/concerns/planning_hierarchy.rb
Normal file
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module PlanningHierarchy
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
# rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
def planning_hierarchy
|
||||
return access_denied! unless can?(current_user, :read_planning_hierarchy, @project)
|
||||
|
||||
return render_404 unless Feature.enabled?(:work_items_hierarchy, @project, default_enabled: :yaml)
|
||||
|
||||
render 'shared/planning_hierarchy'
|
||||
end
|
||||
# rubocop:enable Gitlab/ModuleWithInstanceVariables
|
||||
end
|
||||
|
||||
PlanningHierarchy.prepend_mod_with('PlanningHierarchy')
|
|
@ -10,6 +10,7 @@ class ProjectsController < Projects::ApplicationController
|
|||
include ImportUrlParams
|
||||
include FiltersEvents
|
||||
include SourcegraphDecorator
|
||||
include PlanningHierarchy
|
||||
|
||||
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
|
||||
|
||||
|
@ -54,6 +55,7 @@ class ProjectsController < Projects::ApplicationController
|
|||
feature_category :team_planning, [:preview_markdown, :new_issuable_address]
|
||||
feature_category :importers, [:export, :remove_export, :generate_new_export, :download_export]
|
||||
feature_category :code_review, [:unfoldered_environment_names]
|
||||
feature_category :portfolio_management, [:planning_hierarchy]
|
||||
|
||||
urgency :low, [:refs]
|
||||
urgency :high, [:unfoldered_environment_names]
|
||||
|
|
|
@ -240,6 +240,7 @@ class ProjectPolicy < BasePolicy
|
|||
enable :read_wiki
|
||||
enable :read_issue
|
||||
enable :read_label
|
||||
enable :read_planning_hierarchy
|
||||
enable :read_milestone
|
||||
enable :read_snippet
|
||||
enable :read_project_member
|
||||
|
@ -572,6 +573,7 @@ class ProjectPolicy < BasePolicy
|
|||
enable :read_issue_board_list
|
||||
enable :read_wiki
|
||||
enable :read_label
|
||||
enable :read_planning_hierarchy
|
||||
enable :read_milestone
|
||||
enable :read_snippet
|
||||
enable :read_project_member
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
|
||||
= sprite_icon('admin')
|
||||
- if @notification_setting
|
||||
.js-vue-notification-dropdown{ data: { disabled: emails_disabled.to_s, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), group_id: @group.id, container_class: 'gl-mx-2 gl-mt-3 gl-vertical-align-top' } }
|
||||
.js-vue-notification-dropdown{ data: { disabled: emails_disabled.to_s, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), group_id: @group.id, container_class: 'gl-mx-2 gl-mt-3 gl-vertical-align-top', no_flip: 'true' } }
|
||||
- if can_create_subgroups
|
||||
.gl-px-2.gl-sm-w-auto.gl-w-full
|
||||
= link_to _("New subgroup"), new_group_path(parent_id: @group.id), class: "btn btn-default gl-button gl-mt-3 gl-sm-w-auto gl-w-full", data: { qa_selector: 'new_subgroup_button' }
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
= sprite_icon('admin')
|
||||
.gl-display-flex.gl-align-items-start.gl-mr-3
|
||||
- if @notification_setting
|
||||
.js-vue-notification-dropdown{ data: { button_size: "small", disabled: emails_disabled.to_s, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), project_id: @project.id } }
|
||||
.js-vue-notification-dropdown{ data: { button_size: "small", disabled: emails_disabled.to_s, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), project_id: @project.id, no_flip: 'true' } }
|
||||
|
||||
.count-buttons.gl-display-flex.gl-align-items-flex-start
|
||||
= render 'projects/buttons/star'
|
||||
|
|
5
app/views/shared/planning_hierarchy.html.haml
Normal file
5
app/views/shared/planning_hierarchy.html.haml
Normal file
|
@ -0,0 +1,5 @@
|
|||
- page_title _("Planning hierarchy")
|
||||
- has_sub_epics = @project&.licensed_feature_available?(:subepics)
|
||||
- has_epics = @project&.licensed_feature_available?(:epics)
|
||||
|
||||
#js-work-items-hierarchy{ data: { has_sub_epics: has_sub_epics.to_s, has_epics: has_epics.to_s, illustration_path: image_path('illustrations/rocket-launch-md.svg') } }
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: work_items_hierarchy
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79315
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350451
|
||||
milestone: '14.8'
|
||||
type: development
|
||||
group: group::product planning
|
||||
default_enabled: false
|
|
@ -465,6 +465,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
|||
namespace :integrations do
|
||||
resource :shimo, only: [:show]
|
||||
end
|
||||
|
||||
get :planning_hierarchy
|
||||
end
|
||||
# End of the /-/ scope.
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
body: |
|
||||
To improve performance, we are limiting the number of projects returned from the `GET /groups/:id/` API call to 100. A complete list of projects can still be retrieved with the `GET /groups/:id/projects` API call.
|
||||
|
||||
- name: "GitLab OAuth implicit grant deprecation"
|
||||
- name: "GitLab OAuth implicit grant"
|
||||
removal_date: "2021-06-22"
|
||||
removal_milestone: "14.0"
|
||||
reporter: ogolowinski
|
||||
|
@ -16,4 +16,4 @@
|
|||
body: |
|
||||
GitLab is deprecating the [OAuth 2 implicit grant flow](https://docs.gitlab.com/ee/api/oauth2.html#implicit-grant-flow) as it has been removed for [OAuth 2.1](https://oauth.net/2.1/).
|
||||
|
||||
Beginning in 14.0, new applications can't be created with the OAuth 2 implicit grant flow. Existing OAuth implicit grant flows are no longer supported in 14.4. Migrate your existing applications to other supported [OAuth2 flows](https://docs.gitlab.com/ee/api/oauth2.html#supported-oauth2-flows) before release 14.4.
|
||||
Migrate your existing applications to other supported [OAuth2 flows](https://docs.gitlab.com/ee/api/oauth2.html#supported-oauth2-flows).
|
||||
|
|
|
@ -32,7 +32,7 @@ GitLab supports the following authorization flows:
|
|||
hosted, first-party services. GitLab recommends against use of this flow.
|
||||
|
||||
The draft specification for [OAuth 2.1](https://oauth.net/2.1/) specifically omits both the
|
||||
Implicit grant and Resource Owner Password Credentials flows. It will be deprecated in the next OAuth specification version.
|
||||
Implicit grant and Resource Owner Password Credentials flows.
|
||||
|
||||
Refer to the [OAuth RFC](https://tools.ietf.org/html/rfc6749) to find out
|
||||
how all those flows work and pick the right one for your use case.
|
||||
|
@ -239,19 +239,13 @@ You can now make requests to the API with the access token returned.
|
|||
|
||||
### Implicit grant flow
|
||||
|
||||
NOTE:
|
||||
For a detailed flow diagram, see the [RFC specification](https://tools.ietf.org/html/rfc6749#section-4.2).
|
||||
|
||||
WARNING:
|
||||
Implicit grant flow is inherently insecure and the IETF has removed it in [OAuth 2.1](https://oauth.net/2.1/).
|
||||
For this reason, [support for it is deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/288516).
|
||||
In GitLab 14.0, new applications can't be created using it. In GitLab 14.4, support for it is
|
||||
scheduled to be removed for existing applications.
|
||||
It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/288516) for use in GitLab 14.0, and is planned for
|
||||
[removal](https://gitlab.com/gitlab-org/gitlab/-/issues/344609) in GitLab 15.0.
|
||||
|
||||
We recommend that you use [Authorization code with PKCE](#authorization-code-with-proof-key-for-code-exchange-pkce) instead. If you choose to use Implicit flow, be sure to verify the
|
||||
`application id` (or `client_id`) associated with the access token before granting
|
||||
access to the data. To learn more, read
|
||||
[Retrieving the token information](#retrieve-the-token-information)).
|
||||
We recommend that you use [Authorization code with PKCE](#authorization-code-with-proof-key-for-code-exchange-pkce)
|
||||
instead.
|
||||
|
||||
Unlike the authorization code flow, the client receives an `access token`
|
||||
immediately as a result of the authorization request. The flow does not use the
|
||||
|
@ -415,7 +409,7 @@ The following is an example response:
|
|||
|
||||
The fields `scopes` and `expires_in_seconds` are included in the response.
|
||||
|
||||
These are aliases for `scope` and `expires_in` respectively, and have been included to
|
||||
These fields are aliases for `scope` and `expires_in` respectively, and have been included to
|
||||
prevent breaking changes introduced in [doorkeeper 5.0.2](https://github.com/doorkeeper-gem/doorkeeper/wiki/Migration-from-old-versions#from-4x-to-5x).
|
||||
|
||||
Don't rely on these fields as they are slated for removal in a later release.
|
||||
|
|
|
@ -211,16 +211,17 @@ averaged.
|
|||
|
||||
To define a coverage-parsing regular expression:
|
||||
|
||||
1. On the top bar, select **Menu > Projects** and find your project.
|
||||
1. On the left sidebar, select **Settings > CI/CD**.
|
||||
1. Expand **General pipelines**.
|
||||
1. In the **Test coverage parsing** field, enter a regular expression.
|
||||
Leave blank to disable this feature.
|
||||
- In the GitLab UI:
|
||||
|
||||
Alternatively, provide a regular expression using the [`coverage`](../yaml/index.md#coverage)
|
||||
keyword in your project's `.gitlab-ci.yml`.
|
||||
1. On the top bar, select **Menu > Projects** and find your project.
|
||||
1. On the left sidebar, select **Settings > CI/CD**.
|
||||
1. Expand **General pipelines**.
|
||||
1. In the **Test coverage parsing** field, enter a regular expression. Leave blank to disable this feature.
|
||||
|
||||
You can use <https://rubular.com> to test your regex. The regex returns the **last**
|
||||
- Using the project's `.gitlab-ci.yml`, provide a regular expression using the [`coverage`](../yaml/index.md#coverage)
|
||||
keyword.
|
||||
|
||||
You can use <https://rubular.com> to test your regular expression. The regular expression returns the **last**
|
||||
match found in the output.
|
||||
|
||||
### Test coverage examples
|
||||
|
|
|
@ -510,6 +510,9 @@ When reviewing merge requests added by wider community contributors:
|
|||
fetching of malicious packages.
|
||||
- Review links and images, especially in documentation MRs.
|
||||
- When in doubt, ask someone from `@gitlab-com/gl-security/appsec` to review the merge request **before manually starting any merge request pipeline**.
|
||||
- Only set the milestone when the merge request is likely to be included in
|
||||
the current milestone. This is to avoid confusion around when it'll be
|
||||
merged and avoid moving milestone too often when it's not yet ready.
|
||||
|
||||
If the MR source branch is more than 1,000 commits behind the target branch:
|
||||
|
||||
|
|
|
@ -106,6 +106,7 @@ reproduction.
|
|||
- [Lazy loaded images can cause Capybara to mis-click](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18713)
|
||||
- [Triggering JS events before the event handlers are set up](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18742)
|
||||
- [Wait for the image to be lazy-loaded when asserting on a Markdown image's `src` attribute](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25408)
|
||||
- [Avoid asserting against flash notice banners](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79432)
|
||||
|
||||
#### Capybara viewport size related issues
|
||||
|
||||
|
|
|
@ -175,7 +175,7 @@ As [announced in GitLab 13.3](https://about.gitlab.com/releases/2020/08/22/gitla
|
|||
- `geo_postgresql['fdw_external_password']`
|
||||
- `gitlab-_rails['geo_migrated_local_files_clean_up_worker_cron']`
|
||||
|
||||
### GitLab OAuth implicit grant deprecation
|
||||
### GitLab OAuth implicit grant
|
||||
|
||||
WARNING:
|
||||
This feature was changed or removed in 14.0
|
||||
|
@ -185,7 +185,7 @@ changes to your code, settings, or workflow.
|
|||
|
||||
GitLab is deprecating the [OAuth 2 implicit grant flow](https://docs.gitlab.com/ee/api/oauth2.html#implicit-grant-flow) as it has been removed for [OAuth 2.1](https://oauth.net/2.1/).
|
||||
|
||||
Beginning in 14.0, new applications can't be created with the OAuth 2 implicit grant flow. Existing OAuth implicit grant flows are no longer supported in 14.4. Migrate your existing applications to other supported [OAuth2 flows](https://docs.gitlab.com/ee/api/oauth2.html#supported-oauth2-flows) before release 14.4.
|
||||
Migrate your existing applications to other supported [OAuth2 flows](https://docs.gitlab.com/ee/api/oauth2.html#supported-oauth2-flows).
|
||||
|
||||
### GitLab Runner helper image in GitLab.com Container Registry
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ results. On failure, the analyzer outputs an
|
|||
- [GitLab Runner](../../../ci/runners/index.md) available, with the
|
||||
[`docker` executor](https://docs.gitlab.com/runner/executors/docker.html).
|
||||
- Target application deployed. For more details, read [Deployment options](#deployment-options).
|
||||
- DAST runs in the `test` stage, which is available by default. If you redefine the stages in the `.gitlab-ci.yml` file, the `test` stage is required.
|
||||
- DAST runs in the `dast` stage, which must be added manually to your `.gitlab-ci.yml`.
|
||||
|
||||
### Deployment options
|
||||
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
|
@ -20,6 +20,20 @@ To learn about hierarchies in general, common frameworks, and using GitLab for
|
|||
portfolio management, see
|
||||
[How to use GitLab for Agile portfolio planning and project management](https://about.gitlab.com/blog/2020/11/11/gitlab-for-agile-portfolio-planning-project-management/).
|
||||
|
||||
## View planning hierarchies
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/340844/) in GitLab 14.8 and is behind the feature flag `work_items_hierarchy`.
|
||||
|
||||
To view the planning hierarchy in a project:
|
||||
|
||||
1. On the top bar, select **Menu > Projects** and find your project.
|
||||
1. On the left sidebar, select **Project information > Planning hierarchy**.
|
||||
|
||||
Under **Current structure**, you can see a hierarchy diagram that matches your current planning hierarchy.
|
||||
The work items outside your subscription plan show up below **Unavailable structure**.
|
||||
|
||||
![Screenshot showing hierarchy page](img/view-project-work-item-hierarchy_v14_8.png)
|
||||
|
||||
## Hierarchies with epics
|
||||
|
||||
With epics, you can achieve the following hierarchy:
|
||||
|
|
27
lib/sidebars/concerns/work_item_hierarchy.rb
Normal file
27
lib/sidebars/concerns/work_item_hierarchy.rb
Normal file
|
@ -0,0 +1,27 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# This module has the necessary methods to render
|
||||
# work items hierarchy menu
|
||||
module Sidebars
|
||||
module Concerns
|
||||
module WorkItemHierarchy
|
||||
def hierarchy_menu_item(container, url, path)
|
||||
unless show_hierarachy_menu_item?(container)
|
||||
return ::Sidebars::NilMenuItem.new(item_id: :hierarchy)
|
||||
end
|
||||
|
||||
::Sidebars::MenuItem.new(
|
||||
title: _('Planning hierarchy'),
|
||||
link: url,
|
||||
active_routes: { path: path },
|
||||
item_id: :hierarchy
|
||||
)
|
||||
end
|
||||
|
||||
def show_hierarachy_menu_item?(container)
|
||||
Feature.enabled?(:work_items_hierarchy, container, default_enabled: :yaml) &&
|
||||
can?(context.current_user, :read_planning_hierarchy, container)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -4,10 +4,13 @@ module Sidebars
|
|||
module Projects
|
||||
module Menus
|
||||
class ProjectInformationMenu < ::Sidebars::Menu
|
||||
include ::Sidebars::Concerns::WorkItemHierarchy
|
||||
|
||||
override :configure_menu_items
|
||||
def configure_menu_items
|
||||
add_item(activity_menu_item)
|
||||
add_item(labels_menu_item)
|
||||
add_item(hierarchy_menu_item(context.project, project_planning_hierarchy_path(context.project), 'projects#planning_hierarchy'))
|
||||
add_item(members_menu_item)
|
||||
|
||||
true
|
||||
|
|
|
@ -7113,6 +7113,9 @@ msgstr ""
|
|||
msgid "Child"
|
||||
msgstr ""
|
||||
|
||||
msgid "Child epic"
|
||||
msgstr ""
|
||||
|
||||
msgid "Child epic does not exist."
|
||||
msgstr ""
|
||||
|
||||
|
@ -17725,6 +17728,33 @@ msgstr[1] ""
|
|||
msgid "Hide values"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hierarchy|Current structure"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hierarchy|Deliver value more efficiently by breaking down necessary work into a hierarchical structure. This structure helps teams understand scope, priorities, and how work cascades up toward larger goals."
|
||||
msgstr ""
|
||||
|
||||
msgid "Hierarchy|Help us improve work items in GitLab!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hierarchy|Is there a framework or type of work item you wish you had access to in GitLab? Give us your feedback and help us build the experiences valuable to you."
|
||||
msgstr ""
|
||||
|
||||
msgid "Hierarchy|Planning hierarchy"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hierarchy|Take the work items survey"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hierarchy|These items are unavailable in the current structure."
|
||||
msgstr ""
|
||||
|
||||
msgid "Hierarchy|Unavailable structure"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hierarchy|You can start using these items now."
|
||||
msgstr ""
|
||||
|
||||
msgid "High or unknown vulnerabilities present"
|
||||
msgstr ""
|
||||
|
||||
|
@ -26636,6 +26666,9 @@ msgstr ""
|
|||
msgid "Plan:"
|
||||
msgstr ""
|
||||
|
||||
msgid "Planning hierarchy"
|
||||
msgstr ""
|
||||
|
||||
msgid "PlantUML"
|
||||
msgstr ""
|
||||
|
||||
|
@ -30373,6 +30406,9 @@ msgstr ""
|
|||
msgid "Required only if you are not using role instance credentials."
|
||||
msgstr ""
|
||||
|
||||
msgid "Requirement"
|
||||
msgstr ""
|
||||
|
||||
msgid "Requirement %{reference} has been added"
|
||||
msgstr ""
|
||||
|
||||
|
@ -33033,6 +33069,9 @@ msgstr ""
|
|||
msgid "Show all breadcrumbs"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show all epics"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show all issues."
|
||||
msgstr ""
|
||||
|
||||
|
@ -33045,6 +33084,9 @@ msgstr ""
|
|||
msgid "Show archived projects only"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show closed epics"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show command"
|
||||
msgstr ""
|
||||
|
||||
|
@ -33081,6 +33123,9 @@ msgstr ""
|
|||
msgid "Show one file at a time"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show open epics"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show the Closed list"
|
||||
msgstr ""
|
||||
|
||||
|
@ -35376,6 +35421,9 @@ msgstr ""
|
|||
msgid "Test Cases"
|
||||
msgstr ""
|
||||
|
||||
msgid "Test case"
|
||||
msgstr ""
|
||||
|
||||
msgid "Test coverage parsing"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -275,7 +275,7 @@ function rspec_paralellized_job() {
|
|||
export MEMORY_TEST_PATH="tmp/memory_test/${report_name}_memory.csv"
|
||||
|
||||
if [[ -n $RSPEC_TESTS_MAPPING_ENABLED ]]; then
|
||||
tooling/bin/parallel_rspec --rspec_args "$(rspec_args)" --filter "tmp/matching_tests.txt" || rspec_run_status=$?
|
||||
tooling/bin/parallel_rspec --rspec_args "$(rspec_args "${rspec_opts}")" --filter "tmp/matching_tests.txt" || rspec_run_status=$?
|
||||
else
|
||||
tooling/bin/parallel_rspec --rspec_args "$(rspec_args "${rspec_opts}")" || rspec_run_status=$?
|
||||
fi
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Projects > Members > Member leaves project' do
|
||||
include Spec::Support::Helpers::Features::MembersHelpers
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project, :repository) }
|
||||
|
||||
|
@ -25,10 +27,14 @@ RSpec.describe 'Projects > Members > Member leaves project' do
|
|||
visit project_path(project, leave: 1)
|
||||
|
||||
page.accept_confirm
|
||||
|
||||
wait_for_all_requests
|
||||
expect(find('.flash-notice')).to have_content "You left the \"#{project.full_name}\" project"
|
||||
|
||||
expect(current_path).to eq(dashboard_projects_path)
|
||||
expect(project.users.exists?(user.id)).to be_falsey
|
||||
|
||||
sign_in(project.first_owner)
|
||||
|
||||
visit project_project_members_path(project)
|
||||
|
||||
expect(members_table).not_to have_content(user.name)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -195,6 +195,14 @@ describe('NotificationsDropdown', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('passes provided `noFlip` value to `GlDropdown`', () => {
|
||||
wrapper = createComponent({
|
||||
noFlip: true,
|
||||
});
|
||||
|
||||
expect(findDropdown().attributes('no-flip')).toBe('true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when selecting an item', () => {
|
||||
|
|
63
spec/frontend/work_items_hierarchy/components/app_spec.js
Normal file
63
spec/frontend/work_items_hierarchy/components/app_spec.js
Normal file
|
@ -0,0 +1,63 @@
|
|||
import { nextTick } from 'vue';
|
||||
import { createLocalVue, mount } from '@vue/test-utils';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { GlBanner } from '@gitlab/ui';
|
||||
import App from '~/work_items_hierarchy/components/app.vue';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VueApollo);
|
||||
|
||||
describe('WorkItemsHierarchy App', () => {
|
||||
let wrapper;
|
||||
const createComponent = (props = {}, data = {}) => {
|
||||
wrapper = extendedWrapper(
|
||||
mount(App, {
|
||||
localVue,
|
||||
provide: {
|
||||
illustrationPath: '/foo.svg',
|
||||
licensePlan: 'free',
|
||||
...props,
|
||||
},
|
||||
data() {
|
||||
return data;
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('survey banner', () => {
|
||||
it('shows when the banner is visible', () => {
|
||||
createComponent({}, { bannerVisible: true });
|
||||
|
||||
expect(wrapper.find(GlBanner).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('hide when close is called', async () => {
|
||||
createComponent({}, { bannerVisible: true });
|
||||
|
||||
wrapper.findByTestId('close-icon').trigger('click');
|
||||
|
||||
await nextTick();
|
||||
|
||||
expect(wrapper.find(GlBanner).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Unavailable structure', () => {
|
||||
it.each`
|
||||
licensePlan | visible
|
||||
${'free'} | ${true}
|
||||
${'premium'} | ${true}
|
||||
${'ultimate'} | ${false}
|
||||
`('visibility is $visible when plan is $licensePlan', ({ licensePlan, visible }) => {
|
||||
createComponent({ licensePlan });
|
||||
|
||||
expect(wrapper.findByTestId('unavailable-structure').exists()).toBe(visible);
|
||||
});
|
||||
});
|
||||
});
|
118
spec/frontend/work_items_hierarchy/components/hierarchy_spec.js
Normal file
118
spec/frontend/work_items_hierarchy/components/hierarchy_spec.js
Normal file
|
@ -0,0 +1,118 @@
|
|||
import { createLocalVue, mount } from '@vue/test-utils';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { GlBadge } from '@gitlab/ui';
|
||||
import Hierarchy from '~/work_items_hierarchy/components/hierarchy.vue';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import RESPONSE from '~/work_items_hierarchy/static_response';
|
||||
import { workItemTypes } from '~/work_items_hierarchy/constants';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(VueApollo);
|
||||
|
||||
describe('WorkItemsHierarchy Hierarchy', () => {
|
||||
let wrapper;
|
||||
|
||||
const workItemsFromResponse = (response) => {
|
||||
return response.reduce(
|
||||
(itemTypes, item) => {
|
||||
const key = item.available ? 'available' : 'unavailable';
|
||||
itemTypes[key].push({
|
||||
...item,
|
||||
...workItemTypes[item.type],
|
||||
nestedTypes: item.nestedTypes
|
||||
? item.nestedTypes.map((type) => workItemTypes[type])
|
||||
: null,
|
||||
});
|
||||
return itemTypes;
|
||||
},
|
||||
{ available: [], unavailable: [] },
|
||||
);
|
||||
};
|
||||
|
||||
const createComponent = (props = {}) => {
|
||||
wrapper = extendedWrapper(
|
||||
mount(Hierarchy, {
|
||||
localVue,
|
||||
propsData: {
|
||||
workItemTypes: props.workItemTypes,
|
||||
...props,
|
||||
},
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('available structure', () => {
|
||||
let items = [];
|
||||
|
||||
beforeEach(() => {
|
||||
items = workItemsFromResponse(RESPONSE.ultimate).available;
|
||||
createComponent({ workItemTypes: items });
|
||||
});
|
||||
|
||||
it('renders all work items', () => {
|
||||
expect(wrapper.findAllByTestId('work-item-wrapper')).toHaveLength(items.length);
|
||||
});
|
||||
|
||||
it('does not render badges', () => {
|
||||
expect(wrapper.find(GlBadge).exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unavailable structure', () => {
|
||||
let items = [];
|
||||
|
||||
beforeEach(() => {
|
||||
items = workItemsFromResponse(RESPONSE.premium).unavailable;
|
||||
createComponent({ workItemTypes: items });
|
||||
});
|
||||
|
||||
it('renders all work items', () => {
|
||||
expect(wrapper.findAllByTestId('work-item-wrapper')).toHaveLength(items.length);
|
||||
});
|
||||
|
||||
it('renders license badges for all work items', () => {
|
||||
expect(wrapper.findAll(GlBadge)).toHaveLength(items.length);
|
||||
});
|
||||
|
||||
it('does not render svg icon for linking', () => {
|
||||
expect(wrapper.findByTestId('hierarchy-rounded-arrow-tail').exists()).toBe(false);
|
||||
expect(wrapper.findByTestId('level-up-icon').exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('nested work items', () => {
|
||||
describe.each`
|
||||
licensePlan | arrowTailVisible | levelUpIconVisible | arrowDownIconVisible
|
||||
${'ultimate'} | ${true} | ${true} | ${true}
|
||||
${'premium'} | ${false} | ${false} | ${true}
|
||||
${'free'} | ${false} | ${false} | ${false}
|
||||
`(
|
||||
'when $licensePlan license',
|
||||
({ licensePlan, arrowTailVisible, levelUpIconVisible, arrowDownIconVisible }) => {
|
||||
let items = [];
|
||||
beforeEach(() => {
|
||||
items = workItemsFromResponse(RESPONSE[licensePlan]).available;
|
||||
createComponent({ workItemTypes: items });
|
||||
});
|
||||
|
||||
it(`${arrowTailVisible ? 'render' : 'does not render'} arrow tail svg`, () => {
|
||||
expect(wrapper.findByTestId('hierarchy-rounded-arrow-tail').exists()).toBe(
|
||||
arrowTailVisible,
|
||||
);
|
||||
});
|
||||
|
||||
it(`${levelUpIconVisible ? 'render' : 'does not render'} arrow tail svg`, () => {
|
||||
expect(wrapper.findByTestId('level-up-icon').exists()).toBe(levelUpIconVisible);
|
||||
});
|
||||
|
||||
it(`${arrowDownIconVisible ? 'render' : 'does not render'} arrow tail svg`, () => {
|
||||
expect(wrapper.findByTestId('arrow-down-icon').exists()).toBe(arrowDownIconVisible);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
16
spec/frontend/work_items_hierarchy/hierarchy_util_spec.js
Normal file
16
spec/frontend/work_items_hierarchy/hierarchy_util_spec.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { inferLicensePlan } from '~/work_items_hierarchy/hierarchy_util';
|
||||
import { LICENSE_PLAN } from '~/work_items_hierarchy/constants';
|
||||
|
||||
describe('inferLicensePlan', () => {
|
||||
it.each`
|
||||
epics | subEpics | licensePlan
|
||||
${true} | ${true} | ${LICENSE_PLAN.ULTIMATE}
|
||||
${true} | ${false} | ${LICENSE_PLAN.PREMIUM}
|
||||
${false} | ${false} | ${LICENSE_PLAN.FREE}
|
||||
`(
|
||||
'returns $licensePlan when epic is $epics and sub-epic is $subEpics',
|
||||
({ epics, subEpics, licensePlan }) => {
|
||||
expect(inferLicensePlan({ hasEpics: epics, hasSubEpics: subEpics })).toBe(licensePlan);
|
||||
},
|
||||
);
|
||||
});
|
36
spec/lib/sidebars/concerns/work_item_hierarchy_spec.rb
Normal file
36
spec/lib/sidebars/concerns/work_item_hierarchy_spec.rb
Normal file
|
@ -0,0 +1,36 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Sidebars::Concerns::WorkItemHierarchy do
|
||||
shared_examples 'hierarchy menu' do
|
||||
let(:item_id) { :hierarchy }
|
||||
|
||||
context 'when the feature is disabled does not render' do
|
||||
before do
|
||||
stub_feature_flags(work_items_hierarchy: false)
|
||||
end
|
||||
|
||||
specify { is_expected.to be_nil }
|
||||
end
|
||||
|
||||
context 'when the feature is enabled does render' do
|
||||
before do
|
||||
stub_feature_flags(work_items_hierarchy: true)
|
||||
end
|
||||
|
||||
specify { is_expected.not_to be_nil }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Project hierarchy menu item' do
|
||||
let_it_be_with_reload(:project) { create(:project, :repository) }
|
||||
|
||||
let(:user) { project.owner }
|
||||
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
|
||||
|
||||
subject { Sidebars::Projects::Menus::ProjectInformationMenu.new(context).renderable_items.index { |e| e.item_id == item_id } }
|
||||
|
||||
it_behaves_like 'hierarchy menu'
|
||||
end
|
||||
end
|
|
@ -59,5 +59,25 @@ RSpec.describe Sidebars::Projects::Menus::ProjectInformationMenu do
|
|||
specify { is_expected.to be_nil }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Hierarchy' do
|
||||
let(:item_id) { :hierarchy }
|
||||
|
||||
context 'when the feature is disabled' do
|
||||
before do
|
||||
stub_feature_flags(work_items_hierarchy: false)
|
||||
end
|
||||
|
||||
specify { is_expected.to be_nil }
|
||||
end
|
||||
|
||||
context 'when the feature is enabled' do
|
||||
before do
|
||||
stub_feature_flags(work_items_hierarchy: true)
|
||||
end
|
||||
|
||||
specify { is_expected.not_to be_nil }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
34
spec/requests/concerns/planning_hierarchy_spec.rb
Normal file
34
spec/requests/concerns/planning_hierarchy_spec.rb
Normal file
|
@ -0,0 +1,34 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe PlanningHierarchy, type: :request do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project) { create(:project, group: group) }
|
||||
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
describe 'GET #planning_hierarchy' do
|
||||
it 'renders planning hierarchy' do
|
||||
stub_feature_flags(work_items_hierarchy: true)
|
||||
|
||||
get project_planning_hierarchy_path(project)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response.body).to match(/id="js-work-items-hierarchy"/)
|
||||
end
|
||||
|
||||
it 'renders 404 page' do
|
||||
stub_feature_flags(work_items_hierarchy: false)
|
||||
|
||||
get project_planning_hierarchy_path(project)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
expect(response.body).not_to match(/id="js-work-items-hierarchy"/)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -22,6 +22,7 @@ RSpec.shared_context 'project navbar structure' do
|
|||
nav_sub_items: [
|
||||
_('Activity'),
|
||||
_('Labels'),
|
||||
_('Planning hierarchy'),
|
||||
_('Members')
|
||||
]
|
||||
},
|
||||
|
|
|
@ -17,7 +17,7 @@ RSpec.shared_context 'ProjectPolicy context' do
|
|||
%i[
|
||||
award_emoji create_issue create_merge_request_in create_note
|
||||
create_project read_issue_board read_issue read_issue_iid read_issue_link
|
||||
read_label read_issue_board_list read_milestone read_note read_project
|
||||
read_label read_planning_hierarchy read_issue_board_list read_milestone read_note read_project
|
||||
read_project_for_iids read_project_member read_release read_snippet
|
||||
read_wiki upload_file
|
||||
]
|
||||
|
|
Loading…
Reference in a new issue