diff options
Diffstat (limited to 'app/assets/javascripts/vue_merge_request_widget/components/widget')
5 files changed, 264 insertions, 53 deletions
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue new file mode 100644 index 00000000000..d1ade2886f4 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue @@ -0,0 +1,98 @@ +<script> +import { GlBadge, GlLink, GlSafeHtmlDirective } from '@gitlab/ui'; +import Actions from '../action_buttons.vue'; +import { generateText } from '../extensions/utils'; +import ContentRow from './widget_content_row.vue'; + +export default { + name: 'DynamicContent', + components: { + GlBadge, + GlLink, + Actions, + ContentRow, + }, + directives: { + SafeHtml: GlSafeHtmlDirective, + }, + props: { + data: { + type: Object, + required: true, + }, + widgetName: { + type: String, + required: true, + }, + level: { + type: Number, + required: false, + default: 2, + }, + }, + computed: { + statusIcon() { + return this.data.icon?.name || undefined; + }, + generatedText() { + return generateText(this.data.text); + }, + generatedSubtext() { + return generateText(this.data.subtext); + }, + generatedSupportingText() { + return generateText(this.data.supportingText); + }, + }, + methods: { + onClickedAction(action) { + this.$emit('clickedAction', action); + }, + }, +}; +</script> + +<template> + <content-row + :level="level" + :status-icon-name="statusIcon" + :widget-name="widgetName" + :header="data.header" + > + <template #body> + <div class="gl-display-flex gl-flex-direction-column"> + <div> + <p v-safe-html="generatedText" class="gl-mb-0"></p> + <gl-link v-if="data.link" :href="data.link.href">{{ data.link.text }}</gl-link> + <p v-if="data.supportingText" v-safe-html="generatedSupportingText" class="gl-mb-0"></p> + <gl-badge v-if="data.badge" :variant="data.badge.variant || 'info'"> + {{ data.badge.text }} + </gl-badge> + <actions + :widget="widgetName" + :tertiary-buttons="data.actions" + class="gl-ml-auto gl-pl-3" + @clickedAction="onClickedAction" + /> + <p v-if="data.subtext" v-safe-html="generatedSubtext" class="gl-m-0 gl-font-sm"></p> + </div> + <ul + v-if="data.children && data.children.length > 0 && level === 2" + class="gl-m-0 gl-p-0 gl-list-style-none" + > + <li> + <dynamic-content + v-for="(childData, index) in data.children" + :key="childData.id || index" + :data="childData" + :widget-name="widgetName" + :level="3" + data-qa-selector="child_content" + @clickedAction="onClickedAction" + /> + </li> + </ul> + </div> + </template> + </content-row> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue new file mode 100644 index 00000000000..ff17de343d6 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue @@ -0,0 +1,67 @@ +<script> +import { GlLoadingIcon, GlIcon } from '@gitlab/ui'; +import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; +import { EXTENSION_ICON_CLASS, EXTENSION_ICON_NAMES } from '../../constants'; + +export default { + components: { + GlLoadingIcon, + GlIcon, + }, + props: { + level: { + type: Number, + required: false, + default: 1, + }, + name: { + type: String, + required: false, + default: '', + }, + isLoading: { + type: Boolean, + required: false, + default: false, + }, + iconName: { + type: String, + required: false, + default: null, + }, + }, + computed: { + iconAriaLabel() { + return `${capitalizeFirstCharacter(this.iconName)} ${this.name}`; + }, + iconSize() { + return this.level === 1 ? 16 : 12; + }, + }, + EXTENSION_ICON_NAMES, + EXTENSION_ICON_CLASS, +}; +</script> + +<template> + <div :class="[$options.EXTENSION_ICON_CLASS[iconName]]" class="gl-mr-3"> + <gl-loading-icon v-if="isLoading" size="md" inline /> + <div + v-else + class="gl-display-flex gl-align-items-center gl-justify-content-center gl-rounded-full gl-bg-gray-10" + :class="{ + 'gl-p-2': level === 1, + }" + > + <div class="gl-rounded-full gl-bg-white"> + <gl-icon + :name="$options.EXTENSION_ICON_NAMES[iconName]" + :size="iconSize" + :aria-label="iconAriaLabel" + :data-qa-selector="`status_${iconName}_icon`" + class="gl-display-block" + /> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue index c9fc2dde0bd..94359d7d6ac 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue @@ -4,10 +4,11 @@ import * as Sentry from '@sentry/browser'; import { normalizeHeaders } from '~/lib/utils/common_utils'; import { sprintf, __ } from '~/locale'; import Poll from '~/lib/utils/poll'; -import StatusIcon from '../extensions/status_icon.vue'; import ActionButtons from '../action_buttons.vue'; import { EXTENSION_ICONS } from '../../constants'; -import ContentSection from './widget_content_section.vue'; +import ContentRow from './widget_content_row.vue'; +import DynamicContent from './dynamic_content.vue'; +import StatusIcon from './status_icon.vue'; const FETCH_TYPE_COLLAPSED = 'collapsed'; const FETCH_TYPE_EXPANDED = 'expanded'; @@ -18,7 +19,8 @@ export default { StatusIcon, GlButton, GlLoadingIcon, - ContentSection, + ContentRow, + DynamicContent, }, directives: { GlTooltip: GlTooltipDirective, @@ -59,7 +61,7 @@ export default { }, // If the content slot is not used, this value will be used as a fallback. content: { - type: Object, + type: Array, required: false, default: undefined, }, @@ -187,7 +189,7 @@ export default { <template> <section class="media-section" data-testid="widget-extension"> - <div class="media gl-p-5"> + <div class="gl-p-5 gl-align-items-center gl-display-flex"> <status-icon :level="1" :name="widgetName" @@ -227,23 +229,34 @@ export default { </div> <div v-if="!isCollapsed || contentError" - class="mr-widget-grouped-section gl-relative" + class="gl-relative gl-bg-gray-10" data-testid="widget-extension-collapsed-section" > <div v-if="isLoadingExpandedContent" class="report-block-container gl-text-center"> - <gl-loading-icon size="sm" inline /> {{ __('Loading...') }} + <gl-loading-icon size="sm" inline /> {{ loadingText }} + </div> + <div v-else class="gl-px-5 gl-display-flex"> + <content-row + v-if="contentError" + :level="2" + :status-icon-name="$options.failedStatusIcon" + :widget-name="widgetName" + > + <template #body> + {{ contentError }} + </template> + </content-row> + <div v-else class="gl-w-full"> + <slot name="content"> + <dynamic-content + v-for="(data, index) in content" + :key="data.id || index" + :data="data" + :widget-name="widgetName" + /> + </slot> + </div> </div> - <content-section - v-else-if="contentError" - class="report-block-container" - :status-icon-name="$options.failedStatusIcon" - :widget-name="widgetName" - > - {{ contentError }} - </content-section> - <slot v-else name="content"> - {{ content }} - </slot> </div> </section> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue new file mode 100644 index 00000000000..ee81f0950a8 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue @@ -0,0 +1,68 @@ +<script> +import { GlSafeHtmlDirective } from '@gitlab/ui'; +import { EXTENSION_ICONS } from '../../constants'; +import { generateText } from '../extensions/utils'; +import StatusIcon from './status_icon.vue'; + +export default { + components: { + StatusIcon, + }, + directives: { + SafeHtml: GlSafeHtmlDirective, + }, + props: { + level: { + type: Number, + required: true, + validator: (value) => value === 2 || value === 3, + }, + statusIconName: { + type: String, + default: '', + required: false, + validator: (value) => value === '' || Object.keys(EXTENSION_ICONS).includes(value), + }, + widgetName: { + type: String, + required: true, + }, + header: { + type: [String, Array], + default: '', + required: false, + }, + }, + computed: { + generatedHeader() { + return generateText(Array.isArray(this.header) ? this.header[0] : this.header); + }, + generatedSubheader() { + return Array.isArray(this.header) && this.header[1] ? generateText(this.header[1]) : ''; + }, + }, +}; +</script> +<template> + <div + class="gl-w-full gl-display-flex mr-widget-content-row gl-align-items-baseline" + :class="{ 'gl-border-t gl-py-3 gl-pl-7': level === 2 }" + > + <status-icon v-if="statusIconName" :level="2" :name="widgetName" :icon-name="statusIconName" /> + <div> + <slot name="header"> + <div v-if="header" class="gl-mb-2"> + <strong v-safe-html="generatedHeader" class="gl-display-block"></strong + ><span + v-if="generatedSubheader" + v-safe-html="generatedSubheader" + class="gl-display-block" + ></span> + </div> + </slot> + <div class="gl-display-flex gl-align-items-baseline gl-w-full"> + <slot name="body"></slot> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_section.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_section.vue deleted file mode 100644 index 61e3744b5dc..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_section.vue +++ /dev/null @@ -1,35 +0,0 @@ -<script> -import { EXTENSION_ICONS } from '../../constants'; -import StatusIcon from '../extensions/status_icon.vue'; - -export default { - components: { - StatusIcon, - }, - props: { - statusIconName: { - type: String, - default: '', - required: false, - validator: (value) => value === '' || Object.keys(EXTENSION_ICONS).includes(value), - }, - widgetName: { - type: String, - required: true, - }, - }, -}; -</script> -<template> - <div class="gl-px-7"> - <div class="gl-pl-4 gl-display-flex"> - <status-icon - v-if="statusIconName" - :level="2" - :name="widgetName" - :icon-name="statusIconName" - /> - <slot name="default"></slot> - </div> - </div> -</template> |