diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-20 14:36:54 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-20 14:36:54 +0000 |
commit | f61bb2a16a514b71bf33aabbbb999d6732016a24 (patch) | |
tree | 9548caa89e60b4f40b99bbd1dac030420b812aa8 /app/assets/javascripts/pages | |
parent | 35fc54e5d261f8898e390aea7c2f5ec5fdf0539d (diff) | |
download | gitlab-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')
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]); |