diff options
Diffstat (limited to 'app/assets/javascripts/work_items_hierarchy/components')
-rw-r--r-- | app/assets/javascripts/work_items_hierarchy/components/app.vue | 101 | ||||
-rw-r--r-- | app/assets/javascripts/work_items_hierarchy/components/hierarchy.vue | 119 |
2 files changed, 220 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> |