diff options
Diffstat (limited to 'app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue')
-rw-r--r-- | app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue | 171 |
1 files changed, 122 insertions, 49 deletions
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue index 6f10f788952..549cf64fb08 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue @@ -9,17 +9,20 @@ import { GlIntersectionObserver, } from '@gitlab/ui'; import { once } from 'lodash'; +import * as Sentry from '@sentry/browser'; import api from '~/api'; import { sprintf, s__, __ } from '~/locale'; import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue'; -import { EXTENSION_ICON_CLASS } from '../../constants'; +import { EXTENSION_ICON_CLASS, EXTENSION_ICONS } from '../../constants'; import StatusIcon from './status_icon.vue'; import Actions from './actions.vue'; +import { generateText } from './utils'; export const LOADING_STATES = { collapsedLoading: 'collapsedLoading', collapsedError: 'collapsedError', expandedLoading: 'expandedLoading', + expandedError: 'expandedError', }; export default { @@ -40,8 +43,8 @@ export default { data() { return { loadingState: LOADING_STATES.collapsedLoading, - collapsedData: null, - fullData: null, + collapsedData: {}, + fullData: [], isCollapsed: true, showFade: false, }; @@ -53,6 +56,9 @@ export default { widgetLoadingText() { return this.$options.i18n?.loading || __('Loading...'); }, + widgetErrorText() { + return this.$options.i18n?.error || __('Failed to load'); + }, isLoadingSummary() { return this.loadingState === LOADING_STATES.collapsedLoading; }, @@ -60,11 +66,16 @@ export default { return this.loadingState === LOADING_STATES.expandedLoading; }, isCollapsible() { - if (this.isLoadingSummary) { - return false; - } - - return true; + return !this.isLoadingSummary && this.loadingState !== LOADING_STATES.collapsedError; + }, + hasFullData() { + return this.fullData.length > 0; + }, + hasFetchError() { + return ( + this.loadingState === LOADING_STATES.collapsedError || + this.loadingState === LOADING_STATES.expandedError + ); }, collapseButtonLabel() { return sprintf( @@ -75,6 +86,7 @@ export default { ); }, statusIconName() { + if (this.hasFetchError) return EXTENSION_ICONS.error; if (this.isLoadingSummary) return null; return this.statusIcon(this.collapsedData); @@ -82,6 +94,20 @@ export default { tertiaryActionsButtons() { return this.tertiaryButtons ? this.tertiaryButtons() : undefined; }, + hydratedSummary() { + const structuredOutput = this.summary(this.collapsedData); + const summary = { + subject: generateText( + typeof structuredOutput === 'string' ? structuredOutput : structuredOutput.subject, + ), + }; + + if (structuredOutput.meta) { + summary.meta = generateText(structuredOutput.meta); + } + + return summary; + }, }, watch: { isCollapsed(newVal) { @@ -93,15 +119,7 @@ export default { }, }, mounted() { - this.fetchCollapsedData(this.$props) - .then((data) => { - this.collapsedData = data; - this.loadingState = null; - }) - .catch((e) => { - this.loadingState = LOADING_STATES.collapsedError; - throw e; - }); + this.loadCollapsedData(); }, methods: { triggerRedisTracking: once(function triggerRedisTracking() { @@ -114,8 +132,22 @@ export default { this.triggerRedisTracking(); }, + loadCollapsedData() { + this.loadingState = LOADING_STATES.collapsedLoading; + + this.fetchCollapsedData(this.$props) + .then((data) => { + this.collapsedData = data; + this.loadingState = null; + }) + .catch((e) => { + this.loadingState = LOADING_STATES.collapsedError; + + Sentry.captureException(e); + }); + }, loadAllData() { - if (this.fullData) return; + if (this.hasFullData) return; this.loadingState = LOADING_STATES.expandedLoading; @@ -125,10 +157,14 @@ export default { this.fullData = data; }) .catch((e) => { - this.loadingState = null; - throw e; + this.loadingState = LOADING_STATES.expandedError; + + Sentry.captureException(e); }); }, + isArray(arr) { + return Array.isArray(arr); + }, appear(index) { if (index === this.fullData.length - 1) { this.showFade = false; @@ -139,6 +175,7 @@ export default { this.showFade = true; } }, + generateText, }, EXTENSION_ICON_CLASS, }; @@ -153,20 +190,29 @@ export default { :icon-name="statusIconName" /> <div - class="media-body gl-display-flex gl-flex-direction-row!" + class="media-body gl-display-flex gl-flex-direction-row! gl-align-self-center" data-testid="widget-extension-top-level" > <div class="gl-flex-grow-1"> <template v-if="isLoadingSummary">{{ widgetLoadingText }}</template> - <div v-else v-safe-html="summary(collapsedData)"></div> + <template v-else-if="hasFetchError">{{ widgetErrorText }}</template> + <div v-else> + <span v-safe-html="hydratedSummary.subject"></span> + <template v-if="hydratedSummary.meta"> + <br /> + <span v-safe-html="hydratedSummary.meta" class="gl-font-sm"></span> + </template> + </div> </div> <actions :widget="$options.label || $options.name" :tertiary-buttons="tertiaryActionsButtons" /> - <div class="gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3 gl-h-6"> + <div + v-if="isCollapsible" + class="gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3 gl-h-6" + > <gl-button - v-if="isCollapsible" v-gl-tooltip :title="collapseButtonLabel" :aria-expanded="`${!isCollapsed}`" @@ -189,7 +235,7 @@ export default { <gl-loading-icon size="sm" inline /> {{ __('Loading...') }} </div> <smart-virtual-list - v-else-if="fullData" + v-else-if="hasFullData" :length="fullData.length" :remain="20" :size="32" @@ -203,37 +249,64 @@ export default { :class="{ 'gl-border-b-solid gl-border-b-1 gl-border-gray-100': index !== fullData.length - 1, }" - class="gl-display-flex gl-align-items-center gl-py-3 gl-pl-7" + class="gl-py-3 gl-pl-7" data-testid="extension-list-item" > - <status-icon v-if="data.icon" :icon-name="data.icon.name" :size="12" class="gl-pl-0" /> - <gl-intersection-observer - :options="{ rootMargin: '100px', thresholds: 0.1 }" - class="gl-flex-wrap gl-display-flex gl-w-full" - @appear="appear(index)" - @disappear="disappear(index)" - > - <div - v-safe-html="data.text" - class="gl-mr-4 gl-display-flex gl-align-items-center" - ></div> - <div v-if="data.link"> - <gl-link :href="data.link.href">{{ data.link.text }}</gl-link> + <div class="gl-w-full"> + <div v-if="data.header" class="gl-mb-2"> + <template v-if="isArray(data.header)"> + <component + :is="headerI === 0 ? 'strong' : 'span'" + v-for="(header, headerI) in data.header" + :key="headerI" + v-safe-html="generateText(header)" + class="gl-display-block" + /> + </template> + <strong v-else v-safe-html="generateText(data.header)"></strong> + </div> + <div class="gl-display-flex"> + <status-icon + v-if="data.icon" + :icon-name="data.icon.name" + :size="12" + class="gl-pl-0" + /> + <gl-intersection-observer + :options="{ rootMargin: '100px', thresholds: 0.1 }" + class="gl-w-full" + @appear="appear(index)" + @disappear="disappear(index)" + > + <div class="gl-flex-wrap gl-display-flex gl-w-full"> + <div class="gl-mr-4 gl-display-flex gl-align-items-center"> + <p v-safe-html="generateText(data.text)" class="gl-m-0"></p> + </div> + <div v-if="data.link"> + <gl-link :href="data.link.href">{{ data.link.text }}</gl-link> + </div> + <gl-badge v-if="data.badge" :variant="data.badge.variant || 'info'"> + {{ data.badge.text }} + </gl-badge> + <actions + :widget="$options.label || $options.name" + :tertiary-buttons="data.actions" + class="gl-ml-auto" + /> + </div> + <p + v-if="data.subtext" + v-safe-html="generateText(data.subtext)" + class="gl-m-0 gl-font-sm" + ></p> + </gl-intersection-observer> </div> - <gl-badge v-if="data.badge" :variant="data.badge.variant || 'info'"> - {{ data.badge.text }} - </gl-badge> - <actions - :widget="$options.label || $options.name" - :tertiary-buttons="data.actions" - class="gl-ml-auto" - /> - </gl-intersection-observer> + </div> </li> </smart-virtual-list> <div :class="{ show: showFade }" - class="fade mr-extenson-scrim gl-absolute gl-left-0 gl-bottom-0 gl-w-full gl-h-7" + class="fade mr-extenson-scrim gl-absolute gl-left-0 gl-bottom-0 gl-w-full gl-h-7 gl-pointer-events-none" ></div> </div> </section> |