summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue
diff options
context:
space:
mode:
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.vue171
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>