summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/work_items_hierarchy
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/work_items_hierarchy')
-rw-r--r--app/assets/javascripts/work_items_hierarchy/components/app.vue101
-rw-r--r--app/assets/javascripts/work_items_hierarchy/components/hierarchy.vue119
-rw-r--r--app/assets/javascripts/work_items_hierarchy/constants.js62
-rw-r--r--app/assets/javascripts/work_items_hierarchy/hierarchy_util.js10
-rw-r--r--app/assets/javascripts/work_items_hierarchy/static_response.js142
-rw-r--r--app/assets/javascripts/work_items_hierarchy/work_items_hierarchy_bundle.js26
6 files changed, 460 insertions, 0 deletions
diff --git a/app/assets/javascripts/work_items_hierarchy/components/app.vue b/app/assets/javascripts/work_items_hierarchy/components/app.vue
new file mode 100644
index 00000000000..621cfe5bace
--- /dev/null
+++ b/app/assets/javascripts/work_items_hierarchy/components/app.vue
@@ -0,0 +1,101 @@
+<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 skipItem = workItemTypes[item.type].isWorkItem && !window.gon?.features?.workItems;
+
+ if (skipItem) {
+ return itemTypes;
+ }
+ const key = item.available ? 'available' : 'unavailable';
+ const nestedTypes = item.nestedTypes?.map((type) => workItemTypes[type]);
+
+ itemTypes[key].push({
+ ...item,
+ ...workItemTypes[item.type],
+ nestedTypes,
+ });
+
+ 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>
diff --git a/app/assets/javascripts/work_items_hierarchy/components/hierarchy.vue b/app/assets/javascripts/work_items_hierarchy/components/hierarchy.vue
new file mode 100644
index 00000000000..9b81218b6e4
--- /dev/null
+++ b/app/assets/javascripts/work_items_hierarchy/components/hierarchy.vue
@@ -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>
diff --git a/app/assets/javascripts/work_items_hierarchy/constants.js b/app/assets/javascripts/work_items_hierarchy/constants.js
new file mode 100644
index 00000000000..c14fe67af4d
--- /dev/null
+++ b/app/assets/javascripts/work_items_hierarchy/constants.js
@@ -0,0 +1,62 @@
+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',
+ isWorkItem: true,
+ },
+ 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,
+ },
+};
diff --git a/app/assets/javascripts/work_items_hierarchy/hierarchy_util.js b/app/assets/javascripts/work_items_hierarchy/hierarchy_util.js
new file mode 100644
index 00000000000..61d93acdb91
--- /dev/null
+++ b/app/assets/javascripts/work_items_hierarchy/hierarchy_util.js
@@ -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;
+}
diff --git a/app/assets/javascripts/work_items_hierarchy/static_response.js b/app/assets/javascripts/work_items_hierarchy/static_response.js
new file mode 100644
index 00000000000..d1e2e486082
--- /dev/null
+++ b/app/assets/javascripts/work_items_hierarchy/static_response.js
@@ -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;
diff --git a/app/assets/javascripts/work_items_hierarchy/work_items_hierarchy_bundle.js b/app/assets/javascripts/work_items_hierarchy/work_items_hierarchy_bundle.js
new file mode 100644
index 00000000000..2258c725301
--- /dev/null
+++ b/app/assets/javascripts/work_items_hierarchy/work_items_hierarchy_bundle.js
@@ -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);
+ },
+ });
+};