diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-27 15:08:33 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-10-27 15:08:33 +0300 |
commit | eb004dc626d3a1c9497e8b9dc0f3f578afd05fd9 (patch) | |
tree | 1cf688ab28dd0629e74a6a458d8ed5bee1c59642 /app/assets/javascripts/vue_merge_request_widget | |
parent | fae19e0b68830e140d93c7463368a16c13887b54 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/vue_merge_request_widget')
8 files changed, 307 insertions, 0 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 new file mode 100644 index 00000000000..eff26729fa7 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue @@ -0,0 +1,157 @@ +<script> +import { GlButton, GlLoadingIcon, GlIcon, GlLink, GlBadge, GlSafeHtmlDirective } from '@gitlab/ui'; +import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue'; +import StatusIcon from '../mr_widget_status_icon.vue'; + +export const LOADING_STATES = { + collapsedLoading: 'collapsedLoading', + collapsedError: 'collapsedError', + expandedLoading: 'expandedLoading', +}; + +export default { + components: { + GlButton, + GlLoadingIcon, + GlIcon, + GlLink, + GlBadge, + SmartVirtualList, + StatusIcon, + }, + directives: { + SafeHtml: GlSafeHtmlDirective, + }, + data() { + return { + loadingState: LOADING_STATES.collapsedLoading, + collapsedData: null, + fullData: null, + isCollapsed: true, + }; + }, + computed: { + isLoadingSummary() { + return this.loadingState === LOADING_STATES.collapsedLoading; + }, + isLoadingExpanded() { + return this.loadingState === LOADING_STATES.expandedLoading; + }, + isCollapsible() { + if (this.isLoadingSummary) { + return false; + } + + return true; + }, + statusIconName() { + if (this.isLoadingSummary) { + return 'loading'; + } + + if (this.loadingState === LOADING_STATES.collapsedError) { + return 'warning'; + } + + return this.statusIcon(this.collapsedData); + }, + }, + watch: { + isCollapsed(newVal) { + if (!newVal) { + this.loadAllData(); + } else { + this.loadingState = null; + } + }, + }, + mounted() { + this.fetchCollapsedData(this.$props) + .then(data => { + this.collapsedData = data; + this.loadingState = null; + }) + .catch(e => { + this.loadingState = LOADING_STATES.collapsedError; + throw e; + }); + }, + methods: { + toggleCollapsed() { + this.isCollapsed = !this.isCollapsed; + }, + loadAllData() { + if (this.fullData) return; + + this.loadingState = LOADING_STATES.expandedLoading; + + this.fetchFullData(this.$props) + .then(data => { + this.loadingState = null; + this.fullData = data; + }) + .catch(e => { + this.loadingState = null; + throw e; + }); + }, + }, +}; +</script> + +<template> + <section class="media-section mr-widget-border-top"> + <div class="media gl-p-5"> + <status-icon :status="statusIconName" class="align-self-center" /> + <div class="media-body d-flex flex-align-self-center align-items-center"> + <div class="code-text"> + <template v-if="isLoadingSummary"> + {{ __('Loading...') }} + </template> + <div v-else v-safe-html="summary(collapsedData)"></div> + </div> + <gl-button + v-if="isCollapsible" + size="small" + class="float-right align-self-center" + @click="toggleCollapsed" + > + {{ isCollapsed ? __('Expand') : __('Collapse') }} + </gl-button> + </div> + </div> + <div v-if="!isCollapsed" class="mr-widget-grouped-section"> + <div v-if="isLoadingExpanded" class="report-block-container"> + <gl-loading-icon inline /> {{ __('Loading...') }} + </div> + <smart-virtual-list + v-else-if="fullData" + :length="fullData.length" + :remain="20" + :size="32" + wtag="ul" + wclass="report-block-list" + class="report-block-container" + > + <li v-for="data in fullData" :key="data.id" class="d-flex align-items-center"> + <div v-if="data.icon" :class="data.icon.class" class="d-flex"> + <gl-icon :name="data.icon.name" :size="24" /> + </div> + <div + class="gl-mt-2 gl-mb-2 align-content-around align-items-start flex-wrap align-self-center d-flex" + > + <div class="gl-mr-4"> + {{ data.text }} + </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> + </div> + </li> + </smart-virtual-list> + </div> + </section> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js new file mode 100644 index 00000000000..5014c12dc30 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/container.js @@ -0,0 +1,27 @@ +import { extensions } from './index'; + +export default { + props: { + mr: { + type: Object, + required: true, + }, + }, + render(h) { + return h( + 'div', + {}, + extensions.map(extension => + h(extension, { + props: extensions[0].props.reduce( + (acc, key) => ({ + ...acc, + [key]: this.mr[key], + }), + {}, + ), + }), + ), + ); + }, +}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js new file mode 100644 index 00000000000..2bfaec8a1c9 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js @@ -0,0 +1,30 @@ +import ExtensionBase from './base.vue'; + +// Holds all the currently registered extensions +export const extensions = []; + +export const registerExtension = extension => { + // Pushes into the extenions array a dynamically created Vue component + // that gets exteneded from `base.vue` + extensions.push({ + extends: ExtensionBase, + name: extension.name, + props: extension.props, + computed: { + ...Object.keys(extension.computed).reduce( + (acc, computedKey) => ({ + ...acc, + // Making the computed property a method allows us to pass in arguments + // this allows for each computed property to recieve some data + [computedKey]() { + return extension.computed[computedKey]; + }, + }), + {}, + ), + }, + methods: { + ...extension.methods, + }, + }); +}; diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js new file mode 100644 index 00000000000..2d21ced1b28 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.js @@ -0,0 +1,66 @@ +/* eslint-disable */ +import issuesCollapsedQuery from './issues_collapsed.query.graphql'; +import issuesQuery from './issues.query.graphql'; + +export default { + // Give the extension a name + // Make it easier to track in Vue dev tools + name: 'WidgetIssues', + // Add an array of props + // These then get mapped to values stored in the MR Widget store + props: ['targetProjectFullPath'], + // Add any extra computed props in here + computed: { + // Small summary text to be displayed in the collapsed state + // Receives the collapsed data as an argument + summary(count) { + return `<strong>${count}</strong> open issue`; + }, + // Status icon to be used next to the summary text + // Receives the collapsed data as an argument + statusIcon(count) { + return count > 0 ? 'warning' : 'success'; + }, + }, + methods: { + // Fetches the collapsed data + // Ideally, this request should return the smallest amount of data possible + // Receives an object of all the props passed in to the extension + fetchCollapsedData({ targetProjectFullPath }) { + return this.$apollo + .query({ query: issuesCollapsedQuery, variables: { projectPath: targetProjectFullPath } }) + .then(({ data }) => data.project.issues.count); + }, + // Fetches the full data when the extension is expanded + // Receives an object of all the props passed in to the extension + fetchFullData({ targetProjectFullPath }) { + return this.$apollo + .query({ query: issuesQuery, variables: { projectPath: targetProjectFullPath } }) + .then(({ data }) => { + // Return some transformed data to be rendered in the expanded state + return data.project.issues.nodes.map(issue => ({ + id: issue.id, // Required: The ID of the object + text: issue.title, // Required: The text to get used on each row + // Icon to get rendered on the side of each row + icon: { + // Required: Name maps to an icon in GitLabs SVG + name: + issue.state === 'closed' ? 'status_failed_borderless' : 'status_success_borderless', + // Optional: An extra class to be added to the icon for additional styling + class: issue.state === 'closed' ? 'text-danger' : 'text-success', + }, + // Badges get rendered next to the text on each row + badge: issue.state === 'closed' && { + text: 'Closed', // Required: Text to be used inside of the badge + // variant: 'info', // Optional: The variant of the badge, maps to GitLab UI variants + }, + // Each row can have its own link that will take the user elsewhere + // link: { + // href: 'https://google.com', // Required: href for the link + // text: 'Link text', // Required: Text to be used inside the link + // }, + })); + }); + }, + }, +}; diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/issues.query.graphql b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.query.graphql new file mode 100644 index 00000000000..690f571c083 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/extensions/issues.query.graphql @@ -0,0 +1,13 @@ +query getAllIssues($projectPath: ID!) { + project(fullPath: $projectPath) { + issues { + nodes { + id + title + webPath + webUrl + state + } + } + } +} diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/issues_collapsed.query.graphql b/app/assets/javascripts/vue_merge_request_widget/extensions/issues_collapsed.query.graphql new file mode 100644 index 00000000000..389a81e0a61 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/extensions/issues_collapsed.query.graphql @@ -0,0 +1,7 @@ +query getIssues($projectPath: ID!) { + project(fullPath: $projectPath) { + issues { + count + } + } +} diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js index 87e56dfcbdf..72d4e7063ad 100644 --- a/app/assets/javascripts/vue_merge_request_widget/index.js +++ b/app/assets/javascripts/vue_merge_request_widget/index.js @@ -3,6 +3,8 @@ import MrWidgetOptions from 'ee_else_ce/vue_merge_request_widget/mr_widget_optio import VueApollo from 'vue-apollo'; import Translate from '../vue_shared/translate'; import createDefaultClient from '~/lib/graphql'; +import { registerExtension } from './components/extensions'; +import issueExtension from './extensions/issues'; Vue.use(Translate); Vue.use(VueApollo); @@ -17,6 +19,8 @@ export default () => { gl.mrWidgetData.gitlabLogo = gon.gitlab_logo; gl.mrWidgetData.defaultAvatarUrl = gon.default_avatar_url; + registerExtension(issueExtension); + const vm = new Vue({ ...MrWidgetOptions, apolloProvider }); window.gl.mrWidget = { diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index f1eeb331b9b..190d790f584 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -37,6 +37,7 @@ import FailedToMerge from './components/states/mr_widget_failed_to_merge.vue'; import MrWidgetAutoMergeEnabled from './components/states/mr_widget_auto_merge_enabled.vue'; import AutoMergeFailed from './components/states/mr_widget_auto_merge_failed.vue'; import CheckingState from './components/states/mr_widget_checking.vue'; +// import ExtensionsContainer from './components/extensions/container'; import eventHub from './event_hub'; import notify from '~/lib/utils/notify'; import SourceBranchRemovalStatus from './components/source_branch_removal_status.vue'; @@ -57,6 +58,7 @@ export default { }, components: { Loading, + // ExtensionsContainer, 'mr-widget-header': WidgetHeader, 'mr-widget-suggest-pipeline': WidgetSuggestPipeline, 'mr-widget-merge-help': WidgetMergeHelp, @@ -454,6 +456,7 @@ export default { :service="service" /> <div class="mr-section-container mr-widget-workflow"> + <!-- <extensions-container :mr="mr" /> --> <grouped-codequality-reports-app v-if="shouldRenderCodeQuality" :base-path="mr.codeclimate.base_path" |