summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/pages
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-04-20 14:36:54 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-04-20 14:36:54 +0000
commitf61bb2a16a514b71bf33aabbbb999d6732016a24 (patch)
tree9548caa89e60b4f40b99bbd1dac030420b812aa8 /app/assets/javascripts/pages
parent35fc54e5d261f8898e390aea7c2f5ec5fdf0539d (diff)
downloadgitlab-ce-13.11.0-rc42.tar.gz
Add latest changes from gitlab-org/gitlab@13-11-stable-eev13.11.0-rc42
Diffstat (limited to 'app/assets/javascripts/pages')
-rw-r--r--app/assets/javascripts/pages/admin/abuse_reports/index.js3
-rw-r--r--app/assets/javascripts/pages/admin/admin.js13
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/general/components/signup_checkbox.vue50
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue417
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/general/index.js26
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/gitpod.js24
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/index.js4
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/signup_restrictions.js31
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/utils.js21
-rw-r--r--app/assets/javascripts/pages/admin/broadcast_messages/index.js6
-rw-r--r--app/assets/javascripts/pages/admin/groups/new/index.js4
-rw-r--r--app/assets/javascripts/pages/admin/labels/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/labels/index/index.js3
-rw-r--r--app/assets/javascripts/pages/admin/labels/new/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/runners/index/index.js (renamed from app/assets/javascripts/pages/admin/runners/index.js)0
-rw-r--r--app/assets/javascripts/pages/admin/runners/show/index.js3
-rw-r--r--app/assets/javascripts/pages/admin/services/edit/index.js6
-rw-r--r--app/assets/javascripts/pages/admin/spam_logs/index.js3
-rw-r--r--app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue4
-rw-r--r--app/assets/javascripts/pages/admin/users/new/index.js52
-rw-r--r--app/assets/javascripts/pages/dashboard/activity/index.js3
-rw-r--r--app/assets/javascripts/pages/dashboard/todos/index/todos.js35
-rw-r--r--app/assets/javascripts/pages/groups/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/group_members/index.js5
-rw-r--r--app/assets/javascripts/pages/groups/labels/index/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/new/fetch_group_path_availability.js11
-rw-r--r--app/assets/javascripts/pages/groups/new/group_path_validator.js9
-rw-r--r--app/assets/javascripts/pages/groups/new/index.js6
-rw-r--r--app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js3
-rw-r--r--app/assets/javascripts/pages/groups/settings/index.js5
-rw-r--r--app/assets/javascripts/pages/groups/settings/integrations/index.js3
-rw-r--r--app/assets/javascripts/pages/groups/settings/packages_and_registries/index.js3
-rw-r--r--app/assets/javascripts/pages/groups/settings/repository/show/index.js3
-rw-r--r--app/assets/javascripts/pages/profiles/show/index.js135
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js23
-rw-r--r--app/assets/javascripts/pages/projects/branches/index/index.js11
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue7
-rw-r--r--app/assets/javascripts/pages/projects/hooks/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/issues/form.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/index/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/issues/show.js14
-rw-r--r--app/assets/javascripts/pages/projects/jobs/index/index.js34
-rw-r--r--app/assets/javascripts/pages/projects/jobs/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/labels/index/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue64
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_b.vue6
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_info_card.vue2
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_card.vue52
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue43
-rw-r--r--app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js50
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js14
-rw-r--r--app/assets/javascripts/pages/projects/packages/infrastructure_registry/index/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/pages/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/path_locks/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue2
-rw-r--r--app/assets/javascripts/pages/projects/project.js17
-rw-r--r--app/assets/javascripts/pages/projects/project_members/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/settings/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/settings/integrations/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/settings/operations/show/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/settings/repository/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue7
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue87
-rw-r--r--app/assets/javascripts/pages/projects/tags/index/index.js2
-rw-r--r--app/assets/javascripts/pages/shared/mount_runner_instructions.js9
-rw-r--r--app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue253
-rw-r--r--app/assets/javascripts/pages/shared/wikis/index.js23
-rw-r--r--app/assets/javascripts/pages/shared/wikis/wikis.js75
-rw-r--r--app/assets/javascripts/pages/users/activity_calendar.js2
70 files changed, 1387 insertions, 358 deletions
diff --git a/app/assets/javascripts/pages/admin/abuse_reports/index.js b/app/assets/javascripts/pages/admin/abuse_reports/index.js
index 0a4311ec73a..a88d35796f7 100644
--- a/app/assets/javascripts/pages/admin/abuse_reports/index.js
+++ b/app/assets/javascripts/pages/admin/abuse_reports/index.js
@@ -1,5 +1,8 @@
+import initDeprecatedRemoveRowBehavior from '~/behaviors/deprecated_remove_row_behavior';
import UsersSelect from '~/users_select';
import AbuseReports from './abuse_reports';
new AbuseReports(); /* eslint-disable-line no-new */
new UsersSelect(); /* eslint-disable-line no-new */
+
+document.addEventListener('DOMContentLoaded', initDeprecatedRemoveRowBehavior);
diff --git a/app/assets/javascripts/pages/admin/admin.js b/app/assets/javascripts/pages/admin/admin.js
index 2732fc191be..6b7bfbf217d 100644
--- a/app/assets/javascripts/pages/admin/admin.js
+++ b/app/assets/javascripts/pages/admin/admin.js
@@ -1,16 +1,6 @@
import $ from 'jquery';
import { refreshCurrentPage } from '../../lib/utils/url_utility';
-function showDenylistType() {
- if ($('input[name="denylist_type"]:checked').val() === 'file') {
- $('.js-denylist-file').show();
- $('.js-denylist-raw').hide();
- } else {
- $('.js-denylist-file').hide();
- $('.js-denylist-raw').show();
- }
-}
-
export default function adminInit() {
$('input#user_force_random_password').on('change', function randomPasswordClick() {
const $elems = $('#user_password, #user_password_confirmation');
@@ -27,7 +17,4 @@ export default function adminInit() {
});
$('li.project_member, li.group_member').on('ajax:success', refreshCurrentPage);
-
- $("input[name='denylist_type']").on('click', showDenylistType);
- showDenylistType();
}
diff --git a/app/assets/javascripts/pages/admin/application_settings/general/components/signup_checkbox.vue b/app/assets/javascripts/pages/admin/application_settings/general/components/signup_checkbox.vue
new file mode 100644
index 00000000000..2217792d7f3
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/application_settings/general/components/signup_checkbox.vue
@@ -0,0 +1,50 @@
+<script>
+import { GlFormCheckbox } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlFormCheckbox,
+ },
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ helpText: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ label: {
+ type: String,
+ required: true,
+ },
+ value: {
+ type: Boolean,
+ required: true,
+ },
+ dataQaSelector: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <input :name="name" type="hidden" :value="value ? '1' : '0'" data-testid="input" />
+
+ <gl-form-checkbox
+ :checked="value"
+ :data-qa-selector="dataQaSelector"
+ @input="$emit('input', $event)"
+ >
+ <span data-testid="label">{{ label }}</span>
+ <template v-if="helpText" #help>
+ <span data-testid="helpText">{{ helpText }}</span>
+ </template>
+ </gl-form-checkbox>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue b/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue
new file mode 100644
index 00000000000..9850113d4be
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue
@@ -0,0 +1,417 @@
+<script>
+import {
+ GlButton,
+ GlFormGroup,
+ GlFormInput,
+ GlFormRadio,
+ GlFormRadioGroup,
+ GlSprintf,
+ GlLink,
+ GlModal,
+} from '@gitlab/ui';
+import { toSafeInteger } from 'lodash';
+import csrf from '~/lib/utils/csrf';
+import { __, s__, sprintf } from '~/locale';
+import SignupCheckbox from './signup_checkbox.vue';
+
+const DENYLIST_TYPE_RAW = 'raw';
+const DENYLIST_TYPE_FILE = 'file';
+
+export default {
+ csrf,
+ DENYLIST_TYPE_RAW,
+ DENYLIST_TYPE_FILE,
+ components: {
+ GlButton,
+ GlFormGroup,
+ GlFormInput,
+ GlFormRadio,
+ GlFormRadioGroup,
+ GlSprintf,
+ GlLink,
+ SignupCheckbox,
+ GlModal,
+ },
+ inject: [
+ 'host',
+ 'settingsPath',
+ 'signupEnabled',
+ 'requireAdminApprovalAfterUserSignup',
+ 'sendUserConfirmationEmail',
+ 'minimumPasswordLength',
+ 'minimumPasswordLengthMin',
+ 'minimumPasswordLengthMax',
+ 'minimumPasswordLengthHelpLink',
+ 'domainAllowlistRaw',
+ 'newUserSignupsCap',
+ 'domainDenylistEnabled',
+ 'denylistTypeRawSelected',
+ 'domainDenylistRaw',
+ 'emailRestrictionsEnabled',
+ 'supportedSyntaxLinkUrl',
+ 'emailRestrictions',
+ 'afterSignUpText',
+ ],
+ data() {
+ return {
+ showModal: false,
+ form: {
+ signupEnabled: this.signupEnabled,
+ requireAdminApproval: this.requireAdminApprovalAfterUserSignup,
+ sendConfirmationEmail: this.sendUserConfirmationEmail,
+ minimumPasswordLength: this.minimumPasswordLength,
+ minimumPasswordLengthMin: this.minimumPasswordLengthMin,
+ minimumPasswordLengthMax: this.minimumPasswordLengthMax,
+ minimumPasswordLengthHelpLink: this.minimumPasswordLengthHelpLink,
+ domainAllowlistRaw: this.domainAllowlistRaw,
+ userCap: this.newUserSignupsCap,
+ domainDenylistEnabled: this.domainDenylistEnabled,
+ denylistType: this.denylistTypeRawSelected
+ ? this.$options.DENYLIST_TYPE_RAW
+ : this.$options.DENYLIST_TYPE_FILE,
+ domainDenylistRaw: this.domainDenylistRaw,
+ emailRestrictionsEnabled: this.emailRestrictionsEnabled,
+ supportedSyntaxLinkUrl: this.supportedSyntaxLinkUrl,
+ emailRestrictions: this.emailRestrictions,
+ afterSignUpText: this.afterSignUpText,
+ },
+ };
+ },
+ computed: {
+ isOldUserCapUnlimited() {
+ // User cap is set to unlimited if no value is provided in the field
+ return this.newUserSignupsCap === '';
+ },
+ isNewUserCapUnlimited() {
+ // User cap is set to unlimited if no value is provided in the field
+ return this.form.userCap === '';
+ },
+ hasUserCapChangedFromUnlimitedToLimited() {
+ return this.isOldUserCapUnlimited && !this.isNewUserCapUnlimited;
+ },
+ hasUserCapChangedFromLimitedToUnlimited() {
+ return !this.isOldUserCapUnlimited && this.isNewUserCapUnlimited;
+ },
+ hasUserCapBeenIncreased() {
+ if (this.hasUserCapChangedFromUnlimitedToLimited) {
+ return false;
+ }
+
+ const oldValueAsInteger = toSafeInteger(this.newUserSignupsCap);
+ const newValueAsInteger = toSafeInteger(this.form.userCap);
+
+ return this.hasUserCapChangedFromLimitedToUnlimited || newValueAsInteger > oldValueAsInteger;
+ },
+ canUsersBeAccidentallyApproved() {
+ const hasUserCapBeenToggledOff =
+ this.requireAdminApprovalAfterUserSignup && !this.form.requireAdminApproval;
+
+ return this.hasUserCapBeenIncreased || hasUserCapBeenToggledOff;
+ },
+ signupEnabledHelpText() {
+ const text = sprintf(
+ s__(
+ 'ApplicationSettings|When enabled, any user visiting %{host} will be able to create an account.',
+ ),
+ {
+ host: this.host,
+ },
+ );
+
+ return text;
+ },
+ requireAdminApprovalHelpText() {
+ const text = sprintf(
+ s__(
+ 'ApplicationSettings|When enabled, any user visiting %{host} and creating an account will have to be explicitly approved by an admin before they can sign in. This setting is effective only if sign-ups are enabled.',
+ ),
+ {
+ host: this.host,
+ },
+ );
+
+ return text;
+ },
+ },
+ watch: {
+ showModal(value) {
+ if (value === true) {
+ this.$refs[this.$options.modal.id].show();
+ } else {
+ this.$refs[this.$options.modal.id].hide();
+ }
+ },
+ },
+ methods: {
+ submitButtonHandler() {
+ if (this.canUsersBeAccidentallyApproved) {
+ this.showModal = true;
+
+ return;
+ }
+
+ this.submitForm();
+ },
+ submitForm() {
+ this.$refs.form.submit();
+ },
+ modalHideHandler() {
+ this.showModal = false;
+ },
+ },
+ i18n: {
+ buttonText: s__('ApplicationSettings|Save changes'),
+ signupEnabledLabel: s__('ApplicationSettings|Sign-up enabled'),
+ requireAdminApprovalLabel: s__('ApplicationSettings|Require admin approval for new sign-ups'),
+ sendConfirmationEmailLabel: s__('ApplicationSettings|Send confirmation email on sign-up'),
+ minimumPasswordLengthLabel: s__(
+ 'ApplicationSettings|Minimum password length (number of characters)',
+ ),
+ domainAllowListLabel: s__('ApplicationSettings|Allowed domains for sign-ups'),
+ domainAllowListDescription: s__(
+ 'ApplicationSettings|ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com',
+ ),
+ userCapLabel: s__('ApplicationSettings|User cap'),
+ userCapDescription: s__(
+ 'ApplicationSettings|Once the instance reaches the user cap, any user who is added or requests access will have to be approved by an admin. Leave the field empty for unlimited.',
+ ),
+ domainDenyListGroupLabel: s__('ApplicationSettings|Domain denylist'),
+ domainDenyListLabel: s__('ApplicationSettings|Enable domain denylist for sign ups'),
+ domainDenyListTypeFileLabel: s__('ApplicationSettings|Upload denylist file'),
+ domainDenyListTypeRawLabel: s__('ApplicationSettings|Enter denylist manually'),
+ domainDenyListFileLabel: s__('ApplicationSettings|Denylist file'),
+ domainDenyListFileDescription: s__(
+ 'ApplicationSettings|Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines or commas for multiple entries.',
+ ),
+ domainDenyListListLabel: s__('ApplicationSettings|Denied domains for sign-ups'),
+ domainDenyListListDescription: s__(
+ 'ApplicationSettings|Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com',
+ ),
+ domainPlaceholder: s__('ApplicationSettings|domain.com'),
+ emailRestrictionsEnabledGroupLabel: s__('ApplicationSettings|Email restrictions'),
+ emailRestrictionsEnabledLabel: s__(
+ 'ApplicationSettings|Enable email restrictions for sign ups',
+ ),
+ emailRestrictionsGroupLabel: s__('ApplicationSettings|Email restrictions for sign-ups'),
+ afterSignUpTextGroupLabel: s__('ApplicationSettings|After sign up text'),
+ afterSignUpTextGroupDescription: s__('ApplicationSettings|Markdown enabled'),
+ },
+ modal: {
+ id: 'signup-settings-modal',
+ actionPrimary: {
+ text: s__('ApplicationSettings|Approve users'),
+ attributes: {
+ variant: 'confirm',
+ },
+ },
+ actionCancel: {
+ text: __('Cancel'),
+ },
+ title: s__('ApplicationSettings|Approve all users in the pending approval status?'),
+ text: s__(
+ 'ApplicationSettings|By making this change, you will automatically approve all users in pending approval status.',
+ ),
+ },
+};
+</script>
+
+<template>
+ <form
+ ref="form"
+ accept-charset="UTF-8"
+ data-testid="form"
+ method="post"
+ :action="settingsPath"
+ enctype="multipart/form-data"
+ >
+ <input type="hidden" name="utf8" value="āœ“" />
+ <input type="hidden" name="_method" value="patch" />
+ <input type="hidden" name="authenticity_token" :value="$options.csrf.token" />
+
+ <section class="gl-mb-8">
+ <signup-checkbox
+ v-model="form.signupEnabled"
+ class="gl-mb-5"
+ name="application_setting[signup_enabled]"
+ :help-text="signupEnabledHelpText"
+ :label="$options.i18n.signupEnabledLabel"
+ data-qa-selector="signup_enabled_checkbox"
+ />
+
+ <signup-checkbox
+ v-model="form.requireAdminApproval"
+ class="gl-mb-5"
+ name="application_setting[require_admin_approval_after_user_signup]"
+ :help-text="requireAdminApprovalHelpText"
+ :label="$options.i18n.requireAdminApprovalLabel"
+ data-qa-selector="require_admin_approval_after_user_signup_checkbox"
+ data-testid="require-admin-approval-checkbox"
+ />
+
+ <signup-checkbox
+ v-model="form.sendConfirmationEmail"
+ class="gl-mb-5"
+ name="application_setting[send_user_confirmation_email]"
+ :label="$options.i18n.sendConfirmationEmailLabel"
+ />
+
+ <gl-form-group
+ :label="$options.i18n.userCapLabel"
+ :description="$options.i18n.userCapDescription"
+ >
+ <gl-form-input
+ v-model="form.userCap"
+ type="text"
+ name="application_setting[new_user_signups_cap]"
+ data-testid="user-cap-input"
+ />
+ </gl-form-group>
+
+ <gl-form-group :label="$options.i18n.minimumPasswordLengthLabel">
+ <gl-form-input
+ v-model="form.minimumPasswordLength"
+ :min="form.minimumPasswordLengthMin"
+ :max="form.minimumPasswordLengthMax"
+ type="number"
+ name="application_setting[minimum_password_length]"
+ />
+
+ <gl-sprintf
+ :message="
+ s__(
+ 'ApplicationSettings|See GitLab\'s %{linkStart}Password Policy Guidelines%{linkEnd}',
+ )
+ "
+ >
+ <template #link="{ content }">
+ <gl-link :href="form.minimumPasswordLengthHelpLink" target="_blank">{{
+ content
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-form-group>
+
+ <gl-form-group
+ :description="$options.i18n.domainAllowListDescription"
+ :label="$options.i18n.domainAllowListLabel"
+ >
+ <textarea
+ v-model="form.domainAllowlistRaw"
+ :placeholder="$options.i18n.domainPlaceholder"
+ rows="8"
+ class="form-control gl-form-input"
+ name="application_setting[domain_allowlist_raw]"
+ ></textarea>
+ </gl-form-group>
+
+ <gl-form-group :label="$options.i18n.domainDenyListGroupLabel">
+ <signup-checkbox
+ v-model="form.domainDenylistEnabled"
+ name="application_setting[domain_denylist_enabled]"
+ :label="$options.i18n.domainDenyListLabel"
+ />
+ </gl-form-group>
+
+ <gl-form-radio-group v-model="form.denylistType" name="denylist_type" class="gl-mb-5">
+ <gl-form-radio :value="$options.DENYLIST_TYPE_FILE">{{
+ $options.i18n.domainDenyListTypeFileLabel
+ }}</gl-form-radio>
+ <gl-form-radio :value="$options.DENYLIST_TYPE_RAW">{{
+ $options.i18n.domainDenyListTypeRawLabel
+ }}</gl-form-radio>
+ </gl-form-radio-group>
+
+ <gl-form-group
+ v-if="form.denylistType === $options.DENYLIST_TYPE_FILE"
+ :description="$options.i18n.domainDenyListFileDescription"
+ :label="$options.i18n.domainDenyListFileLabel"
+ label-for="domain-denylist-file-input"
+ data-testid="domain-denylist-file-input-group"
+ >
+ <input
+ id="domain-denylist-file-input"
+ class="form-control gl-form-input"
+ type="file"
+ accept=".txt,.conf"
+ name="application_setting[domain_denylist_file]"
+ />
+ </gl-form-group>
+
+ <gl-form-group
+ v-if="form.denylistType !== $options.DENYLIST_TYPE_FILE"
+ :description="$options.i18n.domainDenyListListDescription"
+ :label="$options.i18n.domainDenyListListLabel"
+ data-testid="domain-denylist-raw-input-group"
+ >
+ <textarea
+ v-model="form.domainDenylistRaw"
+ :placeholder="$options.i18n.domainPlaceholder"
+ rows="8"
+ class="form-control gl-form-input"
+ name="application_setting[domain_denylist_raw]"
+ ></textarea>
+ </gl-form-group>
+
+ <gl-form-group :label="$options.i18n.emailRestrictionsEnabledGroupLabel">
+ <signup-checkbox
+ v-model="form.emailRestrictionsEnabled"
+ name="application_setting[email_restrictions_enabled]"
+ :label="$options.i18n.emailRestrictionsEnabledLabel"
+ />
+ </gl-form-group>
+
+ <gl-form-group :label="$options.i18n.emailRestrictionsGroupLabel">
+ <textarea
+ v-model="form.emailRestrictions"
+ rows="4"
+ class="form-control gl-form-input"
+ name="application_setting[email_restrictions]"
+ ></textarea>
+
+ <gl-sprintf
+ :message="
+ s__(
+ 'ApplicationSettings|Restricts sign-ups for email addresses that match the given regex. See the %{linkStart}supported syntax%{linkEnd} for more information.',
+ )
+ "
+ >
+ <template #link="{ content }">
+ <gl-link :href="form.supportedSyntaxLinkUrl" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-form-group>
+
+ <gl-form-group
+ :label="$options.i18n.afterSignUpTextGroupLabel"
+ :description="$options.i18n.afterSignUpTextGroupDescription"
+ >
+ <textarea
+ v-model="form.afterSignUpText"
+ rows="4"
+ class="form-control gl-form-input"
+ name="application_setting[after_sign_up_text]"
+ ></textarea>
+ </gl-form-group>
+ </section>
+
+ <gl-button
+ data-qa-selector="save_changes_button"
+ variant="confirm"
+ @click.prevent="submitButtonHandler"
+ >
+ {{ $options.i18n.buttonText }}
+ </gl-button>
+
+ <gl-modal
+ :ref="$options.modal.id"
+ :modal-id="$options.modal.id"
+ :action-cancel="$options.modal.actionCancel"
+ :action-primary="$options.modal.actionPrimary"
+ :title="$options.modal.title"
+ @primary="submitForm"
+ @hide="modalHideHandler"
+ >
+ {{ $options.modal.text }}
+ </gl-modal>
+ </form>
+</template>
diff --git a/app/assets/javascripts/pages/admin/application_settings/general/index.js b/app/assets/javascripts/pages/admin/application_settings/general/index.js
index eda1a9d3599..c48d99da990 100644
--- a/app/assets/javascripts/pages/admin/application_settings/general/index.js
+++ b/app/assets/javascripts/pages/admin/application_settings/general/index.js
@@ -1,27 +1,9 @@
-import Vue from 'vue';
-import IntegrationHelpText from '~/vue_shared/components/integrations_help_text.vue';
import initUserInternalRegexPlaceholder from '../account_and_limits';
+import initGitpod from '../gitpod';
+import initSignupRestrictions from '../signup_restrictions';
(() => {
initUserInternalRegexPlaceholder();
-
- const el = document.querySelector('#js-gitpod-settings-help-text');
- if (!el) {
- return;
- }
-
- const { message, messageUrl } = el.dataset;
-
- // eslint-disable-next-line no-new
- new Vue({
- el,
- render(createElement) {
- return createElement(IntegrationHelpText, {
- props: {
- message,
- messageUrl,
- },
- });
- },
- });
+ initGitpod();
+ initSignupRestrictions();
})();
diff --git a/app/assets/javascripts/pages/admin/application_settings/gitpod.js b/app/assets/javascripts/pages/admin/application_settings/gitpod.js
new file mode 100644
index 00000000000..74e46617d52
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/application_settings/gitpod.js
@@ -0,0 +1,24 @@
+import Vue from 'vue';
+import IntegrationHelpText from '~/vue_shared/components/integrations_help_text.vue';
+
+export default function initGitpod() {
+ const el = document.querySelector('#js-gitpod-settings-help-text');
+
+ if (!el) {
+ return false;
+ }
+
+ const { message, messageUrl } = el.dataset;
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(IntegrationHelpText, {
+ props: {
+ message,
+ messageUrl,
+ },
+ });
+ },
+ });
+}
diff --git a/app/assets/javascripts/pages/admin/application_settings/index.js b/app/assets/javascripts/pages/admin/application_settings/index.js
index e3c6b0f6f5b..a6e3a7dc08a 100644
--- a/app/assets/javascripts/pages/admin/application_settings/index.js
+++ b/app/assets/javascripts/pages/admin/application_settings/index.js
@@ -4,9 +4,7 @@ import initSearchSettings from '~/search_settings';
import selfMonitor from '~/self_monitor';
import initSettingsPanels from '~/settings_panels';
-if (gon.features?.ciInstanceVariablesUi) {
- initVariableList('js-instance-variables');
-}
+initVariableList('js-instance-variables');
selfMonitor();
// Initialize expandable settings panels
initSettingsPanels();
diff --git a/app/assets/javascripts/pages/admin/application_settings/signup_restrictions.js b/app/assets/javascripts/pages/admin/application_settings/signup_restrictions.js
new file mode 100644
index 00000000000..70b896f6372
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/application_settings/signup_restrictions.js
@@ -0,0 +1,31 @@
+import Vue from 'vue';
+import SignupForm from './general/components/signup_form.vue';
+import { getParsedDataset } from './utils';
+
+export default function initSignupRestrictions(elementSelector = '#js-signup-form') {
+ const el = document.querySelector(elementSelector);
+
+ if (!el) {
+ return false;
+ }
+
+ const parsedDataset = getParsedDataset({
+ dataset: el.dataset,
+ booleanAttributes: [
+ 'signupEnabled',
+ 'requireAdminApprovalAfterUserSignup',
+ 'sendUserConfirmationEmail',
+ 'domainDenylistEnabled',
+ 'denylistTypeRawSelected',
+ 'emailRestrictionsEnabled',
+ ],
+ });
+
+ return new Vue({
+ el,
+ provide: {
+ ...parsedDataset,
+ },
+ render: (createElement) => createElement(SignupForm),
+ });
+}
diff --git a/app/assets/javascripts/pages/admin/application_settings/utils.js b/app/assets/javascripts/pages/admin/application_settings/utils.js
new file mode 100644
index 00000000000..5462a13d523
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/application_settings/utils.js
@@ -0,0 +1,21 @@
+import { includes } from 'lodash';
+import { parseBoolean } from '~/lib/utils/common_utils';
+
+/**
+ * Returns a new dataset that has all the values of keys indicated in
+ * booleanAttributes transformed by the parseBoolean() helper function
+ *
+ * @param {Object}
+ * @returns {Object}
+ */
+export const getParsedDataset = ({ dataset = {}, booleanAttributes = [] } = {}) => {
+ const parsedDataset = {};
+
+ Object.keys(dataset).forEach((key) => {
+ parsedDataset[key] = includes(booleanAttributes, key)
+ ? parseBoolean(dataset[key])
+ : dataset[key];
+ });
+
+ return parsedDataset;
+};
diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/index.js b/app/assets/javascripts/pages/admin/broadcast_messages/index.js
index d6cc6a850eb..b7db6443658 100644
--- a/app/assets/javascripts/pages/admin/broadcast_messages/index.js
+++ b/app/assets/javascripts/pages/admin/broadcast_messages/index.js
@@ -1,3 +1,7 @@
+import initDeprecatedRemoveRowBehavior from '~/behaviors/deprecated_remove_row_behavior';
import initBroadcastMessagesForm from './broadcast_message';
-document.addEventListener('DOMContentLoaded', initBroadcastMessagesForm);
+document.addEventListener('DOMContentLoaded', () => {
+ initBroadcastMessagesForm();
+ initDeprecatedRemoveRowBehavior();
+});
diff --git a/app/assets/javascripts/pages/admin/groups/new/index.js b/app/assets/javascripts/pages/admin/groups/new/index.js
index 94f7cfd55be..1630cfb8253 100644
--- a/app/assets/javascripts/pages/admin/groups/new/index.js
+++ b/app/assets/javascripts/pages/admin/groups/new/index.js
@@ -2,9 +2,9 @@ import initFilePickers from '~/file_pickers';
import BindInOut from '../../../../behaviors/bind_in_out';
import Group from '../../../../group';
-document.addEventListener('DOMContentLoaded', () => {
+(() => {
BindInOut.initAll();
initFilePickers();
return new Group();
-});
+})();
diff --git a/app/assets/javascripts/pages/admin/labels/edit/index.js b/app/assets/javascripts/pages/admin/labels/edit/index.js
index 5de1d4d6344..f7c25347e75 100644
--- a/app/assets/javascripts/pages/admin/labels/edit/index.js
+++ b/app/assets/javascripts/pages/admin/labels/edit/index.js
@@ -1,3 +1,3 @@
import Labels from '../../../../labels';
-document.addEventListener('DOMContentLoaded', () => new Labels());
+new Labels(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/admin/labels/index/index.js b/app/assets/javascripts/pages/admin/labels/index/index.js
new file mode 100644
index 00000000000..e5ab5d43bbf
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/labels/index/index.js
@@ -0,0 +1,3 @@
+import initDeprecatedRemoveRowBehavior from '~/behaviors/deprecated_remove_row_behavior';
+
+document.addEventListener('DOMContentLoaded', initDeprecatedRemoveRowBehavior);
diff --git a/app/assets/javascripts/pages/admin/labels/new/index.js b/app/assets/javascripts/pages/admin/labels/new/index.js
index 5de1d4d6344..f7c25347e75 100644
--- a/app/assets/javascripts/pages/admin/labels/new/index.js
+++ b/app/assets/javascripts/pages/admin/labels/new/index.js
@@ -1,3 +1,3 @@
import Labels from '../../../../labels';
-document.addEventListener('DOMContentLoaded', () => new Labels());
+new Labels(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/admin/runners/index.js b/app/assets/javascripts/pages/admin/runners/index/index.js
index 45ed3ac6bd8..45ed3ac6bd8 100644
--- a/app/assets/javascripts/pages/admin/runners/index.js
+++ b/app/assets/javascripts/pages/admin/runners/index/index.js
diff --git a/app/assets/javascripts/pages/admin/runners/show/index.js b/app/assets/javascripts/pages/admin/runners/show/index.js
new file mode 100644
index 00000000000..d1853772fda
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/runners/show/index.js
@@ -0,0 +1,3 @@
+import { initRunnerDetail } from '~/runner/runner_details';
+
+initRunnerDetail();
diff --git a/app/assets/javascripts/pages/admin/services/edit/index.js b/app/assets/javascripts/pages/admin/services/edit/index.js
index 3d692ef4dcc..b8080ddff77 100644
--- a/app/assets/javascripts/pages/admin/services/edit/index.js
+++ b/app/assets/javascripts/pages/admin/services/edit/index.js
@@ -1,6 +1,4 @@
import IntegrationSettingsForm from '~/integrations/integration_settings_form';
-document.addEventListener('DOMContentLoaded', () => {
- const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
- integrationSettingsForm.init();
-});
+const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
+integrationSettingsForm.init();
diff --git a/app/assets/javascripts/pages/admin/spam_logs/index.js b/app/assets/javascripts/pages/admin/spam_logs/index.js
new file mode 100644
index 00000000000..e5ab5d43bbf
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/spam_logs/index.js
@@ -0,0 +1,3 @@
+import initDeprecatedRemoveRowBehavior from '~/behaviors/deprecated_remove_row_behavior';
+
+document.addEventListener('DOMContentLoaded', initDeprecatedRemoveRowBehavior);
diff --git a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
index d2b83f980d7..20407334b3f 100644
--- a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
+++ b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
@@ -119,8 +119,8 @@ export default {
<gl-button @click="onCancel">{{ s__('Cancel') }}</gl-button>
<gl-button
:disabled="!canSubmit"
- category="primary"
- variant="warning"
+ category="secondary"
+ variant="danger"
@click="onSecondaryAction"
>
{{ secondaryAction }}
diff --git a/app/assets/javascripts/pages/admin/users/new/index.js b/app/assets/javascripts/pages/admin/users/new/index.js
index 7b7d4c169ef..34c10e44f4c 100644
--- a/app/assets/javascripts/pages/admin/users/new/index.js
+++ b/app/assets/javascripts/pages/admin/users/new/index.js
@@ -1,51 +1,3 @@
-import $ from 'jquery';
+import { setupInternalUserRegexHandler } from '~/admin/users/new';
-export default class UserInternalRegexHandler {
- constructor() {
- this.regexPattern = $('[data-user-internal-regex-pattern]').data('user-internal-regex-pattern');
- if (this.regexPattern && this.regexPattern !== '') {
- this.regexOptions = $('[data-user-internal-regex-options]').data(
- 'user-internal-regex-options',
- );
- this.external = $('#user_external');
- this.warningMessage = $('#warning_external_automatically_set');
- this.addListenerToEmailField();
- this.addListenerToUserExternalCheckbox();
- }
- }
-
- addListenerToEmailField() {
- $('#user_email').on('input', (event) => {
- this.setExternalCheckbox(event.currentTarget.value);
- });
- }
-
- addListenerToUserExternalCheckbox() {
- this.external.on('click', () => {
- this.warningMessage.addClass('hidden');
- });
- }
-
- isEmailInternal(email) {
- const regex = new RegExp(this.regexPattern, this.regexOptions);
- return regex.test(email);
- }
-
- setExternalCheckbox(email) {
- const isChecked = this.external.prop('checked');
- if (this.isEmailInternal(email)) {
- if (isChecked) {
- this.external.prop('checked', false);
- this.warningMessage.removeClass('hidden');
- }
- } else if (!isChecked) {
- this.external.prop('checked', true);
- this.warningMessage.addClass('hidden');
- }
- }
-}
-
-document.addEventListener('DOMContentLoaded', () => {
- // eslint-disable-next-line
- new UserInternalRegexHandler();
-});
+setupInternalUserRegexHandler();
diff --git a/app/assets/javascripts/pages/dashboard/activity/index.js b/app/assets/javascripts/pages/dashboard/activity/index.js
index 1b887cad496..8b7c36a0976 100644
--- a/app/assets/javascripts/pages/dashboard/activity/index.js
+++ b/app/assets/javascripts/pages/dashboard/activity/index.js
@@ -1,3 +1,4 @@
import Activities from '~/activities';
-document.addEventListener('DOMContentLoaded', () => new Activities());
+// eslint-disable-next-line no-new
+new Activities();
diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
index d53cd405504..42341436b55 100644
--- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js
+++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
@@ -1,6 +1,8 @@
/* eslint-disable class-methods-use-this, no-unneeded-ternary */
import $ from 'jquery';
+import { getGroups } from '~/api/groups_api';
+import { getProjects } from '~/api/projects_api';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { deprecatedCreateFlash as flash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
@@ -41,14 +43,37 @@ export default class Todos {
}
initFilters() {
- this.initFilterDropdown($('.js-group-search'), 'group_id', ['text']);
- this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']);
+ this.initAjaxFilterDropdown(getGroups, $('.js-group-search'), 'group_id');
+ this.initAjaxFilterDropdown(getProjects, $('.js-project-search'), 'project_id');
this.initFilterDropdown($('.js-type-search'), 'type');
this.initFilterDropdown($('.js-action-search'), 'action_id');
return new UsersSelect();
}
+ initAjaxFilterDropdown(apiMethod, $dropdown, fieldName) {
+ initDeprecatedJQueryDropdown($dropdown, {
+ fieldName,
+ selectable: true,
+ filterable: true,
+ filterRemote: true,
+ data(search, callback) {
+ return apiMethod(search, {}, (data) => {
+ callback(
+ data.map((d) => ({
+ id: d.id,
+ text: d.full_name || d.name_with_namespace,
+ })),
+ );
+ });
+ },
+ clicked: () => {
+ const $formEl = $dropdown.closest('form.filter-form');
+ $formEl.submit();
+ },
+ });
+ }
+
initFilterDropdown($dropdown, fieldName, searchFields) {
initDeprecatedJQueryDropdown($dropdown, {
fieldName,
@@ -58,12 +83,6 @@ export default class Todos {
data: $dropdown.data('data'),
clicked: () => {
const $formEl = $dropdown.closest('form.filter-form');
- const mutexDropdowns = {
- group_id: 'project_id',
- project_id: 'group_id',
- };
-
- $formEl.find(`input[name="${mutexDropdowns[fieldName]}"]`).remove();
$formEl.submit();
},
});
diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js
index 176d2406751..49b9822795c 100644
--- a/app/assets/javascripts/pages/groups/edit/index.js
+++ b/app/assets/javascripts/pages/groups/edit/index.js
@@ -4,6 +4,7 @@ import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory';
import initFilePickers from '~/file_pickers';
import TransferDropdown from '~/groups/transfer_dropdown';
import groupsSelect from '~/groups_select';
+import { initCascadingSettingsLockPopovers } from '~/namespaces/cascading_settings';
import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
import projectSelect from '~/project_select';
import initSearchSettings from '~/search_settings';
@@ -26,6 +27,7 @@ document.addEventListener('DOMContentLoaded', () => {
projectSelect();
initSearchSettings();
+ initCascadingSettingsLockPopovers();
return new TransferDropdown();
});
diff --git a/app/assets/javascripts/pages/groups/group_members/index.js b/app/assets/javascripts/pages/groups/group_members/index.js
index ab70fa572ba..b0a70055835 100644
--- a/app/assets/javascripts/pages/groups/group_members/index.js
+++ b/app/assets/javascripts/pages/groups/group_members/index.js
@@ -8,6 +8,7 @@ import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigg
import { s__ } from '~/locale';
import memberExpirationDate from '~/member_expiration_date';
import { initMembersApp } from '~/members';
+import { MEMBER_TYPES } from '~/members/constants';
import { groupLinkRequestFormatter } from '~/members/utils';
import UsersSelect from '~/users_select';
import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue';
@@ -29,6 +30,7 @@ function mountRemoveMemberModal() {
const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions'];
initMembersApp(document.querySelector('.js-group-members-list'), {
+ namespace: MEMBER_TYPES.user,
tableFields: SHARED_FIELDS.concat(['source', 'granted']),
tableAttrs: { tr: { 'data-qa-selector': 'member_row' } },
tableSortableFields: ['account', 'granted', 'maxRole', 'lastSignIn'],
@@ -43,6 +45,7 @@ initMembersApp(document.querySelector('.js-group-members-list'), {
});
initMembersApp(document.querySelector('.js-group-group-links-list'), {
+ namespace: MEMBER_TYPES.group,
tableFields: SHARED_FIELDS.concat('granted'),
tableAttrs: {
table: { 'data-qa-selector': 'groups_list' },
@@ -51,6 +54,7 @@ initMembersApp(document.querySelector('.js-group-group-links-list'), {
requestFormatter: groupLinkRequestFormatter,
});
initMembersApp(document.querySelector('.js-group-invited-members-list'), {
+ namespace: MEMBER_TYPES.invite,
tableFields: SHARED_FIELDS.concat('invited'),
requestFormatter: groupMemberRequestFormatter,
filteredSearchBar: {
@@ -62,6 +66,7 @@ initMembersApp(document.querySelector('.js-group-invited-members-list'), {
},
});
initMembersApp(document.querySelector('.js-group-access-requests-list'), {
+ namespace: MEMBER_TYPES.accessRequest,
tableFields: SHARED_FIELDS.concat('requested'),
requestFormatter: groupMemberRequestFormatter,
});
diff --git a/app/assets/javascripts/pages/groups/labels/index/index.js b/app/assets/javascripts/pages/groups/labels/index/index.js
index 87d522d7654..95c2c7cd7d0 100644
--- a/app/assets/javascripts/pages/groups/labels/index/index.js
+++ b/app/assets/javascripts/pages/groups/labels/index/index.js
@@ -1,3 +1,5 @@
+import initDeleteLabelModal from '~/delete_label_modal';
import initLabels from '~/init_labels';
initLabels();
+initDeleteLabelModal();
diff --git a/app/assets/javascripts/pages/groups/new/fetch_group_path_availability.js b/app/assets/javascripts/pages/groups/new/fetch_group_path_availability.js
index 1d68ccd724d..301e0b4f7a2 100644
--- a/app/assets/javascripts/pages/groups/new/fetch_group_path_availability.js
+++ b/app/assets/javascripts/pages/groups/new/fetch_group_path_availability.js
@@ -1,7 +1,12 @@
+import { buildApiUrl } from '~/api/api_utils';
import axios from '~/lib/utils/axios_utils';
-const rootUrl = gon.relative_url_root;
+const NAMESPACE_EXISTS_PATH = '/api/:version/namespaces/:id/exists';
-export default function fetchGroupPathAvailability(groupPath) {
- return axios.get(`${rootUrl}/users/${groupPath}/suggests`);
+export default function fetchGroupPathAvailability(groupPath, parentId) {
+ const url = buildApiUrl(NAMESPACE_EXISTS_PATH).replace(':id', encodeURIComponent(groupPath));
+
+ return axios.get(url, {
+ params: { parent_id: parentId },
+ });
}
diff --git a/app/assets/javascripts/pages/groups/new/group_path_validator.js b/app/assets/javascripts/pages/groups/new/group_path_validator.js
index 89dccea2812..a0ff98645fb 100644
--- a/app/assets/javascripts/pages/groups/new/group_path_validator.js
+++ b/app/assets/javascripts/pages/groups/new/group_path_validator.js
@@ -8,6 +8,7 @@ import fetchGroupPathAvailability from './fetch_group_path_availability';
const debounceTimeoutDuration = 1000;
const invalidInputClass = 'gl-field-error-outline';
const successInputClass = 'gl-field-success-outline';
+const parentIdSelector = 'group_parent_id';
const successMessageSelector = '.validation-success';
const pendingMessageSelector = '.validation-pending';
const unavailableMessageSelector = '.validation-error';
@@ -20,9 +21,10 @@ export default class GroupPathValidator extends InputValidator {
const container = opts.container || '';
const validateElements = document.querySelectorAll(`${container} .js-validate-group-path`);
+ const parentIdElement = document.getElementById(parentIdSelector);
this.debounceValidateInput = debounce((inputDomElement) => {
- GroupPathValidator.validateGroupPathInput(inputDomElement);
+ GroupPathValidator.validateGroupPathInput(inputDomElement, parentIdElement);
}, debounceTimeoutDuration);
validateElements.forEach((element) =>
@@ -37,13 +39,14 @@ export default class GroupPathValidator extends InputValidator {
this.debounceValidateInput(inputDomElement);
}
- static validateGroupPathInput(inputDomElement) {
+ static validateGroupPathInput(inputDomElement, parentIdElement) {
const groupPath = inputDomElement.value;
+ const parentId = parentIdElement.value;
if (inputDomElement.checkValidity() && groupPath.length > 1) {
GroupPathValidator.setMessageVisibility(inputDomElement, pendingMessageSelector);
- fetchGroupPathAvailability(groupPath)
+ fetchGroupPathAvailability(groupPath, parentId)
.then(({ data }) => data)
.then((data) => {
GroupPathValidator.setInputState(inputDomElement, !data.exists);
diff --git a/app/assets/javascripts/pages/groups/new/index.js b/app/assets/javascripts/pages/groups/new/index.js
index 322ad2c79e7..569b5afd676 100644
--- a/app/assets/javascripts/pages/groups/new/index.js
+++ b/app/assets/javascripts/pages/groups/new/index.js
@@ -5,10 +5,8 @@ import Group from '~/group';
import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs';
import GroupPathValidator from './group_path_validator';
-const parentId = $('#group_parent_id');
-if (!parentId.val()) {
- new GroupPathValidator(); // eslint-disable-line no-new
-}
+new GroupPathValidator(); // eslint-disable-line no-new
+
BindInOut.initAll();
initFilePickers();
diff --git a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
index 0c3fdcf3e75..636eea5d7ac 100644
--- a/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/groups/settings/ci_cd/show/index.js
@@ -4,7 +4,6 @@ import initSharedRunnersForm from '~/group_settings/mount_shared_runners';
import { FILTERED_SEARCH } from '~/pages/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import { initInstallRunner } from '~/pages/shared/mount_runner_instructions';
-import initSearchSettings from '~/search_settings';
import initSettingsPanels from '~/settings_panels';
// Initialize expandable settings panels
@@ -21,5 +20,3 @@ initSharedRunnersForm();
initVariableList();
initInstallRunner();
-
-initSearchSettings();
diff --git a/app/assets/javascripts/pages/groups/settings/index.js b/app/assets/javascripts/pages/groups/settings/index.js
new file mode 100644
index 00000000000..cb787c60002
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/settings/index.js
@@ -0,0 +1,5 @@
+import initRevokeButton from '~/deploy_tokens/init_revoke_button';
+import initSearchSettings from '~/search_settings';
+
+initSearchSettings();
+initRevokeButton();
diff --git a/app/assets/javascripts/pages/groups/settings/integrations/index.js b/app/assets/javascripts/pages/groups/settings/integrations/index.js
new file mode 100644
index 00000000000..53068f72d3f
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/settings/integrations/index.js
@@ -0,0 +1,3 @@
+import initIntegrationsList from '~/integrations/index';
+
+initIntegrationsList();
diff --git a/app/assets/javascripts/pages/groups/settings/packages_and_registries/index.js b/app/assets/javascripts/pages/groups/settings/packages_and_registries/index.js
index d13bf026777..3b922622d2c 100644
--- a/app/assets/javascripts/pages/groups/settings/packages_and_registries/index.js
+++ b/app/assets/javascripts/pages/groups/settings/packages_and_registries/index.js
@@ -1,6 +1,3 @@
import bundle from '~/packages_and_registries/settings/group/bundle';
-import initSearchSettings from '~/search_settings';
bundle();
-
-document.addEventListener('DOMContentLoaded', initSearchSettings);
diff --git a/app/assets/javascripts/pages/groups/settings/repository/show/index.js b/app/assets/javascripts/pages/groups/settings/repository/show/index.js
index 2c9867653de..92405f205cb 100644
--- a/app/assets/javascripts/pages/groups/settings/repository/show/index.js
+++ b/app/assets/javascripts/pages/groups/settings/repository/show/index.js
@@ -1,10 +1,7 @@
import DueDateSelectors from '~/due_date_select';
-import initSearchSettings from '~/search_settings';
import initSettingsPanels from '~/settings_panels';
// Initialize expandable settings panels
initSettingsPanels();
new DueDateSelectors(); // eslint-disable-line no-new
-
-initSearchSettings();
diff --git a/app/assets/javascripts/pages/profiles/show/index.js b/app/assets/javascripts/pages/profiles/show/index.js
index a24c6ca7754..b5441127797 100644
--- a/app/assets/javascripts/pages/profiles/show/index.js
+++ b/app/assets/javascripts/pages/profiles/show/index.js
@@ -7,81 +7,78 @@ import { __ } from '~/locale';
import EmojiMenu from './emoji_menu';
const defaultStatusEmoji = 'speech_balloon';
+const toggleEmojiMenuButtonSelector = '.js-toggle-emoji-menu';
+const toggleEmojiMenuButton = document.querySelector(toggleEmojiMenuButtonSelector);
+const statusEmojiField = document.getElementById('js-status-emoji-field');
+const statusMessageField = document.getElementById('js-status-message-field');
-document.addEventListener('DOMContentLoaded', () => {
- const toggleEmojiMenuButtonSelector = '.js-toggle-emoji-menu';
- const toggleEmojiMenuButton = document.querySelector(toggleEmojiMenuButtonSelector);
- const statusEmojiField = document.getElementById('js-status-emoji-field');
- const statusMessageField = document.getElementById('js-status-message-field');
+const toggleNoEmojiPlaceholder = (isVisible) => {
+ const placeholderElement = document.getElementById('js-no-emoji-placeholder');
+ placeholderElement.classList.toggle('hidden', !isVisible);
+};
- const toggleNoEmojiPlaceholder = (isVisible) => {
- const placeholderElement = document.getElementById('js-no-emoji-placeholder');
- placeholderElement.classList.toggle('hidden', !isVisible);
- };
+const findStatusEmoji = () => toggleEmojiMenuButton.querySelector('gl-emoji');
+const removeStatusEmoji = () => {
+ const statusEmoji = findStatusEmoji();
+ if (statusEmoji) {
+ statusEmoji.remove();
+ }
+};
- const findStatusEmoji = () => toggleEmojiMenuButton.querySelector('gl-emoji');
- const removeStatusEmoji = () => {
- const statusEmoji = findStatusEmoji();
- if (statusEmoji) {
- statusEmoji.remove();
- }
- };
+const selectEmojiCallback = (emoji, emojiTag) => {
+ statusEmojiField.value = emoji;
+ toggleNoEmojiPlaceholder(false);
+ removeStatusEmoji();
+ toggleEmojiMenuButton.innerHTML += emojiTag;
+};
- const selectEmojiCallback = (emoji, emojiTag) => {
- statusEmojiField.value = emoji;
- toggleNoEmojiPlaceholder(false);
- removeStatusEmoji();
- toggleEmojiMenuButton.innerHTML += emojiTag;
- };
-
- const clearEmojiButton = document.getElementById('js-clear-user-status-button');
- clearEmojiButton.addEventListener('click', () => {
- statusEmojiField.value = '';
- statusMessageField.value = '';
- removeStatusEmoji();
- toggleNoEmojiPlaceholder(true);
- });
+const clearEmojiButton = document.getElementById('js-clear-user-status-button');
+clearEmojiButton.addEventListener('click', () => {
+ statusEmojiField.value = '';
+ statusMessageField.value = '';
+ removeStatusEmoji();
+ toggleNoEmojiPlaceholder(true);
+});
- const emojiAutocomplete = new GfmAutoComplete();
- emojiAutocomplete.setup($(statusMessageField), { emojis: true });
+const emojiAutocomplete = new GfmAutoComplete();
+emojiAutocomplete.setup($(statusMessageField), { emojis: true });
- const userNameInput = document.getElementById('user_name');
- userNameInput.addEventListener('input', () => {
- const EMOJI_REGEX = emojiRegex();
- if (EMOJI_REGEX.test(userNameInput.value)) {
- // set field to invalid so it gets detected by GlFieldErrors
- userNameInput.setCustomValidity(__('Invalid field'));
- } else {
- userNameInput.setCustomValidity('');
- }
- });
+const userNameInput = document.getElementById('user_name');
+userNameInput.addEventListener('input', () => {
+ const EMOJI_REGEX = emojiRegex();
+ if (EMOJI_REGEX.test(userNameInput.value)) {
+ // set field to invalid so it gets detected by GlFieldErrors
+ userNameInput.setCustomValidity(__('Invalid field'));
+ } else {
+ userNameInput.setCustomValidity('');
+ }
+});
- Emoji.initEmojiMap()
- .then(() => {
- const emojiMenu = new EmojiMenu(
- Emoji,
- toggleEmojiMenuButtonSelector,
- 'js-status-emoji-menu',
- selectEmojiCallback,
- );
- emojiMenu.bindEvents();
+Emoji.initEmojiMap()
+ .then(() => {
+ const emojiMenu = new EmojiMenu(
+ Emoji,
+ toggleEmojiMenuButtonSelector,
+ 'js-status-emoji-menu',
+ selectEmojiCallback,
+ );
+ emojiMenu.bindEvents();
- const defaultEmojiTag = Emoji.glEmojiTag(defaultStatusEmoji);
- statusMessageField.addEventListener('input', () => {
- const hasStatusMessage = statusMessageField.value.trim() !== '';
- const statusEmoji = findStatusEmoji();
- if (hasStatusMessage && statusEmoji) {
- return;
- }
+ const defaultEmojiTag = Emoji.glEmojiTag(defaultStatusEmoji);
+ statusMessageField.addEventListener('input', () => {
+ const hasStatusMessage = statusMessageField.value.trim() !== '';
+ const statusEmoji = findStatusEmoji();
+ if (hasStatusMessage && statusEmoji) {
+ return;
+ }
- if (hasStatusMessage) {
- toggleNoEmojiPlaceholder(false);
- toggleEmojiMenuButton.innerHTML += defaultEmojiTag;
- } else if (statusEmoji.dataset.name === defaultStatusEmoji) {
- toggleNoEmojiPlaceholder(true);
- removeStatusEmoji();
- }
- });
- })
- .catch(() => createFlash(__('Failed to load emoji list.')));
-});
+ if (hasStatusMessage) {
+ toggleNoEmojiPlaceholder(false);
+ toggleEmojiMenuButton.innerHTML += defaultEmojiTag;
+ } else if (statusEmoji.dataset.name === defaultStatusEmoji) {
+ toggleNoEmojiPlaceholder(true);
+ removeStatusEmoji();
+ }
+ });
+ })
+ .catch(() => createFlash(__('Failed to load emoji list.')));
diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js
index 10bac6d60c2..fc2702b8c37 100644
--- a/app/assets/javascripts/pages/projects/blob/show/index.js
+++ b/app/assets/javascripts/pages/projects/blob/show/index.js
@@ -5,10 +5,29 @@ import GpgBadges from '~/gpg_badges';
import initBlob from '~/pages/projects/init_blob';
import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
+import BlobContentViewer from '~/repository/components/blob_content_viewer.vue';
import '~/sourcegraph/load';
-new BlobViewer(); // eslint-disable-line no-new
-initBlob();
+const viewBlobEl = document.querySelector('#js-view-blob-app');
+
+if (viewBlobEl) {
+ const { blobPath } = viewBlobEl.dataset;
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: viewBlobEl,
+ render(createElement) {
+ return createElement(BlobContentViewer, {
+ props: {
+ path: blobPath,
+ },
+ });
+ },
+ });
+} else {
+ new BlobViewer(); // eslint-disable-line no-new
+ initBlob();
+}
const CommitPipelineStatusEl = document.querySelector('.js-commit-pipeline-status');
const statusLink = document.querySelector('.commit-actions .ci-status-link');
diff --git a/app/assets/javascripts/pages/projects/branches/index/index.js b/app/assets/javascripts/pages/projects/branches/index/index.js
index 72861855c5a..27ec746ad02 100644
--- a/app/assets/javascripts/pages/projects/branches/index/index.js
+++ b/app/assets/javascripts/pages/projects/branches/index/index.js
@@ -1,7 +1,16 @@
+import initDeprecatedRemoveRowBehavior from '~/behaviors/deprecated_remove_row_behavior';
import AjaxLoadingSpinner from '~/branches/ajax_loading_spinner';
+import BranchSortDropdown from '~/branches/branch_sort_dropdown';
import DeleteModal from '~/branches/branches_delete_modal';
import initDiverganceGraph from '~/branches/divergence_graph';
AjaxLoadingSpinner.init();
new DeleteModal(); // eslint-disable-line no-new
-initDiverganceGraph(document.querySelector('.js-branch-list').dataset.divergingCountsEndpoint);
+
+const { divergingCountsEndpoint, defaultBranch } = document.querySelector(
+ '.js-branch-list',
+).dataset;
+
+initDiverganceGraph(divergingCountsEndpoint, defaultBranch);
+BranchSortDropdown();
+initDeprecatedRemoveRowBehavior();
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
index 7112b23775d..288d6711682 100644
--- a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
+++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
@@ -13,6 +13,7 @@ import {
GlFormRadioGroup,
GlFormSelect,
} from '@gitlab/ui';
+import { kebabCase } from 'lodash';
import { buildApiUrl } from '~/api/api_utils';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
@@ -145,6 +146,10 @@ export default {
this.fork.visibility = visibility;
}
},
+ // eslint-disable-next-line func-names
+ 'fork.name': function (newVal) {
+ this.fork.slug = kebabCase(newVal);
+ },
},
mounted() {
this.fetchNamespaces();
@@ -213,6 +218,7 @@ export default {
id="fork-url"
v-model="selectedNamespace"
data-testid="fork-url-input"
+ data-qa-selector="fork_namespace_dropdown"
required
>
<template slot="first">
@@ -286,6 +292,7 @@ export default {
category="primary"
variant="confirm"
data-testid="submit-button"
+ data-qa-selector="fork_project_button"
:loading="isSaving"
>
{{ s__('ForkProject|Fork project') }}
diff --git a/app/assets/javascripts/pages/projects/hooks/index.js b/app/assets/javascripts/pages/projects/hooks/index.js
new file mode 100644
index 00000000000..2a120a690ef
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/hooks/index.js
@@ -0,0 +1,3 @@
+import initSearchSettings from '~/search_settings';
+
+initSearchSettings();
diff --git a/app/assets/javascripts/pages/projects/issues/form.js b/app/assets/javascripts/pages/projects/issues/form.js
index 4e35f28ab06..c0da0069a99 100644
--- a/app/assets/javascripts/pages/projects/issues/form.js
+++ b/app/assets/javascripts/pages/projects/issues/form.js
@@ -5,6 +5,7 @@ import IssuableForm from 'ee_else_ce/issuable_form';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import GLForm from '~/gl_form';
import initSuggestions from '~/issuable_suggestions';
+import initIssuableTypeSelector from '~/issuable_type_selector';
import LabelsSelect from '~/labels_select';
import MilestoneSelect from '~/milestone_select';
import IssuableTemplateSelectors from '~/templates/issuable_template_selectors';
@@ -20,4 +21,5 @@ export default () => {
});
initSuggestions();
+ initIssuableTypeSelector();
};
diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js
index 366f8dc61bc..85489ae8687 100644
--- a/app/assets/javascripts/pages/projects/issues/index/index.js
+++ b/app/assets/javascripts/pages/projects/issues/index/index.js
@@ -20,7 +20,12 @@ initFilteredSearch({
useDefaultState: true,
});
-new IssuableIndex(ISSUABLE_INDEX.ISSUE);
+if (gon.features?.vueIssuesList) {
+ new IssuableIndex();
+} else {
+ new IssuableIndex(ISSUABLE_INDEX.ISSUE);
+}
+
new ShortcutsNavigation();
new UsersSelect();
diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js
index 992bf3c54ff..2b679a83eac 100644
--- a/app/assets/javascripts/pages/projects/issues/show.js
+++ b/app/assets/javascripts/pages/projects/issues/show.js
@@ -3,6 +3,8 @@ import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import initIssuableSidebar from '~/init_issuable_sidebar';
import initInviteMemberModal from '~/invite_member/init_invite_member_modal';
import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger';
+import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
+import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import { IssuableType } from '~/issuable_show/constants';
import Issue from '~/issue';
import '~/notes/index';
@@ -34,6 +36,8 @@ export default function initShowIssue() {
initIssueHeaderActions(store);
initSentryErrorStackTraceApp();
initRelatedMergeRequestsApp();
+ initInviteMembersModal();
+ initInviteMembersTrigger();
import(/* webpackChunkName: 'design_management' */ '~/design_management')
.then((module) => module.default())
@@ -42,10 +46,18 @@ export default function initShowIssue() {
new ZenMode(); // eslint-disable-line no-new
if (issueType !== IssuableType.TestCase) {
+ const awardEmojiEl = document.getElementById('js-vue-awards-block');
+
new Issue(); // eslint-disable-line no-new
new ShortcutsIssuable(); // eslint-disable-line no-new
initIssuableSidebar();
- loadAwardsHandler();
+ if (awardEmojiEl) {
+ import('~/emoji/awards_app')
+ .then((m) => m.default(awardEmojiEl))
+ .catch(() => {});
+ } else {
+ loadAwardsHandler();
+ }
initInviteMemberModal();
initInviteMemberTrigger();
}
diff --git a/app/assets/javascripts/pages/projects/jobs/index/index.js b/app/assets/javascripts/pages/projects/jobs/index/index.js
index 681d151b77f..75194499a7f 100644
--- a/app/assets/javascripts/pages/projects/jobs/index/index.js
+++ b/app/assets/javascripts/pages/projects/jobs/index/index.js
@@ -1,17 +1,23 @@
import Vue from 'vue';
+import initJobsTable from '~/jobs/components/table';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
-const remainingTimeElements = document.querySelectorAll('.js-remaining-time');
-remainingTimeElements.forEach(
- (el) =>
- new Vue({
- el,
- render(h) {
- return h(GlCountdown, {
- props: {
- endDateString: el.dateTime,
- },
- });
- },
- }),
-);
+if (gon.features?.jobsTableVue) {
+ initJobsTable();
+} else {
+ const remainingTimeElements = document.querySelectorAll('.js-remaining-time');
+
+ remainingTimeElements.forEach(
+ (el) =>
+ new Vue({
+ el,
+ render(h) {
+ return h(GlCountdown, {
+ props: {
+ endDateString: el.dateTime,
+ },
+ });
+ },
+ }),
+ );
+}
diff --git a/app/assets/javascripts/pages/projects/jobs/show/index.js b/app/assets/javascripts/pages/projects/jobs/show/index.js
index d57dbeb1242..6fef057dee0 100644
--- a/app/assets/javascripts/pages/projects/jobs/show/index.js
+++ b/app/assets/javascripts/pages/projects/jobs/show/index.js
@@ -1,3 +1,3 @@
import initJobDetails from '~/jobs';
-document.addEventListener('DOMContentLoaded', initJobDetails);
+initJobDetails();
diff --git a/app/assets/javascripts/pages/projects/labels/index/index.js b/app/assets/javascripts/pages/projects/labels/index/index.js
index 9f782c07101..94ab0d64de4 100644
--- a/app/assets/javascripts/pages/projects/labels/index/index.js
+++ b/app/assets/javascripts/pages/projects/labels/index/index.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import initDeleteLabelModal from '~/delete_label_modal';
import initLabels from '~/init_labels';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import Translate from '~/vue_shared/translate';
@@ -9,6 +10,7 @@ Vue.use(Translate);
const initLabelIndex = () => {
initLabels();
+ initDeleteLabelModal();
const onRequestFinished = ({ labelUrl, successful }) => {
const button = document.querySelector(
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue
index 32ca623ca45..ef9e13f7ccf 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_a.vue
@@ -1,11 +1,17 @@
<script>
-import { GlLink } from '@gitlab/ui';
-import { ACTION_LABELS } from '../constants';
+import { GlProgressBar, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { ACTION_LABELS, ACTION_SECTIONS } from '../constants';
+import LearnGitlabSectionCard from './learn_gitlab_section_card.vue';
export default {
- components: { GlLink },
+ components: { GlProgressBar, GlSprintf, LearnGitlabSectionCard },
i18n: {
- ACTION_LABELS,
+ title: s__('LearnGitLab|Learn GitLab'),
+ description: s__(
+ 'LearnGitLab|Ready to get started with GitLab? Follow these steps to set up your workspace, plan and commit changes, and deploy your project.',
+ ),
+ percentageCompleted: s__(`LearnGitLab|%{percentage}%{percentSymbol} completed`),
},
props: {
actions: {
@@ -13,15 +19,49 @@ export default {
type: Object,
},
},
+ maxValue: Object.keys(ACTION_LABELS).length,
+ sections: Object.keys(ACTION_SECTIONS),
+ computed: {
+ progressValue() {
+ return Object.values(this.actions).filter((a) => a.completed).length;
+ },
+ progressPercentage() {
+ return Math.round((this.progressValue / this.$options.maxValue) * 100);
+ },
+ },
+ methods: {
+ actionsFor(section) {
+ const actions = Object.fromEntries(
+ Object.entries(this.actions).filter(
+ ([action]) => ACTION_LABELS[action].section === section,
+ ),
+ );
+ return actions;
+ },
+ },
};
</script>
<template>
- <ul>
- <li v-for="(value, action) in actions" :key="action">
- <span v-if="value.completed">{{ $options.i18n.ACTION_LABELS[action].title }}</span>
- <span v-else>
- <gl-link :href="value.url">{{ $options.i18n.ACTION_LABELS[action].title }}</gl-link>
- </span>
- </li>
- </ul>
+ <div>
+ <div class="row">
+ <div class="gl-mb-7 gl-ml-5">
+ <h1 class="gl-font-size-h1">{{ $options.i18n.title }}</h1>
+ <p class="gl-text-gray-700 gl-mb-0">{{ $options.i18n.description }}</p>
+ </div>
+ </div>
+ <div class="gl-mb-3">
+ <p class="gl-text-gray-500 gl-mb-2" data-testid="completion-percentage">
+ <gl-sprintf :message="$options.i18n.percentageCompleted">
+ <template #percentage>{{ progressPercentage }}</template>
+ <template #percentSymbol>%</template>
+ </gl-sprintf>
+ </p>
+ <gl-progress-bar :value="progressValue" :max="$options.maxValue" />
+ </div>
+ <div class="row row-cols-1 row-cols-md-3 gl-mt-5">
+ <div v-for="section in $options.sections" :key="section" class="col gl-mb-6">
+ <learn-gitlab-section-card :section="section" :actions="actionsFor(section)" />
+ </div>
+ </div>
+ </div>
</template>
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_b.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_b.vue
index 230054ff76e..8f92ce95dbf 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_b.vue
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_b.vue
@@ -1,5 +1,6 @@
<script>
import { GlProgressBar, GlSprintf } from '@gitlab/ui';
+import { pick } from 'lodash';
import { s__ } from '~/locale';
import { ACTION_LABELS } from '../constants';
import LearnGitlabInfoCard from './learn_gitlab_info_card.vue';
@@ -42,7 +43,7 @@ export default {
infoProps(action) {
return {
...this.actions[action],
- ...ACTION_LABELS[action],
+ ...pick(ACTION_LABELS[action], ['title', 'actionLabel', 'description', 'trialRequired']),
};
},
progressValue() {
@@ -96,6 +97,9 @@ export default {
<div class="row row-cols-2 row-cols-md-3 row-cols-lg-4">
<div class="col gl-mb-6">
+ <learn-gitlab-info-card v-bind="infoProps('issueCreated')" />
+ </div>
+ <div class="col gl-mb-6">
<learn-gitlab-info-card v-bind="infoProps('mergeRequestCreated')" />
</div>
</div>
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_info_card.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_info_card.vue
index 3d2a8eed9d4..6cd3bbc359b 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_info_card.vue
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_info_card.vue
@@ -61,7 +61,7 @@ export default {
<div
class="gl-text-center gl-display-flex gl-justify-content-center gl-align-items-center gl-flex-direction-column learn-gitlab-info-card-content"
>
- <img :src="svg" />
+ <img :src="svg" :alt="actionLabel" />
<h6>{{ title }}</h6>
<p class="gl-font-sm gl-text-gray-700">{{ description }}</p>
<gl-link :href="url" target="_blank">{{ actionLabel }}</gl-link>
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_card.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_card.vue
new file mode 100644
index 00000000000..db694a66afd
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_card.vue
@@ -0,0 +1,52 @@
+<script>
+import { GlCard } from '@gitlab/ui';
+import { imagePath } from '~/lib/utils/common_utils';
+import { ACTION_LABELS, ACTION_SECTIONS } from '../constants';
+
+import LearnGitlabSectionLink from './learn_gitlab_section_link.vue';
+
+export default {
+ name: 'LearnGitlabSectionCard',
+ components: { GlCard, LearnGitlabSectionLink },
+ i18n: {
+ ...ACTION_SECTIONS,
+ },
+ props: {
+ section: {
+ required: true,
+ type: String,
+ },
+ actions: {
+ required: true,
+ type: Object,
+ },
+ },
+ computed: {
+ sortedActions() {
+ return Object.entries(this.actions).sort(
+ (a1, a2) => ACTION_LABELS[a1[0]].position - ACTION_LABELS[a2[0]].position,
+ );
+ },
+ },
+ methods: {
+ svg(section) {
+ return imagePath(`learn_gitlab/section_${section}.svg`);
+ },
+ },
+};
+</script>
+<template>
+ <gl-card class="gl-pt-0 learn-gitlab-section-card">
+ <div class="learn-gitlab-section-card-header">
+ <img :src="svg(section)" />
+ <h2 class="gl-font-lg gl-mb-3">{{ $options.i18n[section].title }}</h2>
+ <p class="gl-text-gray-700 gl-mb-6">{{ $options.i18n[section].description }}</p>
+ </div>
+ <learn-gitlab-section-link
+ v-for="[action, value] in sortedActions"
+ :key="action"
+ :action="action"
+ :value="value"
+ />
+ </gl-card>
+</template>
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue
new file mode 100644
index 00000000000..6f51c7372fd
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue
@@ -0,0 +1,43 @@
+<script>
+import { GlLink, GlIcon } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { ACTION_LABELS } from '../constants';
+
+export default {
+ name: 'LearnGitlabSectionLink',
+ components: { GlLink, GlIcon },
+ i18n: {
+ ACTION_LABELS,
+ trialOnly: s__('LearnGitlab|Trial only'),
+ },
+ props: {
+ action: {
+ required: true,
+ type: String,
+ },
+ value: {
+ required: true,
+ type: Object,
+ },
+ },
+ computed: {
+ trialOnly() {
+ return ACTION_LABELS[this.action].trialRequired;
+ },
+ },
+};
+</script>
+<template>
+ <div class="gl-mb-4">
+ <span v-if="value.completed" class="gl-text-green-500">
+ <gl-icon name="check-circle-filled" :size="16" data-testid="completed-icon" />
+ {{ $options.i18n.ACTION_LABELS[action].title }}
+ </span>
+ <span v-else>
+ <gl-link :href="value.url">{{ $options.i18n.ACTION_LABELS[action].title }}</gl-link>
+ </span>
+ <span v-if="trialOnly" class="gl-font-style-italic gl-text-gray-500" data-testid="trial-only">
+ - {{ $options.i18n.trialOnly }}
+ </span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js b/app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js
index 80f04b0cf44..9e204aa6746 100644
--- a/app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js
+++ b/app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js
@@ -5,6 +5,8 @@ export const ACTION_LABELS = {
title: s__('LearnGitLab|Create or import a repository'),
actionLabel: s__('LearnGitLab|Create or import a repository'),
description: s__('LearnGitLab|Create or import your first repository into your new project.'),
+ section: 'workspace',
+ position: 1,
},
userAdded: {
title: s__('LearnGitLab|Invite your colleagues'),
@@ -12,16 +14,22 @@ export const ACTION_LABELS = {
description: s__(
'LearnGitLab|GitLab works best as a team. Invite your colleague to enjoy all features.',
),
+ section: 'workspace',
+ position: 0,
},
pipelineCreated: {
title: s__('LearnGitLab|Set up CI/CD'),
actionLabel: s__('LearnGitLab|Set-up CI/CD'),
description: s__('LearnGitLab|Save time by automating your integration and deployment tasks.'),
+ section: 'workspace',
+ position: 2,
},
trialStarted: {
title: s__('LearnGitLab|Start a free Ultimate trial'),
actionLabel: s__('LearnGitLab|Try GitLab Ultimate for free'),
description: s__('LearnGitLab|Try all GitLab features for 30 days, no credit card required.'),
+ section: 'workspace',
+ position: 3,
},
codeOwnersEnabled: {
title: s__('LearnGitLab|Add code owners'),
@@ -30,21 +38,59 @@ export const ACTION_LABELS = {
'LearnGitLab|Prevent unexpected changes to important assets by assigning ownership of files and paths.',
),
trialRequired: true,
+ section: 'workspace',
+ position: 4,
},
requiredMrApprovalsEnabled: {
title: s__('LearnGitLab|Add merge request approval'),
actionLabel: s__('LearnGitLab|Enable require merge approvals'),
description: s__('LearnGitLab|Route code reviews to the right reviewers, every time.'),
trialRequired: true,
+ section: 'workspace',
+ position: 5,
},
mergeRequestCreated: {
title: s__('LearnGitLab|Submit a merge request'),
actionLabel: s__('LearnGitLab|Submit a merge request (MR)'),
description: s__('LearnGitLab|Review and edit proposed changes to source code.'),
+ section: 'plan',
+ position: 1,
},
securityScanEnabled: {
- title: s__('LearnGitLab|Run a security scan'),
- actionLabel: s__('LearnGitLab|Run a Security scan'),
+ title: s__('LearnGitLab|Run a Security scan using CI/CD'),
+ actionLabel: s__('LearnGitLab|Run a Security scan using CI/CD'),
description: s__('LearnGitLab|Scan your code to uncover vulnerabilities before deploying.'),
+ section: 'deploy',
+ position: 1,
+ },
+ issueCreated: {
+ title: s__('LearnGitLab|Create an issue'),
+ actionLabel: s__('LearnGitLab|Create an issue'),
+ description: s__(
+ 'LearnGitLab|Create/import issues (tickets) to collaborate on ideas and plan work.',
+ ),
+ section: 'plan',
+ position: 0,
+ },
+};
+
+export const ACTION_SECTIONS = {
+ workspace: {
+ title: s__('LearnGitLab|Set up your workspace'),
+ description: s__(
+ "LearnGitLab|Complete these tasks first so you can enjoy GitLab's features to their fullest:",
+ ),
+ },
+ plan: {
+ title: s__('LearnGitLab|Plan and execute'),
+ description: s__(
+ 'LearnGitLab|Create a workflow for your new workspace, and learn how GitLab features work together:',
+ ),
+ },
+ deploy: {
+ title: s__('LearnGitLab|Deploy'),
+ description: s__(
+ 'LearnGitLab|Use your new GitLab workflow to deploy your application, monitor its health, and keep it secure:',
+ ),
},
};
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
index d4d5e9f2711..a5118e3529a 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
@@ -5,21 +5,33 @@ import initPipelines from '~/commit/pipelines/pipelines_bundle';
import initIssuableSidebar from '~/init_issuable_sidebar';
import initInviteMemberModal from '~/invite_member/init_invite_member_modal';
import initInviteMemberTrigger from '~/invite_member/init_invite_member_trigger';
+import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
+import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import { handleLocationHash } from '~/lib/utils/common_utils';
import StatusBox from '~/merge_request/components/status_box.vue';
import initSourcegraph from '~/sourcegraph';
import ZenMode from '~/zen_mode';
export default function initMergeRequestShow() {
+ const awardEmojiEl = document.getElementById('js-vue-awards-block');
+
new ZenMode(); // eslint-disable-line no-new
initIssuableSidebar();
initPipelines();
new ShortcutsIssuable(true); // eslint-disable-line no-new
handleLocationHash();
initSourcegraph();
- loadAwardsHandler();
+ if (awardEmojiEl) {
+ import('~/emoji/awards_app')
+ .then((m) => m.default(awardEmojiEl))
+ .catch(() => {});
+ } else {
+ loadAwardsHandler();
+ }
initInviteMemberModal();
initInviteMemberTrigger();
+ initInviteMembersModal();
+ initInviteMembersTrigger();
const el = document.querySelector('.js-mr-status-box');
// eslint-disable-next-line no-new
diff --git a/app/assets/javascripts/pages/projects/packages/infrastructure_registry/index/index.js b/app/assets/javascripts/pages/projects/packages/infrastructure_registry/index/index.js
new file mode 100644
index 00000000000..dfb750eca41
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/packages/infrastructure_registry/index/index.js
@@ -0,0 +1,3 @@
+import initList from '~/packages_and_registries/infrastructure_registry/list_app_bundle';
+
+initList();
diff --git a/app/assets/javascripts/pages/projects/pages/index.js b/app/assets/javascripts/pages/projects/pages/index.js
new file mode 100644
index 00000000000..2a120a690ef
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/pages/index.js
@@ -0,0 +1,3 @@
+import initSearchSettings from '~/search_settings';
+
+initSearchSettings();
diff --git a/app/assets/javascripts/pages/projects/path_locks/index.js b/app/assets/javascripts/pages/projects/path_locks/index.js
new file mode 100644
index 00000000000..e5ab5d43bbf
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/path_locks/index.js
@@ -0,0 +1,3 @@
+import initDeprecatedRemoveRowBehavior from '~/behaviors/deprecated_remove_row_behavior';
+
+document.addEventListener('DOMContentLoaded', initDeprecatedRemoveRowBehavior);
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
index 3b19231720a..159c619e16c 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
@@ -139,7 +139,7 @@ export default {
v-model="cronInterval"
:placeholder="__('Define a custom pattern with cron syntax')"
:name="inputNameAttribute"
- class="form-control inline cron-interval-input"
+ class="form-control inline cron-interval-input gl-form-input"
type="text"
required="true"
@input="onCustomInput"
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index da8dc527d79..91f376060f8 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -123,10 +123,19 @@ export default class Project {
const loc = window.location.href;
if (loc.includes('/-/')) {
- const refs = this.fullData.Branches.concat(this.fullData.Tags);
- const currentRef = refs.find((ref) => loc.indexOf(ref) > -1);
- if (currentRef) {
- const targetPath = loc.split(currentRef)[1].slice(1).split('#')[0];
+ // Since the current ref in renderRow is outdated on page changes
+ // (To be addressed in: https://gitlab.com/gitlab-org/gitlab/-/issues/327085)
+ // We are deciphering the current ref from the dropdown data instead
+ const currentRef = $dropdown.data('ref');
+ // The split and startWith is to ensure an exact word match
+ // and avoid partial match ie. currentRef is "dev" and loc is "development"
+ const splitPathAfterRefPortion = loc.split(currentRef)[1];
+ const doesPathContainRef = splitPathAfterRefPortion?.startsWith('/');
+
+ if (doesPathContainRef) {
+ // We are ignoring the url containing the ref portion
+ // and plucking the thereafter portion to reconstructure the url that is correct
+ const targetPath = splitPathAfterRefPortion?.slice(1).split('#')[0];
selectedUrl.searchParams.set('path', targetPath);
selectedUrl.hash = window.location.hash;
}
diff --git a/app/assets/javascripts/pages/projects/project_members/index.js b/app/assets/javascripts/pages/projects/project_members/index.js
index 4aea5614bfb..471798d2931 100644
--- a/app/assets/javascripts/pages/projects/project_members/index.js
+++ b/app/assets/javascripts/pages/projects/project_members/index.js
@@ -7,6 +7,7 @@ import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigg
import { s__ } from '~/locale';
import memberExpirationDate from '~/member_expiration_date';
import { initMembersApp } from '~/members';
+import { MEMBER_TYPES } from '~/members/constants';
import { groupLinkRequestFormatter } from '~/members/utils';
import { projectMemberRequestFormatter } from '~/projects/members/utils';
import UsersSelect from '~/users_select';
@@ -42,6 +43,7 @@ new UsersSelect(); // eslint-disable-line no-new
const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions'];
initMembersApp(document.querySelector('.js-project-members-list'), {
+ namespace: MEMBER_TYPES.user,
tableFields: SHARED_FIELDS.concat(['source', 'granted']),
tableAttrs: { tr: { 'data-qa-selector': 'member_row' } },
tableSortableFields: ['account', 'granted', 'maxRole', 'lastSignIn'],
@@ -56,6 +58,7 @@ initMembersApp(document.querySelector('.js-project-members-list'), {
});
initMembersApp(document.querySelector('.js-project-group-links-list'), {
+ namespace: MEMBER_TYPES.group,
tableFields: SHARED_FIELDS.concat('granted'),
tableAttrs: {
table: { 'data-qa-selector': 'groups_list' },
@@ -72,11 +75,13 @@ initMembersApp(document.querySelector('.js-project-group-links-list'), {
});
initMembersApp(document.querySelector('.js-project-invited-members-list'), {
+ namespace: MEMBER_TYPES.invite,
tableFields: SHARED_FIELDS.concat('invited'),
requestFormatter: projectMemberRequestFormatter,
});
initMembersApp(document.querySelector('.js-project-access-requests-list'), {
+ namespace: MEMBER_TYPES.accessRequest,
tableFields: SHARED_FIELDS.concat('requested'),
requestFormatter: projectMemberRequestFormatter,
});
diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
index b7e8d4b03ac..be9259ec3ca 100644
--- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
@@ -6,7 +6,6 @@ import initDeployFreeze from '~/deploy_freeze';
import { initInstallRunner } from '~/pages/shared/mount_runner_instructions';
import initSharedRunnersToggle from '~/projects/settings/mount_shared_runners_toggle';
import registrySettingsApp from '~/registry/settings/registry_settings_bundle';
-import initSearchSettings from '~/search_settings';
import initSettingsPanels from '~/settings_panels';
document.addEventListener('DOMContentLoaded', () => {
@@ -43,6 +42,4 @@ document.addEventListener('DOMContentLoaded', () => {
}
initInstallRunner();
-
- initSearchSettings();
});
diff --git a/app/assets/javascripts/pages/projects/settings/index.js b/app/assets/javascripts/pages/projects/settings/index.js
new file mode 100644
index 00000000000..cb787c60002
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/settings/index.js
@@ -0,0 +1,5 @@
+import initRevokeButton from '~/deploy_tokens/init_revoke_button';
+import initSearchSettings from '~/search_settings';
+
+initSearchSettings();
+initRevokeButton();
diff --git a/app/assets/javascripts/pages/projects/settings/integrations/show/index.js b/app/assets/javascripts/pages/projects/settings/integrations/show/index.js
index bf9ccdbf9a8..01ad87160c5 100644
--- a/app/assets/javascripts/pages/projects/settings/integrations/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/integrations/show/index.js
@@ -1,4 +1,7 @@
+import initIntegrationsList from '~/integrations/index';
import PersistentUserCallout from '~/persistent_user_callout';
const callout = document.querySelector('.js-webhooks-moved-alert');
PersistentUserCallout.factory(callout);
+
+initIntegrationsList();
diff --git a/app/assets/javascripts/pages/projects/settings/operations/show/index.js b/app/assets/javascripts/pages/projects/settings/operations/show/index.js
index 4a800ab150d..3a46241e2eb 100644
--- a/app/assets/javascripts/pages/projects/settings/operations/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/operations/show/index.js
@@ -3,7 +3,6 @@ import mountErrorTrackingForm from '~/error_tracking_settings';
import mountGrafanaIntegration from '~/grafana_integration';
import initIncidentsSettings from '~/incidents_settings';
import mountOperationSettings from '~/operation_settings';
-import initSearchSettings from '~/search_settings';
import initSettingsPanels from '~/settings_panels';
initIncidentsSettings();
@@ -14,7 +13,3 @@ if (!IS_EE) {
initSettingsPanels();
}
mountAlertsSettings(document.querySelector('.js-alerts-settings'));
-
-document.addEventListener('DOMContentLoaded', () => {
- initSearchSettings();
-});
diff --git a/app/assets/javascripts/pages/projects/settings/repository/show/index.js b/app/assets/javascripts/pages/projects/settings/repository/show/index.js
index c7bcbb83051..e90954c14c5 100644
--- a/app/assets/javascripts/pages/projects/settings/repository/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/repository/show/index.js
@@ -1,5 +1,4 @@
import MirrorRepos from '~/mirrors/mirror_repos';
-import initSearchSettings from '~/search_settings';
import initForm from '../form';
document.addEventListener('DOMContentLoaded', () => {
@@ -7,6 +6,4 @@ document.addEventListener('DOMContentLoaded', () => {
const mirrorReposContainer = document.querySelector('.js-mirror-settings');
if (mirrorReposContainer) new MirrorRepos(mirrorReposContainer).init();
-
- initSearchSettings();
});
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue
index d62df77ad2c..c110c1d4d62 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue
@@ -12,6 +12,11 @@ export default {
event: 'change',
},
props: {
+ label: {
+ type: String,
+ required: false,
+ default: '',
+ },
name: {
type: String,
required: false,
@@ -82,6 +87,8 @@ export default {
class="gl-mr-3"
:value="featureEnabled"
:disabled="disabledInput"
+ :label="label"
+ label-position="hidden"
@change="toggleFeature"
/>
<div class="select-wrapper gl-flex-fill-1">
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index 0b58cb4731d..0b7b4c0ded1 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -22,6 +22,21 @@ const PAGE_FEATURE_ACCESS_LEVEL = s__('ProjectSettings|Everyone');
export default {
i18n: {
...CVE_ID_REQUEST_BUTTON_I18N,
+ analyticsLabel: s__('ProjectSettings|Analytics'),
+ containerRegistryLabel: s__('ProjectSettings|Container registry'),
+ forksLabel: s__('ProjectSettings|Forks'),
+ issuesLabel: s__('ProjectSettings|Issues'),
+ lfsLabel: s__('ProjectSettings|Git Large File Storage (LFS)'),
+ mergeRequestsLabel: s__('ProjectSettings|Merge requests'),
+ operationsLabel: s__('ProjectSettings|Operations'),
+ packagesLabel: s__('ProjectSettings|Packages'),
+ pagesLabel: s__('ProjectSettings|Pages'),
+ ciCdLabel: s__('CI/CD'),
+ repositoryLabel: s__('ProjectSettings|Repository'),
+ requirementsLabel: s__('ProjectSettings|Requirements'),
+ securityAndComplianceLabel: s__('ProjectSettings|Security & Compliance'),
+ snippetsLabel: s__('ProjectSettings|Snippets'),
+ wikiLabel: s__('ProjectSettings|Wiki'),
},
components: {
@@ -423,11 +438,12 @@ export default {
>
<project-setting-row
ref="issues-settings"
- :label="s__('ProjectSettings|Issues')"
+ :label="$options.i18n.issuesLabel"
:help-text="s__('ProjectSettings|Lightweight issue tracking system.')"
>
<project-feature-setting
v-model="issuesAccessLevel"
+ :label="$options.i18n.issuesLabel"
:options="featureAccessLevelOptions"
name="project[project_feature_attributes][issues_access_level]"
/>
@@ -440,6 +456,8 @@ export default {
v-model="cveIdRequestEnabled"
class="gl-my-2"
:disabled="cveIdRequestIsDisabled"
+ :label="$options.i18n.cve_request_toggle_label"
+ label-position="hidden"
name="project[project_setting_attributes][cve_id_request_enabled]"
data-testid="cve_id_request_toggle"
/>
@@ -447,11 +465,12 @@ export default {
</project-setting-row>
<project-setting-row
ref="repository-settings"
- :label="s__('ProjectSettings|Repository')"
+ :label="$options.i18n.repositoryLabel"
:help-text="repositoryHelpText"
>
<project-feature-setting
v-model="repositoryAccessLevel"
+ :label="$options.i18n.repositoryLabel"
:options="featureAccessLevelOptions"
name="project[project_feature_attributes][repository_access_level]"
/>
@@ -459,11 +478,12 @@ export default {
<div class="project-feature-setting-group gl-pl-7 gl-sm-pl-5">
<project-setting-row
ref="merge-request-settings"
- :label="s__('ProjectSettings|Merge requests')"
+ :label="$options.i18n.mergeRequestsLabel"
:help-text="s__('ProjectSettings|Submit changes to be merged upstream.')"
>
<project-feature-setting
v-model="mergeRequestsAccessLevel"
+ :label="$options.i18n.mergeRequestsLabel"
:options="repoFeatureAccessLevelOptions"
:disabled-input="!repositoryEnabled"
name="project[project_feature_attributes][merge_requests_access_level]"
@@ -471,33 +491,22 @@ export default {
</project-setting-row>
<project-setting-row
ref="fork-settings"
- :label="s__('ProjectSettings|Forks')"
+ :label="$options.i18n.forksLabel"
:help-text="s__('ProjectSettings|Users can copy the repository to a new project.')"
>
<project-feature-setting
v-model="forkingAccessLevel"
+ :label="$options.i18n.forksLabel"
:options="featureAccessLevelOptions"
:disabled-input="!repositoryEnabled"
name="project[project_feature_attributes][forking_access_level]"
/>
</project-setting-row>
<project-setting-row
- ref="pipeline-settings"
- :label="s__('ProjectSettings|Pipelines')"
- :help-text="s__('ProjectSettings|Build, test, and deploy your changes.')"
- >
- <project-feature-setting
- v-model="buildsAccessLevel"
- :options="repoFeatureAccessLevelOptions"
- :disabled-input="!repositoryEnabled"
- name="project[project_feature_attributes][builds_access_level]"
- />
- </project-setting-row>
- <project-setting-row
v-if="registryAvailable"
ref="container-registry-settings"
:help-path="registryHelpPath"
- :label="s__('ProjectSettings|Container registry')"
+ :label="$options.i18n.containerRegistryLabel"
:help-text="
s__('ProjectSettings|Every project can have its own space to store its Docker images')
"
@@ -513,6 +522,8 @@ export default {
v-model="containerRegistryEnabled"
class="gl-my-2"
:disabled="!repositoryEnabled"
+ :label="$options.i18n.containerRegistryLabel"
+ label-position="hidden"
name="project[container_registry_enabled]"
/>
</project-setting-row>
@@ -520,7 +531,7 @@ export default {
v-if="lfsAvailable"
ref="git-lfs-settings"
:help-path="lfsHelpPath"
- :label="s__('ProjectSettings|Git Large File Storage (LFS)')"
+ :label="$options.i18n.lfsLabel"
:help-text="
s__('ProjectSettings|Manages large files such as audio, video, and graphics files.')
"
@@ -529,6 +540,8 @@ export default {
v-model="lfsEnabled"
class="gl-my-2"
:disabled="!repositoryEnabled"
+ :label="$options.i18n.lfsLabel"
+ label-position="hidden"
name="project[lfs_enabled]"
/>
<p v-if="!lfsEnabled && lfsObjectsExist">
@@ -553,7 +566,7 @@ export default {
v-if="packagesAvailable"
ref="package-settings"
:help-path="packagesHelpPath"
- :label="s__('ProjectSettings|Packages')"
+ :label="$options.i18n.packagesLabel"
:help-text="
s__('ProjectSettings|Every project can have its own space to store its packages.')
"
@@ -562,17 +575,33 @@ export default {
v-model="packagesEnabled"
class="gl-my-2"
:disabled="!repositoryEnabled"
+ :label="$options.i18n.packagesLabel"
+ label-position="hidden"
name="project[packages_enabled]"
/>
</project-setting-row>
</div>
<project-setting-row
+ ref="pipeline-settings"
+ :label="$options.i18n.ciCdLabel"
+ :help-text="s__('ProjectSettings|Build, test, and deploy your changes.')"
+ >
+ <project-feature-setting
+ v-model="buildsAccessLevel"
+ :label="$options.i18n.ciCdLabel"
+ :options="repoFeatureAccessLevelOptions"
+ :disabled-input="!repositoryEnabled"
+ name="project[project_feature_attributes][builds_access_level]"
+ />
+ </project-setting-row>
+ <project-setting-row
ref="analytics-settings"
- :label="s__('ProjectSettings|Analytics')"
+ :label="$options.i18n.analyticsLabel"
:help-text="s__('ProjectSettings|View project analytics.')"
>
<project-feature-setting
v-model="analyticsAccessLevel"
+ :label="$options.i18n.analyticsLabel"
:options="featureAccessLevelOptions"
name="project[project_feature_attributes][analytics_access_level]"
/>
@@ -580,43 +609,47 @@ export default {
<project-setting-row
v-if="requirementsAvailable"
ref="requirements-settings"
- :label="s__('ProjectSettings|Requirements')"
+ :label="$options.i18n.requirementsLabel"
:help-text="s__('ProjectSettings|Requirements management system.')"
>
<project-feature-setting
v-model="requirementsAccessLevel"
+ :label="$options.i18n.requirementsLabel"
:options="featureAccessLevelOptions"
name="project[project_feature_attributes][requirements_access_level]"
/>
</project-setting-row>
<project-setting-row
- :label="s__('ProjectSettings|Security & Compliance')"
+ :label="$options.i18n.securityAndComplianceLabel"
:help-text="s__('ProjectSettings|Security & Compliance for this project')"
>
<project-feature-setting
v-model="securityAndComplianceAccessLevel"
+ :label="$options.i18n.securityAndComplianceLabel"
:options="featureAccessLevelOptions"
name="project[project_feature_attributes][security_and_compliance_access_level]"
/>
</project-setting-row>
<project-setting-row
ref="wiki-settings"
- :label="s__('ProjectSettings|Wiki')"
+ :label="$options.i18n.wikiLabel"
:help-text="s__('ProjectSettings|Pages for project documentation.')"
>
<project-feature-setting
v-model="wikiAccessLevel"
+ :label="$options.i18n.wikiLabel"
:options="featureAccessLevelOptions"
name="project[project_feature_attributes][wiki_access_level]"
/>
</project-setting-row>
<project-setting-row
ref="snippet-settings"
- :label="s__('ProjectSettings|Snippets')"
+ :label="$options.i18n.snippetsLabel"
:help-text="s__('ProjectSettings|Share code with others outside the project.')"
>
<project-feature-setting
v-model="snippetsAccessLevel"
+ :label="$options.i18n.snippetsLabel"
:options="featureAccessLevelOptions"
name="project[project_feature_attributes][snippets_access_level]"
/>
@@ -625,26 +658,28 @@ export default {
v-if="pagesAvailable && pagesAccessControlEnabled"
ref="pages-settings"
:help-path="pagesHelpPath"
- :label="s__('ProjectSettings|Pages')"
+ :label="$options.i18n.pagesLabel"
:help-text="
s__('ProjectSettings|With GitLab Pages you can host your static websites on GitLab.')
"
>
<project-feature-setting
v-model="pagesAccessLevel"
+ :label="$options.i18n.pagesLabel"
:options="pagesFeatureAccessLevelOptions"
name="project[project_feature_attributes][pages_access_level]"
/>
</project-setting-row>
<project-setting-row
ref="operations-settings"
- :label="s__('ProjectSettings|Operations')"
+ :label="$options.i18n.operationsLabel"
:help-text="
s__('ProjectSettings|Configure your project resources and monitor their health.')
"
>
<project-feature-setting
v-model="operationsAccessLevel"
+ :label="$options.i18n.operationsLabel"
:options="featureAccessLevelOptions"
name="project[project_feature_attributes][operations_access_level]"
/>
diff --git a/app/assets/javascripts/pages/projects/tags/index/index.js b/app/assets/javascripts/pages/projects/tags/index/index.js
index 98560c1193b..9e48dd9e463 100644
--- a/app/assets/javascripts/pages/projects/tags/index/index.js
+++ b/app/assets/javascripts/pages/projects/tags/index/index.js
@@ -1,3 +1,4 @@
+import TagSortDropdown from '~/tags';
import { initRemoveTag } from '../remove_tag';
initRemoveTag({
@@ -5,3 +6,4 @@ initRemoveTag({
document.querySelector(`[data-path="${path}"]`).closest('.js-tag-list').remove();
},
});
+TagSortDropdown();
diff --git a/app/assets/javascripts/pages/shared/mount_runner_instructions.js b/app/assets/javascripts/pages/shared/mount_runner_instructions.js
index 51028e585b8..e83c73edfde 100644
--- a/app/assets/javascripts/pages/shared/mount_runner_instructions.js
+++ b/app/assets/javascripts/pages/shared/mount_runner_instructions.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
-import InstallRunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue';
+import RunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue';
Vue.use(VueApollo);
@@ -10,7 +10,6 @@ export function initInstallRunner(componentId = 'js-install-runner') {
if (installRunnerEl) {
const defaultClient = createDefaultClient();
- const { projectPath, groupPath } = installRunnerEl.dataset;
const apolloProvider = new VueApollo({
defaultClient,
@@ -20,12 +19,8 @@ export function initInstallRunner(componentId = 'js-install-runner') {
new Vue({
el: installRunnerEl,
apolloProvider,
- provide: {
- projectPath,
- groupPath,
- },
render(createElement) {
- return createElement(InstallRunnerInstructions);
+ return createElement(RunnerInstructions);
},
});
}
diff --git a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
new file mode 100644
index 00000000000..6afc33ec8a5
--- /dev/null
+++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
@@ -0,0 +1,253 @@
+<script>
+import { GlForm, GlIcon, GlLink, GlButton, GlSprintf } from '@gitlab/ui';
+import csrf from '~/lib/utils/csrf';
+import { setUrlFragment } from '~/lib/utils/url_utility';
+import { __, s__, sprintf } from '~/locale';
+import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+
+const MARKDOWN_LINK_TEXT = {
+ markdown: '[Link Title](page-slug)',
+ rdoc: '{Link title}[link:page-slug]',
+ asciidoc: 'link:page-slug[Link title]',
+ org: '[[page-slug]]',
+};
+
+export default {
+ components: {
+ GlForm,
+ GlSprintf,
+ GlIcon,
+ GlLink,
+ GlButton,
+ MarkdownField,
+ },
+ inject: ['formatOptions', 'pageInfo'],
+ data() {
+ return {
+ title: this.pageInfo.title?.trim() || '',
+ format: this.pageInfo.format || 'markdown',
+ content: this.pageInfo.content?.trim() || '',
+ commitMessage: '',
+ };
+ },
+ computed: {
+ csrfToken() {
+ return csrf.token;
+ },
+ formAction() {
+ return this.pageInfo.persisted ? this.pageInfo.path : this.pageInfo.createPath;
+ },
+ helpPath() {
+ return setUrlFragment(
+ this.pageInfo.helpPath,
+ this.pageInfo.persisted ? 'move-a-wiki-page' : 'create-a-new-wiki-page',
+ );
+ },
+ commitMessageI18n() {
+ return this.pageInfo.persisted
+ ? s__('WikiPage|Update %{pageTitle}')
+ : s__('WikiPage|Create %{pageTitle}');
+ },
+ linkExample() {
+ return MARKDOWN_LINK_TEXT[this.format];
+ },
+ submitButtonText() {
+ if (this.pageInfo.persisted) return __('Save changes');
+ return s__('WikiPage|Create page');
+ },
+ cancelFormPath() {
+ if (this.pageInfo.persisted) return this.pageInfo.path;
+ return this.pageInfo.wikiPath;
+ },
+ wikiSpecificMarkdownHelpPath() {
+ return setUrlFragment(this.pageInfo.markdownHelpPath, 'wiki-specific-markdown');
+ },
+ },
+ mounted() {
+ this.updateCommitMessage();
+ },
+ methods: {
+ handleFormSubmit() {
+ window.removeEventListener('beforeunload', this.onBeforeUnload);
+ },
+
+ handleContentChange() {
+ window.addEventListener('beforeunload', this.onBeforeUnload);
+ },
+
+ onBeforeUnload() {
+ return '';
+ },
+
+ updateCommitMessage() {
+ if (!this.title) return;
+
+ // Replace hyphens with spaces
+ const newTitle = this.title.replace(/-+/g, ' ');
+
+ const newCommitMessage = sprintf(this.commitMessageI18n, { pageTitle: newTitle }, false);
+ this.commitMessage = newCommitMessage;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form
+ :action="formAction"
+ method="post"
+ class="wiki-form common-note-form gl-mt-3 js-quick-submit"
+ @submit="handleFormSubmit"
+ >
+ <input :value="csrfToken" type="hidden" name="authenticity_token" />
+ <input v-if="pageInfo.persisted" type="hidden" name="_method" value="put" />
+ <input
+ :v-if="pageInfo.persisted"
+ type="hidden"
+ name="wiki[last_commit_sha]"
+ :value="pageInfo.lastCommitSha"
+ />
+ <div class="form-group row">
+ <div class="col-sm-2 col-form-label">
+ <label class="control-label-full-width" for="wiki_title">{{ s__('WikiPage|Title') }}</label>
+ </div>
+ <div class="col-sm-10">
+ <input
+ id="wiki_title"
+ v-model.trim="title"
+ name="wiki[title]"
+ type="text"
+ class="form-control"
+ data-qa-selector="wiki_title_textbox"
+ :required="true"
+ :autofocus="!pageInfo.persisted"
+ :placeholder="s__('WikiPage|Page title')"
+ @input="updateCommitMessage"
+ />
+ <span class="gl-display-inline-block gl-max-w-full gl-mt-2 gl-text-gray-600">
+ <gl-icon class="gl-mr-n1" name="bulb" />
+ {{
+ pageInfo.persisted
+ ? s__(
+ 'WikiPage|Tip: You can move this page by adding the path to the beginning of the title.',
+ )
+ : s__(
+ 'WikiPage|Tip: You can specify the full path for the new file. We will automatically create any missing directories.',
+ )
+ }}
+ <gl-link :href="helpPath" target="_blank" data-testid="wiki-title-help-link"
+ ><gl-icon name="question-o" /> {{ __('More Information.') }}</gl-link
+ >
+ </span>
+ </div>
+ </div>
+ <div class="form-group row">
+ <div class="col-sm-2 col-form-label">
+ <label class="control-label-full-width" for="wiki_format">{{
+ s__('WikiPage|Format')
+ }}</label>
+ </div>
+ <div class="col-sm-10">
+ <select id="wiki_format" v-model="format" class="form-control" name="wiki[format]">
+ <option v-for="(key, label) of formatOptions" :key="key" :value="key">
+ {{ label }}
+ </option>
+ </select>
+ </div>
+ </div>
+ <div class="form-group row">
+ <div class="col-sm-2 col-form-label">
+ <label class="control-label-full-width" for="wiki_content">{{
+ s__('WikiPage|Content')
+ }}</label>
+ </div>
+ <div class="col-sm-10">
+ <markdown-field
+ :markdown-preview-path="pageInfo.markdownPreviewPath"
+ :can-attach-file="true"
+ :enable-autocomplete="true"
+ :textarea-value="content"
+ :markdown-docs-path="pageInfo.markdownHelpPath"
+ :uploads-path="pageInfo.uploadsPath"
+ class="bordered-box"
+ >
+ <template #textarea>
+ <textarea
+ id="wiki_content"
+ ref="textarea"
+ v-model.trim="content"
+ name="wiki[content]"
+ class="note-textarea js-gfm-input js-autosize markdown-area"
+ dir="auto"
+ data-supports-quick-actions="false"
+ data-qa-selector="wiki_content_textarea"
+ :autofocus="pageInfo.persisted"
+ :aria-label="s__('WikiPage|Content')"
+ :placeholder="s__('WikiPage|Write your content or drag files hereā€¦')"
+ @input="handleContentChange"
+ >
+ </textarea>
+ </template>
+ </markdown-field>
+ <div class="clearfix"></div>
+ <div class="error-alert"></div>
+
+ <div class="form-text gl-text-gray-600">
+ <gl-sprintf
+ :message="
+ s__(
+ 'WikiPage|To link to a (new) page, simply type %{linkExample}. More examples are in the %{linkStart}documentation%{linkEnd}.',
+ )
+ "
+ >
+ <template #linkExample
+ ><code>{{ linkExample }}</code></template
+ >
+ <template
+ #link="// eslint-disable-next-line vue/no-template-shadow
+ { content }"
+ ><gl-link
+ :href="wikiSpecificMarkdownHelpPath"
+ target="_blank"
+ data-testid="wiki-markdown-help-link"
+ >{{ content }}</gl-link
+ ></template
+ >
+ </gl-sprintf>
+ </div>
+ </div>
+ </div>
+ <div class="form-group row">
+ <div class="col-sm-2 col-form-label">
+ <label class="control-label-full-width" for="wiki_message">{{
+ s__('WikiPage|Commit message')
+ }}</label>
+ </div>
+ <div class="col-sm-10">
+ <input
+ id="wiki_message"
+ v-model.trim="commitMessage"
+ name="wiki[message]"
+ type="text"
+ class="form-control"
+ data-qa-selector="wiki_message_textbox"
+ :placeholder="s__('WikiPage|Commit message')"
+ />
+ </div>
+ </div>
+ <div class="form-actions">
+ <gl-button
+ category="primary"
+ variant="confirm"
+ type="submit"
+ data-qa-selector="wiki_submit_button"
+ data-testid="wiki-submit-button"
+ :disabled="!content || !title"
+ >{{ submitButtonText }}</gl-button
+ >
+ <gl-button :href="cancelFormPath" class="float-right" data-testid="wiki-cancel-button">{{
+ __('Cancel')
+ }}</gl-button>
+ </div>
+ </gl-form>
+</template>
diff --git a/app/assets/javascripts/pages/shared/wikis/index.js b/app/assets/javascripts/pages/shared/wikis/index.js
index c382a372260..c04cd0b3fa4 100644
--- a/app/assets/javascripts/pages/shared/wikis/index.js
+++ b/app/assets/javascripts/pages/shared/wikis/index.js
@@ -1,12 +1,14 @@
import $ from 'jquery';
import Vue from 'vue';
import ShortcutsWiki from '~/behaviors/shortcuts/shortcuts_wiki';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import csrf from '~/lib/utils/csrf';
import Translate from '~/vue_shared/translate';
import GLForm from '../../../gl_form';
import ZenMode from '../../../zen_mode';
import deleteWikiModal from './components/delete_wiki_modal.vue';
import wikiAlert from './components/wiki_alert.vue';
+import wikiForm from './components/wiki_form.vue';
import Wikis from './wikis';
const createModalVueApp = () => {
@@ -61,7 +63,28 @@ const createAlertVueApp = () => {
}
};
+const createWikiFormApp = () => {
+ const el = document.getElementById('js-wiki-form');
+
+ if (el) {
+ const { pageInfo, formatOptions } = el.dataset;
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ provide: {
+ formatOptions: JSON.parse(formatOptions),
+ pageInfo: convertObjectPropsToCamelCase(JSON.parse(pageInfo)),
+ },
+ render(createElement) {
+ return createElement(wikiForm);
+ },
+ });
+ }
+};
+
export default () => {
createModalVueApp();
createAlertVueApp();
+ createWikiFormApp();
};
diff --git a/app/assets/javascripts/pages/shared/wikis/wikis.js b/app/assets/javascripts/pages/shared/wikis/wikis.js
index 4b4d2f7d238..7d0b0c90c8d 100644
--- a/app/assets/javascripts/pages/shared/wikis/wikis.js
+++ b/app/assets/javascripts/pages/shared/wikis/wikis.js
@@ -1,15 +1,7 @@
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
-import { s__, sprintf } from '~/locale';
import Tracking from '~/tracking';
import showToast from '~/vue_shared/plugins/global_toast';
-const MARKDOWN_LINK_TEXT = {
- markdown: '[Link Title](page-slug)',
- rdoc: '{Link title}[link:page-slug]',
- asciidoc: 'link:page-slug[Link title]',
- org: '[[page-slug]]',
-};
-
const TRACKING_EVENT_NAME = 'view_wiki_page';
const TRACKING_CONTEXT_SCHEMA = 'iglu:com.gitlab/wiki_page_context/jsonschema/1-0-1';
@@ -23,78 +15,11 @@ export default class Wikis {
sidebarToggles[i].addEventListener('click', (e) => this.handleToggleSidebar(e));
}
- this.isNewWikiPage = Boolean(document.querySelector('.js-new-wiki-page'));
- this.editTitleInput = document.querySelector('form.wiki-form #wiki_title');
- this.commitMessageInput = document.querySelector('form.wiki-form #wiki_message');
- this.submitButton = document.querySelector('.js-wiki-btn-submit');
- this.commitMessageI18n = this.isNewWikiPage
- ? s__('WikiPageCreate|Create %{pageTitle}')
- : s__('WikiPageEdit|Update %{pageTitle}');
-
- if (this.editTitleInput) {
- // Initialize the commit message on load
- if (this.editTitleInput.value) this.setWikiCommitMessage(this.editTitleInput.value);
-
- // Set the commit message as the page title is changed
- this.editTitleInput.addEventListener('keyup', (e) => this.handleWikiTitleChange(e));
- }
-
window.addEventListener('resize', () => this.renderSidebar());
this.renderSidebar();
- const changeFormatSelect = document.querySelector('#wiki_format');
- const linkExample = document.querySelector('.js-markup-link-example');
-
- if (changeFormatSelect) {
- changeFormatSelect.addEventListener('change', (e) => {
- linkExample.innerHTML = MARKDOWN_LINK_TEXT[e.target.value];
- });
- }
-
- this.wikiTextarea = document.querySelector('form.wiki-form #wiki_content');
- const wikiForm = document.querySelector('form.wiki-form');
-
- if (this.wikiTextarea) {
- this.wikiTextarea.addEventListener('input', () => this.handleWikiContentChange());
-
- wikiForm.addEventListener('submit', () => {
- window.onbeforeunload = null;
- });
- }
-
Wikis.trackPageView();
Wikis.showToasts();
-
- this.updateSubmitButton();
- }
-
- handleWikiContentChange() {
- this.updateSubmitButton();
-
- window.onbeforeunload = () => '';
- }
-
- handleWikiTitleChange(e) {
- this.updateSubmitButton();
- this.setWikiCommitMessage(e.target.value);
- }
-
- updateSubmitButton() {
- if (!this.wikiTextarea) return;
-
- const isEnabled = Boolean(this.wikiTextarea.value.trim() && this.editTitleInput.value.trim());
- if (isEnabled) this.submitButton.removeAttribute('disabled');
- else this.submitButton.setAttribute('disabled', 'true');
- }
-
- setWikiCommitMessage(rawTitle) {
- let title = rawTitle;
-
- // Replace hyphens with spaces
- if (title) title = title.replace(/-+/g, ' ');
-
- const newCommitMessage = sprintf(this.commitMessageI18n, { pageTitle: title }, false);
- this.commitMessageInput.value = newCommitMessage;
}
handleToggleSidebar(e) {
diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index 3ff455fad32..d236dc4610a 100644
--- a/app/assets/javascripts/pages/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
@@ -39,7 +39,7 @@ function formatTooltipText({ date, count }) {
if (count > 0) {
contribText = n__('%d contribution', '%d contributions', count);
}
- return `${contribText}<br />${dateDayName} ${dateText}`;
+ return `${contribText}<br /><span class="gl-text-gray-300">${dateDayName} ${dateText}</span>`;
}
const initColorKey = () => d3.scaleLinear().range(['#acd5f2', '#254e77']).domain([0, 3]);