diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-16 18:25:58 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-16 18:25:58 +0000 |
commit | a5f4bba440d7f9ea47046a0a561d49adf0a1e6d4 (patch) | |
tree | fb69158581673816a8cd895f9d352dcb3c678b1e /app/assets/javascripts/pages | |
parent | d16b2e8639e99961de6ddc93909f3bb5c1445ba1 (diff) | |
download | gitlab-ce-a5f4bba440d7f9ea47046a0a561d49adf0a1e6d4.tar.gz |
Add latest changes from gitlab-org/gitlab@14-0-stable-eev14.0.0-rc42
Diffstat (limited to 'app/assets/javascripts/pages')
36 files changed, 606 insertions, 247 deletions
diff --git a/app/assets/javascripts/pages/admin/application_settings/integrations/index.js b/app/assets/javascripts/pages/admin/application_settings/integrations/index.js index f318b6f62d5..53068f72d3f 100644 --- a/app/assets/javascripts/pages/admin/application_settings/integrations/index.js +++ b/app/assets/javascripts/pages/admin/application_settings/integrations/index.js @@ -1,8 +1,3 @@ import initIntegrationsList from '~/integrations/index'; -import PersistentUserCallout from '~/persistent_user_callout'; - -const callout = document.querySelector('.js-admin-integrations-moved'); - -PersistentUserCallout.factory(callout); initIntegrationsList(); diff --git a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue index 798eeee48bf..ffccc1419a6 100644 --- a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue +++ b/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue @@ -1,6 +1,6 @@ <script> import { GlModal } from '@gitlab/ui'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { redirectTo } from '~/lib/utils/url_utility'; import { __, s__ } from '~/locale'; @@ -31,7 +31,9 @@ export default { redirectTo(response.request.responseURL); }) .catch((error) => { - createFlash(s__('AdminArea|Stopping jobs failed')); + createFlash({ + message: s__('AdminArea|Stopping jobs failed'), + }); throw error; }); }, diff --git a/app/assets/javascripts/pages/admin/runners/index/index.js b/app/assets/javascripts/pages/admin/runners/index/index.js index 45ed3ac6bd8..d5563470394 100644 --- a/app/assets/javascripts/pages/admin/runners/index/index.js +++ b/app/assets/javascripts/pages/admin/runners/index/index.js @@ -2,6 +2,7 @@ import AdminRunnersFilteredSearchTokenKeys from '~/filtered_search/admin_runners import { FILTERED_SEARCH } from '~/pages/constants'; import initFilteredSearch from '~/pages/search/init_filtered_search'; import { initInstallRunner } from '~/pages/shared/mount_runner_instructions'; +import { initRunnerList } from '~/runner/runner_list'; initFilteredSearch({ page: FILTERED_SEARCH.ADMIN_RUNNERS, @@ -10,3 +11,7 @@ initFilteredSearch({ }); initInstallRunner(); + +if (gon.features?.runnerListViewVueUi) { + initRunnerList(); +} diff --git a/app/assets/javascripts/pages/groups/group_members/index.js b/app/assets/javascripts/pages/groups/group_members/index.js index b0a70055835..13656ee9b16 100644 --- a/app/assets/javascripts/pages/groups/group_members/index.js +++ b/app/assets/javascripts/pages/groups/group_members/index.js @@ -29,46 +29,43 @@ 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'], - requestFormatter: groupMemberRequestFormatter, - filteredSearchBar: { - show: true, - tokens: ['two_factor', 'with_inherited_permissions'], - searchParam: 'search', - placeholder: s__('Members|Filter members'), - recentSearchesStorageKey: 'group_members', +initMembersApp(document.querySelector('.js-group-members-list-app'), { + [MEMBER_TYPES.user]: { + tableFields: SHARED_FIELDS.concat(['source', 'granted']), + tableAttrs: { tr: { 'data-qa-selector': 'member_row' } }, + tableSortableFields: ['account', 'granted', 'maxRole', 'lastSignIn'], + requestFormatter: groupMemberRequestFormatter, + filteredSearchBar: { + show: true, + tokens: ['two_factor', 'with_inherited_permissions'], + searchParam: 'search', + placeholder: s__('Members|Filter members'), + recentSearchesStorageKey: 'group_members', + }, }, -}); - -initMembersApp(document.querySelector('.js-group-group-links-list'), { - namespace: MEMBER_TYPES.group, - tableFields: SHARED_FIELDS.concat('granted'), - tableAttrs: { - table: { 'data-qa-selector': 'groups_list' }, - tr: { 'data-qa-selector': 'group_row' }, + [MEMBER_TYPES.group]: { + tableFields: SHARED_FIELDS.concat('granted'), + tableAttrs: { + table: { 'data-qa-selector': 'groups_list' }, + tr: { 'data-qa-selector': 'group_row' }, + }, + requestFormatter: groupLinkRequestFormatter, }, - requestFormatter: groupLinkRequestFormatter, -}); -initMembersApp(document.querySelector('.js-group-invited-members-list'), { - namespace: MEMBER_TYPES.invite, - tableFields: SHARED_FIELDS.concat('invited'), - requestFormatter: groupMemberRequestFormatter, - filteredSearchBar: { - show: true, - tokens: [], - searchParam: 'search_invited', - placeholder: s__('Members|Search invited'), - recentSearchesStorageKey: 'group_invited_members', + [MEMBER_TYPES.invite]: { + tableFields: SHARED_FIELDS.concat('invited'), + requestFormatter: groupMemberRequestFormatter, + filteredSearchBar: { + show: true, + tokens: [], + searchParam: 'search_invited', + placeholder: s__('Members|Search invited'), + recentSearchesStorageKey: 'group_invited_members', + }, + }, + [MEMBER_TYPES.accessRequest]: { + tableFields: SHARED_FIELDS.concat('requested'), + requestFormatter: groupMemberRequestFormatter, }, -}); -initMembersApp(document.querySelector('.js-group-access-requests-list'), { - namespace: MEMBER_TYPES.accessRequest, - tableFields: SHARED_FIELDS.concat('requested'), - requestFormatter: groupMemberRequestFormatter, }); groupsSelect(); diff --git a/app/assets/javascripts/pages/groups/new/components/app.vue b/app/assets/javascripts/pages/groups/new/components/app.vue new file mode 100644 index 00000000000..9aac364d20e --- /dev/null +++ b/app/assets/javascripts/pages/groups/new/components/app.vue @@ -0,0 +1,55 @@ +<script> +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 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|Export groups with all their related data and move to a new GitLab instance.', + ), + illustration: importGroupIllustration, + details: 'Migrate your existing groups from another instance of GitLab.', + }, +]; + +export default { + components: { + NewNamespacePage, + }, + props: { + hasErrors: { + type: Boolean, + required: false, + default: false, + }, + }, + PANELS, +}; +</script> + +<template> + <new-namespace-page + :jump-to-last-persisted-panel="hasErrors" + :initial-breadcrumb="s__('New group')" + :panels="$options.PANELS" + :title="s__('GroupsNew|Create new group')" + persistence-key="new_group_last_active_tab" + /> +</template> 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 new file mode 100644 index 00000000000..ea08a0821a8 --- /dev/null +++ b/app/assets/javascripts/pages/groups/new/components/create_group_description_details.vue @@ -0,0 +1,44 @@ +<script> +import { GlSprintf, GlLink } from '@gitlab/ui'; +import { helpPagePath } from '~/helpers/help_page_helper'; + +export default { + components: { + GlLink, + GlSprintf, + }, + paths: { + groupsHelpPath: helpPagePath('user/group/index'), + subgroupsHelpPath: helpPagePath('user/group/subgroups/index'), + }, +}; +</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. Groups can also be nested by creating subgroups.', + ) + " + > + <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}.') + " + > + <template #link="{ content }"> + <gl-link :href="$options.paths.subgroupsHelpPath" target="_blank">{{ content }}</gl-link> + </template> + </gl-sprintf> + </p> + </div> +</template> diff --git a/app/assets/javascripts/pages/groups/new/index.js b/app/assets/javascripts/pages/groups/new/index.js index 569b5afd676..7557edb1b49 100644 --- a/app/assets/javascripts/pages/groups/new/index.js +++ b/app/assets/javascripts/pages/groups/new/index.js @@ -1,8 +1,9 @@ -import $ from 'jquery'; +import Vue from 'vue'; import BindInOut from '~/behaviors/bind_in_out'; import initFilePickers from '~/file_pickers'; import Group from '~/group'; -import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import NewGroupCreationApp from './components/app.vue'; import GroupPathValidator from './group_path_validator'; new GroupPathValidator(); // eslint-disable-line no-new @@ -12,15 +13,21 @@ initFilePickers(); new Group(); // eslint-disable-line no-new -const CONTAINER_SELECTOR = '.group-edit-container .nav-tabs'; -const DEFAULT_ACTION = '#create-group-pane'; -// eslint-disable-next-line no-new -new LinkedTabs({ - defaultAction: DEFAULT_ACTION, - parentEl: CONTAINER_SELECTOR, - hashedTabs: true, -}); - -if (window.location.hash) { - $(CONTAINER_SELECTOR).find(`a[href="${window.location.hash}"]`).tab('show'); +function initNewGroupCreation(el) { + const { hasErrors } = el.dataset; + + const props = { + hasErrors: parseBoolean(hasErrors), + }; + + return new Vue({ + el, + render(h) { + return h(NewGroupCreationApp, { props }); + }, + }); } + +const el = document.querySelector('.js-new-group-creation'); + +initNewGroupCreation(el); 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 636eea5d7ac..a8d7a83cdd6 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 @@ -3,6 +3,7 @@ import GroupRunnersFilteredSearchTokenKeys from '~/filtered_search/group_runners import initSharedRunnersForm from '~/group_settings/mount_shared_runners'; import { FILTERED_SEARCH } from '~/pages/constants'; import initFilteredSearch from '~/pages/search/init_filtered_search'; +import { initRunnerAwsDeployments } from '~/pages/shared/mount_runner_aws_deployments'; import { initInstallRunner } from '~/pages/shared/mount_runner_instructions'; import initSettingsPanels from '~/settings_panels'; @@ -20,3 +21,4 @@ initSharedRunnersForm(); initVariableList(); initInstallRunner(); +initRunnerAwsDeployments(); diff --git a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue index 8d4997586dd..e42e89ce021 100644 --- a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue +++ b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue @@ -1,6 +1,6 @@ <script> import { GlModal } from '@gitlab/ui'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { visitUrl } from '~/lib/utils/url_utility'; import { s__, sprintf } from '~/locale'; @@ -63,7 +63,9 @@ export default { visitUrl(response.data.url); }) .catch((error) => { - createFlash(error); + createFlash({ + message: error, + }); }) .finally(() => { this.visible = false; diff --git a/app/assets/javascripts/pages/profiles/show/index.js b/app/assets/javascripts/pages/profiles/show/index.js index b5441127797..226ef4c4e23 100644 --- a/app/assets/javascripts/pages/profiles/show/index.js +++ b/app/assets/javascripts/pages/profiles/show/index.js @@ -2,7 +2,7 @@ import emojiRegex from 'emoji-regex'; import $ from 'jquery'; import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete'; import * as Emoji from '~/emoji'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import { __ } from '~/locale'; import EmojiMenu from './emoji_menu'; @@ -81,4 +81,8 @@ Emoji.initEmojiMap() } }); }) - .catch(() => createFlash(__('Failed to load emoji list.'))); + .catch(() => + createFlash({ + message: __('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 8a8ce70e998..6cc0095f5a5 100644 --- a/app/assets/javascripts/pages/projects/blob/show/index.js +++ b/app/assets/javascripts/pages/projects/blob/show/index.js @@ -1,5 +1,6 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; +import TableOfContents from '~/blob/components/table_contents.vue'; import PipelineTourSuccessModal from '~/blob/pipeline_tour_success_modal.vue'; import BlobViewer from '~/blob/viewer/index'; import GpgBadges from '~/gpg_badges'; @@ -19,12 +20,16 @@ const apolloProvider = new VueApollo({ const viewBlobEl = document.querySelector('#js-view-blob-app'); if (viewBlobEl) { - const { blobPath, projectPath } = viewBlobEl.dataset; + const { blobPath, projectPath, targetBranch, originalBranch } = viewBlobEl.dataset; // eslint-disable-next-line no-new new Vue({ el: viewBlobEl, apolloProvider, + provide: { + targetBranch, + originalBranch, + }, render(createElement) { return createElement(BlobContentViewer, { props: { @@ -92,3 +97,15 @@ if (successPipelineEl) { }, }); } + +const tableContentsEl = document.querySelector('.js-table-contents'); + +if (tableContentsEl) { + // eslint-disable-next-line no-new + new Vue({ + el: tableContentsEl, + render(h) { + return h(TableOfContents); + }, + }); +} diff --git a/app/assets/javascripts/pages/projects/branches/index/index.js b/app/assets/javascripts/pages/projects/branches/index/index.js index 27ec746ad02..97dc76908af 100644 --- a/app/assets/javascripts/pages/projects/branches/index/index.js +++ b/app/assets/javascripts/pages/projects/branches/index/index.js @@ -3,6 +3,8 @@ 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'; +import initDeleteBranchButton from '~/branches/init_delete_branch_button'; +import initDeleteBranchModal from '~/branches/init_delete_branch_modal'; AjaxLoadingSpinner.init(); new DeleteModal(); // eslint-disable-line no-new @@ -14,3 +16,9 @@ const { divergingCountsEndpoint, defaultBranch } = document.querySelector( initDiverganceGraph(divergingCountsEndpoint, defaultBranch); BranchSortDropdown(); initDeprecatedRemoveRowBehavior(); + +document + .querySelectorAll('.js-delete-branch-button') + .forEach((elem) => initDeleteBranchButton(elem)); + +initDeleteBranchModal(); diff --git a/app/assets/javascripts/pages/projects/feature_flags_user_lists/index/index.js b/app/assets/javascripts/pages/projects/feature_flags_user_lists/index/index.js new file mode 100644 index 00000000000..519e04e14fb --- /dev/null +++ b/app/assets/javascripts/pages/projects/feature_flags_user_lists/index/index.js @@ -0,0 +1,25 @@ +/* eslint-disable no-new */ + +import Vue from 'vue'; +import Vuex from 'vuex'; +import UserLists from '~/user_lists/components/user_lists.vue'; +import createStore from '~/user_lists/store/index'; + +Vue.use(Vuex); + +const el = document.querySelector('#js-user-lists'); + +const { featureFlagsHelpPagePath, errorStateSvgPath, projectId, newUserListPath } = el.dataset; + +new Vue({ + el, + store: createStore({ projectId }), + provide: { + featureFlagsHelpPagePath, + errorStateSvgPath, + newUserListPath, + }, + render(createElement) { + return createElement(UserLists); + }, +}); diff --git a/app/assets/javascripts/pages/projects/forks/new/components/app.vue b/app/assets/javascripts/pages/projects/forks/new/components/app.vue index 02b357d389b..7fb41c6e7b7 100644 --- a/app/assets/javascripts/pages/projects/forks/new/components/app.vue +++ b/app/assets/javascripts/pages/projects/forks/new/components/app.vue @@ -38,6 +38,10 @@ export default { type: String, required: true, }, + restrictedVisibilityLevels: { + type: Array, + required: true, + }, }, }; </script> @@ -66,6 +70,7 @@ export default { :project-path="projectPath" :project-description="projectDescription" :project-visibility="projectVisibility" + :restricted-visibility-levels="restrictedVisibilityLevels" /> </div> </div> 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 07cc0ce46bc..75c3b6d564c 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 @@ -26,10 +26,10 @@ const PRIVATE_VISIBILITY = 'private'; const INTERNAL_VISIBILITY = 'internal'; const PUBLIC_VISIBILITY = 'public'; -const ALLOWED_VISIBILITY = { - private: [PRIVATE_VISIBILITY], - internal: [INTERNAL_VISIBILITY, PRIVATE_VISIBILITY], - public: [INTERNAL_VISIBILITY, PRIVATE_VISIBILITY, PUBLIC_VISIBILITY], +const VISIBILITY_LEVEL = { + [PRIVATE_VISIBILITY]: 0, + [INTERNAL_VISIBILITY]: 10, + [PUBLIC_VISIBILITY]: 20, }; const initFormField = ({ value, required = true, skipValidation = false }) => ({ @@ -95,6 +95,10 @@ export default { type: String, required: true, }, + restrictedVisibilityLevels: { + type: Array, + required: true, + }, }, data() { const form = { @@ -111,10 +115,7 @@ export default { required: false, skipValidation: true, }), - visibility: initFormField({ - value: this.projectVisibility, - skipValidation: true, - }), + visibility: initFormField({ value: this.getInitialVisibilityValue() }), }, }; return { @@ -127,14 +128,38 @@ export default { projectUrl() { return `${gon.gitlab_url}/`; }, - projectAllowedVisibility() { - return ALLOWED_VISIBILITY[this.projectVisibility]; + projectVisibilityLevel() { + return VISIBILITY_LEVEL[this.projectVisibility]; + }, + namespaceVisibilityLevel() { + const visibility = this.form.fields.namespace.value?.visibility || PUBLIC_VISIBILITY; + return VISIBILITY_LEVEL[visibility]; + }, + visibilityLevelCap() { + return Math.min(this.projectVisibilityLevel, this.namespaceVisibilityLevel); + }, + restrictedVisibilityLevelsSet() { + return new Set(this.restrictedVisibilityLevels); }, - namespaceAllowedVisibility() { - return ( - ALLOWED_VISIBILITY[this.form.fields.namespace.value?.visibility] || - ALLOWED_VISIBILITY[PUBLIC_VISIBILITY] + allowedVisibilityLevels() { + const allowedLevels = Object.entries(VISIBILITY_LEVEL).reduce( + (levels, [levelName, levelValue]) => { + if ( + !this.restrictedVisibilityLevelsSet.has(levelValue) && + levelValue <= this.visibilityLevelCap + ) { + levels.push(levelName); + } + return levels; + }, + [], ); + + if (!allowedLevels.length) { + return [PRIVATE_VISIBILITY]; + } + + return allowedLevels; }, visibilityLevels() { return [ @@ -142,7 +167,9 @@ export default { text: s__('ForkProject|Private'), value: PRIVATE_VISIBILITY, icon: 'lock', - help: s__('ForkProject|The project can be accessed without any authentication.'), + help: s__( + 'ForkProject|Project access must be granted explicitly to each user. If this project is part of a group, access will be granted to members of the group.', + ), disabled: this.isVisibilityLevelDisabled(PRIVATE_VISIBILITY), }, { @@ -156,9 +183,7 @@ export default { text: s__('ForkProject|Public'), value: PUBLIC_VISIBILITY, icon: 'earth', - help: s__( - 'ForkProject|Project access must be granted explicitly to each user. If this project is part of a group, access will be granted to members of the group.', - ), + help: s__('ForkProject|The project can be accessed without any authentication.'), disabled: this.isVisibilityLevelDisabled(PUBLIC_VISIBILITY), }, ]; @@ -166,12 +191,9 @@ export default { }, watch: { // eslint-disable-next-line func-names - 'form.fields.namespace.value': function (newVal) { - const { visibility } = newVal; - - if (this.projectAllowedVisibility.includes(visibility)) { - this.form.fields.visibility.value = visibility; - } + 'form.fields.namespace.value': function () { + this.form.fields.visibility.value = + this.restrictedVisibilityLevels.length !== 0 ? null : PRIVATE_VISIBILITY; }, // eslint-disable-next-line func-names 'form.fields.name.value': function (newVal) { @@ -186,11 +208,11 @@ export default { const { data } = await axios.get(this.endpoint); this.namespaces = data.namespaces; }, - isVisibilityLevelDisabled(visibilityLevel) { - return !( - this.projectAllowedVisibility.includes(visibilityLevel) && - this.namespaceAllowedVisibility.includes(visibilityLevel) - ); + isVisibilityLevelDisabled(visibility) { + return !this.allowedVisibilityLevels.includes(visibility); + }, + getInitialVisibilityValue() { + return this.restrictedVisibilityLevels.length !== 0 ? null : this.projectVisibility; }, async onSubmit() { this.form.showValidation = true; @@ -222,7 +244,11 @@ export default { redirectTo(data.web_url); return; } catch (error) { - createFlash({ message: error }); + createFlash({ + message: s__( + 'ForkProject|An error occurred while forking the project. Please try again.', + ), + }); } }, }, @@ -322,7 +348,11 @@ export default { /> </gl-form-group> - <gl-form-group> + <gl-form-group + v-validation:[form.showValidation] + :invalid-feedback="s__('ForkProject|Please select a visibility level')" + :state="form.fields.visibility.state" + > <label> {{ s__('ForkProject|Visibility level') }} <gl-link :href="visibilityHelpPath" target="_blank"> @@ -333,6 +363,7 @@ export default { v-model="form.fields.visibility.value" data-testid="fork-visibility-radio-group" name="visibility" + :aria-label="__('visibility')" required > <gl-form-radio diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list.vue index bc47b124f8b..10753de6cd0 100644 --- a/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list.vue +++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list.vue @@ -1,6 +1,6 @@ <script> import { GlTabs, GlTab, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { __ } from '~/locale'; import ForkGroupsListItem from './fork_groups_list_item.vue'; @@ -44,7 +44,11 @@ export default { .then((response) => { this.namespaces = response.data.namespaces; }) - .catch(() => createFlash(__('There was a problem fetching groups.'))); + .catch(() => + createFlash({ + message: __('There was a problem fetching groups.'), + }), + ); }, }, diff --git a/app/assets/javascripts/pages/projects/forks/new/index.js b/app/assets/javascripts/pages/projects/forks/new/index.js index 372967c8a1e..1a171252048 100644 --- a/app/assets/javascripts/pages/projects/forks/new/index.js +++ b/app/assets/javascripts/pages/projects/forks/new/index.js @@ -16,6 +16,7 @@ if (gon.features.forkProjectForm) { projectPath, projectDescription, projectVisibility, + restrictedVisibilityLevels, } = mountElement.dataset; // eslint-disable-next-line no-new @@ -38,6 +39,7 @@ if (gon.features.forkProjectForm) { projectPath, projectDescription, projectVisibility, + restrictedVisibilityLevels: JSON.parse(restrictedVisibilityLevels), }, }); }, diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js index 45e9643b3f3..1eab3becbc3 100644 --- a/app/assets/javascripts/pages/projects/index.js +++ b/app/assets/javascripts/pages/projects/index.js @@ -1,5 +1,7 @@ import ShortcutsNavigation from '../../behaviors/shortcuts/shortcuts_navigation'; +import { initSidebarTracking } from '../shared/nav/sidebar_tracking'; import Project from './project'; new Project(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new +initSidebarTracking(); diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js index 3143ff5adac..3cea61262ea 100644 --- a/app/assets/javascripts/pages/projects/issues/show.js +++ b/app/assets/javascripts/pages/projects/issues/show.js @@ -1,8 +1,6 @@ import loadAwardsHandler from '~/awards_handler'; import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; import initIssuableSidebar from '~/init_issuable_sidebar'; -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,8 +32,6 @@ export default function initShowIssue() { initIssueHeaderActions(store); initSentryErrorStackTraceApp(); initRelatedMergeRequestsApp(); - initInviteMembersModal(); - initInviteMembersTrigger(); import(/* webpackChunkName: 'design_management' */ '~/design_management') .then((module) => module.default()) diff --git a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue index 81ffaa6f7a3..aaa9bb906b2 100644 --- a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue +++ b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue @@ -1,6 +1,6 @@ <script> import { GlSprintf, GlModal } from '@gitlab/ui'; -import { deprecatedCreateFlash as createFlash } from '~/flash'; +import createFlash from '~/flash'; import axios from '~/lib/utils/axios_utils'; import { visitUrl } from '~/lib/utils/url_utility'; import { s__, __, sprintf } from '~/locale'; @@ -70,7 +70,9 @@ export default { labelUrl: this.url, successful: false, }); - createFlash(error); + createFlash({ + message: error, + }); }); }, }, 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 6cd3202815b..d6b6c9fe06a 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 @@ -4,8 +4,6 @@ import loadAwardsHandler from '~/awards_handler'; import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; import initPipelines from '~/commit/pipelines/pipelines_bundle'; import initIssuableSidebar from '~/init_issuable_sidebar'; -import initInviteMembersModal from '~/invite_members/init_invite_members_modal'; -import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger'; import StatusBox from '~/issuable/components/status_box.vue'; import createDefaultClient from '~/lib/graphql'; import { handleLocationHash } from '~/lib/utils/common_utils'; @@ -29,8 +27,6 @@ export default function initMergeRequestShow() { } else { loadAwardsHandler(); } - initInviteMembersModal(); - initInviteMembersTrigger(); const el = document.querySelector('.js-mr-status-box'); const apolloProvider = new VueApollo({ defaultClient: createDefaultClient() }); diff --git a/app/assets/javascripts/pages/projects/packages/infrastructure_registry/show/index.js b/app/assets/javascripts/pages/projects/packages/infrastructure_registry/show/index.js new file mode 100644 index 00000000000..44d9e2ffb6e --- /dev/null +++ b/app/assets/javascripts/pages/projects/packages/infrastructure_registry/show/index.js @@ -0,0 +1,3 @@ +import initDetails from '~/packages_and_registries/infrastructure_registry/details_app_bundle'; + +initDetails(); 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 159c619e16c..d0ec5668d21 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 @@ -1,7 +1,15 @@ <script> -import { GlFormRadio, GlFormRadioGroup, GlLink, GlSprintf } from '@gitlab/ui'; +import { + GlFormRadio, + GlFormRadioGroup, + GlIcon, + GlLink, + GlSprintf, + GlTooltipDirective, +} from '@gitlab/ui'; import { getWeekdayNames } from '~/lib/utils/datetime_utility'; -import { s__, sprintf } from '~/locale'; +import { __, s__, sprintf } from '~/locale'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; const KEY_EVERY_DAY = 'everyDay'; const KEY_EVERY_WEEK = 'everyWeek'; @@ -12,15 +20,25 @@ export default { components: { GlFormRadio, GlFormRadioGroup, + GlIcon, GlLink, GlSprintf, }, + directives: { + GlTooltip: GlTooltipDirective, + }, + mixins: [glFeatureFlagMixin()], props: { initialCronInterval: { type: String, required: false, default: '', }, + dailyLimit: { + type: String, + required: false, + default: '', + }, }, data() { return { @@ -80,6 +98,17 @@ export default { weekday() { return getWeekdayNames()[this.randomWeekDayIndex]; }, + parsedDailyLimit() { + return this.dailyLimit ? (24 * 60) / this.dailyLimit : null; + }, + scheduleDailyLimitMsg() { + return sprintf( + __( + 'Scheduled pipelines cannot run more frequently than once per %{limit} minutes. A pipeline configured to run more frequently only starts after %{limit} minutes have elapsed since the last time it ran.', + ), + { limit: this.parsedDailyLimit }, + ); + }, }, watch: { cronInterval() { @@ -111,6 +140,11 @@ export default { generateRandomDay() { return Math.floor(Math.random() * 28); }, + showDailyLimitMessage({ value }) { + return ( + value === KEY_CUSTOM && this.glFeatures.ciDailyLimitForPipelineSchedules && this.dailyLimit + ); + }, }, }; </script> @@ -131,7 +165,15 @@ export default { </gl-link> </template> </gl-sprintf> + <template v-else>{{ option.text }}</template> + + <gl-icon + v-if="showDailyLimitMessage(option)" + v-gl-tooltip.hover + name="question" + :title="scheduleDailyLimitMsg" + /> </gl-form-radio> </gl-form-radio-group> <input diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js index ce0e573fed2..9056c76d6ca 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/init_form.js @@ -12,6 +12,7 @@ Vue.use(Translate); function initIntervalPatternInput() { const intervalPatternMount = document.getElementById('interval-pattern-input'); const initialCronInterval = intervalPatternMount?.dataset?.initialInterval; + const dailyLimit = intervalPatternMount.dataset?.dailyLimit; return new Vue({ el: intervalPatternMount, @@ -22,6 +23,7 @@ function initIntervalPatternInput() { return createElement('interval-pattern-input', { props: { initialCronInterval, + dailyLimit, }, }); }, diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js index 91f376060f8..3b24c2c128b 100644 --- a/app/assets/javascripts/pages/projects/project.js +++ b/app/assets/javascripts/pages/projects/project.js @@ -129,7 +129,7 @@ export default class Project { 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 splitPathAfterRefPortion = loc.split('/-/')[1].split(currentRef)[1]; const doesPathContainRef = splitPathAfterRefPortion?.startsWith('/'); if (doesPathContainRef) { diff --git a/app/assets/javascripts/pages/projects/project_members/index.js b/app/assets/javascripts/pages/projects/project_members/index.js index 471798d2931..177dc346c60 100644 --- a/app/assets/javascripts/pages/projects/project_members/index.js +++ b/app/assets/javascripts/pages/projects/project_members/index.js @@ -42,46 +42,41 @@ initInviteMembersForm(); 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'], - requestFormatter: projectMemberRequestFormatter, - filteredSearchBar: { - show: true, - tokens: ['with_inherited_permissions'], - searchParam: 'search', - placeholder: s__('Members|Filter members'), - recentSearchesStorageKey: 'project_members', +initMembersApp(document.querySelector('.js-project-members-list-app'), { + [MEMBER_TYPES.user]: { + tableFields: SHARED_FIELDS.concat(['source', 'granted']), + tableAttrs: { tr: { 'data-qa-selector': 'member_row' } }, + tableSortableFields: ['account', 'granted', 'maxRole', 'lastSignIn'], + requestFormatter: projectMemberRequestFormatter, + filteredSearchBar: { + show: true, + tokens: ['with_inherited_permissions'], + searchParam: 'search', + placeholder: s__('Members|Filter members'), + recentSearchesStorageKey: 'project_members', + }, }, -}); - -initMembersApp(document.querySelector('.js-project-group-links-list'), { - namespace: MEMBER_TYPES.group, - tableFields: SHARED_FIELDS.concat('granted'), - tableAttrs: { - table: { 'data-qa-selector': 'groups_list' }, - tr: { 'data-qa-selector': 'group_row' }, + [MEMBER_TYPES.group]: { + tableFields: SHARED_FIELDS.concat('granted'), + tableAttrs: { + table: { 'data-qa-selector': 'groups_list' }, + tr: { 'data-qa-selector': 'group_row' }, + }, + requestFormatter: groupLinkRequestFormatter, + filteredSearchBar: { + show: true, + tokens: [], + searchParam: 'search_groups', + placeholder: s__('Members|Search groups'), + recentSearchesStorageKey: 'project_group_links', + }, }, - requestFormatter: groupLinkRequestFormatter, - filteredSearchBar: { - show: true, - tokens: [], - searchParam: 'search_groups', - placeholder: s__('Members|Search groups'), - recentSearchesStorageKey: 'project_group_links', + [MEMBER_TYPES.invite]: { + tableFields: SHARED_FIELDS.concat('invited'), + requestFormatter: projectMemberRequestFormatter, + }, + [MEMBER_TYPES.accessRequest]: { + tableFields: SHARED_FIELDS.concat('requested'), + requestFormatter: projectMemberRequestFormatter, }, -}); - -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 10105af3561..db7b3bad6ed 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 @@ -4,6 +4,7 @@ import initSettingsPipelinesTriggers from '~/ci_settings_pipeline_triggers'; import initVariableList from '~/ci_variable_list'; import initDeployFreeze from '~/deploy_freeze'; import registrySettingsApp from '~/packages_and_registries/settings/project/registry_settings_bundle'; +import { initRunnerAwsDeployments } from '~/pages/shared/mount_runner_aws_deployments'; import { initInstallRunner } from '~/pages/shared/mount_runner_instructions'; import initSharedRunnersToggle from '~/projects/settings/mount_shared_runners_toggle'; import initSettingsPanels from '~/settings_panels'; @@ -38,4 +39,5 @@ document.addEventListener('DOMContentLoaded', () => { initArtifactsSettings(); initSharedRunnersToggle(); initInstallRunner(); + initRunnerAwsDeployments(); }); 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 01ad87160c5..53068f72d3f 100644 --- a/app/assets/javascripts/pages/projects/settings/integrations/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/integrations/show/index.js @@ -1,7 +1,3 @@ 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/shared/permissions/components/project_feature_setting.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/project_feature_setting.vue index c110c1d4d62..9fb8be3fdb9 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 @@ -91,7 +91,7 @@ export default { label-position="hidden" @change="toggleFeature" /> - <div class="select-wrapper gl-flex-fill-1"> + <div class="select-wrapper gl-flex-grow-1"> <select :disabled="displaySelectInput" class="form-control project-repo-select select-control" 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 0b7b4c0ded1..11e6b4577e0 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 @@ -381,7 +381,7 @@ export default { :label="s__('ProjectSettings|Project visibility')" > <div class="project-feature-controls gl-display-flex gl-align-items-center gl-my-3 gl-mx-0"> - <div class="select-wrapper gl-flex-fill-1"> + <div class="select-wrapper gl-flex-grow-1"> <select v-model="visibilityLevel" :disabled="!canChangeVisibilityLevel" diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js index 83e43d7ac48..26f8018a968 100644 --- a/app/assets/javascripts/pages/projects/show/index.js +++ b/app/assets/javascripts/pages/projects/show/index.js @@ -3,6 +3,8 @@ import Activities from '~/activities'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import BlobViewer from '~/blob/viewer/index'; import { initUploadForm } from '~/blob_edit/blob_bundle'; +import initInviteMembersModal from '~/invite_members/init_invite_members_modal'; +import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger'; import leaveByUrl from '~/namespaces/leave_by_url'; import initVueNotificationsDropdown from '~/notifications'; import { initUploadFileTrigger } from '~/projects/upload_file_experiment'; @@ -44,3 +46,5 @@ initVueNotificationsDropdown(); new ShortcutsNavigation(); // eslint-disable-line no-new initUploadFileTrigger(); +initInviteMembersModal(); +initInviteMembersTrigger(); diff --git a/app/assets/javascripts/pages/shared/mount_runner_aws_deployments.js b/app/assets/javascripts/pages/shared/mount_runner_aws_deployments.js new file mode 100644 index 00000000000..f3807a33a2b --- /dev/null +++ b/app/assets/javascripts/pages/shared/mount_runner_aws_deployments.js @@ -0,0 +1,17 @@ +import Vue from 'vue'; +import RunnerAwsDeployments from '~/vue_shared/components/runner_aws_deployments/runner_aws_deployments.vue'; + +export function initRunnerAwsDeployments(componentId = 'js-runner-aws-deployments') { + const el = document.getElementById(componentId); + + if (!el) { + return null; + } + + return new Vue({ + el, + render(createElement) { + return createElement(RunnerAwsDeployments); + }, + }); +} diff --git a/app/assets/javascripts/pages/shared/nav/sidebar_tracking.js b/app/assets/javascripts/pages/shared/nav/sidebar_tracking.js new file mode 100644 index 00000000000..79ce1a37d21 --- /dev/null +++ b/app/assets/javascripts/pages/shared/nav/sidebar_tracking.js @@ -0,0 +1,44 @@ +function onSidebarLinkClick() { + const setDataTrackAction = (element, action) => { + element.setAttribute('data-track-action', action); + }; + + const setDataTrackExtra = (element, value) => { + const SIDEBAR_COLLAPSED = 'Collapsed'; + const SIDEBAR_EXPANDED = 'Expanded'; + const sidebarCollapsed = document + .querySelector('.nav-sidebar') + .classList.contains('js-sidebar-collapsed') + ? SIDEBAR_COLLAPSED + : SIDEBAR_EXPANDED; + + element.setAttribute( + 'data-track-extra', + JSON.stringify({ sidebar_display: sidebarCollapsed, menu_display: value }), + ); + }; + + const EXPANDED = 'Expanded'; + const FLY_OUT = 'Fly out'; + const CLICK_MENU_ACTION = 'click_menu'; + const CLICK_MENU_ITEM_ACTION = 'click_menu_item'; + const parentElement = this.parentNode; + const subMenuList = parentElement.closest('.sidebar-sub-level-items'); + + if (subMenuList) { + const isFlyOut = subMenuList.classList.contains('fly-out-list') ? FLY_OUT : EXPANDED; + + setDataTrackExtra(parentElement, isFlyOut); + setDataTrackAction(parentElement, CLICK_MENU_ITEM_ACTION); + } else { + const isFlyOut = parentElement.classList.contains('is-showing-fly-out') ? FLY_OUT : EXPANDED; + + setDataTrackExtra(parentElement, isFlyOut); + setDataTrackAction(parentElement, CLICK_MENU_ACTION); + } +} +export const initSidebarTracking = () => { + document.querySelectorAll('.nav-sidebar li[data-track-label] > a').forEach((link) => { + link.addEventListener('click', onSidebarLinkClick); + }); +}; 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 43753926039..26f6d1d683a 100644 --- a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue +++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue @@ -14,8 +14,17 @@ import axios from '~/lib/utils/axios_utils'; import csrf from '~/lib/utils/csrf'; import { setUrlFragment } from '~/lib/utils/url_utility'; import { s__, sprintf } from '~/locale'; +import Tracking from '~/tracking'; import MarkdownField from '~/vue_shared/components/markdown/field.vue'; -import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import { + WIKI_CONTENT_EDITOR_TRACKING_LABEL, + CONTENT_EDITOR_LOADED_ACTION, + SAVED_USING_CONTENT_EDITOR_ACTION, +} from '../constants'; + +const trackingMixin = Tracking.mixin({ + label: WIKI_CONTENT_EDITOR_TRACKING_LABEL, +}); const MARKDOWN_LINK_TEXT = { markdown: '[Link Title](page-slug)', @@ -53,21 +62,30 @@ export default { ), primaryAction: s__('WikiPage|Retry'), }, - useNewEditor: s__('WikiPage|Use new editor'), + useNewEditor: { + primaryLabel: s__('WikiPage|Use the new editor'), + secondaryLabel: s__('WikiPage|Try this later'), + title: s__('WikiPage|Get a richer editing experience'), + text: s__( + "WikiPage|Try the new visual Markdown editor. Read the %{linkStart}documentation%{linkEnd} to learn what's currently supported.", + ), + }, switchToOldEditor: { - label: s__('WikiPage|Switch to old editor'), - helpText: s__("WikiPage|Switching will discard any changes you've made in the new editor."), + label: s__('WikiPage|Switch me back to the classic editor.'), + helpText: s__( + "WikiPage|This editor is in beta and may not display the page's contents properly. Switching back to the classic editor will discard changes you've made in the new editor.", + ), modal: { - title: s__('WikiPage|Are you sure you want to switch to the old editor?'), - primary: s__('WikiPage|Switch to old editor'), + title: s__('WikiPage|Are you sure you want to switch back to the classic editor?'), + primary: s__('WikiPage|Switch to classic editor'), cancel: s__('WikiPage|Keep editing'), text: s__( - "WikiPage|Switching to the old editor will discard any changes you've made in the new editor.", + "WikiPage|Switching to the classic editor will discard any changes you've made in the new editor.", ), }, }, - helpText: s__( - "WikiPage|This editor is in beta and may not display the page's contents properly.", + feedbackTip: s__( + 'Tell us your experiences with the new Markdown editor %{linkStart}in this feedback issue%{linkEnd}.', ), }, linksHelpText: s__( @@ -86,6 +104,7 @@ export default { }, cancel: s__('WikiPage|Cancel'), }, + contentEditorFeedbackIssue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/332629', components: { GlAlert, GlForm, @@ -104,13 +123,14 @@ export default { directives: { GlModalDirective, }, - mixins: [glFeatureFlagMixin()], + mixins: [trackingMixin], inject: ['formatOptions', 'pageInfo'], data() { return { title: this.pageInfo.title?.trim() || '', format: this.pageInfo.format || 'markdown', - content: this.pageInfo.content?.trim() || '', + content: this.pageInfo.content || '', + isContentEditorAlertDismissed: false, isContentEditorLoading: true, useContentEditor: false, commitMessage: '', @@ -120,6 +140,10 @@ export default { }; }, computed: { + noContent() { + if (this.isContentEditorActive) return this.contentEditor?.empty; + return !this.content.trim(); + }, csrfToken() { return csrf.token; }, @@ -157,14 +181,17 @@ export default { wikiSpecificMarkdownHelpPath() { return setUrlFragment(this.pageInfo.markdownHelpPath, 'wiki-specific-markdown'); }, + contentEditorHelpPath() { + return setUrlFragment(this.pageInfo.helpPath, 'gitlab-flavored-markdown-support'); + }, isMarkdownFormat() { return this.format === 'markdown'; }, - showContentEditorButton() { - return this.isMarkdownFormat && !this.useContentEditor && this.glFeatures.wikiContentEditor; + showContentEditorAlert() { + return this.isMarkdownFormat && !this.useContentEditor && !this.isContentEditorAlertDismissed; }, disableSubmitButton() { - return !this.content || !this.title || this.contentEditorRenderFailed; + return this.noContent || !this.title || this.contentEditorRenderFailed; }, isContentEditorActive() { return this.isMarkdownFormat && this.useContentEditor; @@ -188,6 +215,8 @@ export default { handleFormSubmit() { if (this.useContentEditor) { this.content = this.contentEditor.getSerializedContent(); + + this.trackFormSubmit(); } this.isDirty = false; @@ -236,6 +265,8 @@ export default { try { await this.contentEditor.setSerializedContent(this.content); this.isContentEditorLoading = false; + + this.trackContentEditorLoaded(); } catch (e) { this.contentEditorRenderFailed = true; } @@ -258,6 +289,20 @@ export default { this.$refs.confirmSwitchToOldEditorModal.show(); } }, + + trackContentEditorLoaded() { + this.track(CONTENT_EDITOR_LOADED_ACTION); + }, + + trackFormSubmit() { + if (this.isContentEditorActive) { + this.track(SAVED_USING_CONTENT_EDITOR_ACTION); + } + }, + + dismissContentEditorAlert() { + this.isContentEditorAlertDismissed = true; + }, }, }; </script> @@ -275,11 +320,9 @@ export default { :dismissible="false" variant="danger" :primary-button-text="$options.i18n.contentEditor.renderFailed.primaryAction" - @primaryAction="retryInitContentEditor()" + @primaryAction="retryInitContentEditor" > - <p> - {{ $options.i18n.contentEditor.renderFailed.message }} - </p> + {{ $options.i18n.contentEditor.renderFailed.message }} </gl-alert> <input :value="csrfToken" type="hidden" name="authenticity_token" /> @@ -299,7 +342,7 @@ export default { <div class="col-sm-10"> <input id="wiki_title" - v-model.trim="title" + v-model="title" name="wiki[title]" type="text" class="form-control" @@ -337,46 +380,50 @@ export default { {{ label }} </option> </select> - <div> - <gl-button - v-if="showContentEditorButton" - category="secondary" - variant="confirm" - class="gl-mt-4" - @click="initContentEditor" - >{{ $options.i18n.contentEditor.useNewEditor }}</gl-button - > - <div v-if="isContentEditorActive" class="gl-mt-4 gl-display-flex"> - <div class="gl-mr-4"> - <gl-button category="secondary" variant="confirm" @click="confirmSwitchToOldEditor">{{ - $options.i18n.contentEditor.switchToOldEditor.label - }}</gl-button> - </div> - <div class="gl-mt-2"> - <gl-icon name="warning" /> - {{ $options.i18n.contentEditor.switchToOldEditor.helpText }} - </div> - </div> - <gl-modal - ref="confirmSwitchToOldEditorModal" - modal-id="confirm-switch-to-old-editor" - :title="$options.i18n.contentEditor.switchToOldEditor.modal.title" - :action-primary="{ text: $options.i18n.contentEditor.switchToOldEditor.modal.primary }" - :action-cancel="{ text: $options.i18n.contentEditor.switchToOldEditor.modal.cancel }" - @primary="switchToOldEditor" - > - {{ $options.i18n.contentEditor.switchToOldEditor.modal.text }} - </gl-modal> - </div> </div> </div> - <div class="form-group row"> + <div class="form-group row" data-testid="wiki-form-content-fieldset"> <div class="col-sm-2 col-form-label"> <label class="control-label-full-width" for="wiki_content">{{ $options.i18n.content.label }}</label> </div> <div class="col-sm-10"> + <gl-alert + v-if="showContentEditorAlert" + class="gl-mb-6" + variant="info" + :primary-button-text="$options.i18n.contentEditor.useNewEditor.primaryLabel" + :secondary-button-text="$options.i18n.contentEditor.useNewEditor.secondaryLabel" + :dismiss-label="$options.i18n.contentEditor.useNewEditor.secondaryLabel" + :title="$options.i18n.contentEditor.useNewEditor.title" + @primaryAction="initContentEditor" + @secondaryAction="dismissContentEditorAlert" + @dismiss="dismissContentEditorAlert" + > + <gl-sprintf :message="$options.i18n.contentEditor.useNewEditor.text"> + <template + #link="// eslint-disable-next-line vue/no-template-shadow + { content }" + ><gl-link + :href="contentEditorHelpPath" + target="_blank" + data-testid="content-editor-help-link" + >{{ content }}</gl-link + ></template + > + </gl-sprintf> + </gl-alert> + <gl-modal + ref="confirmSwitchToOldEditorModal" + modal-id="confirm-switch-to-old-editor" + :title="$options.i18n.contentEditor.switchToOldEditor.modal.title" + :action-primary="{ text: $options.i18n.contentEditor.switchToOldEditor.modal.primary }" + :action-cancel="{ text: $options.i18n.contentEditor.switchToOldEditor.modal.cancel }" + @primary="switchToOldEditor" + > + {{ $options.i18n.contentEditor.switchToOldEditor.modal.text }} + </gl-modal> <markdown-field v-if="!isContentEditorActive" :markdown-preview-path="pageInfo.markdownPreviewPath" @@ -391,7 +438,7 @@ export default { <textarea id="wiki_content" ref="textarea" - v-model.trim="content" + v-model="content" name="wiki[content]" class="note-textarea js-gfm-input js-autosize markdown-area" dir="auto" @@ -407,6 +454,20 @@ export default { </markdown-field> <div v-if="isContentEditorActive"> + <gl-alert class="gl-mb-6" variant="tip" :dismissable="false"> + <gl-sprintf :message="$options.i18n.contentEditor.feedbackTip"> + <template + #link="// eslint-disable-next-line vue/no-template-shadow + { content }" + ><gl-link + :href="$options.contentEditorFeedbackIssue" + target="_blank" + data-testid="wiki-markdown-help-link" + >{{ content }}</gl-link + ></template + > + </gl-sprintf> + </gl-alert> <gl-loading-icon v-if="isContentEditorLoading" class="bordered-box gl-w-full gl-py-6" /> <content-editor v-else :content-editor="contentEditor" /> <input id="wiki_content" v-model.trim="content" type="hidden" name="wiki[content]" /> @@ -432,7 +493,10 @@ export default { > </gl-sprintf> <span v-else> - {{ $options.i18n.contentEditor.helpText }} + {{ $options.i18n.contentEditor.switchToOldEditor.helpText }} + <gl-button variant="link" @click="confirmSwitchToOldEditor">{{ + $options.i18n.contentEditor.switchToOldEditor.label + }}</gl-button> </span> </div> </div> diff --git a/app/assets/javascripts/pages/shared/wikis/constants.js b/app/assets/javascripts/pages/shared/wikis/constants.js new file mode 100644 index 00000000000..b358ac9cf52 --- /dev/null +++ b/app/assets/javascripts/pages/shared/wikis/constants.js @@ -0,0 +1,4 @@ +export const WIKI_CONTENT_EDITOR_TRACKING_LABEL = 'wiki_content_editor'; + +export const CONTENT_EDITOR_LOADED_ACTION = 'content_editor_loaded'; +export const SAVED_USING_CONTENT_EDITOR_ACTION = 'saved_using_content_editor'; diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js index c416106fdd8..03dba699461 100644 --- a/app/assets/javascripts/pages/users/activity_calendar.js +++ b/app/assets/javascripts/pages/users/activity_calendar.js @@ -1,4 +1,3 @@ -import { scaleLinear, scaleThreshold } from 'd3-scale'; import { select } from 'd3-selection'; import dateFormat from 'dateformat'; import $ from 'jquery'; @@ -8,7 +7,7 @@ import axios from '~/lib/utils/axios_utils'; import { getDayName, getDayDifference } from '~/lib/utils/datetime_utility'; import { n__, s__, __ } from '~/locale'; -const d3 = { select, scaleLinear, scaleThreshold }; +const d3 = { select }; const firstDayOfWeekChoices = Object.freeze({ sunday: 0, @@ -16,6 +15,14 @@ const firstDayOfWeekChoices = Object.freeze({ saturday: 6, }); +const CONTRIB_LEGENDS = [ + { title: __('No contributions'), min: 0 }, + { title: __('1-9 contributions'), min: 1 }, + { title: __('10-19 contributions'), min: 10 }, + { title: __('20-29 contributions'), min: 20 }, + { title: __('30+ contributions'), min: 30 }, +]; + const LOADING_HTML = ` <div class="text-center"> <div class="spinner spinner-md"></div> @@ -42,7 +49,17 @@ function formatTooltipText({ date, count }) { return `${contribText}<br /><span class="gl-text-gray-300">${dateDayName} ${dateText}</span>`; } -const initColorKey = () => d3.scaleLinear().range(['#acd5f2', '#254e77']).domain([0, 3]); +// Return the contribution level from the number of contributions +export const getLevelFromContributions = (count) => { + if (count <= 0) { + return 0; + } + + const nextLevel = CONTRIB_LEGENDS.findIndex(({ min }) => count < min); + + // If there is no higher level, we are at the end + return nextLevel >= 0 ? nextLevel - 1 : CONTRIB_LEGENDS.length - 1; +}; export default class ActivityCalendar { constructor( @@ -111,10 +128,6 @@ export default class ActivityCalendar { innerArray.push({ count, date, day }); } - // Init color functions - this.colorKey = initColorKey(); - this.color = this.initColor(); - // Init the svg element this.svg = this.renderSvg(container, group); this.renderDays(); @@ -180,9 +193,7 @@ export default class ActivityCalendar { .attr('y', (stamp) => this.dayYPos(stamp.day)) .attr('width', this.daySize) .attr('height', this.daySize) - .attr('fill', (stamp) => - stamp.count !== 0 ? this.color(Math.min(stamp.count, 40)) : '#ededed', - ) + .attr('data-level', (stamp) => getLevelFromContributions(stamp.count)) .attr('title', (stamp) => formatTooltipText(stamp)) .attr('class', 'user-contrib-cell has-tooltip') .attr('data-html', true) @@ -246,50 +257,24 @@ export default class ActivityCalendar { } renderKey() { - const keyValues = [ - __('No contributions'), - __('1-9 contributions'), - __('10-19 contributions'), - __('20-29 contributions'), - __('30+ contributions'), - ]; - const keyColors = [ - '#ededed', - this.colorKey(0), - this.colorKey(1), - this.colorKey(2), - this.colorKey(3), - ]; - this.svg .append('g') .attr('transform', `translate(18, ${this.daySizeWithSpace * 8 + 16})`) .selectAll('rect') - .data(keyColors) + .data(CONTRIB_LEGENDS) .enter() .append('rect') .attr('width', this.daySize) .attr('height', this.daySize) - .attr('x', (color, i) => this.daySizeWithSpace * i) + .attr('x', (_, i) => this.daySizeWithSpace * i) .attr('y', 0) - .attr('fill', (color) => color) - .attr('class', 'has-tooltip') - .attr('title', (color, i) => keyValues[i]) + .attr('data-level', (_, i) => i) + .attr('class', 'user-contrib-cell has-tooltip contrib-legend') + .attr('title', (x) => x.title) .attr('data-container', 'body') .attr('data-html', true); } - initColor() { - const colorRange = [ - '#ededed', - this.colorKey(0), - this.colorKey(1), - this.colorKey(2), - this.colorKey(3), - ]; - return d3.scaleThreshold().domain([0, 10, 20, 30]).range(colorRange); - } - clickDay(stamp) { if (this.currentSelectedDate !== stamp.date) { this.currentSelectedDate = stamp.date; |