summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/projects
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/projects')
-rw-r--r--app/assets/javascripts/projects/clusters_deprecation_alert/components/clusters_deprecation_alert.vue23
-rw-r--r--app/assets/javascripts/projects/clusters_deprecation_alert/index.js21
-rw-r--r--app/assets/javascripts/projects/commit/components/form_modal.vue2
-rw-r--r--app/assets/javascripts/projects/commits/components/author_select.vue2
-rw-r--r--app/assets/javascripts/projects/compare/components/app.vue4
-rw-r--r--app/assets/javascripts/projects/compare/components/revision_card.vue10
-rw-r--r--app/assets/javascripts/projects/default_project_templates.js8
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/app.vue12
-rw-r--r--app/assets/javascripts/projects/project_import_gitlab_project.js15
-rw-r--r--app/assets/javascripts/projects/project_new.js154
-rw-r--r--app/assets/javascripts/projects/project_visibility.js6
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/branch_dropdown.vue110
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/rule_edit.vue38
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js26
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/queries/branches.query.graphql8
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/app.vue16
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/mount_branch_rules.js13
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);
+ },
+ });
+}