diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-03 21:11:16 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-03 21:11:16 +0300 |
commit | 9578c9f9e88421a5dc4d9215f40d932bd30cbabc (patch) | |
tree | 51cc56403430f901de45cb82a6ab5f63c1f37712 /app | |
parent | 7fcda12793acc54ba8de037f50cc3696dbd0f002 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
27 files changed, 535 insertions, 60 deletions
diff --git a/app/assets/javascripts/design_management/components/delete_button.vue b/app/assets/javascripts/design_management/components/delete_button.vue index 273fa3f6be2..fbcce22ec1e 100644 --- a/app/assets/javascripts/design_management/components/delete_button.vue +++ b/app/assets/javascripts/design_management/components/delete_button.vue @@ -73,21 +73,19 @@ export default { </script> <template> - <div class="gl-display-flex gl-align-items-center gl-h-full"> + <div> <gl-modal :modal-id="modalId" :title="$options.modal.title" :action-primary="$options.modal.actionPrimary" :action-cancel="$options.modal.actionCancel" - @ok="$emit('deleteSelectedDesigns')" + @ok="$emit('delete-selected-designs')" > - <p> - {{ - s__( - 'DesignManagement|Archived designs will still be available in previous versions of the design collection.', - ) - }} - </p> + {{ + s__( + 'DesignManagement|Archived designs will still be available in previous versions of the design collection.', + ) + }} </gl-modal> <gl-button v-gl-modal-directive="modalId" diff --git a/app/assets/javascripts/design_management/components/design_destroyer.vue b/app/assets/javascripts/design_management/components/design_destroyer.vue index 01f9cac456d..0178111f651 100644 --- a/app/assets/javascripts/design_management/components/design_destroyer.vue +++ b/app/assets/javascripts/design_management/components/design_destroyer.vue @@ -55,6 +55,7 @@ export default { iid, }" :update="updateStoreAfterDelete" + :tag="null" v-on="$listeners" > <slot v-bind="{ mutate, loading, error }"></slot> diff --git a/app/assets/javascripts/design_management/components/toolbar/index.vue b/app/assets/javascripts/design_management/components/toolbar/index.vue index a3b0f06fb28..8abf1529f3c 100644 --- a/app/assets/javascripts/design_management/components/toolbar/index.vue +++ b/app/assets/javascripts/design_management/components/toolbar/index.vue @@ -130,7 +130,7 @@ export default { button-icon="archive" button-category="secondary" :title="s__('DesignManagement|Archive design')" - @deleteSelectedDesigns="$emit('delete')" + @delete-selected-designs="$emit('delete')" /> </header> </template> diff --git a/app/assets/javascripts/design_management/components/upload/button.vue b/app/assets/javascripts/design_management/components/upload/button.vue index d7b287f663b..394ccb3c483 100644 --- a/app/assets/javascripts/design_management/components/upload/button.vue +++ b/app/assets/javascripts/design_management/components/upload/button.vue @@ -50,7 +50,7 @@ export default { type="file" name="design_file" :accept="$options.VALID_DESIGN_FILE_MIMETYPE.mimetype" - class="hide" + class="gl-display-none" multiple @change="onFileUploadChange" /> diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue index c73c8fb6ca4..99ac38fc554 100644 --- a/app/assets/javascripts/design_management/pages/index.vue +++ b/app/assets/javascripts/design_management/pages/index.vue @@ -365,7 +365,8 @@ export default { v-if="isLatestVersion" variant="link" size="small" - class="gl-mr-4 js-select-all" + class="gl-mr-3" + data-testid="select-all-designs-button" @click="toggleDesignsSelection" >{{ selectAllButtonText }} </gl-button> @@ -385,7 +386,7 @@ export default { data-qa-selector="archive_button" :loading="loading" :has-selected-designs="hasSelectedDesigns" - @deleteSelectedDesigns="mutate()" + @delete-selected-designs="mutate()" > {{ s__('DesignManagement|Archive selected') }} </delete-button> diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue index 9c922f0ec13..fa0f959bce9 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue @@ -7,6 +7,7 @@ import PipelineGraph from './graph_component.vue'; import { getQueryHeaders, reportToSentry, + serializeGqlErr, toggleQueryPollingByVisibility, unwrapPipelineData, } from './utils'; @@ -60,8 +61,8 @@ export default { update(data) { return unwrapPipelineData(this.pipelineProjectPath, data); }, - error() { - this.reportFailure(LOAD_FAILURE); + error({ gqlError }) { + this.reportFailure(LOAD_FAILURE, serializeGqlErr(gqlError)); }, }, }, @@ -112,10 +113,10 @@ export default { refreshPipelineGraph() { this.$apollo.queries.pipeline.refetch(); }, - reportFailure(type) { + reportFailure(type, err = '') { this.showAlert = true; this.alertType = type; - reportToSentry(this.$options.name, this.alertType); + reportToSentry(this.$options.name, `type: ${this.alertType}, info: ${err}`); }, }, }; diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue index 356207cbdd4..cccabf8619b 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue @@ -6,6 +6,7 @@ import LinkedPipeline from './linked_pipeline.vue'; import { getQueryHeaders, reportToSentry, + serializeGqlErr, toggleQueryPollingByVisibility, unwrapPipelineData, validateConfigPaths, @@ -99,12 +100,14 @@ export default { this.loadingPipelineId = null; this.$emit('scrollContainer'); }, - error(err, _vm, _key, type) { + error({ gqlError }, _vm, _key, type) { this.$emit('error', LOAD_FAILURE); reportToSentry( 'linked_pipelines_column', - `error type: ${LOAD_FAILURE}, error: ${err}, apollo error type: ${type}`, + `error type: ${LOAD_FAILURE}, error: ${serializeGqlErr( + gqlError, + )}, apollo error type: ${type}`, ); }, }); diff --git a/app/assets/javascripts/pipelines/components/graph/utils.js b/app/assets/javascripts/pipelines/components/graph/utils.js index 9e4936c1c24..81d8fe7f489 100644 --- a/app/assets/javascripts/pipelines/components/graph/utils.js +++ b/app/assets/javascripts/pipelines/components/graph/utils.js @@ -23,7 +23,6 @@ const getQueryHeaders = (etagResource) => { }, }; }; -/* eslint-enable @gitlab/require-i18n-strings */ const reportToSentry = (component, failureType) => { Sentry.withScope((scope) => { @@ -32,6 +31,25 @@ const reportToSentry = (component, failureType) => { }); }; +const serializeGqlErr = (gqlError) => { + if (!gqlError) { + return 'gqlError data not available.'; + } + + const { locations, message, path } = gqlError; + + return ` + ${message}. + Locations: ${locations + .flatMap((loc) => Object.entries(loc)) + .flat(2) + .join(' ')}. + Path: ${path.join(', ')}. + `; +}; + +/* eslint-enable @gitlab/require-i18n-strings */ + const toggleQueryPollingByVisibility = (queryRef, interval = 10000) => { const stopStartQuery = (query) => { if (!Visibility.hidden()) { @@ -82,6 +100,7 @@ const validateConfigPaths = (value) => value.graphqlResourceEtag?.length > 0; export { getQueryHeaders, reportToSentry, + serializeGqlErr, toggleQueryPollingByVisibility, unwrapPipelineData, validateConfigPaths, diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue new file mode 100644 index 00000000000..81eeead2171 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue @@ -0,0 +1,119 @@ +<script> +import { GlButton, GlTooltipDirective, GlModalDirective } from '@gitlab/ui'; +import { __ } from '~/locale'; +import eventHub from '../../event_hub'; +import PipelinesArtifactsComponent from './pipelines_artifacts.vue'; +import PipelinesManualActions from './pipelines_manual_actions.vue'; + +export default { + i18n: { + cancelTitle: __('Cancel'), + redeployTitle: __('Retry'), + }, + directives: { + GlTooltip: GlTooltipDirective, + GlModalDirective, + }, + components: { + GlButton, + PipelinesManualActions, + PipelinesArtifactsComponent, + }, + props: { + pipeline: { + type: Object, + required: true, + }, + cancelingPipeline: { + type: Number, + required: false, + default: null, + }, + }, + data() { + return { + isRetrying: false, + }; + }, + computed: { + displayPipelineActions() { + return ( + this.pipeline.flags.retryable || + this.pipeline.flags.cancelable || + this.pipeline.details.manual_actions.length || + this.pipeline.details.artifacts.length + ); + }, + actions() { + if (!this.pipeline || !this.pipeline.details) { + return []; + } + const { details } = this.pipeline; + return [...(details.manual_actions || []), ...(details.scheduled_actions || [])]; + }, + isCancelling() { + return this.cancelingPipeline === this.pipeline.id; + }, + }, + watch: { + pipeline() { + this.isRetrying = false; + }, + }, + methods: { + handleCancelClick() { + eventHub.$emit('openConfirmationModal', { + pipeline: this.pipeline, + endpoint: this.pipeline.cancel_path, + }); + }, + handleRetryClick() { + this.isRetrying = true; + eventHub.$emit('retryPipeline', this.pipeline.retry_path); + }, + }, +}; +</script> + +<template> + <div v-if="displayPipelineActions" class="gl-text-right"> + <div class="btn-group"> + <pipelines-manual-actions v-if="actions.length > 0" :actions="actions" /> + + <pipelines-artifacts-component + v-if="pipeline.details.artifacts.length" + :artifacts="pipeline.details.artifacts" + /> + + <gl-button + v-if="pipeline.flags.retryable" + v-gl-tooltip.hover + :aria-label="$options.i18n.redeployTitle" + :title="$options.i18n.redeployTitle" + :disabled="isRetrying" + :loading="isRetrying" + class="js-pipelines-retry-button" + data-qa-selector="pipeline_retry_button" + icon="repeat" + variant="default" + category="secondary" + @click="handleRetryClick" + /> + + <gl-button + v-if="pipeline.flags.cancelable" + v-gl-tooltip.hover + v-gl-modal-directive="'confirmation-modal'" + :aria-label="$options.i18n.cancelTitle" + :title="$options.i18n.cancelTitle" + :loading="isCancelling" + :disabled="isCancelling" + icon="close" + variant="danger" + category="primary" + class="js-pipelines-cancel-button" + @click="handleCancelClick" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_triggerer.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_triggerer.vue index 6955b27cb22..c707b395192 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_triggerer.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_triggerer.vue @@ -29,7 +29,7 @@ export default { }; </script> <template> - <div :class="classes"> + <div :class="classes" data-testid="pipeline-triggerer"> <user-avatar-link v-if="user" :link-href="user.path" diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue index 2a8a0279e62..0de520a2ca7 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue @@ -61,7 +61,7 @@ export default { }; </script> <template> - <div :class="classes"> + <div :class="classes" data-testid="pipeline-url-table-cell"> <gl-link :href="pipeline.path" data-testid="pipeline-url-link" diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_commit.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_commit.vue new file mode 100644 index 00000000000..cc676883c1d --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_commit.vue @@ -0,0 +1,85 @@ +<script> +import { CHILD_VIEW } from '~/pipelines/constants'; +import CommitComponent from '~/vue_shared/components/commit.vue'; + +export default { + components: { + CommitComponent, + }, + props: { + pipeline: { + type: Object, + required: true, + }, + viewType: { + type: String, + required: true, + }, + }, + computed: { + commitAuthor() { + let commitAuthorInformation; + + if (!this.pipeline || !this.pipeline.commit) { + return null; + } + + // 1. person who is an author of a commit might be a GitLab user + if (this.pipeline.commit.author) { + // 2. if person who is an author of a commit is a GitLab user + // they can have a GitLab avatar + if (this.pipeline.commit.author.avatar_url) { + commitAuthorInformation = this.pipeline.commit.author; + + // 3. If GitLab user does not have avatar, they might have a Gravatar + } else if (this.pipeline.commit.author_gravatar_url) { + commitAuthorInformation = { + ...this.pipeline.commit.author, + avatar_url: this.pipeline.commit.author_gravatar_url, + }; + } + // 4. If committer is not a GitLab User, they can have a Gravatar + } else { + commitAuthorInformation = { + avatar_url: this.pipeline.commit.author_gravatar_url, + path: `mailto:${this.pipeline.commit.author_email}`, + username: this.pipeline.commit.author_name, + }; + } + + return commitAuthorInformation; + }, + commitTag() { + return this.pipeline?.ref?.tag; + }, + commitRef() { + return this.pipeline?.ref; + }, + commitUrl() { + return this.pipeline?.commit?.commit_path; + }, + commitShortSha() { + return this.pipeline?.commit?.short_id; + }, + commitTitle() { + return this.pipeline?.commit?.title; + }, + isChildView() { + return this.viewType === CHILD_VIEW; + }, + }, +}; +</script> + +<template> + <commit-component + :tag="commitTag" + :commit-ref="commitRef" + :commit-url="commitUrl" + :merge-request-ref="pipeline.merge_request" + :short-sha="commitShortSha" + :title="commitTitle" + :author="commitAuthor" + :show-ref-info="!isChildView" + /> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue new file mode 100644 index 00000000000..cc3c8d522b3 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue @@ -0,0 +1,37 @@ +<script> +import { CHILD_VIEW } from '~/pipelines/constants'; +import CiBadge from '~/vue_shared/components/ci_badge_link.vue'; + +export default { + components: { + CiBadge, + }, + props: { + pipeline: { + type: Object, + required: true, + }, + viewType: { + type: String, + required: true, + }, + }, + computed: { + pipelineStatus() { + return this.pipeline?.details?.status ?? {}; + }, + isChildView() { + return this.viewType === CHILD_VIEW; + }, + }, +}; +</script> + +<template> + <ci-badge + :status="pipelineStatus" + :show-text="!isChildView" + :icon-classes="'gl-vertical-align-middle!'" + data-qa-selector="pipeline_commit_status" + /> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue index 5894101ebc6..5747287d2ad 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue @@ -1,15 +1,93 @@ <script> import { GlTable, GlTooltipDirective } from '@gitlab/ui'; +import { s__ } from '~/locale'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import eventHub from '../../event_hub'; +import PipelineOperations from './pipeline_operations.vue'; import PipelineStopModal from './pipeline_stop_modal.vue'; +import PipelineTriggerer from './pipeline_triggerer.vue'; +import PipelineUrl from './pipeline_url.vue'; +import PipelinesCommit from './pipelines_commit.vue'; +import PipelinesStatusBadge from './pipelines_status_badge.vue'; import PipelinesTableRowComponent from './pipelines_table_row.vue'; +import PipelineStage from './stage.vue'; +import PipelinesTimeago from './time_ago.vue'; + +const DEFAULT_TD_CLASS = 'gl-p-5!'; +const HIDE_TD_ON_MOBILE = 'gl-display-none! gl-lg-display-table-cell!'; +const DEFAULT_TH_CLASSES = + 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1! gl-font-sm!'; export default { + fields: [ + { + key: 'status', + label: s__('Pipeline|Status'), + thClass: DEFAULT_TH_CLASSES, + columnClass: 'gl-w-10p', + tdClass: DEFAULT_TD_CLASS, + thAttr: { 'data-testid': 'status-th' }, + }, + { + key: 'pipeline', + label: s__('Pipeline|Pipeline'), + thClass: DEFAULT_TH_CLASSES, + tdClass: `${DEFAULT_TD_CLASS} ${HIDE_TD_ON_MOBILE}`, + columnClass: 'gl-w-10p', + thAttr: { 'data-testid': 'pipeline-th' }, + }, + { + key: 'triggerer', + label: s__('Pipeline|Triggerer'), + thClass: DEFAULT_TH_CLASSES, + tdClass: `${DEFAULT_TD_CLASS} ${HIDE_TD_ON_MOBILE}`, + columnClass: 'gl-w-10p', + thAttr: { 'data-testid': 'triggerer-th' }, + }, + { + key: 'commit', + label: s__('Pipeline|Commit'), + thClass: DEFAULT_TH_CLASSES, + tdClass: DEFAULT_TD_CLASS, + columnClass: 'gl-w-20p', + thAttr: { 'data-testid': 'commit-th' }, + }, + { + key: 'stages', + label: s__('Pipeline|Stages'), + thClass: DEFAULT_TH_CLASSES, + tdClass: DEFAULT_TD_CLASS, + columnClass: 'gl-w-15p', + thAttr: { 'data-testid': 'stages-th' }, + }, + { + key: 'timeago', + label: s__('Pipeline|Duration'), + thClass: DEFAULT_TH_CLASSES, + tdClass: DEFAULT_TD_CLASS, + columnClass: 'gl-w-15p', + thAttr: { 'data-testid': 'timeago-th' }, + }, + { + key: 'actions', + label: '', + thClass: DEFAULT_TH_CLASSES, + tdClass: DEFAULT_TD_CLASS, + columnClass: 'gl-w-20p', + thAttr: { 'data-testid': 'actions-th' }, + }, + ], components: { GlTable, - PipelinesTableRowComponent, + PipelinesCommit, + PipelineOperations, + PipelineStage, + PipelinesStatusBadge, PipelineStopModal, + PipelinesTableRowComponent, + PipelinesTimeago, + PipelineTriggerer, + PipelineUrl, }, directives: { GlTooltip: GlTooltipDirective, @@ -43,11 +121,6 @@ export default { cancelingPipeline: null, }; }, - computed: { - legacyTableClass() { - return !this.glFeatures.newPipelinesTable ? 'ci-table' : ''; - }, - }, watch: { pipelines() { this.cancelingPipeline = null; @@ -73,8 +146,8 @@ export default { }; </script> <template> - <div :class="legacyTableClass"> - <div v-if="!glFeatures.newPipelinesTable" data-testid="ci-table"> + <div class="ci-table"> + <div v-if="!glFeatures.newPipelinesTable" data-testid="legacy-ci-table"> <div class="gl-responsive-table-row table-row-header" role="row"> <div class="table-section section-10 js-pipeline-status" role="rowheader"> {{ s__('Pipeline|Status') }} @@ -107,7 +180,71 @@ export default { /> </div> - <gl-table v-else /> + <gl-table + v-else + :fields="$options.fields" + :items="pipelines" + tbody-tr-class="commit" + :tbody-tr-attr="{ 'data-testid': 'pipeline-table-row' }" + stacked="lg" + fixed + > + <template #head(actions)> + <slot name="table-header-actions"></slot> + </template> + + <template #table-colgroup="{ fields }"> + <col v-for="field in fields" :key="field.key" :class="field.columnClass" /> + </template> + + <template #cell(status)="{ item }"> + <pipelines-status-badge :pipeline="item" :view-type="viewType" /> + </template> + + <template #cell(pipeline)="{ item }"> + <pipeline-url + class="gl-text-truncate" + :pipeline="item" + :pipeline-schedule-url="pipelineScheduleUrl" + /> + </template> + + <template #cell(triggerer)="{ item }"> + <pipeline-triggerer :pipeline="item" /> + </template> + + <template #cell(commit)="{ item }"> + <pipelines-commit :pipeline="item" :view-type="viewType" /> + </template> + + <template #cell(stages)="{ item }"> + <div class="stage-cell"> + <div></div> + <template v-if="item.details.stages.length > 0"> + <div + v-for="(stage, index) in item.details.stages" + :key="index" + class="stage-container dropdown" + data-testid="widget-mini-pipeline-graph" + > + <pipeline-stage + :type="$options.pipelinesTable" + :stage="stage" + :update-dropdown="updateGraphDropdown" + /> + </div> + </template> + </div> + </template> + + <template #cell(timeago)="{ item }"> + <pipelines-timeago :pipeline="item" /> + </template> + + <template #cell(actions)="{ item }"> + <pipeline-operations :pipeline="item" :canceling-pipeline="cancelingPipeline" /> + </template> + </gl-table> <pipeline-stop-modal :pipeline="pipeline" @submit="onSubmit" /> </div> diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js index 9699f57b4fc..21b114825a6 100644 --- a/app/assets/javascripts/pipelines/constants.js +++ b/app/assets/javascripts/pipelines/constants.js @@ -33,3 +33,5 @@ export const LOAD_FAILURE = 'load_failure'; export const PARSE_FAILURE = 'parse_failure'; export const POST_FAILURE = 'post_failure'; export const UNSUPPORTED_DATA = 'unsupported_data'; + +export const CHILD_VIEW = 'child'; diff --git a/app/assets/javascripts/ref/components/ref_results_section.vue b/app/assets/javascripts/ref/components/ref_results_section.vue index 87ce4f1a49c..4fa2a92ff03 100644 --- a/app/assets/javascripts/ref/components/ref_results_section.vue +++ b/app/assets/javascripts/ref/components/ref_results_section.vue @@ -11,6 +11,12 @@ export default { GlIcon, }, props: { + showHeader: { + type: Boolean, + required: false, + default: true, + }, + sectionTitle: { type: String, required: true, @@ -84,7 +90,7 @@ export default { <template> <div> - <gl-dropdown-section-header> + <gl-dropdown-section-header v-if="showHeader"> <div class="gl-display-flex align-items-center" data-testid="section-header"> <span class="gl-mr-2 gl-mb-1">{{ sectionTitle }}</span> <gl-badge variant="neutral">{{ totalCountText }}</gl-badge> diff --git a/app/assets/javascripts/ref/components/ref_selector.vue b/app/assets/javascripts/ref/components/ref_selector.vue index 8f2805b36f6..36881552216 100644 --- a/app/assets/javascripts/ref/components/ref_selector.vue +++ b/app/assets/javascripts/ref/components/ref_selector.vue @@ -8,9 +8,16 @@ import { GlIcon, GlLoadingIcon, } from '@gitlab/ui'; -import { debounce } from 'lodash'; +import { debounce, isArray } from 'lodash'; import { mapActions, mapGetters, mapState } from 'vuex'; -import { SEARCH_DEBOUNCE_MS, DEFAULT_I18N } from '../constants'; +import { + ALL_REF_TYPES, + SEARCH_DEBOUNCE_MS, + DEFAULT_I18N, + REF_TYPE_BRANCHES, + REF_TYPE_TAGS, + REF_TYPE_COMMITS, +} from '../constants'; import createStore from '../stores'; import RefResultsSection from './ref_results_section.vue'; @@ -28,6 +35,20 @@ export default { RefResultsSection, }, props: { + enabledRefTypes: { + type: Array, + required: false, + default: () => ALL_REF_TYPES, + validator: (val) => + // It has to be an arrray + isArray(val) && + // with at least one item + val.length > 0 && + // and only "REF_TYPE_BRANCHES", "REF_TYPE_TAGS", and "REF_TYPE_COMMITS" are allowed + val.every((item) => ALL_REF_TYPES.includes(item)) && + // and no duplicates are allowed + val.length === new Set(val).size, + }, value: { type: String, required: false, @@ -62,17 +83,29 @@ export default { }; }, showBranchesSection() { - return Boolean(this.matches.branches.totalCount > 0 || this.matches.branches.error); + return ( + this.enabledRefTypes.includes(REF_TYPE_BRANCHES) && + Boolean(this.matches.branches.totalCount > 0 || this.matches.branches.error) + ); }, showTagsSection() { - return Boolean(this.matches.tags.totalCount > 0 || this.matches.tags.error); + return ( + this.enabledRefTypes.includes(REF_TYPE_TAGS) && + Boolean(this.matches.tags.totalCount > 0 || this.matches.tags.error) + ); }, showCommitsSection() { - return Boolean(this.matches.commits.totalCount > 0 || this.matches.commits.error); + return ( + this.enabledRefTypes.includes(REF_TYPE_COMMITS) && + Boolean(this.matches.commits.totalCount > 0 || this.matches.commits.error) + ); }, showNoResults() { return !this.showBranchesSection && !this.showTagsSection && !this.showCommitsSection; }, + showSectionHeaders() { + return this.enabledRefTypes.length > 1; + }, }, watch: { // Keep the Vuex store synchronized if the parent @@ -97,10 +130,18 @@ export default { }, SEARCH_DEBOUNCE_MS); this.setProjectId(this.projectId); - this.search(this.query); + + this.$watch( + 'enabledRefTypes', + () => { + this.setEnabledRefTypes(this.enabledRefTypes); + this.search(this.query); + }, + { immediate: true }, + ); }, methods: { - ...mapActions(['setProjectId', 'setSelectedRef', 'search']), + ...mapActions(['setEnabledRefTypes', 'setProjectId', 'setSelectedRef', 'search']), focusSearchBox() { this.$refs.searchBox.$el.querySelector('input').focus(); }, @@ -170,6 +211,7 @@ export default { :selected-ref="selectedRef" :error="matches.branches.error" :error-message="i18n.branchesErrorMessage" + :show-header="showSectionHeaders" data-testid="branches-section" @selected="selectRef($event)" /> @@ -185,6 +227,7 @@ export default { :selected-ref="selectedRef" :error="matches.tags.error" :error-message="i18n.tagsErrorMessage" + :show-header="showSectionHeaders" data-testid="tags-section" @selected="selectRef($event)" /> @@ -200,6 +243,7 @@ export default { :selected-ref="selectedRef" :error="matches.commits.error" :error-message="i18n.commitsErrorMessage" + :show-header="showSectionHeaders" data-testid="commits-section" @selected="selectRef($event)" /> diff --git a/app/assets/javascripts/ref/constants.js b/app/assets/javascripts/ref/constants.js index ca82b951377..44d0f50b832 100644 --- a/app/assets/javascripts/ref/constants.js +++ b/app/assets/javascripts/ref/constants.js @@ -1,5 +1,10 @@ import { __ } from '~/locale'; +export const REF_TYPE_BRANCHES = 'REF_TYPE_BRANCHES'; +export const REF_TYPE_TAGS = 'REF_TYPE_TAGS'; +export const REF_TYPE_COMMITS = 'REF_TYPE_COMMITS'; +export const ALL_REF_TYPES = Object.freeze([REF_TYPE_BRANCHES, REF_TYPE_TAGS, REF_TYPE_COMMITS]); + export const X_TOTAL_HEADER = 'x-total'; export const SEARCH_DEBOUNCE_MS = 250; diff --git a/app/assets/javascripts/ref/stores/actions.js b/app/assets/javascripts/ref/stores/actions.js index d9bdd64ace5..3832cc0c21d 100644 --- a/app/assets/javascripts/ref/stores/actions.js +++ b/app/assets/javascripts/ref/stores/actions.js @@ -1,17 +1,26 @@ import Api from '~/api'; +import { REF_TYPE_BRANCHES, REF_TYPE_TAGS, REF_TYPE_COMMITS } from '../constants'; import * as types from './mutation_types'; +export const setEnabledRefTypes = ({ commit }, refTypes) => + commit(types.SET_ENABLED_REF_TYPES, refTypes); + export const setProjectId = ({ commit }, projectId) => commit(types.SET_PROJECT_ID, projectId); export const setSelectedRef = ({ commit }, selectedRef) => commit(types.SET_SELECTED_REF, selectedRef); -export const search = ({ dispatch, commit }, query) => { +export const search = ({ state, dispatch, commit }, query) => { commit(types.SET_QUERY, query); - dispatch('searchBranches'); - dispatch('searchTags'); - dispatch('searchCommits'); + const dispatchIfRefTypeEnabled = (refType, action) => { + if (state.enabledRefTypes.includes(refType)) { + dispatch(action); + } + }; + dispatchIfRefTypeEnabled(REF_TYPE_BRANCHES, 'searchBranches'); + dispatchIfRefTypeEnabled(REF_TYPE_TAGS, 'searchTags'); + dispatchIfRefTypeEnabled(REF_TYPE_COMMITS, 'searchCommits'); }; export const searchBranches = ({ commit, state }) => { diff --git a/app/assets/javascripts/ref/stores/mutation_types.js b/app/assets/javascripts/ref/stores/mutation_types.js index 9f6195f5f3f..c26f4fa00c7 100644 --- a/app/assets/javascripts/ref/stores/mutation_types.js +++ b/app/assets/javascripts/ref/stores/mutation_types.js @@ -1,3 +1,5 @@ +export const SET_ENABLED_REF_TYPES = 'SET_ENABLED_REF_TYPES'; + export const SET_PROJECT_ID = 'SET_PROJECT_ID'; export const SET_SELECTED_REF = 'SET_SELECTED_REF'; export const SET_QUERY = 'SET_QUERY'; diff --git a/app/assets/javascripts/ref/stores/mutations.js b/app/assets/javascripts/ref/stores/mutations.js index 4dc73dabfe2..f91cbae8462 100644 --- a/app/assets/javascripts/ref/stores/mutations.js +++ b/app/assets/javascripts/ref/stores/mutations.js @@ -4,6 +4,9 @@ import { X_TOTAL_HEADER } from '../constants'; import * as types from './mutation_types'; export default { + [types.SET_ENABLED_REF_TYPES](state, refTypes) { + state.enabledRefTypes = refTypes; + }, [types.SET_PROJECT_ID](state, projectId) { state.projectId = projectId; }, diff --git a/app/assets/javascripts/ref/stores/state.js b/app/assets/javascripts/ref/stores/state.js index 65b9d6449d7..3affa8f8d03 100644 --- a/app/assets/javascripts/ref/stores/state.js +++ b/app/assets/javascripts/ref/stores/state.js @@ -1,23 +1,18 @@ +const createRefTypeState = () => ({ + list: [], + totalCount: 0, + error: null, +}); + export default () => ({ + enabledRefTypes: [], projectId: null, query: '', matches: { - branches: { - list: [], - totalCount: 0, - error: null, - }, - tags: { - list: [], - totalCount: 0, - error: null, - }, - commits: { - list: [], - totalCount: 0, - error: null, - }, + branches: createRefTypeState(), + tags: createRefTypeState(), + commits: createRefTypeState(), }, selectedRef: null, requestCount: 0, diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 58086922fde..40312f38e08 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -42,6 +42,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo push_frontend_feature_flag(:codequality_backend_comparison, @project, default_enabled: :yaml) push_frontend_feature_flag(:local_file_reviews, default_enabled: :yaml) push_frontend_feature_flag(:paginated_notes, @project, default_enabled: :yaml) + push_frontend_feature_flag(:new_pipelines_table, @project, default_enabled: :yaml) record_experiment_user(:invite_members_version_a) record_experiment_user(:invite_members_version_b) diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb index 67cb2b9558d..dab2f3bd67a 100644 --- a/app/controllers/root_controller.rb +++ b/app/controllers/root_controller.rb @@ -46,6 +46,8 @@ class RootController < Dashboard::ProjectsController redirect_to(activity_dashboard_path) when 'starred_project_activity' redirect_to(activity_dashboard_path(filter: 'starred')) + when 'followed_user_activity' + redirect_to(activity_dashboard_path(filter: 'followed')) when 'groups' redirect_to(dashboard_groups_path) when 'todos' diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index c2a77abb9c9..12bc509466e 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -29,6 +29,7 @@ module PreferencesHelper stars: _("Starred Projects"), project_activity: _("Your Projects' Activity"), starred_project_activity: _("Starred Projects' Activity"), + followed_user_activity: _("Followed Users' Activity"), groups: _("Your Groups"), todos: _("Your To-Do List"), issues: _("Assigned Issues"), diff --git a/app/models/user.rb b/app/models/user.rb index 9b288d99a7f..dfaef473cc9 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -272,7 +272,7 @@ class User < ApplicationRecord enum layout: { fixed: 0, fluid: 1 } # User's Dashboard preference - enum dashboard: { projects: 0, stars: 1, project_activity: 2, starred_project_activity: 3, groups: 4, todos: 5, issues: 6, merge_requests: 7, operations: 8 } + enum dashboard: { projects: 0, stars: 1, project_activity: 2, starred_project_activity: 3, groups: 4, todos: 5, issues: 6, merge_requests: 7, operations: 8, followed_user_activity: 9 } # User's Project preference enum project_view: { readme: 0, activity: 1, files: 2 } diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml index 4301bf01858..f52bf1551f4 100644 --- a/app/views/shared/milestones/_milestone.html.haml +++ b/app/views/shared/milestones/_milestone.html.haml @@ -25,7 +25,11 @@ • - if total_count > recent_releases.count • - = link_to n_('%{count} more release', '%{count} more releases', more_count) % { count: more_count }, project_releases_path(milestone.project) + - more_text = n_('%{count} more release', '%{count} more releases', more_count) % { count: more_count } + - if milestone.project_milestone? + = link_to more_text, project_releases_path(milestone.project) + - else + = more_text %div = render('shared/milestone_expired', milestone: milestone) - if milestone.group_milestone? |