diff options
Diffstat (limited to 'app/assets/javascripts/projects')
17 files changed, 389 insertions, 79 deletions
diff --git a/app/assets/javascripts/projects/clusters_deprecation_alert/components/clusters_deprecation_alert.vue b/app/assets/javascripts/projects/clusters_deprecation_alert/components/clusters_deprecation_alert.vue new file mode 100644 index 00000000000..e026b3e1060 --- /dev/null +++ b/app/assets/javascripts/projects/clusters_deprecation_alert/components/clusters_deprecation_alert.vue @@ -0,0 +1,23 @@ +<script> +import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui'; +import { helpPagePath } from '~/helpers/help_page_helper'; + +export default { + components: { + GlAlert, + GlSprintf, + GlLink, + }, + inject: ['message'], + docsLink: helpPagePath('user/infrastructure/clusters/migrate_to_gitlab_agent.md'), +}; +</script> +<template> + <gl-alert :dismissible="false" variant="warning" class="gl-mt-5"> + <gl-sprintf :message="message"> + <template #link="{ content }"> + <gl-link :href="$options.docsLink">{{ content }}</gl-link> + </template> + </gl-sprintf> + </gl-alert> +</template> diff --git a/app/assets/javascripts/projects/clusters_deprecation_alert/index.js b/app/assets/javascripts/projects/clusters_deprecation_alert/index.js new file mode 100644 index 00000000000..e17c1900dc1 --- /dev/null +++ b/app/assets/javascripts/projects/clusters_deprecation_alert/index.js @@ -0,0 +1,21 @@ +import Vue from 'vue'; +import ClustersDeprecationAlert from './components/clusters_deprecation_alert.vue'; + +export default () => { + const el = document.querySelector('.js-clusters-deprecation-alert'); + + if (!el) { + return false; + } + + const { message } = el.dataset; + + return new Vue({ + el, + name: 'ClustersDeprecationAlertRoot', + provide: { + message, + }, + render: (createElement) => createElement(ClustersDeprecationAlert), + }); +}; diff --git a/app/assets/javascripts/projects/commit/components/form_modal.vue b/app/assets/javascripts/projects/commit/components/form_modal.vue index f9dd72119d1..d9aaa574fec 100644 --- a/app/assets/javascripts/projects/commit/components/form_modal.vue +++ b/app/assets/javascripts/projects/commit/components/form_modal.vue @@ -53,7 +53,7 @@ export default { actionPrimary: { text: this.i18n.actionPrimaryText, attributes: [ - { variant: 'success' }, + { variant: 'confirm' }, { category: 'primary' }, { 'data-testid': 'submit-commit' }, { 'data-qa-selector': 'submit_commit_button' }, diff --git a/app/assets/javascripts/projects/commits/components/author_select.vue b/app/assets/javascripts/projects/commits/components/author_select.vue index c8a0a3417f3..884ef732144 100644 --- a/app/assets/javascripts/projects/commits/components/author_select.vue +++ b/app/assets/javascripts/projects/commits/components/author_select.vue @@ -57,7 +57,7 @@ export default { if (authorParam) { commitsSearchInput.setAttribute('disabled', true); - commitsSearchInput.setAttribute('data-toggle', 'tooltip'); + commitsSearchInput.dataset.toggle = 'tooltip'; commitsSearchInput.setAttribute('title', tooltipMessage); this.currentAuthor = authorParam; } diff --git a/app/assets/javascripts/projects/compare/components/app.vue b/app/assets/javascripts/projects/compare/components/app.vue index f2c1c843878..3945bed9649 100644 --- a/app/assets/javascripts/projects/compare/components/app.vue +++ b/app/assets/javascripts/projects/compare/components/app.vue @@ -104,7 +104,7 @@ export default { @selectRevision="onSelectRevision" /> <div - class="compare-ellipsis gl-display-flex gl-justify-content-center gl-align-items-center gl-my-4 gl-md-my-0" + class="compare-ellipsis gl-display-flex gl-justify-content-center gl-align-items-center gl-align-self-end gl-my-4 gl-md-my-0" data-testid="ellipsis" > ... @@ -121,7 +121,7 @@ export default { @selectRevision="onSelectRevision" /> </div> - <div class="gl-mt-4"> + <div class="gl-mt-6"> <gl-button category="primary" variant="confirm" @click="onSubmit"> {{ s__('CompareRevisions|Compare') }} </gl-button> diff --git a/app/assets/javascripts/projects/compare/components/revision_card.vue b/app/assets/javascripts/projects/compare/components/revision_card.vue index 02a329221cc..d6ada24604d 100644 --- a/app/assets/javascripts/projects/compare/components/revision_card.vue +++ b/app/assets/javascripts/projects/compare/components/revision_card.vue @@ -1,5 +1,4 @@ <script> -import { GlCard } from '@gitlab/ui'; import RepoDropdown from './repo_dropdown.vue'; import RevisionDropdown from './revision_dropdown.vue'; @@ -7,7 +6,6 @@ export default { components: { RepoDropdown, RevisionDropdown, - GlCard, }, props: { refsProjectPath: { @@ -41,10 +39,10 @@ export default { </script> <template> - <gl-card header-class="gl-py-2 gl-px-3 gl-font-weight-bold" body-class="gl-px-3"> - <template #header> + <div class="revision-card gl-flex-basis-half"> + <h2 class="gl-font-size-h2"> {{ s__(`CompareRevisions|${revisionText}`) }} - </template> + </h2> <div class="gl-sm-display-flex gl-align-items-center"> <repo-dropdown class="gl-sm-w-half" @@ -61,5 +59,5 @@ export default { v-on="$listeners" /> </div> - </gl-card> + </div> </template> diff --git a/app/assets/javascripts/projects/default_project_templates.js b/app/assets/javascripts/projects/default_project_templates.js index 6708b7bd9e2..3671b24b502 100644 --- a/app/assets/javascripts/projects/default_project_templates.js +++ b/app/assets/javascripts/projects/default_project_templates.js @@ -41,6 +41,10 @@ export default { text: s__('ProjectTemplates|Pages/Hugo'), icon: '.template-option .icon-hugo', }, + pelican: { + text: s__('ProjectTemplates|Pages/Pelican'), + icon: '.template-option .icon-pelican', + }, jekyll: { text: s__('ProjectTemplates|Pages/Jekyll'), icon: '.template-option .icon-jekyll', @@ -105,4 +109,8 @@ export default { text: s__('ProjectTemplates|Kotlin Native for Linux'), icon: '.template-option .icon-gitlab_logo', }, + jsonnet: { + text: s__('ProjectTemplates|Jsonnet for Dynamic Child Pipelines'), + icon: '.template-option .icon-gitlab_logo', + }, }; diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app.vue b/app/assets/javascripts/projects/pipelines/charts/components/app.vue index d4b1f7e57d8..35e7554aee2 100644 --- a/app/assets/javascripts/projects/pipelines/charts/components/app.vue +++ b/app/assets/javascripts/projects/pipelines/charts/components/app.vue @@ -12,11 +12,14 @@ export default { DeploymentFrequencyCharts: () => import('ee_component/dora/components/deployment_frequency_charts.vue'), LeadTimeCharts: () => import('ee_component/dora/components/lead_time_charts.vue'), + TimeToRestoreServiceCharts: () => + import('ee_component/dora/components/time_to_restore_service_charts.vue'), ProjectQualitySummary: () => import('ee_component/project_quality_summary/app.vue'), }, piplelinesTabEvent: 'p_analytics_ci_cd_pipelines', deploymentFrequencyTabEvent: 'p_analytics_ci_cd_deployment_frequency', leadTimeTabEvent: 'p_analytics_ci_cd_lead_time', + timeToRestoreServiceTabEvent: 'p_analytics_ci_cd_time_to_restore_service', inject: { shouldRenderDoraCharts: { type: Boolean, @@ -37,7 +40,7 @@ export default { const chartsToShow = ['pipelines']; if (this.shouldRenderDoraCharts) { - chartsToShow.push('deployment-frequency', 'lead-time'); + chartsToShow.push('deployment-frequency', 'lead-time', 'time-to-restore-service'); } if (this.shouldRenderQualitySummary) { @@ -95,6 +98,13 @@ export default { > <lead-time-charts /> </gl-tab> + <gl-tab + :title="s__('DORA4Metrics|Time to restore service')" + data-testid="time-to-restore-service-tab" + @click="trackTabClick($options.timeToRestoreServiceTabEvent)" + > + <time-to-restore-service-charts /> + </gl-tab> </template> <gl-tab v-if="shouldRenderQualitySummary" :title="s__('QualitySummary|Project quality')"> <project-quality-summary /> diff --git a/app/assets/javascripts/projects/project_import_gitlab_project.js b/app/assets/javascripts/projects/project_import_gitlab_project.js index 4f222438500..0cbd4dbf2cf 100644 --- a/app/assets/javascripts/projects/project_import_gitlab_project.js +++ b/app/assets/javascripts/projects/project_import_gitlab_project.js @@ -1,4 +1,3 @@ -import $ from 'jquery'; import { convertToTitleCase, humanize, slugify } from '../lib/utils/text_utility'; import { getParameterValues } from '../lib/utils/url_utility'; import projectNew from './project_new'; @@ -22,24 +21,24 @@ const prepareParameters = () => { export default () => { let hasUserDefinedProjectName = false; - const $projectName = $('.js-project-name'); - const $projectPath = $('.js-path-name'); + const $projectName = document.querySelector('.js-project-name'); + const $projectPath = document.querySelector('.js-path-name'); const { name, path } = prepareParameters(); // get the project name from the URL and set it as input value - $projectName.val(name); + $projectName.value = name; // get the path url and append it in the input - $projectPath.val(path); + $projectPath.value = path; // generate slug when project name changes - $projectName.on('keyup', () => { + $projectName.addEventListener('keyup', () => { projectNew.onProjectNameChange($projectName, $projectPath); - hasUserDefinedProjectName = $projectName.val().trim().length > 0; + hasUserDefinedProjectName = $projectName.value.trim().length > 0; }); // generate project name from the slug if one isn't set - $projectPath.on('keyup', () => + $projectPath.addEventListener('keyup', () => projectNew.onProjectPathChange($projectName, $projectPath, hasUserDefinedProjectName), ); }; diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index 2bf13941f6f..2c2f957a75d 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -39,12 +39,18 @@ const validateImportCredentials = (url, user, password) => { return importCredentialsValidationPromise; }; -const onProjectNameChange = ($projectNameInput, $projectPathInput) => { +const onProjectNameChangeJq = ($projectNameInput, $projectPathInput) => { const slug = slugify(convertUnicodeToAscii($projectNameInput.val())); $projectPathInput.val(slug); }; -const onProjectPathChange = ($projectNameInput, $projectPathInput, hasExistingProjectName) => { +const onProjectNameChange = ($projectNameInput, $projectPathInput) => { + const slug = slugify(convertUnicodeToAscii($projectNameInput.value)); + // eslint-disable-next-line no-param-reassign + $projectPathInput.value = slug; +}; + +const onProjectPathChangeJq = ($projectNameInput, $projectPathInput, hasExistingProjectName) => { const slug = $projectPathInput.val(); if (!hasExistingProjectName) { @@ -52,6 +58,15 @@ const onProjectPathChange = ($projectNameInput, $projectPathInput, hasExistingPr } }; +const onProjectPathChange = ($projectNameInput, $projectPathInput, hasExistingProjectName) => { + const slug = $projectPathInput.value; + + if (!hasExistingProjectName) { + // eslint-disable-next-line no-param-reassign + $projectNameInput.value = convertToTitleCase(humanize(slug, '[-_]')); + } +}; + const selectedNamespaceId = () => document.querySelector('[name="project[selected_namespace_id]"]'); const dropdownButton = () => document.querySelector('.js-group-namespace-dropdown > button'); const namespaceButton = () => document.querySelector('.js-group-namespace-button'); @@ -73,24 +88,31 @@ const validateGroupNamespaceDropdown = (e) => { const setProjectNamePathHandlers = ($projectNameInput, $projectPathInput) => { const specialRepo = document.querySelector('.js-user-readme-repo'); - - // eslint-disable-next-line @gitlab/no-global-event-off - $projectNameInput.off('keyup change').on('keyup change', () => { + const projectNameInputListener = () => { onProjectNameChange($projectNameInput, $projectPathInput); - hasUserDefinedProjectName = $projectNameInput.val().trim().length > 0; - hasUserDefinedProjectPath = $projectPathInput.val().trim().length > 0; - }); + hasUserDefinedProjectName = $projectNameInput.value.trim().length > 0; + hasUserDefinedProjectPath = $projectPathInput.value.trim().length > 0; + }; + + $projectNameInput.removeEventListener('keyup', projectNameInputListener); + $projectNameInput.addEventListener('keyup', projectNameInputListener); + $projectNameInput.removeEventListener('change', projectNameInputListener); + $projectNameInput.addEventListener('change', projectNameInputListener); - // eslint-disable-next-line @gitlab/no-global-event-off - $projectPathInput.off('keyup change').on('keyup change', () => { + const projectPathInputListener = () => { onProjectPathChange($projectNameInput, $projectPathInput, hasUserDefinedProjectName); - hasUserDefinedProjectPath = $projectPathInput.val().trim().length > 0; + hasUserDefinedProjectPath = $projectPathInput.value.trim().length > 0; specialRepo.classList.toggle( 'gl-display-none', - $projectPathInput.val() !== $projectPathInput.data('username'), + $projectPathInput.value !== $projectPathInput.dataset.username, ); - }); + }; + + $projectPathInput.removeEventListener('keyup', projectPathInputListener); + $projectPathInput.addEventListener('keyup', projectPathInputListener); + $projectPathInput.removeEventListener('change', projectPathInputListener); + $projectPathInput.addEventListener('change', projectPathInputListener); document.querySelector('.js-create-project-button').addEventListener('click', (e) => { validateGroupNamespaceDropdown(e); @@ -99,17 +121,17 @@ const setProjectNamePathHandlers = ($projectNameInput, $projectPathInput) => { const deriveProjectPathFromUrl = ($projectImportUrl) => { const $currentProjectName = $projectImportUrl - .parents('.toggle-import-form') - .find('#project_name'); + .closest('.toggle-import-form') + .querySelector('#project_name'); const $currentProjectPath = $projectImportUrl - .parents('.toggle-import-form') - .find('#project_path'); + .closest('.toggle-import-form') + .querySelector('#project_path'); if (hasUserDefinedProjectPath || $currentProjectPath.length === 0) { return; } - let importUrl = $projectImportUrl.val().trim(); + let importUrl = $projectImportUrl.value.trim(); if (importUrl.length === 0) { return; } @@ -125,7 +147,9 @@ const deriveProjectPathFromUrl = ($projectImportUrl) => { // extract everything after the last slash const pathMatch = /\/([^/]+)$/.exec(importUrl); if (pathMatch) { - $currentProjectPath.val(pathMatch[1]); + // eslint-disable-next-line no-unused-vars + const [_, matchingString] = pathMatch; + $currentProjectPath.value = matchingString; onProjectPathChange($currentProjectName, $currentProjectPath, false); } }; @@ -149,19 +173,20 @@ const bindHowToImport = () => { const bindEvents = () => { const $newProjectForm = $('#new_project'); - const $projectImportUrl = $('#project_import_url'); const $projectImportUrlUser = $('#project_import_url_user'); const $projectImportUrlPassword = $('#project_import_url_password'); const $projectImportUrlError = $('.js-import-url-error'); const $projectImportForm = $('form.js-project-import'); - const $projectPath = $('.tab-pane.active #project_path'); const $useTemplateBtn = $('.template-button > input'); - const $projectFieldsForm = $('.project-fields-form'); - const $selectedTemplateText = $('.selected-template'); const $changeTemplateBtn = $('.change-template'); - const $selectedIcon = $('.selected-icon'); - const $projectTemplateButtons = $('.project-templates-buttons'); - const $projectName = $('.tab-pane.active #project_name'); + + const $projectImportUrl = document.querySelector('#project_import_url'); + const $projectPath = document.querySelector('.tab-pane.active #project_path'); + const $projectFieldsForm = document.querySelector('.project-fields-form'); + const $selectedIcon = document.querySelector('.selected-icon'); + const $selectedTemplateText = document.querySelector('.selected-template'); + const $projectName = document.querySelector('.tab-pane.active #project_name'); + const $projectTemplateButtons = document.querySelectorAll('.project-templates-buttons'); if ($newProjectForm.length !== 1 && $projectImportForm.length !== 1) { return; @@ -170,31 +195,38 @@ const bindEvents = () => { bindHowToImport(); $('.btn_import_gitlab_project').on('click contextmenu', () => { - const importHref = $('a.btn_import_gitlab_project').attr('data-href'); - $('.btn_import_gitlab_project').attr( - 'href', - `${importHref}?namespace_id=${$( - '#project_namespace_id', - ).val()}&name=${$projectName.val()}&path=${$projectPath.val()}`, - ); + const importGitlabProjectBtn = document.querySelector('.btn_import_gitlab_project'); + const projectNamespaceId = document.querySelector('#project_namespace_id'); + + const { href: importHref } = importGitlabProjectBtn.dataset; + const newHref = `${importHref}?namespace_id=${projectNamespaceId.value}&name=${$projectName.value}&path=${$projectPath.value}`; + importGitlabProjectBtn.setAttribute('href', newHref); }); + const clearChildren = (el) => { + while (el.firstChild) el.removeChild(el.firstChild); + }; + function chooseTemplate() { - $projectTemplateButtons.addClass('hidden'); - $projectFieldsForm.addClass('selected'); - $selectedIcon.empty(); + $projectTemplateButtons.forEach((ptb) => ptb.classList.add('hidden')); + $projectFieldsForm.classList.add('selected'); - const $selectedTemplate = $(this); - $selectedTemplate.prop('checked', true); + clearChildren($selectedIcon); - const value = $selectedTemplate.val(); + const $selectedTemplate = this; + $selectedTemplate.checked = true; + const { value } = $selectedTemplate; const selectedTemplate = DEFAULT_PROJECT_TEMPLATES[value]; - $selectedTemplateText.text(selectedTemplate.text); - $(selectedTemplate.icon).clone().addClass('d-block').appendTo($selectedIcon); + $selectedTemplateText.textContent = selectedTemplate.text; + const clone = document.querySelector(selectedTemplate.icon).cloneNode(true); + clone.classList.add('d-block'); + + $selectedIcon.append(clone); + + const $activeTabProjectName = document.querySelector('.tab-pane.active #project_name'); + const $activeTabProjectPath = document.querySelector('.tab-pane.active #project_path'); - const $activeTabProjectName = $('.tab-pane.active #project_name'); - const $activeTabProjectPath = $('.tab-pane.active #project_path'); $activeTabProjectName.focus(); setProjectNamePathHandlers($activeTabProjectName, $activeTabProjectPath); } @@ -216,8 +248,8 @@ const bindEvents = () => { $useTemplateBtn.on('keypress', chooseTemplateOnEnter); $changeTemplateBtn.on('click', () => { - $projectTemplateButtons.removeClass('hidden'); - $projectFieldsForm.removeClass('selected'); + $projectTemplateButtons.forEach((ptb) => ptb.classList.remove('hidden')); + $projectFieldsForm.classList.remove('selected'); $useTemplateBtn.prop('checked', false); }); @@ -227,7 +259,7 @@ const bindEvents = () => { const updateUrlPathWarningVisibility = async () => { const { success: isUrlValid, cancelled } = await validateImportCredentials( - $projectImportUrl.val(), + $projectImportUrl.value, $projectImportUrlUser.val(), $projectImportUrlPassword.val(), ); @@ -235,7 +267,7 @@ const bindEvents = () => { return; } - $projectImportUrl.toggleClass(invalidInputClass, !isUrlValid); + $projectImportUrl.classList.toggle(invalidInputClass, !isUrlValid); $projectImportUrlError.toggleClass('hide', isUrlValid); }; const debouncedUpdateUrlPathWarningVisibility = debounce( @@ -244,20 +276,28 @@ const bindEvents = () => { ); let isProjectImportUrlDirty = false; - $projectImportUrl.on('blur', () => { + $projectImportUrl.addEventListener('blur', () => { isProjectImportUrlDirty = true; debouncedUpdateUrlPathWarningVisibility(); }); - $projectImportUrl.on('keyup', () => { + $projectImportUrl.addEventListener('keyup', () => { deriveProjectPathFromUrl($projectImportUrl); }); [$projectImportUrl, $projectImportUrlUser, $projectImportUrlPassword].forEach(($f) => { - $f.on('input', () => { - if (isProjectImportUrlDirty) { - debouncedUpdateUrlPathWarningVisibility(); - } - }); + if ($f?.on) { + $f.on('input', () => { + if (isProjectImportUrlDirty) { + debouncedUpdateUrlPathWarningVisibility(); + } + }); + } else { + $f.addEventListener('input', () => { + if (isProjectImportUrlDirty) { + debouncedUpdateUrlPathWarningVisibility(); + } + }); + } }); $projectImportForm.on('submit', async (e) => { @@ -287,8 +327,8 @@ const bindEvents = () => { $('.js-import-git-toggle-button').on('click', () => { setProjectNamePathHandlers( - $('.tab-pane.active #project_name'), - $('.tab-pane.active #project_path'), + document.querySelector('.tab-pane.active #project_name'), + document.querySelector('.tab-pane.active #project_path'), ); }); @@ -300,6 +340,8 @@ export default { deriveProjectPathFromUrl, onProjectNameChange, onProjectPathChange, + onProjectNameChangeJq, + onProjectPathChangeJq, }; export { bindHowToImport }; diff --git a/app/assets/javascripts/projects/project_visibility.js b/app/assets/javascripts/projects/project_visibility.js index c962554c9f4..d299e106b14 100644 --- a/app/assets/javascripts/projects/project_visibility.js +++ b/app/assets/javascripts/projects/project_visibility.js @@ -1,4 +1,3 @@ -import $ from 'jquery'; import { escape } from 'lodash'; import { __, sprintf } from '~/locale'; import eventHub from '~/projects/new/event_hub'; @@ -63,9 +62,8 @@ export default function initProjectVisibilitySelector() { const namespaceSelector = document.querySelector('select.js-select-namespace'); if (namespaceSelector) { - $('.select2.js-select-namespace').on('change', () => - handleSelect2DropdownChange(namespaceSelector), - ); + const el = document.querySelector('.select2.js-select-namespace'); + el.addEventListener('change', () => handleSelect2DropdownChange(namespaceSelector)); handleSelect2DropdownChange(namespaceSelector); } } diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/branch_dropdown.vue b/app/assets/javascripts/projects/settings/branch_rules/components/branch_dropdown.vue new file mode 100644 index 00000000000..6bbe0ab7d5f --- /dev/null +++ b/app/assets/javascripts/projects/settings/branch_rules/components/branch_dropdown.vue @@ -0,0 +1,110 @@ +<script> +import { GlDropdown, GlDropdownItem, GlDropdownDivider, GlSearchBoxByType } from '@gitlab/ui'; +import { createAlert } from '~/flash'; +import { __, sprintf } from '~/locale'; +import branchesQuery from '../queries/branches.query.graphql'; + +export const i18n = { + fetchBranchesError: __('An error occurred while fetching branches.'), + noMatch: __('No matching results'), +}; + +export default { + i18n, + name: 'BranchDropdown', + components: { + GlDropdown, + GlDropdownItem, + GlDropdownDivider, + GlSearchBoxByType, + }, + apollo: { + branchNames: { + query: branchesQuery, + variables() { + return { + projectPath: this.projectPath, + searchPattern: `*${this.searchTerm}*`, + }; + }, + update({ project: { repository = {} } } = {}) { + return repository.branchNames || []; + }, + error(e) { + createAlert({ + message: this.$options.i18n.fetchBranchesError, + captureError: true, + error: e, + }); + }, + }, + }, + props: { + projectPath: { + type: String, + required: true, + }, + value: { + type: String, + required: false, + default: null, + }, + }, + data() { + return { + searchTerm: '', + branchNames: [], + }; + }, + computed: { + createButtonLabel() { + return sprintf(__('Create wildcard: %{searchTerm}'), { searchTerm: this.searchTerm }); + }, + shouldRenderCreateButton() { + return this.searchTerm && !this.branchNames.includes(this.searchTerm); + }, + isLoading() { + return this.$apollo.queries.branchNames.loading; + }, + }, + methods: { + selectBranch(selected) { + this.$emit('input', selected); + }, + createWildcard() { + this.$emit('createWildcard', this.searchTerm); + }, + isSelected(branch) { + return this.value === branch; + }, + }, +}; +</script> +<template> + <gl-dropdown :text="value || branchNames[0]"> + <gl-search-box-by-type + v-model.trim="searchTerm" + data-testid="branch-search" + debounce="250" + :is-loading="isLoading" + /> + <gl-dropdown-item + v-for="branch in branchNames" + :key="branch" + :is-checked="isSelected(branch)" + is-check-item + @click="selectBranch(branch)" + > + {{ branch }} + </gl-dropdown-item> + <gl-dropdown-item v-if="!branchNames.length && !isLoading" data-testid="no-data">{{ + $options.i18n.noMatch + }}</gl-dropdown-item> + <template v-if="shouldRenderCreateButton"> + <gl-dropdown-divider /> + <gl-dropdown-item data-testid="create-wildcard-button" @click="createWildcard"> + {{ createButtonLabel }} + </gl-dropdown-item> + </template> + </gl-dropdown> +</template> diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/rule_edit.vue b/app/assets/javascripts/projects/settings/branch_rules/components/rule_edit.vue new file mode 100644 index 00000000000..c2e7f4e9b1b --- /dev/null +++ b/app/assets/javascripts/projects/settings/branch_rules/components/rule_edit.vue @@ -0,0 +1,38 @@ +<script> +import { GlFormGroup } from '@gitlab/ui'; +import { __ } from '~/locale'; +import { getParameterByName } from '~/lib/utils/url_utility'; +import BranchDropdown from './branch_dropdown.vue'; + +export default { + name: 'RuleEdit', + i18n: { + branch: __('Branch'), + }, + components: { BranchDropdown, GlFormGroup }, + props: { + projectPath: { + type: String, + required: true, + }, + }, + data() { + return { + branch: getParameterByName('branch'), + }; + }, +}; +</script> + +<template> + <gl-form-group :label="$options.i18n.branch"> + <branch-dropdown + id="branches" + v-model="branch" + class="gl-w-half" + :project-path="projectPath" + @createWildcard="branch = $event" + /> + </gl-form-group> + <!-- TODO - Add branch protections (https://gitlab.com/gitlab-org/gitlab/-/issues/362212) --> +</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 new file mode 100644 index 00000000000..8452542540e --- /dev/null +++ b/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js @@ -0,0 +1,26 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import RuleEdit from './components/rule_edit.vue'; + +export default function mountBranchRules(el) { + if (!el) { + return null; + } + + Vue.use(VueApollo); + + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), + }); + + const { projectPath } = el.dataset; + + return new Vue({ + el, + apolloProvider, + render(h) { + return h(RuleEdit, { props: { projectPath } }); + }, + }); +} diff --git a/app/assets/javascripts/projects/settings/branch_rules/queries/branches.query.graphql b/app/assets/javascripts/projects/settings/branch_rules/queries/branches.query.graphql new file mode 100644 index 00000000000..a532b544757 --- /dev/null +++ b/app/assets/javascripts/projects/settings/branch_rules/queries/branches.query.graphql @@ -0,0 +1,8 @@ +query getBranches($projectPath: ID!, $searchPattern: String!) { + project(fullPath: $projectPath) { + id + repository { + branchNames(searchPattern: $searchPattern, limit: 100, offset: 0) + } + } +} diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue new file mode 100644 index 00000000000..ada951f6867 --- /dev/null +++ b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue @@ -0,0 +1,16 @@ +<script> +import { __ } from '~/locale'; + +export default { + name: 'BranchRules', + i18n: { heading: __('Branch') }, +}; +</script> + +<template> + <div> + <strong>{{ $options.i18n.heading }}</strong> + + <!-- TODO - List branch rules (https://gitlab.com/gitlab-org/gitlab/-/issues/362217) --> + </div> +</template> diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/mount_branch_rules.js b/app/assets/javascripts/projects/settings/repository/branch_rules/mount_branch_rules.js new file mode 100644 index 00000000000..abe0b93081e --- /dev/null +++ b/app/assets/javascripts/projects/settings/repository/branch_rules/mount_branch_rules.js @@ -0,0 +1,13 @@ +import Vue from 'vue'; +import BranchRulesApp from '~/projects/settings/repository/branch_rules/app.vue'; + +export default function mountBranchRules(el) { + if (!el) return null; + + return new Vue({ + el, + render(createElement) { + return createElement(BranchRulesApp); + }, + }); +} |