diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-18 11:17:02 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-18 11:17:02 +0300 |
commit | b39512ed755239198a9c294b6a45e65c05900235 (patch) | |
tree | d234a3efade1de67c46b9e5a38ce813627726aa7 /app/assets/javascripts/pages | |
parent | d31474cf3b17ece37939d20082b07f6657cc79a9 (diff) |
Add latest changes from gitlab-org/gitlab@15-3-stable-eev15.3.0-rc42
Diffstat (limited to 'app/assets/javascripts/pages')
17 files changed, 339 insertions, 82 deletions
diff --git a/app/assets/javascripts/pages/groups/new/components/app.vue b/app/assets/javascripts/pages/groups/new/components/app.vue index 713287f65b4..f01e5e595a3 100644 --- a/app/assets/javascripts/pages/groups/new/components/app.vue +++ b/app/assets/javascripts/pages/groups/new/components/app.vue @@ -2,51 +2,74 @@ import importGroupIllustration from '@gitlab/svgs/dist/illustrations/group-import.svg'; import newGroupIllustration from '@gitlab/svgs/dist/illustrations/group-new.svg'; -import { s__ } from '~/locale'; +import { __, s__ } from '~/locale'; import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue'; import createGroupDescriptionDetails from './create_group_description_details.vue'; -const PANELS = [ - { - name: 'create-group-pane', - selector: '#create-group-pane', - title: s__('GroupsNew|Create group'), - description: s__( - 'GroupsNew|Assemble related projects together and grant members access to several projects at once.', - ), - illustration: newGroupIllustration, - details: createGroupDescriptionDetails, - }, - { - name: 'import-group-pane', - selector: '#import-group-pane', - title: s__('GroupsNew|Import group'), - description: s__('GroupsNew|Import a group and related data from another GitLab instance.'), - illustration: importGroupIllustration, - details: 'Migrate your existing groups from another instance of GitLab.', - }, -]; - export default { components: { NewNamespacePage, }, props: { + parentGroupName: { + type: String, + required: false, + default: '', + }, + importExistingGroupPath: { + type: String, + required: false, + default: '', + }, hasErrors: { type: Boolean, required: false, default: false, }, }, - PANELS, + computed: { + initialBreadcrumb() { + return this.parentGroupName || __('New group'); + }, + panels() { + return [ + { + name: 'create-group-pane', + selector: '#create-group-pane', + title: this.parentGroupName + ? s__('GroupsNew|Create subgroup') + : s__('GroupsNew|Create group'), + description: s__( + 'GroupsNew|Assemble related projects together and grant members access to several projects at once.', + ), + illustration: newGroupIllustration, + details: createGroupDescriptionDetails, + detailProps: { + parentGroupName: this.parentGroupName, + importExistingGroupPath: this.importExistingGroupPath, + }, + }, + { + name: 'import-group-pane', + selector: '#import-group-pane', + title: s__('GroupsNew|Import group'), + description: s__( + 'GroupsNew|Import a group and related data from another GitLab instance.', + ), + illustration: importGroupIllustration, + details: 'Migrate your existing groups from another instance of GitLab.', + }, + ]; + }, + }, }; </script> <template> <new-namespace-page :jump-to-last-persisted-panel="hasErrors" - :initial-breadcrumb="__('New group')" - :panels="$options.PANELS" + :initial-breadcrumb="initialBreadcrumb" + :panels="panels" :title="s__('GroupsNew|Create new group')" persistence-key="new_group_last_active_tab" /> diff --git a/app/assets/javascripts/pages/groups/new/components/create_group_description_details.vue b/app/assets/javascripts/pages/groups/new/components/create_group_description_details.vue index 35193171fb8..be8542628c4 100644 --- a/app/assets/javascripts/pages/groups/new/components/create_group_description_details.vue +++ b/app/assets/javascripts/pages/groups/new/components/create_group_description_details.vue @@ -1,6 +1,22 @@ <script> import { GlSprintf, GlLink } from '@gitlab/ui'; import { helpPagePath } from '~/helpers/help_page_helper'; +import { s__ } from '~/locale'; + +const DESCRIPTION_DETAILS = { + group: [ + s__( + 'GroupsNew|%{linkStart}Groups%{linkEnd} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects.', + ), + s__('GroupsNew|Groups can also be nested by creating %{linkStart}subgroups%{linkEnd}.'), + ], + subgroup: [ + s__( + 'GroupsNew|%{groupsLinkStart}Groups%{groupsLinkEnd} and %{subgroupsLinkStart}subgroups%{subgroupsLinkEnd} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects.', + ), + s__('GroupsNew|You can also %{linkStart}import an existing group%{linkEnd}.'), + ], +}; export default { components: { @@ -11,30 +27,46 @@ export default { groupsHelpPath: helpPagePath('user/group/index'), subgroupsHelpPath: helpPagePath('user/group/subgroups/index'), }, + props: { + parentGroupName: { + type: String, + required: false, + default: '', + }, + importExistingGroupPath: { + type: String, + required: false, + default: '', + }, + }, + descriptionDetails: DESCRIPTION_DETAILS, }; </script> <template> <div> <p> - <gl-sprintf - :message=" - s__( - 'GroupsNew|%{linkStart}Groups%{linkEnd} allow you to manage and collaborate across multiple projects. Members of a group have access to all of its projects.', - ) - " - > + <gl-sprintf v-if="parentGroupName" :message="$options.descriptionDetails.subgroup[0]"> + <template #groupsLink="{ content }"> + <gl-link :href="$options.paths.groupsHelpPath" target="_blank">{{ content }}</gl-link> + </template> + <template #subgroupsLink="{ content }"> + <gl-link :href="$options.paths.subgroupsHelpPath" target="_blank">{{ content }}</gl-link> + </template> + </gl-sprintf> + <gl-sprintf v-else :message="$options.descriptionDetails.group[0]"> <template #link="{ content }"> <gl-link :href="$options.paths.groupsHelpPath" target="_blank">{{ content }}</gl-link> </template> </gl-sprintf> </p> <p> - <gl-sprintf - :message=" - s__('GroupsNew|Groups can also be nested by creating %{linkStart}subgroups%{linkEnd}.') - " - > + <gl-sprintf v-if="parentGroupName" :message="$options.descriptionDetails.subgroup[1]"> + <template #link="{ content }"> + <gl-link :href="importExistingGroupPath">{{ content }}</gl-link> + </template> + </gl-sprintf> + <gl-sprintf v-else :message="$options.descriptionDetails.group[1]"> <template #link="{ content }"> <gl-link :href="$options.paths.subgroupsHelpPath" target="_blank">{{ content }}</gl-link> </template> diff --git a/app/assets/javascripts/pages/groups/new/index.js b/app/assets/javascripts/pages/groups/new/index.js index 7c409010510..7dab5258b24 100644 --- a/app/assets/javascripts/pages/groups/new/index.js +++ b/app/assets/javascripts/pages/groups/new/index.js @@ -16,9 +16,18 @@ BindInOut.initAll(); initFilePickers(); function initNewGroupCreation(el) { - const { hasErrors, verificationRequired, verificationFormUrl, subscriptionsUrl } = el.dataset; + const { + hasErrors, + parentGroupName, + importExistingGroupPath, + verificationRequired, + verificationFormUrl, + subscriptionsUrl, + } = el.dataset; const props = { + parentGroupName, + importExistingGroupPath, hasErrors: parseBoolean(hasErrors), }; diff --git a/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue b/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue index 6748a62e777..9cce6723bf7 100644 --- a/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue +++ b/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue @@ -68,7 +68,7 @@ export default { }), tableCell({ key: 'created_at', - label: __('Date'), + label: __('Start date'), }), tableCell({ key: 'status', diff --git a/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js b/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js index 3fae9809e51..c520042c172 100644 --- a/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js +++ b/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js @@ -2,12 +2,10 @@ import { initAccessTokenTableApp, initExpiresAtField, initNewAccessTokenApp, - initProjectsField, initTokensApp, } from '~/access_tokens'; initAccessTokenTableApp(); initExpiresAtField(); initNewAccessTokenApp(); -initProjectsField(); initTokensApp(); diff --git a/app/assets/javascripts/pages/projects/ci/secure_files/show/index.js b/app/assets/javascripts/pages/projects/ci/secure_files/show/index.js deleted file mode 100644 index 61486606665..00000000000 --- a/app/assets/javascripts/pages/projects/ci/secure_files/show/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import { initCiSecureFiles } from '~/ci_secure_files'; - -initCiSecureFiles(); diff --git a/app/assets/javascripts/pages/projects/graphs/charts/index.js b/app/assets/javascripts/pages/projects/graphs/charts/index.js index c217bc5a727..65e7f48ed24 100644 --- a/app/assets/javascripts/pages/projects/graphs/charts/index.js +++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js @@ -55,18 +55,30 @@ waitForCSSLoaded(() => { }, attrs: { height: LANGUAGE_CHART_HEIGHT, + responsive: true, }, }); }, }); + const { + graphEndpoint, + graphEndDate, + graphStartDate, + graphRef, + graphCsvPath, + } = codeCoverageContainer.dataset; // eslint-disable-next-line no-new new Vue({ el: codeCoverageContainer, render(h) { return h(CodeCoverage, { props: { - graphEndpoint: codeCoverageContainer.dataset?.graphEndpoint, + graphEndpoint, + graphEndDate, + graphStartDate, + graphRef, + graphCsvPath, }, }); }, @@ -92,6 +104,9 @@ waitForCSSLoaded(() => { yAxisTitle: __('No. of commits'), xAxisType: 'category', }, + attrs: { + responsive: true, + }, }); }, }); @@ -125,6 +140,9 @@ waitForCSSLoaded(() => { yAxisTitle: __('No. of commits'), xAxisType: 'category', }, + attrs: { + responsive: true, + }, }); }, }); @@ -149,6 +167,9 @@ waitForCSSLoaded(() => { yAxisTitle: __('No. of commits'), xAxisType: 'category', }, + attrs: { + responsive: true, + }, }); }, }); diff --git a/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue b/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue index 92ae8128285..d7e68484143 100644 --- a/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue +++ b/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue @@ -1,5 +1,5 @@ <script> -import { GlAlert, GlDropdown, GlDropdownItem, GlSprintf } from '@gitlab/ui'; +import { GlAlert, GlButton, GlDropdown, GlDropdownItem, GlSprintf } from '@gitlab/ui'; import { GlAreaChart } from '@gitlab/ui/dist/charts'; import { get } from 'lodash'; import { formatDate } from '~/lib/utils/datetime_utility'; @@ -11,6 +11,7 @@ export default { components: { GlAlert, GlAreaChart, + GlButton, GlDropdown, GlDropdownItem, GlSprintf, @@ -20,6 +21,22 @@ export default { type: String, required: true, }, + graphEndDate: { + type: String, + required: true, + }, + graphStartDate: { + type: String, + required: true, + }, + graphRef: { + type: String, + required: true, + }, + graphCsvPath: { + type: String, + required: true, + }, }, data() { return { @@ -119,6 +136,28 @@ export default { <template> <div> + <div + class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-border-t gl-pt-4 gl-mb-3" + > + <h4 class="gl-m-0" sub-header> + <gl-sprintf + :message="__('Code coverage statistics for %{ref} %{start_date} - %{end_date}')" + > + <template #ref> + <strong> {{ graphRef }} </strong> + </template> + <template #start_date> + <strong> {{ graphStartDate }} </strong> + </template> + <template #end_date> + <strong> {{ graphEndDate }} </strong> + </template> + </gl-sprintf> + </h4> + <gl-button v-if="canShowData" size="small" data-testid="download-button" :href="graphCsvPath"> + {{ __('Download raw data (.csv)') }} + </gl-button> + </div> <div class="gl-mt-3 gl-mb-3"> <gl-alert v-if="hasFetchError" @@ -155,6 +194,7 @@ export default { :data="chartData" :option="chartOptions" :format-tooltip-text="formatTooltipText" + responsive > <template v-if="canShowData" #tooltip-title> {{ tooltipTitle }} diff --git a/app/assets/javascripts/pages/projects/init_blob.js b/app/assets/javascripts/pages/projects/init_blob.js index 7db34816cfe..f7849e8d588 100644 --- a/app/assets/javascripts/pages/projects/init_blob.js +++ b/app/assets/javascripts/pages/projects/init_blob.js @@ -4,6 +4,7 @@ import BlobForkSuggestion from '~/blob/blob_fork_suggestion'; import BlobLinePermalinkUpdater from '~/blob/blob_line_permalink_updater'; import LineHighlighter from '~/blob/line_highlighter'; import initBlobBundle from '~/blob_edit/blob_bundle'; +import addBlobLinksTracking from '~/blob/blob_links_tracking'; export default () => { new LineHighlighter(); // eslint-disable-line no-new @@ -11,10 +12,16 @@ export default () => { // eslint-disable-next-line no-new new BlobLinePermalinkUpdater( document.querySelector('#blob-content-holder'), - '.diff-line-num[data-line-number], .diff-line-num[data-line-number] *', + '.file-line-num[data-line-number], .file-line-num[data-line-number] *', document.querySelectorAll('.js-data-file-blob-permalink-url, .js-blob-blame-link'), ); + const eventsToTrack = [ + { selector: '.file-line-blame', property: 'blame' }, + { selector: '.file-line-num', property: 'link' }, + ]; + addBlobLinksTracking('#blob-content-holder', eventsToTrack); + const fileBlobPermalinkUrlElement = document.querySelector('.js-data-file-blob-permalink-url'); const fileBlobPermalinkUrl = fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href'); diff --git a/app/assets/javascripts/pages/projects/issues/show/index.js b/app/assets/javascripts/pages/projects/issues/show/index.js index ca2b1a08be8..c92958cd8c7 100644 --- a/app/assets/javascripts/pages/projects/issues/show/index.js +++ b/app/assets/javascripts/pages/projects/issues/show/index.js @@ -1,6 +1,6 @@ import { initShow } from '~/issues'; import { store } from '~/notes/stores'; -import initRelatedIssues from '~/related_issues'; +import { initRelatedIssues } from '~/related_issues'; import initSidebarBundle from '~/sidebar/sidebar_bundle'; import initWorkItemLinks from '~/work_items/components/work_item_links'; diff --git a/app/assets/javascripts/pages/projects/pages/new/index.js b/app/assets/javascripts/pages/projects/pages/new/index.js new file mode 100644 index 00000000000..a5157f5b01b --- /dev/null +++ b/app/assets/javascripts/pages/projects/pages/new/index.js @@ -0,0 +1,3 @@ +import initPages from '~/gitlab_pages/new'; + +initPages(); diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js index cd4bc35e74e..9513f42d9c9 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/index/index.js @@ -1,4 +1,6 @@ import Vue from 'vue'; +import { BV_SHOW_MODAL } from '~/lib/utils/constants'; +import PipelineSchedulesTakeOwnershipModal from '~/pipeline_schedules/components/take_ownership_modal.vue'; import PipelineSchedulesCallout from '../shared/components/pipeline_schedules_callout.vue'; function initPipelineSchedules() { @@ -23,4 +25,43 @@ function initPipelineSchedules() { }); } +function initTakeownershipModal() { + const modalId = 'pipeline-take-ownership-modal'; + const buttonSelector = 'js-take-ownership-button'; + const el = document.getElementById(modalId); + const takeOwnershipButtons = document.querySelectorAll(`.${buttonSelector}`); + + if (!el) { + return; + } + + // eslint-disable-next-line no-new + new Vue({ + el, + data() { + return { + url: '', + }; + }, + mounted() { + takeOwnershipButtons.forEach((button) => { + button.addEventListener('click', () => { + const { url } = button.dataset; + + this.url = url; + this.$root.$emit(BV_SHOW_MODAL, modalId, `.${buttonSelector}`); + }); + }); + }, + render(createElement) { + return createElement(PipelineSchedulesTakeOwnershipModal, { + props: { + ownershipUrl: this.url, + }, + }); + }, + }); +} + initPipelineSchedules(); +initTakeownershipModal(); 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 f2c30870a68..c7c331c7de5 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 @@ -29,6 +29,10 @@ export default { lfsLabel: s__('ProjectSettings|Git Large File Storage (LFS)'), mergeRequestsLabel: s__('ProjectSettings|Merge requests'), operationsLabel: s__('ProjectSettings|Operations'), + environmentsLabel: s__('ProjectSettings|Environments'), + environmentsHelpText: s__( + 'ProjectSettings|Every project can make deployments to environments either via CI/CD or API calls. Non-project members have read-only access.', + ), packagesHelpText: s__( 'ProjectSettings|Every project can have its own space to store its packages. Note: The Package Registry is always visible when a project is public.', ), @@ -209,6 +213,7 @@ export default { requirementsAccessLevel: featureAccessLevel.EVERYONE, securityAndComplianceAccessLevel: featureAccessLevel.PROJECT_MEMBERS, operationsAccessLevel: featureAccessLevel.EVERYONE, + environmentsAccessLevel: featureAccessLevel.EVERYONE, containerRegistryAccessLevel: featureAccessLevel.EVERYONE, warnAboutPotentiallyUnwantedCharacters: true, lfsEnabled: true, @@ -282,6 +287,9 @@ export default { return this.operationsAccessLevel > featureAccessLevel.NOT_ENABLED; }, + environmentsEnabled() { + return this.environmentsAccessLevel > featureAccessLevel.NOT_ENABLED; + }, repositoryEnabled() { return this.repositoryAccessLevel > featureAccessLevel.NOT_ENABLED; }, @@ -318,12 +326,8 @@ export default { packageRegistryAccessLevelEnabled() { return this.glFeatures.packageRegistryAccessLevel; }, - showAdditonalSettings() { - if (this.glFeatures.enforceAuthChecksOnUploads) { - return true; - } - - return this.visibilityLevel !== this.visibilityOptions.PRIVATE; + splitOperationsEnabled() { + return this.glFeatures.splitOperationsVisibilityPermissions; }, }, @@ -381,6 +385,10 @@ export default { featureAccessLevel.PROJECT_MEMBERS, this.operationsAccessLevel, ); + this.environmentsAccessLevel = Math.min( + featureAccessLevel.PROJECT_MEMBERS, + this.environmentsAccessLevel, + ); this.containerRegistryAccessLevel = Math.min( featureAccessLevel.PROJECT_MEMBERS, this.containerRegistryAccessLevel, @@ -422,6 +430,8 @@ export default { this.requirementsAccessLevel = featureAccessLevel.EVERYONE; if (this.operationsAccessLevel === featureAccessLevel.PROJECT_MEMBERS) this.operationsAccessLevel = featureAccessLevel.EVERYONE; + if (this.environmentsAccessLevel === featureAccessLevel.PROJECT_MEMBERS) + this.environmentsAccessLevel = featureAccessLevel.EVERYONE; if (this.containerRegistryAccessLevel === featureAccessLevel.PROJECT_MEMBERS) this.containerRegistryAccessLevel = featureAccessLevel.EVERYONE; @@ -545,7 +555,7 @@ export default { </template> </gl-sprintf> </span> - <div v-if="showAdditonalSettings" class="gl-mt-4"> + <div class="gl-mt-4"> <strong class="gl-display-block">{{ s__('ProjectSettings|Additional options') }}</strong> <label v-if="visibilityLevel !== visibilityOptions.PRIVATE" @@ -560,9 +570,7 @@ export default { {{ s__('ProjectSettings|Users can request access') }} </label> <label - v-if=" - visibilityLevel !== visibilityOptions.PUBLIC && glFeatures.enforceAuthChecksOnUploads - " + v-if="visibilityLevel !== visibilityOptions.PUBLIC" class="gl-line-height-28 gl-font-weight-normal gl-display-block gl-mb-0" > <input @@ -866,6 +874,20 @@ export default { /> </project-setting-row> </div> + <template v-if="splitOperationsEnabled"> + <project-setting-row + ref="environments-settings" + :label="$options.i18n.environmentsLabel" + :help-text="$options.i18n.environmentsHelpText" + > + <project-feature-setting + v-model="environmentsAccessLevel" + :label="$options.i18n.environmentsLabel" + :options="featureAccessLevelOptions" + name="project[project_feature_attributes][environments_access_level]" + /> + </project-setting-row> + </template> </div> <project-setting-row v-if="canDisableEmails" ref="email-settings" class="mb-3"> <label class="js-emails-disabled"> diff --git a/app/assets/javascripts/pages/projects/tags/releases/index.js b/app/assets/javascripts/pages/projects/tags/releases/index.js deleted file mode 100644 index cafd880b4be..00000000000 --- a/app/assets/javascripts/pages/projects/tags/releases/index.js +++ /dev/null @@ -1,6 +0,0 @@ -import $ from 'jquery'; -import GLForm from '~/gl_form'; -import ZenMode from '~/zen_mode'; - -new ZenMode(); // eslint-disable-line no-new -new GLForm($('.release-form')); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/registrations/new/index.js b/app/assets/javascripts/pages/registrations/new/index.js index 94a5c1cb29b..897acf9b02c 100644 --- a/app/assets/javascripts/pages/registrations/new/index.js +++ b/app/assets/javascripts/pages/registrations/new/index.js @@ -3,12 +3,17 @@ import { trackNewRegistrations } from '~/google_tag_manager'; import NoEmojiValidator from '~/emoji/no_emoji_validator'; import LengthValidator from '~/pages/sessions/new/length_validator'; import UsernameValidator from '~/pages/sessions/new/username_validator'; +import EmailFormatValidator from '~/pages/sessions/new/email_format_validator'; import Tracking from '~/tracking'; new UsernameValidator(); // eslint-disable-line no-new new LengthValidator(); // eslint-disable-line no-new new NoEmojiValidator(); // eslint-disable-line no-new +if (gon.features.trialEmailValidation) { + new EmailFormatValidator(); // eslint-disable-line no-new +} + trackNewRegistrations(); Tracking.enableFormTracking({ diff --git a/app/assets/javascripts/pages/sessions/new/email_format_validator.js b/app/assets/javascripts/pages/sessions/new/email_format_validator.js new file mode 100644 index 00000000000..6dcf3b50dca --- /dev/null +++ b/app/assets/javascripts/pages/sessions/new/email_format_validator.js @@ -0,0 +1,46 @@ +import InputValidator from '~/validators/input_validator'; + +// It checks if email contains at least one character, number or whatever except +// another "@" or whitespace before "@", at least two characters except +// another "@" or whitespace after "@" and one dot in between +const emailRegexPattern = /[^@\s]+@[^@\s]+\.[^@\s]+/; +const hintMessageSelector = '.validation-hint'; +const warningMessageSelector = '.validation-warning'; + +export default class EmailFormatValidator extends InputValidator { + constructor(opts = {}) { + super(); + + const container = opts.container || ''; + + document + .querySelectorAll(`${container} .js-validate-email`) + .forEach((element) => + element.addEventListener('keyup', EmailFormatValidator.eventHandler.bind(this)), + ); + } + + static eventHandler(event) { + const inputDomElement = event.target; + + EmailFormatValidator.setMessageVisibility(inputDomElement, hintMessageSelector); + EmailFormatValidator.setMessageVisibility(inputDomElement, warningMessageSelector); + EmailFormatValidator.validateEmailInput(inputDomElement); + } + + static validateEmailInput(inputDomElement) { + const validEmail = inputDomElement.checkValidity(); + const validPattern = inputDomElement.value.match(emailRegexPattern); + + EmailFormatValidator.setMessageVisibility( + inputDomElement, + warningMessageSelector, + validEmail && !validPattern, + ); + } + + static setMessageVisibility(inputDomElement, messageSelector, isVisible = false) { + const messageElement = inputDomElement.parentElement.querySelector(messageSelector); + messageElement.classList.toggle('hide', !isVisible); + } +} diff --git a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue index 3c22844434d..9d7d9e376cf 100644 --- a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue +++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue @@ -9,6 +9,7 @@ import { GlFormGroup, GlFormInput, GlFormSelect, + GlSegmentedControl, } from '@gitlab/ui'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; import axios from '~/lib/utils/axios_utils'; @@ -81,9 +82,11 @@ export default { newPage: s__('WikiPage|Create page'), }, cancel: s__('WikiPage|Cancel'), - editSourceButtonText: s__('WikiPage|Edit source'), - editRichTextButtonText: s__('WikiPage|Edit rich text'), }, + switchEditingControlOptions: [ + { text: s__('Wiki Page|Source'), value: 'source' }, + { text: s__('Wiki Page|Rich text'), value: 'richText' }, + ], components: { GlAlert, GlIcon, @@ -94,6 +97,7 @@ export default { GlSprintf, GlLink, GlButton, + GlSegmentedControl, MarkdownField, LocalStorageSync, ContentEditor: () => @@ -105,14 +109,15 @@ export default { inject: ['formatOptions', 'pageInfo'], data() { return { + editingMode: 'source', title: this.pageInfo.title?.trim() || '', format: this.pageInfo.format || 'markdown', content: this.pageInfo.content || '', - useContentEditor: false, commitMessage: '', isDirty: false, contentEditorRenderFailed: false, contentEditorEmpty: false, + switchEditingControlDisabled: false, }; }, computed: { @@ -177,6 +182,9 @@ export default { isContentEditorActive() { return this.isMarkdownFormat && this.useContentEditor; }, + useContentEditor() { + return this.editingMode === 'richText'; + }, }, mounted() { this.updateCommitMessage(); @@ -193,16 +201,15 @@ export default { .then(({ data }) => data.body); }, - toggleEditingMode() { - if (this.useContentEditor) { + toggleEditingMode(editingMode) { + this.editingMode = editingMode; + if (!this.useContentEditor && this.contentEditor) { this.content = this.contentEditor.getSerializedContent(); } - - this.useContentEditor = !this.useContentEditor; }, - setUseContentEditor(value) { - this.useContentEditor = value; + setEditingMode(value) { + this.editingMode = value; }, async handleFormSubmit(e) { @@ -294,6 +301,14 @@ export default { }, }); }, + + enableSwitchEditingControl() { + this.switchEditingControlDisabled = false; + }, + + disableSwitchEditingControl() { + this.switchEditingControlDisabled = true; + }, }, }; </script> @@ -372,20 +387,21 @@ export default { <div class="row" data-testid="wiki-form-content-fieldset"> <div class="col-sm-12 row-sm-5"> <gl-form-group> - <div v-if="isMarkdownFormat" class="gl-display-flex gl-justify-content-end gl-mb-3"> - <gl-button + <div v-if="isMarkdownFormat" class="gl-display-flex gl-justify-content-start gl-mb-3"> + <gl-segmented-control data-testid="toggle-editing-mode-button" data-qa-selector="editing_mode_button" - :data-qa-mode="toggleEditingModeButtonText" - variant="link" - @click="toggleEditingMode" - >{{ toggleEditingModeButtonText }}</gl-button - > + class="gl-display-flex" + :checked="editingMode" + :options="$options.switchEditingControlOptions" + :disabled="switchEditingControlDisabled" + @input="toggleEditingMode" + /> </div> <local-storage-sync storage-key="gl-wiki-content-editor-enabled" - :value="useContentEditor" - @input="setUseContentEditor" + :value="editingMode" + @input="setEditingMode" /> <markdown-field v-if="!isContentEditorActive" @@ -422,6 +438,9 @@ export default { :uploads-path="pageInfo.uploadsPath" @initialized="loadInitialContent" @change="handleContentEditorChange" + @loading="disableSwitchEditingControl" + @loadingSuccess="enableSwitchEditingControl" + @loadingError="enableSwitchEditingControl" /> <input id="wiki_content" v-model.trim="content" type="hidden" name="wiki[content]" /> </div> |