Commit ce8704fa authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'revert-hierarchy' into 'master'

Revert "Merge branch 'rajat/work-hierarchy' into 'master'"

See merge request gitlab-org/gitlab!78395
parents 67df4e20 025fa4da
import { initWorkItemsHierarchy } from '~/work_items_hierarchy/work_items_hierarchy_bundle';
initWorkItemsHierarchy();
import { __ } from '~/locale';
export const widgetTypes = { export const widgetTypes = {
title: 'TITLE', title: 'TITLE',
}; };
export const WI_TITLE_TRACK_LABEL = 'item_title'; export const WI_TITLE_TRACK_LABEL = 'item_title';
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,
},
};
<script>
import { GlBanner } from '@gitlab/ui';
import Cookies from 'js-cookie';
import { parseBoolean } from '~/lib/utils/common_utils';
import { workItemTypes } from '~/work_items/constants';
import RESPONSE from '../static_response';
import { WORK_ITEMS_SURVEY_COOKIE_NAME } 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>
<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>
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',
};
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;
}
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;
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);
},
});
};
...@@ -32,4 +32,3 @@ ...@@ -32,4 +32,3 @@
@import './pages/storage_quota'; @import './pages/storage_quota';
@import './pages/tree'; @import './pages/tree';
@import './pages/users'; @import './pages/users';
@import './pages/hierarchy';
.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);
}
# frozen_string_literal: true
module WorkItemsHierarchy
extend ActiveSupport::Concern
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def planning_hierarchy
return render_404 unless Feature.enabled?(:work_items_hierarchy, @project, default_enabled: :yaml)
render 'shared/planning_hierarchy'
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
end
WorkItemsHierarchy.prepend_mod_with('WorkItemsHierarchy')
...@@ -9,7 +9,6 @@ class ProjectsController < Projects::ApplicationController ...@@ -9,7 +9,6 @@ class ProjectsController < Projects::ApplicationController
include RecordUserLastActivity include RecordUserLastActivity
include ImportUrlParams include ImportUrlParams
include FiltersEvents include FiltersEvents
include WorkItemsHierarchy
include SourcegraphDecorator include SourcegraphDecorator
prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) } prepend_before_action(only: [:show]) { authenticate_sessionless_user!(:rss) }
...@@ -54,7 +53,6 @@ class ProjectsController < Projects::ApplicationController ...@@ -54,7 +53,6 @@ class ProjectsController < Projects::ApplicationController
feature_category :team_planning, [:preview_markdown, :new_issuable_address] feature_category :team_planning, [:preview_markdown, :new_issuable_address]
feature_category :importers, [:export, :remove_export, :generate_new_export, :download_export] feature_category :importers, [:export, :remove_export, :generate_new_export, :download_export]
feature_category :code_review, [:unfoldered_environment_names] feature_category :code_review, [:unfoldered_environment_names]
feature_category :portfolio_management, [:planning_hierarchy]
urgency :low, [:refs] urgency :low, [:refs]
urgency :high, [:unfoldered_environment_names] urgency :high, [:unfoldered_environment_names]
......
...@@ -240,7 +240,6 @@ class ProjectPolicy < BasePolicy ...@@ -240,7 +240,6 @@ class ProjectPolicy < BasePolicy
enable :read_wiki enable :read_wiki
enable :read_issue enable :read_issue
enable :read_label enable :read_label
enable :read_work_items_hierarchy
enable :read_milestone enable :read_milestone
enable :read_snippet enable :read_snippet
enable :read_project_member enable :read_project_member
...@@ -573,7 +572,6 @@ class ProjectPolicy < BasePolicy ...@@ -573,7 +572,6 @@ class ProjectPolicy < BasePolicy
enable :read_issue_board_list enable :read_issue_board_list
enable :read_wiki enable :read_wiki
enable :read_label enable :read_label
enable :read_work_items_hierarchy
enable :read_milestone enable :read_milestone
enable :read_snippet enable :read_snippet
enable :read_project_member enable :read_project_member
......
- page_title _("Planning hierarchy")
- has_sub_epics = Gitlab.ee? && @project&.feature_available?(:subepics)
- has_epics = Gitlab.ee? && @project&.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') } }
---
name: work_items_hierarchy
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76720
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350451
milestone: '14.7'
type: development
group: group::product planning
default_enabled: false
...@@ -641,7 +641,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -641,7 +641,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
post :generate_new_export post :generate_new_export
get :download_export get :download_export
get :activity get :activity
get :planning_hierarchy
get :refs get :refs
put :new_issuable_address put :new_issuable_address
get :unfoldered_environment_names get :unfoldered_environment_names
......
...@@ -20,20 +20,6 @@ To learn about hierarchies in general, common frameworks, and using GitLab for ...@@ -20,20 +20,6 @@ To learn about hierarchies in general, common frameworks, and using GitLab for
portfolio management, see 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/). [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.7 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_7.png)
## Hierarchies with epics ## Hierarchies with epics
With epics, you can achieve the following hierarchy: With epics, you can achieve the following hierarchy:
......
...@@ -38,7 +38,7 @@ RSpec.describe ProjectPolicy do ...@@ -38,7 +38,7 @@ RSpec.describe ProjectPolicy do
%i[ %i[
download_code download_wiki_code read_project read_issue_board read_issue_board_list download_code download_wiki_code read_project read_issue_board read_issue_board_list
read_project_for_iids read_issue_iid read_merge_request_iid read_wiki read_project_for_iids read_issue_iid read_merge_request_iid read_wiki
read_issue read_label read_work_items_hierarchy read_issue_link read_milestone read_iteration read_issue read_label read_issue_link read_milestone read_iteration
read_snippet read_project_member read_note read_cycle_analytics read_snippet read_project_member read_note read_cycle_analytics
read_pipeline read_build read_commit_status read_container_image read_pipeline read_build read_commit_status read_container_image
read_environment read_deployment read_merge_request read_pages read_environment read_deployment read_merge_request read_pages
......
# 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_work_items_hierarchy, container)
end
end
end
end
...@@ -4,13 +4,10 @@ module Sidebars ...@@ -4,13 +4,10 @@ module Sidebars
module Projects module Projects
module Menus module Menus
class ProjectInformationMenu < ::Sidebars::Menu class ProjectInformationMenu < ::Sidebars::Menu
include ::Sidebars::Concerns::WorkItemHierarchy
override :configure_menu_items override :configure_menu_items
def configure_menu_items def configure_menu_items
add_item(activity_menu_item) add_item(activity_menu_item)
add_item(labels_menu_item) add_item(labels_menu_item)
add_item(hierarchy_menu_item(context.project, planning_hierarchy_project_path(context.project), 'projects#planning_hierarchy'))
add_item(members_menu_item) add_item(members_menu_item)
true true
......
...@@ -7080,9 +7080,6 @@ msgstr "" ...@@ -7080,9 +7080,6 @@ msgstr ""
msgid "Child" msgid "Child"
msgstr "" msgstr ""
msgid "Child epic"
msgstr ""
msgid "Child epic does not exist." msgid "Child epic does not exist."
msgstr "" msgstr ""
...@@ -17628,33 +17625,6 @@ msgstr[1] "" ...@@ -17628,33 +17625,6 @@ msgstr[1] ""
msgid "Hide values" msgid "Hide values"
msgstr "" 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" msgid "High or unknown vulnerabilities present"
msgstr "" msgstr ""
...@@ -26539,9 +26509,6 @@ msgstr "" ...@@ -26539,9 +26509,6 @@ msgstr ""
msgid "Plan:" msgid "Plan:"
msgstr "" msgstr ""
msgid "Planning hierarchy"
msgstr ""
msgid "PlantUML" msgid "PlantUML"
msgstr "" msgstr ""
...@@ -30265,9 +30232,6 @@ msgstr "" ...@@ -30265,9 +30232,6 @@ msgstr ""
msgid "Required only if you are not using role instance credentials." msgid "Required only if you are not using role instance credentials."
msgstr "" msgstr ""
msgid "Requirement"
msgstr ""
msgid "Requirement %{reference} has been added" msgid "Requirement %{reference} has been added"
msgstr "" msgstr ""
...@@ -34951,9 +34915,6 @@ msgstr "" ...@@ -34951,9 +34915,6 @@ msgstr ""
msgid "Target-Branch" msgid "Target-Branch"
msgstr "" msgstr ""
msgid "Task"
msgstr ""
msgid "Task ID: %{elastic_task}" msgid "Task ID: %{elastic_task}"
msgstr "" msgstr ""
...@@ -35199,9 +35160,6 @@ msgstr "" ...@@ -35199,9 +35160,6 @@ msgstr ""
msgid "Test Cases" msgid "Test Cases"
msgstr "" msgstr ""
msgid "Test case"
msgstr ""
msgid "Test coverage parsing" msgid "Test coverage parsing"
msgstr "" msgstr ""
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe WorkItemsHierarchy do
controller(ApplicationController) do
include WorkItemsHierarchy
end
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
render_views
before do
sign_in user
routes.draw { get :planning_hierarchy, to: "anonymous#planning_hierarchy" }
controller.instance_variable_set(:@project, project)
end
it 'renders hierarchy' do
stub_feature_flags(work_items_hierarchy: true)
get :planning_hierarchy
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).to match(/id="js-work-items-hierarchy"/)
end
it 'renders 404' do
stub_feature_flags(work_items_hierarchy: false)
get :planning_hierarchy
expect(response).to have_gitlab_http_status(:not_found)
expect(response.body).not_to match(/id="js-work-items-hierarchy"/)
end
end
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.each`
licensePlan
${'free'}
${'premium'}
${'ultimate'}
`('when licensePlan is $licensePlan', ({ licensePlan }) => {
beforeEach(() => {
createComponent({ licensePlan });
});
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
});
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);
});
});
});
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/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);
});
},
);
});
});
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);
},
);
});
# 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,25 +59,5 @@ RSpec.describe Sidebars::Projects::Menus::ProjectInformationMenu do ...@@ -59,25 +59,5 @@ RSpec.describe Sidebars::Projects::Menus::ProjectInformationMenu do
specify { is_expected.to be_nil } specify { is_expected.to be_nil }
end end
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
end end
...@@ -22,7 +22,6 @@ RSpec.shared_context 'project navbar structure' do ...@@ -22,7 +22,6 @@ RSpec.shared_context 'project navbar structure' do
nav_sub_items: [ nav_sub_items: [
_('Activity'), _('Activity'),
_('Labels'), _('Labels'),
_('Planning hierarchy'),
_('Members') _('Members')
] ]
}, },
......
...@@ -17,7 +17,7 @@ RSpec.shared_context 'ProjectPolicy context' do ...@@ -17,7 +17,7 @@ RSpec.shared_context 'ProjectPolicy context' do
%i[ %i[
award_emoji create_issue create_merge_request_in create_note award_emoji create_issue create_merge_request_in create_note
create_project read_issue_board read_issue read_issue_iid read_issue_link create_project read_issue_board read_issue read_issue_iid read_issue_link
read_label read_work_items_hierarchy read_issue_board_list read_milestone read_note read_project read_label read_issue_board_list read_milestone read_note read_project
read_project_for_iids read_project_member read_release read_snippet read_project_for_iids read_project_member read_release read_snippet
read_wiki upload_file read_wiki upload_file
] ]
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment