diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-20 13:49:51 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-20 13:49:51 +0000 |
commit | 71786ddc8e28fbd3cb3fcc4b3ff15e5962a1c82e (patch) | |
tree | 6a2d93ef3fb2d353bb7739e4b57e6541f51cdd71 /app/assets/javascripts/projects | |
parent | a7253423e3403b8c08f8a161e5937e1488f5f407 (diff) | |
download | gitlab-ce-15.9.0-rc42.tar.gz |
Add latest changes from gitlab-org/gitlab@15-9-stable-eev15.9.0-rc42
Diffstat (limited to 'app/assets/javascripts/projects')
23 files changed, 329 insertions, 297 deletions
diff --git a/app/assets/javascripts/projects/commit/components/branches_dropdown.vue b/app/assets/javascripts/projects/commit/components/branches_dropdown.vue index a037e721677..0ed154c47dd 100644 --- a/app/assets/javascripts/projects/commit/components/branches_dropdown.vue +++ b/app/assets/javascripts/projects/commit/components/branches_dropdown.vue @@ -1,12 +1,7 @@ <script> -import { - GlDropdown, - GlSearchBoxByType, - GlDropdownItem, - GlDropdownText, - GlLoadingIcon, -} from '@gitlab/ui'; +import { GlCollapsibleListbox } from '@gitlab/ui'; import { mapActions, mapGetters, mapState } from 'vuex'; +import { debounce } from 'lodash'; import { I18N_NO_RESULTS_MESSAGE, I18N_BRANCH_HEADER, @@ -16,11 +11,7 @@ import { export default { name: 'BranchesDropdown', components: { - GlDropdown, - GlSearchBoxByType, - GlDropdownItem, - GlDropdownText, - GlLoadingIcon, + GlCollapsibleListbox, }, props: { value: { @@ -46,19 +37,17 @@ export default { }, computed: { ...mapGetters(['joinedBranches']), - ...mapState(['isFetching', 'branch', 'branches']), - filteredResults() { - const lowerCasedSearchTerm = this.searchTerm.toLowerCase(); - return this.joinedBranches.filter((resultString) => - resultString.toLowerCase().includes(lowerCasedSearchTerm), - ); + ...mapState(['isFetching']), + listboxItems() { + return this.joinedBranches.map((value) => ({ value, text: value })); }, }, watch: { // Parent component can set the branch value (e.g. when the user selects a different project) // and we need to keep the search term in sync with the selected value value(val) { - this.searchTermChanged(val); + this.searchTerm = val; + this.fetchBranches(this.searchTerm); }, }, mounted() { @@ -67,50 +56,29 @@ export default { methods: { ...mapActions(['fetchBranches']), selectBranch(branch) { - this.$emit('selectBranch', branch); - this.searchTerm = branch; // enables isSelected to work as expected - }, - isSelected(selectedBranch) { - return selectedBranch === this.branch; + this.$emit('input', branch); }, + debouncedSearch: debounce(function debouncedSearch() { + this.fetchBranches(this.searchTerm); + }, 250), searchTermChanged(value) { - this.searchTerm = value; - this.fetchBranches(value); + this.searchTerm = value.trim(); + this.debouncedSearch(value); }, }, }; </script> <template> - <gl-dropdown :text="value" :header-text="$options.i18n.branchHeaderTitle"> - <gl-search-box-by-type - :value="searchTerm" - trim - autocomplete="off" - :debounce="250" - :placeholder="$options.i18n.branchSearchPlaceholder" - data-testid="dropdown-search-box" - @input="searchTermChanged" - /> - <gl-dropdown-item - v-for="branch in filteredResults" - v-show="!isFetching" - :key="branch" - :name="branch" - :is-checked="isSelected(branch)" - is-check-item - data-testid="dropdown-item" - @click="selectBranch(branch)" - > - {{ branch }} - </gl-dropdown-item> - <gl-dropdown-text v-show="isFetching" data-testid="dropdown-text-loading-icon"> - <gl-loading-icon size="sm" class="gl-mx-auto" /> - </gl-dropdown-text> - <gl-dropdown-text - v-if="!filteredResults.length && !isFetching" - data-testid="empty-result-message" - > - <span class="gl-text-gray-500">{{ $options.i18n.noResultsMessage }}</span> - </gl-dropdown-text> - </gl-dropdown> + <gl-collapsible-listbox + :header-text="$options.i18n.branchHeaderTitle" + :toggle-text="value" + :items="listboxItems" + searchable + :search-placeholder="$options.i18n.branchSearchPlaceholder" + :searching="isFetching" + :selected="value" + :no-results-text="$options.i18n.noResultsMessage" + @search="searchTermChanged" + @select="selectBranch" + /> </template> diff --git a/app/assets/javascripts/projects/commit/components/form_modal.vue b/app/assets/javascripts/projects/commit/components/form_modal.vue index 1febe8ceaab..f78afef1c17 100644 --- a/app/assets/javascripts/projects/commit/components/form_modal.vue +++ b/app/assets/javascripts/projects/commit/components/form_modal.vue @@ -141,11 +141,7 @@ export default { :value="targetProjectId" /> - <projects-dropdown - class="gl-w-half" - :value="targetProjectName" - @selectProject="setSelectedProject" - /> + <projects-dropdown :value="targetProjectName" @selectProject="setSelectedProject" /> </gl-form-group> <gl-form-group @@ -155,12 +151,7 @@ export default { > <input id="start_branch" type="hidden" name="start_branch" :value="branch" /> - <branches-dropdown - class="gl-w-half" - :value="branch" - :blanked="isRevert" - @selectBranch="setBranch" - /> + <branches-dropdown :value="branch" :blanked="isRevert" @input="setBranch" /> </gl-form-group> <gl-form-checkbox diff --git a/app/assets/javascripts/projects/commit/components/projects_dropdown.vue b/app/assets/javascripts/projects/commit/components/projects_dropdown.vue index 6288bcdaad0..d43f5b99e2c 100644 --- a/app/assets/javascripts/projects/commit/components/projects_dropdown.vue +++ b/app/assets/javascripts/projects/commit/components/projects_dropdown.vue @@ -1,5 +1,5 @@ <script> -import { GlDropdown, GlSearchBoxByType, GlDropdownItem, GlDropdownText } from '@gitlab/ui'; +import { GlCollapsibleListbox } from '@gitlab/ui'; import { mapGetters, mapState } from 'vuex'; import { I18N_NO_RESULTS_MESSAGE, @@ -10,10 +10,7 @@ import { export default { name: 'ProjectsDropdown', components: { - GlDropdown, - GlSearchBoxByType, - GlDropdownItem, - GlDropdownText, + GlCollapsibleListbox, }, props: { value: { @@ -41,17 +38,20 @@ export default { project.name.toLowerCase().includes(lowerCasedFilterTerm), ); }, + listboxItems() { + return this.filteredResults.map(({ id, name }) => ({ value: id, text: name })); + }, selectedProject() { return this.sortedProjects.find((project) => project.id === this.targetProjectId) || {}; }, }, methods: { - selectProject(project) { - this.$emit('selectProject', project.id); - this.filterTerm = project.name; // when we select a project, we want the dropdown to filter to the selected project - }, - isSelected(selectedProject) { - return selectedProject === this.selectedProject; + selectProject(value) { + this.$emit('selectProject', value); + + // when we select a project, we want the dropdown to filter to the selected project + const project = this.listboxItems.find((x) => x.value === value); + this.filterTerm = project?.text || ''; }, filterTermChanged(value) { this.filterTerm = value; @@ -60,28 +60,15 @@ export default { }; </script> <template> - <gl-dropdown :text="selectedProject.name" :header-text="$options.i18n.projectHeaderTitle"> - <gl-search-box-by-type - :value="filterTerm" - trim - autocomplete="off" - :placeholder="$options.i18n.projectSearchPlaceholder" - data-testid="dropdown-search-box" - @input="filterTermChanged" - /> - <gl-dropdown-item - v-for="project in filteredResults" - :key="project.name" - :name="project.name" - :is-checked="isSelected(project)" - is-check-item - data-testid="dropdown-item" - @click="selectProject(project)" - > - {{ project.name }} - </gl-dropdown-item> - <gl-dropdown-text v-if="!filteredResults.length" data-testid="empty-result-message"> - <span class="gl-text-gray-500">{{ $options.i18n.noResultsMessage }}</span> - </gl-dropdown-text> - </gl-dropdown> + <gl-collapsible-listbox + :header-text="$options.i18n.projectHeaderTitle" + :items="listboxItems" + searchable + :search-placeholder="$options.i18n.projectSearchPlaceholder" + :selected="selectedProject.id" + :toggle-text="selectedProject.name" + :no-results-text="$options.i18n.noResultsMessage" + @search="filterTermChanged" + @select="selectProject" + /> </template> diff --git a/app/assets/javascripts/projects/commit/store/getters.js b/app/assets/javascripts/projects/commit/store/getters.js index e0c36df8a75..b039ee3ba63 100644 --- a/app/assets/javascripts/projects/commit/store/getters.js +++ b/app/assets/javascripts/projects/commit/store/getters.js @@ -1,7 +1,7 @@ -import { uniq } from 'lodash'; +import { uniq, uniqBy } from 'lodash'; export const joinedBranches = (state) => { return uniq(state.branches).sort(); }; -export const sortedProjects = (state) => uniq(state.projects).sort(); +export const sortedProjects = (state) => uniqBy(state.projects, 'id').sort(); diff --git a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue index 0256eec6d56..dafc4bc5abf 100644 --- a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue +++ b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue @@ -6,6 +6,7 @@ import { getQueryHeaders, toggleQueryPollingByVisibility, } from '~/pipelines/components/graph/utils'; +import { keepLatestDownstreamPipelines } from '~/pipelines/components/parsing_utils'; import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue'; import { formatStages } from '../utils'; import getLinkedPipelinesQuery from '../graphql/queries/get_linked_pipelines.query.graphql'; @@ -91,7 +92,8 @@ export default { }, computed: { downstreamPipelines() { - return this.pipeline?.downstream?.nodes; + const downstream = this.pipeline?.downstream?.nodes; + return keepLatestDownstreamPipelines(downstream); }, pipelinePath() { return this.pipeline?.path ?? ''; diff --git a/app/assets/javascripts/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql b/app/assets/javascripts/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql index c6a0d48626a..9257cc7de7b 100644 --- a/app/assets/javascripts/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql +++ b/app/assets/javascripts/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql @@ -18,6 +18,10 @@ query getLinkedPipelines($fullPath: ID!, $iid: ID!) { icon label } + sourceJob { + id + retried + } } } upstream { diff --git a/app/assets/javascripts/projects/merge_requests/index.js b/app/assets/javascripts/projects/merge_requests/index.js deleted file mode 100644 index 25a70121d68..00000000000 --- a/app/assets/javascripts/projects/merge_requests/index.js +++ /dev/null @@ -1,18 +0,0 @@ -import Vue from 'vue'; -import ReportAbuseDropdownItem from './components/report_abuse_dropdown_item.vue'; - -export const initReportAbuse = () => { - const el = document.getElementById('js-report-abuse-dropdown-item'); - - if (!el) return false; - - const { reportAbusePath, reportedUserId, reportedFromUrl } = el.dataset; - - return new Vue({ - el, - provide: { reportAbusePath, reportedUserId, reportedFromUrl }, - render(createElement) { - return createElement(ReportAbuseDropdownItem); - }, - }); -}; diff --git a/app/assets/javascripts/projects/project_name_rules.js b/app/assets/javascripts/projects/project_name_rules.js index eeef1fb5afc..4f62aa29ce4 100644 --- a/app/assets/javascripts/projects/project_name_rules.js +++ b/app/assets/javascripts/projects/project_name_rules.js @@ -1,28 +1,29 @@ import { __ } from '~/locale'; -const rulesReg = [ - { - reg: /^[a-zA-Z0-9\u{00A9}-\u{1f9ff}_]/u, - msg: __("Name must start with a letter, digit, emoji, or '_'"), - }, - { - reg: /^[a-zA-Z0-9\p{Pd}\u{002B}\u{00A9}-\u{1f9ff}_. ]+$/u, - msg: __("Name can contain only letters, digits, emojis, '_', '.', '+', dashes, or spaces"), - }, -]; +export const START_RULE = { + reg: /^[a-zA-Z0-9\u{00A9}-\u{1f9ff}_]/u, + msg: __('Name must start with a letter, digit, emoji, or underscore.'), +}; + +export const CONTAINS_RULE = { + reg: /^[a-zA-Z0-9\p{Pd}\u{002B}\u{00A9}-\u{1f9ff}_. ]+$/u, + msg: __( + 'Name can contain only lowercase or uppercase letters, digits, emojis, spaces, dots, underscores, dashes, or pluses.', + ), +}; + +const rulesReg = [START_RULE, CONTAINS_RULE]; /** * * @param {string} text * @returns {string} msg */ -function checkRules(text) { +export const checkRules = (text) => { for (const item of rulesReg) { if (!item.reg.test(text)) { return item.msg; } } return ''; -} - -export { checkRules }; +}; diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index d71e80dffcf..99ea02aaa4f 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -90,13 +90,16 @@ const validateGroupNamespaceDropdown = (e) => { const checkProjectName = (projectNameInput) => { const msg = checkRules(projectNameInput.value); - const projectNameError = document.querySelector('#project_name_error'); + const projectNameError = document.querySelector('#js-project-name-error'); + const projectNameDescription = document.getElementById('js-project-name-description'); if (!projectNameError) return; if (msg) { projectNameError.innerText = msg; - projectNameError.classList.remove('hidden'); + projectNameError.classList.remove('gl-display-none'); + projectNameDescription.classList.add('gl-display-none'); } else { - projectNameError.classList.add('hidden'); + projectNameError.classList.add('gl-display-none'); + projectNameDescription.classList.remove('gl-display-none'); } }; diff --git a/app/assets/javascripts/projects/project_visibility.js b/app/assets/javascripts/projects/project_visibility.js index 84b8936c17f..2dd5f821d90 100644 --- a/app/assets/javascripts/projects/project_visibility.js +++ b/app/assets/javascripts/projects/project_visibility.js @@ -44,21 +44,6 @@ function setVisibilityOptions({ name, visibility, showPath, editPath }) { }); } -function handleSelect2DropdownChange(namespaceSelector) { - if (!namespaceSelector || !('selectedIndex' in namespaceSelector)) { - return; - } - const selectedNamespace = namespaceSelector.options[namespaceSelector.selectedIndex]; - setVisibilityOptions(selectedNamespace.dataset); -} - export default function initProjectVisibilitySelector() { eventHub.$on('update-visibility', setVisibilityOptions); - - const namespaceSelector = document.querySelector('select.js-select-namespace'); - if (namespaceSelector) { - const el = document.querySelector('.select2.js-select-namespace'); - el.addEventListener('change', () => handleSelect2DropdownChange(namespaceSelector)); - handleSelect2DropdownChange(namespaceSelector); - } } diff --git a/app/assets/javascripts/projects/prune_objects_button.js b/app/assets/javascripts/projects/prune_objects_button.js new file mode 100644 index 00000000000..dba73f6a19d --- /dev/null +++ b/app/assets/javascripts/projects/prune_objects_button.js @@ -0,0 +1,23 @@ +import Vue from 'vue'; +import PruneUnreachableObjectsButton from './prune_unreachable_objects_button.vue'; + +export default (selector = '#js-project-prune-unreachable-objects-button') => { + const el = document.querySelector(selector); + + if (!el) return; + + const { pruneObjectsPath, pruneObjectsDocPath } = el.dataset; + + // eslint-disable-next-line no-new + new Vue({ + el, + render(createElement) { + return createElement(PruneUnreachableObjectsButton, { + props: { + pruneObjectsPath, + pruneObjectsDocPath, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/projects/prune_unreachable_objects_button.vue b/app/assets/javascripts/projects/prune_unreachable_objects_button.vue new file mode 100644 index 00000000000..1387fbb78c0 --- /dev/null +++ b/app/assets/javascripts/projects/prune_unreachable_objects_button.vue @@ -0,0 +1,75 @@ +<script> +import { GlButton, GlLink, GlModal, GlModalDirective } from '@gitlab/ui'; +import csrf from '~/lib/utils/csrf'; +import { s__ } from '~/locale'; + +export default { + components: { + GlButton, + GlLink, + GlModal, + }, + PRUNE_UNREACHABLE_OBJECTS_MODAL_ID: 'prune-objects-modal', + MODAL_ACTION_PRIMARY: { + text: s__('UpdateProject|Prune'), + attributes: [{ variant: 'danger' }], + }, + MODAL_ACTION_CANCEL: { + text: s__('UpdateProject|Cancel'), + }, + directives: { + GlModal: GlModalDirective, + }, + props: { + pruneObjectsPath: { + type: String, + required: true, + }, + pruneObjectsDocPath: { + type: String, + required: true, + }, + }, + computed: { + csrfToken() { + return csrf.token; + }, + }, + methods: { + submitForm() { + this.$refs.form.submit(); + }, + }, +}; +</script> + +<template> + <form ref="form" :action="pruneObjectsPath" method="post"> + <input :value="csrfToken" type="hidden" name="authenticity_token" /> + <input value="true" type="hidden" name="prune" /> + <gl-modal + :modal-id="$options.PRUNE_UNREACHABLE_OBJECTS_MODAL_ID" + :title="s__('UpdateProject|Are you sure you want to prune unreachable objects?')" + :action-primary="$options.MODAL_ACTION_PRIMARY" + :action-cancel="$options.MODAL_ACTION_CANCEL" + size="sm" + :no-focus-on-show="true" + @ok="submitForm" + > + <p> + {{ s__('UpdateProject|Pruning unreachable objects can lead to repository corruption.') }} + <gl-link :href="pruneObjectsDocPath" target="_blank"> + {{ s__('UpdateProject|Learn more.') }} + </gl-link> + {{ s__('UpdateProject|Are you sure you want to prune?') }} + </p> + </gl-modal> + <gl-button + v-gl-modal="$options.PRUNE_UNREACHABLE_OBJECTS_MODAL_ID" + category="primary" + variant="danger" + > + {{ s__('UpdateProject|Prune unreachable objects') }} + </gl-button> + </form> +</template> diff --git a/app/assets/javascripts/projects/merge_requests/components/report_abuse_dropdown_item.vue b/app/assets/javascripts/projects/report_abuse/components/report_abuse_dropdown_item.vue index 31890249f41..ff76ca7c862 100644 --- a/app/assets/javascripts/projects/merge_requests/components/report_abuse_dropdown_item.vue +++ b/app/assets/javascripts/projects/report_abuse/components/report_abuse_dropdown_item.vue @@ -12,6 +12,7 @@ export default { MountingPortal, AbuseCategorySelector, }, + inject: ['reportedUserId', 'reportedFromUrl'], i18n: { reportAbuse: s__('ReportAbuse|Report abuse to administrator'), }, @@ -21,21 +22,23 @@ export default { }; }, methods: { - openDrawer() { - this.open = true; - }, - closeDrawer() { - this.open = false; + toggleDrawer(open) { + this.open = open; }, }, }; </script> <template> <span> - <gl-dropdown-item @click="openDrawer">{{ $options.i18n.reportAbuse }}</gl-dropdown-item> + <gl-dropdown-item @click="toggleDrawer(true)">{{ $options.i18n.reportAbuse }}</gl-dropdown-item> <mounting-portal mount-to="#js-report-abuse-drawer" name="abuse-category-selector" append> - <abuse-category-selector :show-drawer="open" @close-drawer="closeDrawer" /> + <abuse-category-selector + :reported-user-id="reportedUserId" + :reported-from-url="reportedFromUrl" + :show-drawer="open" + @close-drawer="toggleDrawer(false)" + /> </mounting-portal> </span> </template> diff --git a/app/assets/javascripts/projects/report_abuse/index.js b/app/assets/javascripts/projects/report_abuse/index.js new file mode 100644 index 00000000000..9bcfdbf6165 --- /dev/null +++ b/app/assets/javascripts/projects/report_abuse/index.js @@ -0,0 +1,25 @@ +import Vue from 'vue'; +import ReportAbuseDropdownItem from './components/report_abuse_dropdown_item.vue'; + +export const initReportAbuse = () => { + const items = document.querySelectorAll('.js-report-abuse-dropdown-item'); + + items.forEach((el) => { + if (!el) return false; + + const { reportAbusePath, reportedUserId, reportedFromUrl } = el.dataset; + + return new Vue({ + el, + name: 'ReportAbuseDropdownItemRoot', + provide: { + reportAbusePath, + reportedUserId: parseInt(reportedUserId, 10), + reportedFromUrl, + }, + render(createElement) { + return createElement(ReportAbuseDropdownItem); + }, + }); + }); +}; diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/edit/protections/push_protections.vue b/app/assets/javascripts/projects/settings/branch_rules/components/edit/protections/push_protections.vue index 541923bb735..95e140f30a9 100644 --- a/app/assets/javascripts/projects/settings/branch_rules/components/edit/protections/push_protections.vue +++ b/app/assets/javascripts/projects/settings/branch_rules/components/edit/protections/push_protections.vue @@ -4,7 +4,7 @@ import { s__ } from '~/locale'; import { helpPagePath } from '~/helpers/help_page_helper'; export const i18n = { - allowedToPush: s__('BranchRules|Allowed to push'), + allowedToPush: s__('BranchRules|Allowed to push and merge'), forcePushTitle: s__( 'BranchRules|Allow all users with push access to %{linkStart}force push%{linkEnd}.', ), diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js b/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js index 61c37a2348a..a98c2439cde 100644 --- a/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js +++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js @@ -1,10 +1,10 @@ import { s__ } from '~/locale'; export const I18N = { - manageProtectionsLinkTitle: s__('BranchRules|Manage in Protected Branches'), - targetBranch: s__('BranchRules|Target Branch'), + manageProtectionsLinkTitle: s__('BranchRules|Manage in protected branches'), + targetBranch: s__('BranchRules|Target branch'), branchNameOrPattern: s__('BranchRules|Branch name or pattern'), - branch: s__('BranchRules|Target Branch'), + branch: s__('BranchRules|Target branch'), allBranches: s__('BranchRules|All branches'), matchingBranchesLinkTitle: s__('BranchRules|%{total} matching %{subject}'), protectBranchTitle: s__('BranchRules|Protect branch'), @@ -20,7 +20,7 @@ export const I18N = { ), disallowForcePushDescription: s__('BranchRules|Force push is not allowed.'), approvalsTitle: s__('BranchRules|Approvals'), - manageApprovalsLinkTitle: s__('BranchRules|Manage in Merge Request Approvals'), + manageApprovalsLinkTitle: s__('BranchRules|Manage in merge request approvals'), approvalsDescription: s__( 'BranchRules|Approvals to ensure separation of duties for new merge requests. %{linkStart}Learn more.%{linkEnd}', ), @@ -28,9 +28,9 @@ export const I18N = { statusChecksDescription: s__( 'BranchRules|Check for a status response in merge requests. Failures do not block merges. %{linkStart}Learn more.%{linkEnd}', ), - statusChecksLinkTitle: s__('BranchRules|Manage in Status checks'), + statusChecksLinkTitle: s__('BranchRules|Manage in status checks'), statusChecksHeader: s__('BranchRules|Status checks (%{total})'), - allowedToPushHeader: s__('BranchRules|Allowed to push (%{total})'), + allowedToPushHeader: s__('BranchRules|Allowed to push and merge (%{total})'), allowedToMergeHeader: s__('BranchRules|Allowed to merge (%{total})'), approvalsHeader: s__('BranchRules|Required approvals (%{total})'), noData: s__('BranchRules|No data to display'), diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue index 6260c8dd4d0..740868e1d75 100644 --- a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue +++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue @@ -3,7 +3,7 @@ import { GlSprintf, GlLink, GlLoadingIcon } from '@gitlab/ui'; import { sprintf, n__ } from '~/locale'; import { getParameterByName, mergeUrlParams } from '~/lib/utils/url_utility'; import { helpPagePath } from '~/helpers/help_page_helper'; -import branchRulesQuery from '../../queries/branch_rules_details.query.graphql'; +import branchRulesQuery from 'ee_else_ce/projects/settings/branch_rules/queries/branch_rules_details.query.graphql'; import { getAccessLevels } from '../../../utils'; import Protection from './protection.vue'; import { @@ -12,22 +12,16 @@ import { BRANCH_PARAM_NAME, WILDCARDS_HELP_PATH, PROTECTED_BRANCHES_HELP_PATH, - APPROVALS_HELP_PATH, - STATUS_CHECKS_HELP_PATH, } from './constants'; const wildcardsHelpDocLink = helpPagePath(WILDCARDS_HELP_PATH); const protectedBranchesHelpDocLink = helpPagePath(PROTECTED_BRANCHES_HELP_PATH); -const approvalsHelpDocLink = helpPagePath(APPROVALS_HELP_PATH); -const statusChecksHelpDocLink = helpPagePath(STATUS_CHECKS_HELP_PATH); export default { name: 'RuleView', i18n: I18N, wildcardsHelpDocLink, protectedBranchesHelpDocLink, - approvalsHelpDocLink, - statusChecksHelpDocLink, components: { Protection, GlSprintf, GlLink, GlLoadingIcon }, inject: { projectPath: { @@ -36,12 +30,6 @@ export default { protectedBranchesPath: { default: '', }, - approvalRulesPath: { - default: '', - }, - statusChecksPath: { - default: '', - }, branchesPath: { default: '', }, @@ -58,7 +46,7 @@ export default { const branchRule = branchRules.nodes.find((rule) => rule.name === this.branch); this.branchRule = branchRule; this.branchProtection = branchRule?.branchProtection; - this.approvalRules = branchRule?.approvalRules; + this.approvalRules = branchRule?.approvalRules?.nodes || []; this.statusChecks = branchRule?.externalStatusChecks?.nodes || []; this.matchingBranchesCount = branchRule?.matchingBranchesCount; }, @@ -98,20 +86,6 @@ export default { total: this.pushAccessLevels?.total || 0, }); }, - approvalsHeader() { - const total = this.approvals.reduce( - (sum, { approvalsRequired }) => sum + approvalsRequired, - 0, - ); - return sprintf(this.$options.i18n.approvalsHeader, { - total, - }); - }, - statusChecksHeader() { - return sprintf(this.$options.i18n.statusChecksHeader, { - total: this.statusChecks.length, - }); - }, allBranches() { return this.branch === ALL_BRANCHES_WILDCARD; }, @@ -131,8 +105,13 @@ export default { const subject = n__('branch', 'branches', total); return sprintf(this.$options.i18n.matchingBranchesLinkTitle, { total, subject }); }, - approvals() { - return this.approvalRules?.nodes || []; + // needed to override EE component + statusChecksHeader() { + return ''; + }, + // needed to override EE component + approvalsHeader() { + return ''; }, }, methods: { @@ -199,40 +178,46 @@ export default { :groups="mergeAccessLevels.groups" /> + <!-- EE start --> <!-- Approvals --> - <h4 class="gl-mb-1 gl-mt-5">{{ $options.i18n.approvalsTitle }}</h4> - <gl-sprintf :message="$options.i18n.approvalsDescription"> - <template #link="{ content }"> - <gl-link :href="$options.approvalsHelpDocLink"> - {{ content }} - </gl-link> - </template> - </gl-sprintf> + <template v-if="approvalsHeader"> + <h4 class="gl-mb-1 gl-mt-5">{{ $options.i18n.approvalsTitle }}</h4> + <gl-sprintf :message="$options.i18n.approvalsDescription"> + <template #link="{ content }"> + <gl-link :href="$options.approvalsHelpDocLink"> + {{ content }} + </gl-link> + </template> + </gl-sprintf> - <protection - class="gl-mt-3" - :header="approvalsHeader" - :header-link-title="$options.i18n.manageApprovalsLinkTitle" - :header-link-href="approvalRulesPath" - :approvals="approvals" - /> + <protection + class="gl-mt-3" + :header="approvalsHeader" + :header-link-title="$options.i18n.manageApprovalsLinkTitle" + :header-link-href="approvalRulesPath" + :approvals="approvalRules" + /> + </template> <!-- Status checks --> - <h4 class="gl-mb-1 gl-mt-5">{{ $options.i18n.statusChecksTitle }}</h4> - <gl-sprintf :message="$options.i18n.statusChecksDescription"> - <template #link="{ content }"> - <gl-link :href="$options.statusChecksHelpDocLink"> - {{ content }} - </gl-link> - </template> - </gl-sprintf> + <template v-if="statusChecksHeader"> + <h4 class="gl-mb-1 gl-mt-5">{{ $options.i18n.statusChecksTitle }}</h4> + <gl-sprintf :message="$options.i18n.statusChecksDescription"> + <template #link="{ content }"> + <gl-link :href="$options.statusChecksHelpDocLink"> + {{ content }} + </gl-link> + </template> + </gl-sprintf> - <protection - class="gl-mt-3" - :header="statusChecksHeader" - :header-link-title="$options.i18n.statusChecksLinkTitle" - :header-link-href="statusChecksPath" - :status-checks="statusChecks" - /> + <protection + class="gl-mt-3" + :header="statusChecksHeader" + :header-link-title="$options.i18n.statusChecksLinkTitle" + :header-link-href="statusChecksPath" + :status-checks="statusChecks" + /> + </template> + <!-- EE end --> </div> </template> diff --git a/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js b/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js index 7639acc1181..081d6cec958 100644 --- a/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js +++ b/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createDefaultClient from '~/lib/graphql'; -import View from './components/view/index.vue'; +import View from 'ee_else_ce/projects/settings/branch_rules/components/view/index.vue'; export default function mountBranchRules(el) { if (!el) { diff --git a/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql b/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql index a832e59aa67..aa736469749 100644 --- a/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql +++ b/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql @@ -4,24 +4,14 @@ query getBranchRulesDetails($projectPath: ID!) { branchRules { nodes { name + matchingBranchesCount branchProtection { allowForcePush - codeOwnerApprovalRequired mergeAccessLevels { edges { node { accessLevel accessLevelDescription - group { - id - avatarUrl - } - user { - id - name - avatarUrl - webUrl - } } } } @@ -30,45 +20,10 @@ query getBranchRulesDetails($projectPath: ID!) { node { accessLevel accessLevelDescription - group { - id - avatarUrl - } - user { - id - name - avatarUrl - webUrl - } - } - } - } - } - approvalRules { - nodes { - id - name - type - approvalsRequired - eligibleApprovers { - nodes { - id - name - username - webUrl - avatarUrl } } } } - externalStatusChecks { - nodes { - id - name - externalUrl - } - } - matchingBranchesCount } } } diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue index 9b669024a8b..f3d392a0ec4 100644 --- a/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue +++ b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue @@ -1,23 +1,22 @@ <script> -import { s__ } from '~/locale'; +import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui'; import { createAlert } from '~/flash'; import branchRulesQuery from 'ee_else_ce/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql'; +import { expandSection } from '~/settings_panels'; +import { scrollToElement } from '~/lib/utils/common_utils'; import BranchRule from './components/branch_rule.vue'; - -export const i18n = { - queryError: s__( - 'ProtectedBranch|An error occurred while loading branch rules. Please try again.', - ), - emptyState: s__( - 'ProtectedBranch|Protected branches, merge request approvals, and status checks will appear here once configured.', - ), -}; +import { I18N, PROTECTED_BRANCHES_ANCHOR, BRANCH_PROTECTION_MODAL_ID } from './constants'; export default { name: 'BranchRules', - i18n, + i18n: I18N, components: { BranchRule, + GlButton, + GlModal, + }, + directives: { + GlModal: GlModalDirective, }, apollo: { branchRules: { @@ -36,20 +35,27 @@ export default { }, }, inject: { - projectPath: { - default: '', - }, + projectPath: { default: '' }, }, data() { return { branchRules: [], }; }, + methods: { + showProtectedBranches() { + // Protected branches section is on the same page as the branch rules section. + expandSection(this.$options.protectedBranchesAnchor); + scrollToElement(this.$options.protectedBranchesAnchor); + }, + }, + modalId: BRANCH_PROTECTION_MODAL_ID, + protectedBranchesAnchor: PROTECTED_BRANCHES_ANCHOR, }; </script> <template> - <div class="settings-content"> + <div class="settings-content gl-mb-0"> <branch-rule v-for="(rule, index) in branchRules" :key="`${rule.name}-${index}`" @@ -61,6 +67,21 @@ export default { :matching-branches-count="rule.matchingBranchesCount" /> - <span v-if="!branchRules.length" data-testid="empty">{{ $options.i18n.emptyState }}</span> + <div v-if="!branchRules.length" data-testid="empty">{{ $options.i18n.emptyState }}</div> + + <gl-button v-gl-modal="$options.modalId" class="gl-mt-5" category="secondary" variant="info">{{ + $options.i18n.addBranchRule + }}</gl-button> + + <gl-modal + :ref="$options.modalId" + :modal-id="$options.modalId" + :title="$options.i18n.addBranchRule" + :ok-title="$options.i18n.createProtectedBranch" + @ok="showProtectedBranches" + > + <p>{{ $options.i18n.branchRuleModalDescription }}</p> + <p>{{ $options.i18n.branchRuleModalContent }}</p> + </gl-modal> </div> </template> diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue index 4a24df4b0dc..fa96eee5f92 100644 --- a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue +++ b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue @@ -13,7 +13,7 @@ export const i18n = { approvalRules: s__('BranchRules|%{total} approval %{subject}'), matchingBranches: s__('BranchRules|%{total} matching %{subject}'), pushAccessLevels: s__('BranchRules|Allowed to merge'), - mergeAccessLevels: s__('BranchRules|Allowed to push'), + mergeAccessLevels: s__('BranchRules|Allowed to push and merge'), }; export default { @@ -106,7 +106,7 @@ export default { }, approvalDetails() { const approvalDetails = []; - if (this.isWildcard) { + if (this.isWildcard || this.matchingBranchesCount > 1) { approvalDetails.push(this.matchingBranchesText); } if (this.branchProtection?.allowForcePush) { diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/constants.js b/app/assets/javascripts/projects/settings/repository/branch_rules/constants.js new file mode 100644 index 00000000000..4413d8eab4e --- /dev/null +++ b/app/assets/javascripts/projects/settings/repository/branch_rules/constants.js @@ -0,0 +1,22 @@ +import { s__ } from '~/locale'; + +export const I18N = { + queryError: s__( + 'ProtectedBranch|An error occurred while loading branch rules. Please try again.', + ), + emptyState: s__( + 'ProtectedBranch|After you configure a protected branch, merge request approval, or status check, it appears here.', + ), + addBranchRule: s__('BranchRules|Add branch rule'), + branchRuleModalDescription: s__( + 'BranchRules|To create a branch rule, you first need to create a protected branch.', + ), + branchRuleModalContent: s__( + 'BranchRules|After a protected branch is created, it will show up in the list as a branch rule.', + ), + createProtectedBranch: s__('BranchRules|Create protected branch'), +}; + +export const PROTECTED_BRANCHES_ANCHOR = '#js-protected-branches-settings'; + +export const BRANCH_PROTECTION_MODAL_ID = 'addBranchRuleModal'; diff --git a/app/assets/javascripts/projects/settings/utils.js b/app/assets/javascripts/projects/settings/utils.js index 7bcfde39178..ea4574119c0 100644 --- a/app/assets/javascripts/projects/settings/utils.js +++ b/app/assets/javascripts/projects/settings/utils.js @@ -9,7 +9,7 @@ export const getAccessLevels = (accessLevels = {}) => { } else if (node.group) { accessLevelTypes.groups.push(node); } else { - accessLevelTypes.roles.push(node); + accessLevelTypes.roles.push({ accessLevelDescription: node.accessLevelDescription }); } }); |