diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-20 16:49:51 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-20 16:49:51 +0300 |
commit | 71786ddc8e28fbd3cb3fcc4b3ff15e5962a1c82e (patch) | |
tree | 6a2d93ef3fb2d353bb7739e4b57e6541f51cdd71 /app/assets/javascripts/pages | |
parent | a7253423e3403b8c08f8a161e5937e1488f5f407 (diff) |
Add latest changes from gitlab-org/gitlab@15-9-stable-eev15.9.0-rc42
Diffstat (limited to 'app/assets/javascripts/pages')
57 files changed, 638 insertions, 1018 deletions
diff --git a/app/assets/javascripts/pages/abuse_reports/index.js b/app/assets/javascripts/pages/abuse_reports/index.js new file mode 100644 index 00000000000..feceeb0b10a --- /dev/null +++ b/app/assets/javascripts/pages/abuse_reports/index.js @@ -0,0 +1,3 @@ +import { initLinkToSpam } from '~/abuse_reports'; + +initLinkToSpam(); diff --git a/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js b/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js index 455c637a6b3..8b8147425bc 100644 --- a/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js +++ b/app/assets/javascripts/pages/admin/application_settings/account_and_limits.js @@ -20,10 +20,65 @@ function setUserInternalRegexPlaceholder(checkbox) { } } -export default function initUserInternalRegexPlaceholder() { +function initUserInternalRegexPlaceholder() { const checkbox = document.getElementById('application_setting_user_default_external'); setUserInternalRegexPlaceholder(checkbox); checkbox.addEventListener('change', () => { setUserInternalRegexPlaceholder(checkbox); }); } + +/** + * Sets up logic inside "Dormant users" subsection: + * - checkbox enables/disables additional input + * - shows/hides an inline error on input validation + */ +function initDeactivateDormantUsersPeriodInputSection() { + const DISPLAY_NONE_CLASS = 'gl-display-none'; + + /** @type {HTMLInputElement} */ + const checkbox = document.getElementById('application_setting_deactivate_dormant_users'); + /** @type {HTMLInputElement} */ + const input = document.getElementById('application_setting_deactivate_dormant_users_period'); + /** @type {HTMLDivElement} */ + const errorLabel = document.getElementById( + 'application_setting_deactivate_dormant_users_period_error', + ); + + if (!checkbox || !input || !errorLabel) return; + + const hideInputErrorLabel = () => { + if (input.checkValidity()) { + errorLabel.classList.add(DISPLAY_NONE_CLASS); + } + }; + + const handleInputInvalidState = (event) => { + event.preventDefault(); + event.stopImmediatePropagation(); + errorLabel.classList.remove(DISPLAY_NONE_CLASS); + return false; + }; + + const updateInputDisabledState = () => { + input.disabled = !checkbox.checked; + if (input.disabled) { + hideInputErrorLabel(); + } + }; + + // Show error when input is invalid + input.addEventListener('invalid', handleInputInvalidState); + // Hide error when input changes + input.addEventListener('input', hideInputErrorLabel); + input.addEventListener('change', hideInputErrorLabel); + + // Handle checkbox change and set initial state + checkbox.addEventListener('change', updateInputDisabledState); + updateInputDisabledState(); +} + +export default function initAccountAndLimitsSection() { + initUserInternalRegexPlaceholder(); + initDeactivateDormantUsersPeriodInputSection(); +} 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 c48d99da990..8a810ca649c 100644 --- a/app/assets/javascripts/pages/admin/application_settings/general/index.js +++ b/app/assets/javascripts/pages/admin/application_settings/general/index.js @@ -1,9 +1,9 @@ -import initUserInternalRegexPlaceholder from '../account_and_limits'; +import initAccountAndLimitsSection from '../account_and_limits'; import initGitpod from '../gitpod'; import initSignupRestrictions from '../signup_restrictions'; (() => { - initUserInternalRegexPlaceholder(); + initAccountAndLimitsSection(); initGitpod(); initSignupRestrictions(); })(); diff --git a/app/assets/javascripts/pages/admin/application_settings/index.js b/app/assets/javascripts/pages/admin/application_settings/index.js index f1e92cf195a..366be334e87 100644 --- a/app/assets/javascripts/pages/admin/application_settings/index.js +++ b/app/assets/javascripts/pages/admin/application_settings/index.js @@ -1,5 +1,4 @@ import initVariableList from '~/ci/ci_variable_list'; -import projectSelect from '~/project_select'; import initSearchSettings from '~/search_settings'; import selfMonitor from '~/self_monitor'; import initSettingsPanels from '~/settings_panels'; @@ -8,5 +7,4 @@ initVariableList('js-instance-variables'); selfMonitor(); // Initialize expandable settings panels initSettingsPanels(); -projectSelect(); initSearchSettings(); diff --git a/app/assets/javascripts/pages/admin/hooks/index.js b/app/assets/javascripts/pages/admin/hooks/index.js new file mode 100644 index 00000000000..82e601426f1 --- /dev/null +++ b/app/assets/javascripts/pages/admin/hooks/index.js @@ -0,0 +1,3 @@ +import { initHookTestDropdowns } from '~/webhooks'; + +initHookTestDropdowns(); diff --git a/app/assets/javascripts/pages/admin/jobs/index/components/cancel_jobs.vue b/app/assets/javascripts/pages/admin/jobs/index/components/cancel_jobs.vue new file mode 100644 index 00000000000..72cfc005782 --- /dev/null +++ b/app/assets/javascripts/pages/admin/jobs/index/components/cancel_jobs.vue @@ -0,0 +1,37 @@ +<script> +import { GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui'; +import CancelJobsModal from './cancel_jobs_modal.vue'; +import { CANCEL_JOBS_MODAL_ID, CANCEL_JOBS_BUTTON_TEXT, CANCEL_BUTTON_TOOLTIP } from './constants'; + +export default { + name: 'CancelJobs', + components: { + GlButton, + CancelJobsModal, + }, + directives: { + GlModal: GlModalDirective, + GlTooltip: GlTooltipDirective, + }, + props: { + url: { + type: String, + required: true, + }, + }, + modalId: CANCEL_JOBS_MODAL_ID, + buttonText: CANCEL_JOBS_BUTTON_TEXT, + buttonTooltip: CANCEL_BUTTON_TOOLTIP, +}; +</script> +<template> + <div> + <gl-button + v-gl-modal="$options.modalId" + v-gl-tooltip="$options.buttonTooltip" + variant="danger" + >{{ $options.buttonText }}</gl-button + > + <cancel-jobs-modal :modal-id="$options.modalId" :url="url" @confirm="$emit('confirm')" /> + </div> +</template> diff --git a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue b/app/assets/javascripts/pages/admin/jobs/index/components/cancel_jobs_modal.vue index b608b3b9492..d5857294617 100644 --- a/app/assets/javascripts/pages/admin/jobs/index/components/stop_jobs_modal.vue +++ b/app/assets/javascripts/pages/admin/jobs/index/components/cancel_jobs_modal.vue @@ -5,10 +5,9 @@ import axios from '~/lib/utils/axios_utils'; import { redirectTo } from '~/lib/utils/url_utility'; import { CANCEL_TEXT, - STOP_JOBS_MODAL_ID, - STOP_JOBS_FAILED_TEXT, - STOP_JOBS_MODAL_TITLE, - STOP_JOBS_WARNING, + CANCEL_JOBS_FAILED_TEXT, + CANCEL_JOBS_MODAL_TITLE, + CANCEL_JOBS_WARNING, PRIMARY_ACTION_TEXT, } from './constants'; @@ -21,6 +20,10 @@ export default { type: String, required: true, }, + modalId: { + type: String, + required: true, + }, }, methods: { onSubmit() { @@ -32,7 +35,7 @@ export default { }) .catch((error) => { createAlert({ - message: STOP_JOBS_FAILED_TEXT, + message: CANCEL_JOBS_FAILED_TEXT, }); throw error; }); @@ -45,20 +48,19 @@ export default { cancelAction: { text: CANCEL_TEXT, }, - STOP_JOBS_WARNING, - STOP_JOBS_MODAL_ID, - STOP_JOBS_MODAL_TITLE, + CANCEL_JOBS_WARNING, + CANCEL_JOBS_MODAL_TITLE, }; </script> <template> <gl-modal - :modal-id="$options.STOP_JOBS_MODAL_ID" + :modal-id="modalId" :action-primary="$options.primaryAction" :action-cancel="$options.cancelAction" + :title="$options.CANCEL_JOBS_MODAL_TITLE" @primary="onSubmit" > - <template #modal-title>{{ $options.STOP_JOBS_MODAL_TITLE }}</template> - {{ $options.STOP_JOBS_WARNING }} + {{ $options.CANCEL_JOBS_WARNING }} </gl-modal> </template> diff --git a/app/assets/javascripts/pages/admin/jobs/index/components/constants.js b/app/assets/javascripts/pages/admin/jobs/index/components/constants.js index 9e2d464bc4d..cfde1fc0a2b 100644 --- a/app/assets/javascripts/pages/admin/jobs/index/components/constants.js +++ b/app/assets/javascripts/pages/admin/jobs/index/components/constants.js @@ -1,11 +1,12 @@ import { s__, __ } from '~/locale'; -export const STOP_JOBS_MODAL_ID = 'stop-jobs-modal'; -export const STOP_JOBS_MODAL_TITLE = s__('AdminArea|Stop all jobs?'); -export const STOP_JOBS_BUTTON_TEXT = s__('AdminArea|Stop all jobs'); +export const CANCEL_JOBS_MODAL_ID = 'cancel-jobs-modal'; +export const CANCEL_JOBS_MODAL_TITLE = s__('AdminArea|Are you sure?'); +export const CANCEL_JOBS_BUTTON_TEXT = s__('AdminArea|Cancel all jobs'); +export const CANCEL_BUTTON_TOOLTIP = s__('AdminArea|Cancel all running and pending jobs'); export const CANCEL_TEXT = __('Cancel'); -export const STOP_JOBS_FAILED_TEXT = s__('AdminArea|Stopping jobs failed'); -export const PRIMARY_ACTION_TEXT = s__('AdminArea|Stop jobs'); -export const STOP_JOBS_WARNING = s__( - 'AdminArea|You’re about to stop all jobs. This will halt all current jobs that are running.', +export const CANCEL_JOBS_FAILED_TEXT = s__('AdminArea|Canceling jobs failed'); +export const PRIMARY_ACTION_TEXT = s__('AdminArea|Yes, proceed'); +export const CANCEL_JOBS_WARNING = s__( + "AdminArea|You're about to cancel all running and pending jobs across this instance. Do you want to proceed?", ); diff --git a/app/assets/javascripts/pages/admin/jobs/index/components/table/admin_jobs_table_app.vue b/app/assets/javascripts/pages/admin/jobs/index/components/table/admin_jobs_table_app.vue new file mode 100644 index 00000000000..c5a0509b625 --- /dev/null +++ b/app/assets/javascripts/pages/admin/jobs/index/components/table/admin_jobs_table_app.vue @@ -0,0 +1,19 @@ +<script> +export default { + inject: { + jobStatuses: { + default: null, + }, + url: { + default: '', + }, + emptyStateSvgPath: { + default: '', + }, + }, +}; +</script> + +<template> + <div>{{ __('Jobs') }}</div> +</template> diff --git a/app/assets/javascripts/pages/admin/jobs/index/index.js b/app/assets/javascripts/pages/admin/jobs/index/index.js index c82b186f671..9df52557212 100644 --- a/app/assets/javascripts/pages/admin/jobs/index/index.js +++ b/app/assets/javascripts/pages/admin/jobs/index/index.js @@ -1,31 +1,33 @@ import Vue from 'vue'; import { BV_SHOW_MODAL } from '~/lib/utils/constants'; import Translate from '~/vue_shared/translate'; -import { STOP_JOBS_MODAL_ID } from './components/constants'; -import StopJobsModal from './components/stop_jobs_modal.vue'; +import { CANCEL_JOBS_MODAL_ID } from './components/constants'; +import CancelJobsModal from './components/cancel_jobs_modal.vue'; +import AdminJobsTableApp from './components/table/admin_jobs_table_app.vue'; Vue.use(Translate); function initJobs() { const buttonId = 'js-stop-jobs-button'; - const stopJobsButton = document.getElementById(buttonId); - if (stopJobsButton) { + const cancelJobsButton = document.getElementById(buttonId); + if (cancelJobsButton) { // eslint-disable-next-line no-new new Vue({ - el: `#js-${STOP_JOBS_MODAL_ID}`, + el: `#js-${CANCEL_JOBS_MODAL_ID}`, components: { - StopJobsModal, + CancelJobsModal, }, mounted() { - stopJobsButton.classList.remove('disabled'); - stopJobsButton.addEventListener('click', () => { - this.$root.$emit(BV_SHOW_MODAL, STOP_JOBS_MODAL_ID, `#${buttonId}`); + cancelJobsButton.classList.remove('disabled'); + cancelJobsButton.addEventListener('click', () => { + this.$root.$emit(BV_SHOW_MODAL, CANCEL_JOBS_MODAL_ID, `#${buttonId}`); }); }, render(createElement) { - return createElement(STOP_JOBS_MODAL_ID, { + return createElement(CANCEL_JOBS_MODAL_ID, { props: { - url: stopJobsButton.dataset.url, + url: cancelJobsButton.dataset.url, + modalId: CANCEL_JOBS_MODAL_ID, }, }); }, @@ -33,4 +35,28 @@ function initJobs() { } } -initJobs(); +export function initAdminJobsApp() { + const containerEl = document.getElementById('admin-jobs-app'); + + if (!containerEl) return false; + + const { jobStatuses, emptyStateSvgPath, url } = containerEl.dataset; + + return new Vue({ + el: containerEl, + provide: { + url, + emptyStateSvgPath, + jobStatuses: JSON.parse(jobStatuses), + }, + render(createElement) { + return createElement(AdminJobsTableApp); + }, + }); +} + +if (gon.features.adminJobsVue) { + initAdminJobsApp(); +} else { + initJobs(); +} diff --git a/app/assets/javascripts/pages/admin/projects/components/namespace_select.vue b/app/assets/javascripts/pages/admin/projects/components/namespace_select.vue index c75c031b0b1..fa8f78839f3 100644 --- a/app/assets/javascripts/pages/admin/projects/components/namespace_select.vue +++ b/app/assets/javascripts/pages/admin/projects/components/namespace_select.vue @@ -1,34 +1,31 @@ <script> -import { - GlDropdown, - GlDropdownItem, - GlDropdownDivider, - GlSearchBoxByType, - GlLoadingIcon, -} from '@gitlab/ui'; +import { debounce } from 'lodash'; +import { GlCollapsibleListbox } from '@gitlab/ui'; import Api from '~/api'; +import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants'; import { __ } from '~/locale'; export default { i18n: { - dropdownHeader: __('Namespaces'), + headerText: __('Namespaces'), searchPlaceholder: __('Search for Namespace'), - anyNamespace: __('Any namespace'), + reset: __('Clear'), }, components: { - GlDropdown, - GlDropdownItem, - GlDropdownDivider, - GlLoadingIcon, - GlSearchBoxByType, + GlCollapsibleListbox, }, props: { - showAny: { - type: Boolean, + origSelectedId: { + type: String, + required: false, + default: '', + }, + origSelectedText: { + type: String, required: false, - default: false, + default: '', }, - placeholder: { + toggleTextPlaceholder: { type: String, required: false, default: __('Namespace'), @@ -42,56 +39,72 @@ export default { data() { return { namespaceOptions: [], - selectedNamespaceId: null, - selectedNamespace: null, + selectedNamespaceId: this.origSelectedId, + selectedNamespaceText: this.origSelectedText, searchTerm: '', isLoading: false, }; }, computed: { - selectedNamespaceName() { - if (this.selectedNamespaceId === null) { - return this.placeholder; - } - return this.selectedNamespace; + toggleText() { + return this.selectedNamespaceText || this.toggleTextPlaceholder; }, }, watch: { - searchTerm() { - this.fetchNamespaces(this.searchTerm); + selectedNamespaceId(val) { + if (!val) { + this.selectedNamespaceText = null; + } + + this.selectedNamespaceText = this.namespaceOptions.find(({ value }) => value === val)?.text; }, }, mounted() { this.fetchNamespaces(); }, methods: { - fetchNamespaces(filter) { + fetchNamespaces() { this.isLoading = true; this.namespaceOptions = []; - return Api.namespaces(filter, (namespaces) => { - this.namespaceOptions = namespaces; + + return Api.namespaces(this.searchTerm, (namespaces) => { + this.namespaceOptions = this.formatNamespaceOptions(namespaces); this.isLoading = false; }); }, - selectNamespace(key) { - this.selectedNamespaceId = this.namespaceOptions[key].id; - this.selectedNamespace = this.getNamespaceString(this.namespaceOptions[key]); - this.$emit('setNamespace', this.selectedNamespaceId); + formatNamespaceOptions(namespaces) { + if (!namespaces) { + return []; + } + + return namespaces.map((namespace) => { + return { + value: String(namespace.id), + text: this.getNamespaceString(namespace), + }; + }); }, - selectAnyNamespace() { - this.selectedNamespaceId = null; - this.selectedNamespace = null; - this.$emit('setNamespace', null); + selectNamespace(value) { + this.selectedNamespaceId = value; + this.$emit('setNamespace', this.selectedNamespaceId); }, getNamespaceString(namespace) { return `${namespace.kind}: ${namespace.full_path}`; }, + search: debounce(function debouncedSearch(searchQuery) { + this.searchTerm = searchQuery?.trim(); + this.fetchNamespaces(); + }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS), + onReset() { + this.selectedNamespaceId = null; + this.$emit('setNamespace', null); + }, }, }; </script> <template> - <div class="gl-display-flex"> + <div class="gl-display-flex gl-w-full"> <input v-if="fieldName" :name="fieldName" @@ -99,45 +112,19 @@ export default { type="hidden" data-testid="hidden-input" /> - <gl-dropdown - :text="selectedNamespaceName" - :header-text="$options.i18n.dropdownHeader" - toggle-class="dropdown-menu-toggle large" - data-testid="namespace-dropdown" - :right="true" - > - <template #header> - <gl-search-box-by-type - v-model.trim="searchTerm" - class="namespace-search-box" - debounce="250" - :placeholder="$options.i18n.searchPlaceholder" - /> - </template> - - <template v-if="showAny"> - <gl-dropdown-item @click="selectAnyNamespace"> - {{ $options.i18n.anyNamespace }} - </gl-dropdown-item> - <gl-dropdown-divider /> - </template> - - <gl-loading-icon v-if="isLoading" /> - - <gl-dropdown-item - v-for="(namespace, key) in namespaceOptions" - :key="namespace.id" - @click="selectNamespace(key)" - > - {{ getNamespaceString(namespace) }} - </gl-dropdown-item> - </gl-dropdown> + <gl-collapsible-listbox + :items="namespaceOptions" + :header-text="$options.i18n.headerText" + :reset-button-label="$options.i18n.reset" + :toggle-text="toggleText" + :search-placeholder="$options.i18n.searchPlaceholder" + :searching="isLoading" + :selected="selectedNamespaceId" + toggle-class="gl-w-full gl-flex-direction-column gl-align-items-stretch!" + searchable + @reset="onReset" + @search="search" + @select="selectNamespace" + /> </div> </template> - -<style scoped> -/* workaround position: relative imposed by .top-area .nav-controls */ -.namespace-search-box >>> input { - position: static; -} -</style> diff --git a/app/assets/javascripts/pages/admin/projects/index.js b/app/assets/javascripts/pages/admin/projects/index.js index 3098d06510b..49ee89de772 100644 --- a/app/assets/javascripts/pages/admin/projects/index.js +++ b/app/assets/javascripts/pages/admin/projects/index.js @@ -1,5 +1,4 @@ import Vue from 'vue'; -import { parseBoolean } from '~/lib/utils/common_utils'; import { mergeUrlParams } from '~/lib/utils/url_utility'; import ProjectsList from '~/projects_list'; import NamespaceSelect from './components/namespace_select.vue'; @@ -12,16 +11,17 @@ function mountNamespaceSelect() { return false; } - const { showAny, fieldName, placeholder, updateLocation } = el.dataset; + const { fieldName, toggleTextPlaceholder, selectedId, selectedText, updateLocation } = el.dataset; return new Vue({ el, render(createComponent) { return createComponent(NamespaceSelect, { props: { - showAny: parseBoolean(showAny), fieldName, - placeholder, + toggleTextPlaceholder, + origSelectedId: selectedId, + origSelectedText: selectedText, }, on: { setNamespace(newNamespace) { diff --git a/app/assets/javascripts/pages/admin/runners/new/index.js b/app/assets/javascripts/pages/admin/runners/new/index.js new file mode 100644 index 00000000000..5048ad7b57a --- /dev/null +++ b/app/assets/javascripts/pages/admin/runners/new/index.js @@ -0,0 +1,3 @@ +import { initAdminNewRunner } from '~/ci/runner/admin_new_runner'; + +initAdminNewRunner(); diff --git a/app/assets/javascripts/pages/dashboard/issues/index.js b/app/assets/javascripts/pages/dashboard/issues/index.js index 08c247a498b..2ca11e96f69 100644 --- a/app/assets/javascripts/pages/dashboard/issues/index.js +++ b/app/assets/javascripts/pages/dashboard/issues/index.js @@ -3,7 +3,7 @@ import { mountIssuesDashboardApp } from '~/issues/dashboard'; import initManualOrdering from '~/issues/manual_ordering'; import { FILTERED_SEARCH } from '~/filtered_search/constants'; import initFilteredSearch from '~/pages/search/init_filtered_search'; -import projectSelect from '~/project_select'; +import { initNewResourceDropdown } from '~/vue_shared/components/new_resource_dropdown/init_new_resource_dropdown'; initFilteredSearch({ page: FILTERED_SEARCH.ISSUES, @@ -11,7 +11,7 @@ initFilteredSearch({ useDefaultState: true, }); -projectSelect(); +initNewResourceDropdown(); initManualOrdering(); mountIssuesDashboardApp(); diff --git a/app/assets/javascripts/pages/dashboard/merge_requests/index.js b/app/assets/javascripts/pages/dashboard/merge_requests/index.js index 1350837476b..a8c59ea6f3d 100644 --- a/app/assets/javascripts/pages/dashboard/merge_requests/index.js +++ b/app/assets/javascripts/pages/dashboard/merge_requests/index.js @@ -2,7 +2,9 @@ import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; import { FILTERED_SEARCH } from '~/filtered_search/constants'; import initFilteredSearch from '~/pages/search/init_filtered_search'; -import projectSelect from '~/project_select'; +import { initNewResourceDropdown } from '~/vue_shared/components/new_resource_dropdown/init_new_resource_dropdown'; +import { RESOURCE_TYPE_MERGE_REQUEST } from '~/vue_shared/components/new_resource_dropdown/constants'; +import searchUserProjectsWithMergeRequestsEnabled from '~/vue_shared/components/new_resource_dropdown/graphql/search_user_projects_with_merge_requests_enabled.query.graphql'; addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys, true); @@ -12,4 +14,7 @@ initFilteredSearch({ useDefaultState: true, }); -projectSelect(); +initNewResourceDropdown({ + resourceType: RESOURCE_TYPE_MERGE_REQUEST, + query: searchUserProjectsWithMergeRequestsEnabled, +}); diff --git a/app/assets/javascripts/pages/dashboard/milestones/index/index.js b/app/assets/javascripts/pages/dashboard/milestones/index/index.js index b526fce6f7b..88061d9ca22 100644 --- a/app/assets/javascripts/pages/dashboard/milestones/index/index.js +++ b/app/assets/javascripts/pages/dashboard/milestones/index/index.js @@ -1,3 +1,12 @@ -import projectSelect from '~/project_select'; +import { initNewResourceDropdown } from '~/vue_shared/components/new_resource_dropdown/init_new_resource_dropdown'; +import { RESOURCE_TYPE_MILESTONE } from '~/vue_shared/components/new_resource_dropdown/constants'; +import searchUserGroupsAndProjects from '~/vue_shared/components/new_resource_dropdown/graphql/search_user_groups_and_projects.query.graphql'; -projectSelect(); +initNewResourceDropdown({ + resourceType: RESOURCE_TYPE_MILESTONE, + query: searchUserGroupsAndProjects, + extractProjects: (data) => [ + ...(data?.user?.groups?.nodes ?? []), + ...(data?.projects?.nodes ?? []), + ], +}); diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js index c5d62ae5daf..2fdf3c42935 100644 --- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js +++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js @@ -133,10 +133,10 @@ export default class Todos { restoreBtn.classList.add('hidden'); doneBtn.classList.remove('hidden'); } else if (target === doneBtn) { - row.classList.add('done-reversible', 'gl-bg-gray-50', 'gl-border-gray-100'); + row.classList.add('done-reversible', 'gl-bg-gray-10', 'gl-border-gray-50'); restoreBtn.classList.remove('hidden'); } else if (target === restoreBtn) { - row.classList.remove('done-reversible', 'gl-bg-gray-50', 'gl-border-gray-100'); + row.classList.remove('done-reversible', 'gl-bg-gray-10', 'gl-border-gray-50'); doneBtn.classList.remove('hidden'); } else { row.parentNode.removeChild(row); @@ -147,17 +147,17 @@ export default class Todos { e.stopPropagation(); e.preventDefault(); - const target = e.currentTarget; - target.setAttribute('disabled', true); - target.classList.add('disabled'); + const { currentTarget } = e; + currentTarget.setAttribute('disabled', true); + currentTarget.classList.add('disabled'); - target.querySelector('.gl-spinner-container').classList.add('gl-mr-2'); + currentTarget.querySelector('.gl-spinner-container').classList.add('gl-mr-2'); - axios[target.dataset.method](target.dataset.href, { + axios[currentTarget.dataset.method](currentTarget.href, { ids: this.todo_ids, }) .then(({ data }) => { - this.updateAllState(target, data); + this.updateAllState(currentTarget, data); this.updateBadges(data); }) .catch(() => diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js index fb685247bd4..dec06fe6f4d 100644 --- a/app/assets/javascripts/pages/groups/edit/index.js +++ b/app/assets/javascripts/pages/groups/edit/index.js @@ -2,10 +2,10 @@ import { GROUP_BADGE } from '~/badges/constants'; import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory'; import initFilePickers from '~/file_pickers'; import initTransferGroupForm from '~/groups/init_transfer_group_form'; -import { initGroupSelects } from '~/vue_shared/components/group_select/init_group_selects'; +import { initGroupSelects } from '~/vue_shared/components/entity_select/init_group_selects'; +import { initProjectSelects } from '~/vue_shared/components/entity_select/init_project_selects'; import { initCascadingSettingsLockPopovers } from '~/namespaces/cascading_settings'; import mountBadgeSettings from '~/pages/shared/mount_badge_settings'; -import projectSelect from '~/project_select'; import initSearchSettings from '~/search_settings'; import initSettingsPanels from '~/settings_panels'; import initConfirmDanger from '~/init_confirm_danger'; @@ -22,7 +22,8 @@ mountBadgeSettings(GROUP_BADGE); // Initialize Subgroups selector initGroupSelects(); -projectSelect(); +// Initialize project selectors +initProjectSelects(); initSearchSettings(); initCascadingSettingsLockPopovers(); diff --git a/app/assets/javascripts/pages/groups/group_members/index.js b/app/assets/javascripts/pages/groups/group_members/index.js index ceda2c8fa17..1b3c7ba5a52 100644 --- a/app/assets/javascripts/pages/groups/group_members/index.js +++ b/app/assets/javascripts/pages/groups/group_members/index.js @@ -12,7 +12,6 @@ const SHARED_FIELDS = ['account', 'maxRole', 'expiration', 'actions']; const APP_OPTIONS = { [MEMBER_TYPES.user]: { tableFields: SHARED_FIELDS.concat(['source', 'activity']), - tableAttrs: { tr: { 'data-qa-selector': 'member_row' } }, tableSortableFields: [ 'account', 'granted', @@ -32,10 +31,6 @@ const APP_OPTIONS = { }, [MEMBER_TYPES.group]: { tableFields: SHARED_FIELDS.concat(['source', 'granted']), - tableAttrs: { - table: { 'data-qa-selector': 'groups_list' }, - tr: { 'data-qa-selector': 'group_row' }, - }, requestFormatter: groupLinkRequestFormatter, filteredSearchBar: { show: true, diff --git a/app/assets/javascripts/pages/groups/merge_requests/index.js b/app/assets/javascripts/pages/groups/merge_requests/index.js index bf0147ca885..2cf75fcf666 100644 --- a/app/assets/javascripts/pages/groups/merge_requests/index.js +++ b/app/assets/javascripts/pages/groups/merge_requests/index.js @@ -3,7 +3,9 @@ import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered import { FILTERED_SEARCH } from '~/filtered_search/constants'; import { initBulkUpdateSidebar } from '~/issuable'; import initFilteredSearch from '~/pages/search/init_filtered_search'; -import projectSelect from '~/project_select'; +import { initNewResourceDropdown } from '~/vue_shared/components/new_resource_dropdown/init_new_resource_dropdown'; +import { RESOURCE_TYPE_MERGE_REQUEST } from '~/vue_shared/components/new_resource_dropdown/constants'; +import searchUserGroupProjectsWithMergeRequestsEnabled from '~/vue_shared/components/new_resource_dropdown/graphql/search_user_group_projects_with_merge_requests_enabled.query.graphql'; const ISSUABLE_BULK_UPDATE_PREFIX = 'merge_request_'; @@ -16,4 +18,8 @@ initFilteredSearch({ useDefaultState: true, filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys, }); -projectSelect(); +initNewResourceDropdown({ + resourceType: RESOURCE_TYPE_MERGE_REQUEST, + query: searchUserGroupProjectsWithMergeRequestsEnabled, + extractProjects: (data) => data?.group?.projects?.nodes, +}); diff --git a/app/assets/javascripts/pages/groups/usage_quotas/index.js b/app/assets/javascripts/pages/groups/usage_quotas/index.js new file mode 100644 index 00000000000..dab2d0b17d2 --- /dev/null +++ b/app/assets/javascripts/pages/groups/usage_quotas/index.js @@ -0,0 +1,3 @@ +import initUsageQuotas from '~/usage_quotas'; + +initUsageQuotas(); diff --git a/app/assets/javascripts/pages/profiles/saved_replies/index.js b/app/assets/javascripts/pages/profiles/saved_replies/index.js new file mode 100644 index 00000000000..ef227b82172 --- /dev/null +++ b/app/assets/javascripts/pages/profiles/saved_replies/index.js @@ -0,0 +1,3 @@ +import { initSavedReplies } from '~/saved_replies'; + +initSavedReplies(); diff --git a/app/assets/javascripts/pages/projects/airflow/dags/index/index.js b/app/assets/javascripts/pages/projects/airflow/dags/index/index.js new file mode 100644 index 00000000000..1d7cf4a5b8e --- /dev/null +++ b/app/assets/javascripts/pages/projects/airflow/dags/index/index.js @@ -0,0 +1,27 @@ +import Vue from 'vue'; +import AirflowDags from '~/airflow/dags/components/dags.vue'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; + +const initShowDags = () => { + const element = document.querySelector('#js-show-airflow-dags'); + if (!element) { + return null; + } + + const dags = JSON.parse(element.dataset.dags); + const pagination = convertObjectPropsToCamelCase(JSON.parse(element.dataset.pagination)); + + return new Vue({ + el: element, + render(h) { + return h(AirflowDags, { + props: { + dags, + pagination, + }, + }); + }, + }); +}; + +initShowDags(); diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js index 46704d96552..667fd89af55 100644 --- a/app/assets/javascripts/pages/projects/commit/show/index.js +++ b/app/assets/javascripts/pages/projects/commit/show/index.js @@ -16,6 +16,7 @@ import syntaxHighlight from '~/syntax_highlight'; import ZenMode from '~/zen_mode'; import '~/sourcegraph/load'; import DiffStats from '~/diffs/components/diff_stats.vue'; +import { initReportAbuse } from '~/projects/report_abuse'; const hasPerfBar = document.querySelector('.with-performance-bar'); const performanceHeight = hasPerfBar ? 35 : 0; @@ -26,6 +27,7 @@ new ShortcutsNavigation(); initCommitBoxInfo(); initDeprecatedNotes(); +initReportAbuse(); const loadDiffStats = () => { const diffStatsElements = document.querySelectorAll('#js-diff-stats'); @@ -67,6 +69,7 @@ if (filesContainer.length) { handleLocationHash(); new Diff(); loadDiffStats(); + initReportAbuse(); }) .catch(() => { createAlert({ message: __('An error occurred while retrieving diff files') }); diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js index c0eb2a8fd77..82035008459 100644 --- a/app/assets/javascripts/pages/projects/edit/index.js +++ b/app/assets/javascripts/pages/projects/edit/index.js @@ -10,6 +10,8 @@ import initSearchSettings from '~/search_settings'; import initSettingsPanels from '~/settings_panels'; import UserCallout from '~/user_callout'; import initTopicsTokenSelector from '~/projects/settings/topics'; +import { initProjectSelects } from '~/vue_shared/components/entity_select/init_project_selects'; +import initPruneObjectsButton from '~/projects/prune_objects_button'; import initProjectPermissionsSettings from '../shared/permissions'; import initProjectLoadingSpinner from '../shared/save_project_loader'; @@ -17,6 +19,7 @@ initFilePickers(); initConfirmDanger(); initSettingsPanels(); initProjectDeleteButton(); +initPruneObjectsButton(); mountBadgeSettings(PROJECT_BADGE); new UserCallout({ className: 'js-service-desk-callout' }); // eslint-disable-line no-new @@ -30,3 +33,4 @@ dirtySubmitFactory(document.querySelectorAll('.js-general-settings-form, .js-mr- initSearchSettings(); initTopicsTokenSelector(); +initProjectSelects(); diff --git a/app/assets/javascripts/pages/projects/find_file/ref_switcher/index.js b/app/assets/javascripts/pages/projects/find_file/ref_switcher/index.js new file mode 100644 index 00000000000..9a3bb25de70 --- /dev/null +++ b/app/assets/javascripts/pages/projects/find_file/ref_switcher/index.js @@ -0,0 +1,38 @@ +import Vue from 'vue'; +import { s__ } from '~/locale'; +import Translate from '~/vue_shared/translate'; +import RefSelector from '~/ref/components/ref_selector.vue'; +import { visitUrl } from '~/lib/utils/url_utility'; +import { generateRefDestinationPath } from './ref_switcher_utils'; + +Vue.use(Translate); + +const REF_SWITCH_HEADER = s__('FindFile|Switch branch/tag'); + +export default () => { + const el = document.getElementById('js-blob-ref-switcher'); + if (!el) return false; + + const { projectId, ref, namespace } = el.dataset; + + return new Vue({ + el, + render(createElement) { + return createElement(RefSelector, { + props: { + projectId, + value: ref, + translations: { + dropdownHeader: REF_SWITCH_HEADER, + searchPlaceholder: REF_SWITCH_HEADER, + }, + }, + on: { + input(selected) { + visitUrl(generateRefDestinationPath(selected, namespace)); + }, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/pages/projects/find_file/ref_switcher/ref_switcher_utils.js b/app/assets/javascripts/pages/projects/find_file/ref_switcher/ref_switcher_utils.js new file mode 100644 index 00000000000..5fecd024f1a --- /dev/null +++ b/app/assets/javascripts/pages/projects/find_file/ref_switcher/ref_switcher_utils.js @@ -0,0 +1,28 @@ +import { joinPaths } from '~/lib/utils/url_utility'; + +/** + * Generates a ref destination url based on the selected ref and current url. + * @param {string} selectedRef - The selected ref from the ref dropdown. + * @param {string} namespace - The destination namespace for the path. + */ +export function generateRefDestinationPath(selectedRef, namespace) { + if (!selectedRef || !namespace) { + return window.location.href; + } + + const { pathname } = window.location; + const encodedHash = '%23'; + + const [projectRootPath] = pathname.split(namespace); + + const destinationPath = joinPaths( + projectRootPath, + namespace, + encodeURI(selectedRef).replace(/#/g, encodedHash), + ); + + const newURL = new URL(window.location); + newURL.pathname = destinationPath; + + return newURL.href; +} diff --git a/app/assets/javascripts/pages/projects/find_file/show/index.js b/app/assets/javascripts/pages/projects/find_file/show/index.js index f47888f0cb8..e207df2434b 100644 --- a/app/assets/javascripts/pages/projects/find_file/show/index.js +++ b/app/assets/javascripts/pages/projects/find_file/show/index.js @@ -1,7 +1,9 @@ import $ from 'jquery'; import ShortcutsFindFile from '~/behaviors/shortcuts/shortcuts_find_file'; import ProjectFindFile from '~/projects/project_find_file'; +import InitBlobRefSwitcher from '../ref_switcher'; +InitBlobRefSwitcher(); const findElement = document.querySelector('.js-file-finder'); const projectFindFile = new ProjectFindFile($('.file-finder-holder'), { url: findElement.dataset.fileFindUrl, 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 2028af8b8f0..85fe3477d7c 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 @@ -16,7 +16,7 @@ import { createAlert } from '~/flash'; import axios from '~/lib/utils/axios_utils'; import csrf from '~/lib/utils/csrf'; import { redirectTo } from '~/lib/utils/url_utility'; -import { s__ } from '~/locale'; +import { s__, __ } from '~/locale'; import validation from '~/vue_shared/directives/validation'; import { VISIBILITY_LEVEL_PRIVATE_STRING, @@ -25,8 +25,24 @@ import { VISIBILITY_LEVELS_STRING_TO_INTEGER, VISIBILITY_LEVELS_INTEGER_TO_STRING, } from '~/visibility_level/constants'; +import { START_RULE, CONTAINS_RULE } from '~/projects/project_name_rules'; import ProjectNamespace from './project_namespace.vue'; +const feedbackMap = { + valueMissing: { + isInvalid: (el) => el.validity?.valueMissing, + message: __('Please fill out this field.'), + }, + nameStartPattern: { + isInvalid: (el) => el.validity?.patternMismatch && !START_RULE.reg.test(el.value), + message: START_RULE.msg, + }, + nameContainsPattern: { + isInvalid: (el) => el.validity?.patternMismatch && !CONTAINS_RULE.reg.test(el.value), + message: CONTAINS_RULE.msg, + }, +}; + const initFormField = ({ value, required = true, skipValidation = false }) => ({ value, required, @@ -48,7 +64,7 @@ export default { ProjectNamespace, }, directives: { - validation: validation(), + validation: validation(feedbackMap), }, inject: { newGroupPath: { @@ -109,6 +125,15 @@ export default { }; }, computed: { + projectNameDescription() { + if (this.form.fields.name.state === false) { + return null; + } + + return s__( + 'ProjectsNew|Must start with a lowercase or uppercase letter, digit, emoji, or underscore. Can also contain dots, pluses, dashes, or spaces.', + ); + }, projectVisibilityLevel() { return VISIBILITY_LEVELS_STRING_TO_INTEGER[this.projectVisibility]; }, @@ -248,6 +273,7 @@ export default { }, }, csrf, + projectNamePattern: `(${START_RULE.reg.source})|(${CONTAINS_RULE.reg.source})`, }; </script> @@ -257,8 +283,10 @@ export default { <gl-form-group :label="__('Project name')" + :description="projectNameDescription" label-for="fork-name" :invalid-feedback="form.fields.name.feedback" + data-testid="fork-name-form-group" > <gl-form-input id="fork-name" @@ -268,6 +296,7 @@ export default { data-testid="fork-name-input" :state="form.fields.name.state" required + :pattern="$options.projectNamePattern" /> </gl-form-group> diff --git a/app/assets/javascripts/pages/projects/graphs/charts/index.js b/app/assets/javascripts/pages/projects/graphs/charts/index.js index 65e7f48ed24..10c794c9ba2 100644 --- a/app/assets/javascripts/pages/projects/graphs/charts/index.js +++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js @@ -2,6 +2,9 @@ import { GlColumnChart } from '@gitlab/ui/dist/charts'; import Vue from 'vue'; import { waitForCSSLoaded } from '~/helpers/startup_css_helper'; import { __ } from '~/locale'; +import { visitUrl } from '~/lib/utils/url_utility'; +import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants'; +import RefSelector from '~/ref/components/ref_selector.vue'; import CodeCoverage from '../components/code_coverage.vue'; import SeriesDataMixin from './series_data_mixin'; @@ -13,6 +16,7 @@ waitForCSSLoaded(() => { const monthContainer = document.getElementById('js-month-chart'); const weekdayContainer = document.getElementById('js-weekday-chart'); const hourContainer = document.getElementById('js-hour-chart'); + const branchSelector = document.getElementById('js-project-graph-ref-switcher'); const LANGUAGE_CHART_HEIGHT = 300; const reorderWeekDays = (weekDays, firstDayOfWeek = 0) => { if (firstDayOfWeek === 0) { @@ -173,4 +177,38 @@ waitForCSSLoaded(() => { }); }, }); + + const { projectId, projectBranch, graphPath } = branchSelector.dataset; + + const GRAPHS_PATH_REGEX = /^(.*?)\/-\/graphs/g; + const graphsPathPrefix = graphPath.match(GRAPHS_PATH_REGEX)?.[0]; + if (!graphsPathPrefix) { + // eslint-disable-next-line @gitlab/require-i18n-strings + throw new Error('Path is not correct'); + } + + // eslint-disable-next-line no-new + new Vue({ + el: branchSelector, + name: 'RefSelector', + render(createComponent) { + return createComponent(RefSelector, { + props: { + enabledRefTypes: [REF_TYPE_BRANCHES, REF_TYPE_TAGS], + value: projectBranch, + translations: { + dropdownHeader: __('Switch branch/tag'), + searchPlaceholder: __('Search branches and tags'), + }, + projectId, + }, + class: 'gl-w-20', + on: { + input(selected) { + visitUrl(`${graphsPathPrefix}/${encodeURIComponent(selected)}/charts`); + }, + }, + }); + }, + }); }); diff --git a/app/assets/javascripts/pages/projects/hooks/index.js b/app/assets/javascripts/pages/projects/hooks/index.js index 9e559354205..f25547f9982 100644 --- a/app/assets/javascripts/pages/projects/hooks/index.js +++ b/app/assets/javascripts/pages/projects/hooks/index.js @@ -1,7 +1,8 @@ import initSearchSettings from '~/search_settings'; -import initWebhookForm from '~/webhooks'; +import initWebhookForm, { initHookTestDropdowns } from '~/webhooks'; import { initPushEventsEditForm } from '~/webhooks/webhook'; initSearchSettings(); initWebhookForm(); initPushEventsEditForm(); +initHookTestDropdowns(); diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js index 37cf345fe77..1075241e172 100644 --- a/app/assets/javascripts/pages/projects/index.js +++ b/app/assets/javascripts/pages/projects/index.js @@ -1,7 +1,5 @@ import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; -import initTerraformNotification from '~/projects/terraform_notification'; import Project from './project'; new Project(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new -initTerraformNotification(); diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/included_in_trial_indicator.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/included_in_trial_indicator.vue deleted file mode 100644 index 693dc6a15ad..00000000000 --- a/app/assets/javascripts/pages/projects/learn_gitlab/components/included_in_trial_indicator.vue +++ /dev/null @@ -1,15 +0,0 @@ -<script> -import { s__ } from '~/locale'; - -export default { - name: 'IncludedInTrialIndicator', - i18n: { - trialOnly: s__('LearnGitlab|- Included in trial'), - }, -}; -</script> -<template> - <span class="gl-font-style-italic gl-text-gray-500" data-testid="trial-only"> - {{ $options.i18n.trialOnly }} - </span> -</template> diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab.vue b/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab.vue deleted file mode 100644 index 54e15b6552c..00000000000 --- a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab.vue +++ /dev/null @@ -1,146 +0,0 @@ -<script> -import { GlProgressBar, GlSprintf, GlAlert } from '@gitlab/ui'; -import eventHub from '~/invite_members/event_hub'; -import { s__ } from '~/locale'; -import { getCookie, removeCookie, parseBoolean } from '~/lib/utils/common_utils'; -import { ACTION_LABELS, ACTION_SECTIONS, INVITE_MODAL_OPEN_COOKIE } from '../constants'; -import LearnGitlabSectionCard from './learn_gitlab_section_card.vue'; - -export default { - components: { GlProgressBar, GlSprintf, GlAlert, LearnGitlabSectionCard }, - i18n: { - 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`), - successfulInvitations: s__( - "LearnGitLab|Your team is growing! You've successfully invited new team members to the %{projectName} project.", - ), - }, - props: { - actions: { - required: true, - type: Object, - }, - sections: { - required: true, - type: Object, - }, - project: { - required: true, - type: Object, - }, - }, - data() { - return { - showSuccessfulInvitationsAlert: false, - actionsData: this.actions, - }; - }, - actionSections: Object.keys(ACTION_SECTIONS), - computed: { - maxValue() { - return Object.keys(this.actionsData).length; - }, - progressValue() { - return Object.values(this.actionsData).filter((a) => a.completed).length; - }, - progressPercentage() { - return Math.round((this.progressValue / this.maxValue) * 100); - }, - }, - mounted() { - if (this.getCookieForInviteMembers()) { - this.openInviteMembersModal('celebrate'); - } - - eventHub.$on('showSuccessfulInvitationsAlert', this.handleShowSuccessfulInvitationsAlert); - }, - beforeDestroy() { - eventHub.$off('showSuccessfulInvitationsAlert', this.handleShowSuccessfulInvitationsAlert); - }, - methods: { - getCookieForInviteMembers() { - const value = parseBoolean(getCookie(INVITE_MODAL_OPEN_COOKIE)); - - removeCookie(INVITE_MODAL_OPEN_COOKIE); - - return value; - }, - openInviteMembersModal(mode) { - eventHub.$emit('openModal', { mode, source: 'learn-gitlab' }); - }, - handleShowSuccessfulInvitationsAlert() { - this.showSuccessfulInvitationsAlert = true; - this.markActionAsCompleted('userAdded'); - }, - actionsFor(section) { - const actions = Object.fromEntries( - Object.entries(this.actionsData).filter( - ([action]) => ACTION_LABELS[action].section === section, - ), - ); - return actions; - }, - svgFor(section) { - return this.sections[section].svg; - }, - markActionAsCompleted(completedAction) { - Object.keys(this.actionsData).forEach((action) => { - if (action === completedAction) { - this.actionsData[action].completed = true; - this.modifySidebarPercentage(); - } - }); - }, - modifySidebarPercentage() { - const el = document.querySelector('.sidebar-top-level-items .active .count'); - el.textContent = `${this.progressPercentage}%`; - }, - }, -}; -</script> -<template> - <div> - <gl-alert - v-if="showSuccessfulInvitationsAlert" - class="gl-mt-5" - @dismiss="showSuccessfulInvitationsAlert = false" - > - <gl-sprintf :message="$options.i18n.successfulInvitations"> - <template #projectName> - <strong>{{ project.name }}</strong> - </template> - </gl-sprintf> - </gl-alert> - <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="maxValue" /> - </div> - <div class="row"> - <div - v-for="section in $options.actionSections" - :key="section" - class="gl-mt-5 col-sm-12 col-mb-6 col-lg-4" - > - <learn-gitlab-section-card - :section="section" - :svg="svgFor(section)" - :actions="actionsFor(section)" - /> - </div> - </div> - </div> -</template> 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 deleted file mode 100644 index e8f0e6c47ee..00000000000 --- a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_card.vue +++ /dev/null @@ -1,56 +0,0 @@ -<script> -import { GlCard } from '@gitlab/ui'; -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, - }, - svg: { - 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, - ); - }, - }, -}; -</script> -<template> - <gl-card - class="gl-pt-0 h-100" - header-class="gl-bg-white gl-border-0 gl-pb-0" - body-class="gl-pt-0" - > - <template #header> - <img :src="svg" /> - <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> - </template> - <template #default> - <learn-gitlab-section-link - v-for="[action, value] in sortedActions" - :key="action" - :action="action" - :value="value" - /> - </template> - </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 deleted file mode 100644 index d9b0dbbb9b0..00000000000 --- a/app/assets/javascripts/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue +++ /dev/null @@ -1,151 +0,0 @@ -<script> -import { uniqueId } from 'lodash'; -import { GlLink, GlIcon, GlButton, GlPopover, GlTooltipDirective as GlTooltip } from '@gitlab/ui'; -import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue'; -import { isExperimentVariant } from '~/experimentation/utils'; -import eventHub from '~/invite_members/event_hub'; -import { s__, __ } from '~/locale'; -import { ACTION_LABELS } from '../constants'; -import IncludedInTrialIndicator from './included_in_trial_indicator.vue'; - -export default { - name: 'LearnGitlabSectionLink', - components: { - GlLink, - GlIcon, - GlButton, - GlPopover, - GitlabExperiment, - IncludedInTrialIndicator, - }, - directives: { - GlTooltip, - }, - i18n: { - contactAdmin: s__('LearnGitlab|Contact your administrator to enable this action.'), - viewAdminList: s__('LearnGitlab|View administrator list'), - watchHow: __('Watch how'), - }, - props: { - action: { - required: true, - type: String, - }, - value: { - required: true, - type: Object, - }, - }, - data() { - return { - popoverId: uniqueId('contact-admin-'), - }; - }, - computed: { - showInviteModalLink() { - return ( - this.action === 'userAdded' && isExperimentVariant('invite_for_help_continuous_onboarding') - ); - }, - openInNewTab() { - return ACTION_LABELS[this.action]?.openInNewTab === true || this.value.openInNewTab === true; - }, - popoverText() { - return this.value.message || this.$options.i18n.contactAdmin; - }, - }, - methods: { - openModal() { - eventHub.$emit('openModal', { source: 'learn_gitlab' }); - }, - actionLabelValue(value) { - return ACTION_LABELS[this.action][value]; - }, - }, -}; -</script> -<template> - <div class="gl-mb-4"> - <div class="flex align-items-center"> - <span v-if="value.completed" class="gl-text-green-500"> - <gl-icon name="check-circle-filled" :size="16" data-testid="completed-icon" /> - {{ actionLabelValue('title') }} - <included-in-trial-indicator v-if="actionLabelValue('trialRequired')" /> - </span> - <div v-else-if="showInviteModalLink"> - <gl-link - data-track-action="click_link" - :data-track-label="actionLabelValue('trackLabel')" - data-track-property="Growth::Activation::Experiment::InviteForHelpContinuousOnboarding" - data-testid="invite-for-help-continuous-onboarding-experiment-link" - @click="openModal" - >{{ actionLabelValue('title') }}</gl-link - > - - <included-in-trial-indicator v-if="actionLabelValue('trialRequired')" /> - </div> - <div v-else-if="value.enabled"> - <gl-link - :target="openInNewTab ? '_blank' : '_self'" - :href="value.url" - data-testid="uncompleted-learn-gitlab-link" - data-qa-selector="uncompleted_learn_gitlab_link" - data-track-action="click_link" - :data-track-label="actionLabelValue('trackLabel')" - >{{ actionLabelValue('title') }}</gl-link - > - - <included-in-trial-indicator v-if="actionLabelValue('trialRequired')" /> - </div> - <template v-else> - <div data-testid="disabled-learn-gitlab-link">{{ actionLabelValue('title') }}</div> - <gl-button - :id="popoverId" - category="tertiary" - icon="question-o" - class="ml-auto" - :aria-label="popoverText" - size="small" - data-testid="contact-admin-popover-trigger" - /> - <gl-popover - :target="popoverId" - placement="top" - triggers="hover focus" - data-testid="contact-admin-popover" - > - <p>{{ popoverText }}</p> - <gl-link - :href="value.url" - class="font-size-inherit" - data-testid="view-administrator-link-text" - > - {{ $options.i18n.viewAdminList }} - </gl-link> - </gl-popover> - </template> - <gitlab-experiment name="video_tutorials_continuous_onboarding"> - <template #control></template> - <template #candidate> - <gl-button - v-if="actionLabelValue('videoTutorial')" - v-gl-tooltip - category="tertiary" - icon="live-preview" - :title="$options.i18n.watchHow" - :aria-label="$options.i18n.watchHow" - :href="actionLabelValue('videoTutorial')" - target="_blank" - class="ml-auto" - size="small" - data-testid="video-tutorial-link" - data-track-action="click_video_link" - :data-track-label="actionLabelValue('trackLabel')" - data-track-property="Growth::Conversion::Experiment::LearnGitLab" - data-track-experiment="video_tutorials_continuous_onboarding" - /> - </template> - </gitlab-experiment> - </div> - </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 deleted file mode 100644 index cb1a0302d91..00000000000 --- a/app/assets/javascripts/pages/projects/learn_gitlab/constants/index.js +++ /dev/null @@ -1,133 +0,0 @@ -import { s__ } from '~/locale'; - -export const ACTION_LABELS = { - gitWrite: { - title: s__('LearnGitLab|Create a repository'), - actionLabel: s__('LearnGitLab|Create a repository'), - description: s__('LearnGitLab|Create or import your first repository into your new project.'), - trackLabel: 'create_a_repository', - section: 'workspace', - position: 1, - }, - userAdded: { - title: s__('LearnGitLab|Invite your colleagues'), - actionLabel: s__('LearnGitLab|Invite your colleagues'), - description: s__( - 'LearnGitLab|GitLab works best as a team. Invite your colleague to enjoy all features.', - ), - trackLabel: 'invite_your_colleagues', - section: 'workspace', - position: 0, - }, - pipelineCreated: { - title: s__("LearnGitLab|Set up your first project's CI/CD"), - actionLabel: s__('LearnGitLab|Set up CI/CD'), - description: s__('LearnGitLab|Save time by automating your integration and deployment tasks.'), - trackLabel: 'set_up_your_first_project_s_ci_cd', - section: 'workspace', - position: 2, - }, - trialStarted: { - title: s__('LearnGitLab|Start a free trial of GitLab Ultimate'), - actionLabel: s__('LearnGitLab|Try GitLab Ultimate for free'), - description: s__('LearnGitLab|Try all GitLab features for 30 days, no credit card required.'), - trackLabel: 'start_a_free_trial_of_gitlab_ultimate', - section: 'workspace', - position: 3, - openInNewTab: true, - }, - codeOwnersEnabled: { - title: s__('LearnGitLab|Add code owners'), - actionLabel: s__('LearnGitLab|Add code owners'), - description: s__( - 'LearnGitLab|Prevent unexpected changes to important assets by assigning ownership of files and paths.', - ), - trackLabel: 'add_code_owners', - trialRequired: true, - section: 'workspace', - position: 4, - openInNewTab: true, - videoTutorial: 'https://vimeo.com/670896787', - }, - requiredMrApprovalsEnabled: { - title: s__('LearnGitLab|Enable require merge approvals'), - actionLabel: s__('LearnGitLab|Enable require merge approvals'), - description: s__('LearnGitLab|Route code reviews to the right reviewers, every time.'), - trackLabel: 'enable_require_merge_approvals', - trialRequired: true, - section: 'workspace', - position: 5, - openInNewTab: true, - videoTutorial: 'https://vimeo.com/670904904', - }, - mergeRequestCreated: { - title: s__('LearnGitLab|Submit a merge request (MR)'), - actionLabel: s__('LearnGitLab|Submit a merge request (MR)'), - description: s__('LearnGitLab|Review and edit proposed changes to source code.'), - trackLabel: 'submit_a_merge_request_mr', - section: 'plan', - 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.', - ), - trackLabel: 'create_an_issue', - section: 'plan', - position: 0, - }, - securityScanEnabled: { - 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.'), - trackLabel: 'run_a_security_scan_using_ci_cd', - section: 'deploy', - position: 1, - }, - licenseScanningRun: { - title: s__('LearnGitLab|Scan dependencies for licenses'), - trackLabel: 'scan_dependencies_for_licenses', - trialRequired: true, - section: 'deploy', - position: 2, - }, - secureDependencyScanningRun: { - title: s__('LearnGitLab|Scan dependencies for vulnerabilities'), - trackLabel: 'scan_dependencies_for_vulnerabilities', - trialRequired: true, - section: 'deploy', - position: 3, - }, - secureDastRun: { - title: s__('LearnGitLab|Analyze your application for vulnerabilities with DAST'), - trackLabel: 'analyze_your_application_for_vulnerabilities_with_dast', - trialRequired: true, - section: 'deploy', - position: 4, - }, -}; - -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:', - ), - }, -}; - -export const INVITE_MODAL_OPEN_COOKIE = 'confetti_post_signup'; diff --git a/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js b/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js deleted file mode 100644 index af4a6f8a0c9..00000000000 --- a/app/assets/javascripts/pages/projects/learn_gitlab/index/index.js +++ /dev/null @@ -1,31 +0,0 @@ -import Vue from 'vue'; -import initInviteMembersModal from '~/invite_members/init_invite_members_modal'; -import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger'; -import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; -import LearnGitlab from '../components/learn_gitlab.vue'; - -function initLearnGitlab() { - const el = document.getElementById('js-learn-gitlab-app'); - - if (!el) { - return false; - } - - const actions = convertObjectPropsToCamelCase(JSON.parse(el.dataset.actions)); - const sections = convertObjectPropsToCamelCase(JSON.parse(el.dataset.sections)); - const project = convertObjectPropsToCamelCase(JSON.parse(el.dataset.project)); - - return new Vue({ - el, - render(createElement) { - return createElement(LearnGitlab, { - props: { actions, sections, project }, - }); - }, - }); -} - -initInviteMembersModal(); -initInviteMembersTrigger(); - -initLearnGitlab(); diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js deleted file mode 100644 index 653f903c6d1..00000000000 --- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js +++ /dev/null @@ -1,66 +0,0 @@ -import $ from 'jquery'; -import axios from '~/lib/utils/axios_utils'; -import { localTimeAgo } from '~/lib/utils/datetime_utility'; -import initCompareAutocomplete from './compare_autocomplete'; -import initTargetProjectDropdown from './target_project_dropdown'; - -const updateCommitList = (url, $emptyState, $loadingIndicator, $commitList, params) => { - $emptyState.hide(); - $loadingIndicator.show(); - $commitList.empty(); - - return axios - .get(url, { - params, - }) - .then(({ data }) => { - $loadingIndicator.hide(); - $commitList.html(data); - localTimeAgo($commitList.get(0).querySelectorAll('.js-timeago')); - - if (!data) { - $emptyState.show(); - } - }); -}; - -export default (mrNewCompareNode) => { - const { sourceBranchUrl, targetBranchUrl } = mrNewCompareNode.dataset; - - if (!window.gon?.features?.mrCompareDropdowns) { - initTargetProjectDropdown(); - } - - const updateSourceBranchCommitList = () => - updateCommitList( - sourceBranchUrl, - $(mrNewCompareNode).find('.js-source-commit-empty'), - $(mrNewCompareNode).find('.js-source-loading'), - $(mrNewCompareNode).find('.mr_source_commit'), - { - ref: $(mrNewCompareNode).find("input[name='merge_request[source_branch]']").val(), - }, - ); - const updateTargetBranchCommitList = () => - updateCommitList( - targetBranchUrl, - $(mrNewCompareNode).find('.js-target-commit-empty'), - $(mrNewCompareNode).find('.js-target-loading'), - $(mrNewCompareNode).find('.mr_target_commit'), - { - target_project_id: $(mrNewCompareNode) - .find("input[name='merge_request[target_project_id]']") - .val(), - ref: $(mrNewCompareNode).find("input[name='merge_request[target_branch]']").val(), - }, - ); - initCompareAutocomplete('branches', ($dropdown) => { - if ($dropdown.is('.js-target-branch')) { - updateTargetBranchCommitList(); - } else if ($dropdown.is('.js-source-branch')) { - updateSourceBranchCommitList(); - } - }); - updateSourceBranchCommitList(); - updateTargetBranchCommitList(); -}; diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js deleted file mode 100644 index 65942464e2b..00000000000 --- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js +++ /dev/null @@ -1,91 +0,0 @@ -/* eslint-disable func-names */ - -import $ from 'jquery'; -import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; -import { createAlert } from '~/flash'; -import axios from '~/lib/utils/axios_utils'; -import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; -import { __ } from '~/locale'; -import { fixTitle } from '~/tooltips'; - -export default function initCompareAutocomplete(limitTo = null, clickHandler = () => {}) { - $('.js-compare-dropdown').each(function () { - const $dropdown = $(this); - const selected = $dropdown.data('selected'); - const defaultText = $dropdown.data('defaultText').trim(); - const $dropdownContainer = $dropdown.closest('.dropdown'); - const $fieldInput = $(`input[name="${$dropdown.data('fieldName')}"]`, $dropdownContainer); - const $filterInput = $('input[type="search"]', $dropdownContainer); - initDeprecatedJQueryDropdown($dropdown, { - data(term, callback) { - const params = { - ref: $dropdown.data('ref'), - search: term, - }; - - if (limitTo) { - params.find = limitTo; - } - - axios - .get($dropdown.data('refsUrl'), { - params, - }) - .then(({ data }) => { - if (limitTo) { - callback(data[capitalizeFirstCharacter(limitTo)] || []); - } else { - callback(data); - } - }) - .catch(() => - createAlert({ - message: __('Error fetching refs'), - }), - ); - }, - selectable: true, - filterable: true, - filterRemote: Boolean($dropdown.data('refsUrl')), - fieldName: $dropdown.data('fieldName'), - filterInput: 'input[type="search"]', - renderRow(ref) { - const link = $('<a />') - .attr('href', '#') - .addClass(ref === selected ? 'is-active' : '') - .text(ref) - .attr('data-ref', ref); - if (ref.header != null) { - return $('<li />').addClass('dropdown-header').text(ref.header); - } - return $('<li />').append(link); - }, - id(obj, $el) { - return $el.attr('data-ref'); - }, - toggleLabel(obj, $el) { - if ($el.hasClass('is-active')) { - return $el.text().trim(); - } - - return defaultText; - }, - clicked: () => clickHandler($dropdown), - }); - $filterInput.on('keyup', (e) => { - const keyCode = e.keyCode || e.which; - if (keyCode !== 13) return; - const text = $filterInput.val(); - $fieldInput.val(text); - $('.dropdown-toggle-text', $dropdown).text(text); - $dropdownContainer.removeClass('open'); - }); - - $dropdownContainer.on('click', '.dropdown-content a', (e) => { - $dropdown.prop('title', e.target.text.replace(/_+?/g, '-')); - if ($dropdown.hasClass('has-tooltip')) { - fixTitle($dropdown); - } - }); - }); -} diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js index b3868653d6a..2718765ee23 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js @@ -1,35 +1,78 @@ -import $ from 'jquery'; import Vue from 'vue'; import initPipelines from '~/commit/pipelines/pipelines_bundle'; import MergeRequest from '~/merge_request'; -import TargetProjectDropdown from '~/merge_requests/components/target_project_dropdown.vue'; -import initCompare from './compare'; +import CompareApp from '~/merge_requests/components/compare_app.vue'; +import { __ } from '~/locale'; const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare'); if (mrNewCompareNode) { - initCompare(mrNewCompareNode); - - const el = document.getElementById('js-target-project-dropdown'); - const { targetProjectsPath, currentProject } = el.dataset; + const targetCompareEl = document.getElementById('js-target-project-dropdown'); + const sourceCompareEl = document.getElementById('js-source-project-dropdown'); + const compareEl = document.querySelector('.js-merge-request-new-compare'); // eslint-disable-next-line no-new new Vue({ - el, - name: 'TargetProjectDropdown', + el: sourceCompareEl, + name: 'SourceCompareApp', provide: { - targetProjectsPath, - currentProject: JSON.parse(currentProject), + currentProject: JSON.parse(sourceCompareEl.dataset.currentProject), + currentBranch: JSON.parse(sourceCompareEl.dataset.currentBranch), + branchCommitPath: compareEl.dataset.sourceBranchUrl, + inputs: { + project: { + id: 'merge_request_source_project_id', + name: 'merge_request[source_project_id]', + }, + branch: { + id: 'merge_request_source_branch', + name: 'merge_request[source_branch]', + }, + }, + i18n: { + projectHeaderText: __('Select source project'), + branchHeaderText: __('Select source branch'), + }, + toggleClass: { + project: 'js-source-project', + branch: 'js-source-branch gl-font-monospace', + }, + branchQaSelector: 'source_branch_dropdown', }, render(h) { - return h(TargetProjectDropdown, { - on: { - 'project-selected': function projectSelectedFunction(refsUrl) { - const $targetBranchDropdown = $('.js-target-branch'); - $targetBranchDropdown.data('refsUrl', refsUrl); - $targetBranchDropdown.data('deprecatedJQueryDropdown').clearMenu(); - }, + return h(CompareApp); + }, + }); + + // eslint-disable-next-line no-new + new Vue({ + el: targetCompareEl, + name: 'TargetCompareApp', + provide: { + currentProject: JSON.parse(targetCompareEl.dataset.currentProject), + currentBranch: JSON.parse(targetCompareEl.dataset.currentBranch), + projectsPath: targetCompareEl.dataset.targetProjectsPath, + branchCommitPath: compareEl.dataset.targetBranchUrl, + inputs: { + project: { + id: 'merge_request_target_project_id', + name: 'merge_request[target_project_id]', }, - }); + branch: { + id: 'merge_request_target_branch', + name: 'merge_request[target_branch]', + }, + }, + i18n: { + projectHeaderText: __('Select target project'), + branchHeaderText: __('Select target branch'), + }, + toggleClass: { + project: 'js-target-project', + branch: 'js-target-branch gl-font-monospace', + }, + }, + render(h) { + return h(CompareApp); }, }); } else { diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js deleted file mode 100644 index e9f0e008435..00000000000 --- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/target_project_dropdown.js +++ /dev/null @@ -1,23 +0,0 @@ -import $ from 'jquery'; -import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; - -export default () => { - const $targetProjectDropdown = $('.js-target-project'); - initDeprecatedJQueryDropdown($targetProjectDropdown, { - selectable: true, - fieldName: $targetProjectDropdown.data('fieldName'), - filterable: true, - id(obj, $el) { - return $el.data('id'); - }, - toggleLabel(obj, $el) { - return $el.text().trim(); - }, - clicked({ $el }) { - $('.mr_target_commit').empty(); - const $targetBranchDropdown = $('.js-target-branch'); - $targetBranchDropdown.data('refsUrl', $el.data('refsUrl')); - $targetBranchDropdown.data('deprecatedJQueryDropdown').clearMenu(); - }, - }); -}; diff --git a/app/assets/javascripts/pages/projects/merge_requests/index/index.js b/app/assets/javascripts/pages/projects/merge_requests/index/index.js index b3a09cc0be3..af75c05b300 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/index/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/index/index.js @@ -5,7 +5,6 @@ import { FILTERED_SEARCH } from '~/filtered_search/constants'; import { initBulkUpdateSidebar, initCsvImportExportButtons, initIssuableByEmail } from '~/issuable'; import { ISSUABLE_INDEX } from '~/issuable/constants'; import initFilteredSearch from '~/pages/search/init_filtered_search'; -import UsersSelect from '~/users_select'; initBulkUpdateSidebar(ISSUABLE_INDEX.MERGE_REQUEST); @@ -18,7 +17,6 @@ initFilteredSearch({ useDefaultState: true, }); -new UsersSelect(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new initIssuableByEmail(); diff --git a/app/assets/javascripts/pages/projects/merge_requests/show/index.js b/app/assets/javascripts/pages/projects/merge_requests/show/index.js index f0a955e5360..91394755367 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/show/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/show/index.js @@ -1,5 +1,5 @@ import initNotesApp from '~/mr_notes/init_notes'; -import { initReportAbuse } from '~/projects/merge_requests'; +import { initReportAbuse } from '~/projects/report_abuse'; import { initMrPage } from '../page'; initMrPage(); diff --git a/app/assets/javascripts/pages/projects/ml/candidates/show/index.js b/app/assets/javascripts/pages/projects/ml/candidates/show/index.js index c1acef5ac13..fee6258eddc 100644 --- a/app/assets/javascripts/pages/projects/ml/candidates/show/index.js +++ b/app/assets/javascripts/pages/projects/ml/candidates/show/index.js @@ -1,27 +1,4 @@ -import Vue from 'vue'; +import { initSimpleApp } from '~/helpers/init_simple_app_helper'; import MlCandidate from '~/ml/experiment_tracking/components/ml_candidate.vue'; -const initShowCandidate = () => { - const element = document.querySelector('#js-show-ml-candidate'); - if (!element) { - return; - } - - const container = document.createElement('div'); - element.appendChild(container); - - const candidate = JSON.parse(element.dataset.candidate); - - // eslint-disable-next-line no-new - new Vue({ - el: container, - provide: { - candidate, - }, - render(h) { - return h(MlCandidate); - }, - }); -}; - -initShowCandidate(); +initSimpleApp('#js-show-ml-candidate', MlCandidate); diff --git a/app/assets/javascripts/pages/projects/ml/experiments/index/index.js b/app/assets/javascripts/pages/projects/ml/experiments/index/index.js new file mode 100644 index 00000000000..e9ffd4b528b --- /dev/null +++ b/app/assets/javascripts/pages/projects/ml/experiments/index/index.js @@ -0,0 +1,24 @@ +import Vue from 'vue'; +import MlExperimentsIndex from '~/ml/experiment_tracking/routes/experiments/index'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; + +const initIndexMlExperiments = () => { + const element = document.querySelector('#js-project-ml-experiments-index'); + if (!element) { + return undefined; + } + + const props = { + experiments: JSON.parse(element.dataset.experiments), + pageInfo: convertObjectPropsToCamelCase(JSON.parse(element.dataset.pageInfo)), + }; + + return new Vue({ + el: element, + render(h) { + return h(MlExperimentsIndex, { props }); + }, + }); +}; + +initIndexMlExperiments(); diff --git a/app/assets/javascripts/pages/projects/ml/experiments/show/index.js b/app/assets/javascripts/pages/projects/ml/experiments/show/index.js index 6947b15dcbe..0e64d8c17db 100644 --- a/app/assets/javascripts/pages/projects/ml/experiments/show/index.js +++ b/app/assets/javascripts/pages/projects/ml/experiments/show/index.js @@ -14,7 +14,7 @@ const initShowExperiment = () => { const candidates = JSON.parse(element.dataset.candidates); const metricNames = JSON.parse(element.dataset.metrics); const paramNames = JSON.parse(element.dataset.params); - const pagination = convertObjectPropsToCamelCase(JSON.parse(element.dataset.pagination)); + const pageInfo = convertObjectPropsToCamelCase(JSON.parse(element.dataset.pageInfo)); // eslint-disable-next-line no-new new Vue({ @@ -23,7 +23,7 @@ const initShowExperiment = () => { candidates, metricNames, paramNames, - pagination, + pageInfo, }, render(h) { return h(MlExperiment); diff --git a/app/assets/javascripts/pages/projects/network/show/index.js b/app/assets/javascripts/pages/projects/network/show/index.js index 2dabcfadfab..414636f0a74 100644 --- a/app/assets/javascripts/pages/projects/network/show/index.js +++ b/app/assets/javascripts/pages/projects/network/show/index.js @@ -1,7 +1,39 @@ import $ from 'jquery'; +import Vue from 'vue'; +import { visitUrl, joinPaths } from '~/lib/utils/url_utility'; import ShortcutsNetwork from '~/behaviors/shortcuts/shortcuts_network'; +import RefSelector from '~/ref/components/ref_selector.vue'; import Network from '../network'; +const initRefSwitcher = () => { + const refSwitcherEl = document.getElementById('js-graph-ref-switcher'); + const NETWORK_PATH_REGEX = /^(.*?)\/-\/network/g; + + if (!refSwitcherEl) return false; + + const { projectId, ref, networkPath } = refSwitcherEl.dataset; + const networkRootPath = networkPath.match(NETWORK_PATH_REGEX)?.[0]; // gets the network path without the ref + + return new Vue({ + el: refSwitcherEl, + render(createElement) { + return createElement(RefSelector, { + props: { + projectId, + value: ref, + }, + on: { + input(selectedRef) { + visitUrl(joinPaths(networkRootPath, selectedRef)); + }, + }, + }); + }, + }); +}; + +initRefSwitcher(); + (() => { if (!$('.network-graph').length) return; diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js index 4c9eb830ff6..5773737c41b 100644 --- a/app/assets/javascripts/pages/projects/project.js +++ b/app/assets/javascripts/pages/projects/project.js @@ -9,7 +9,6 @@ import axios from '~/lib/utils/axios_utils'; import { serializeForm } from '~/lib/utils/forms'; import { mergeUrlParams } from '~/lib/utils/url_utility'; import { __ } from '~/locale'; -import projectSelect from '~/project_select'; const BRANCH_REF_TYPE = 'heads'; const TAG_REF_TYPE = 'tags'; @@ -44,13 +43,6 @@ export default class Project { $(this).parents('.auto-devops-implicitly-enabled-banner').remove(); return e.preventDefault(); }); - - Project.projectSelectDropdown(); - } - - static projectSelectDropdown() { - projectSelect(); - $('.project-item-select').on('click', (e) => Project.changeProject($(e.currentTarget).val())); } static changeProject(url) { diff --git a/app/assets/javascripts/pages/projects/project_members/index.js b/app/assets/javascripts/pages/projects/project_members/index.js index 2fd372a45b8..79a4ed0f9c3 100644 --- a/app/assets/javascripts/pages/projects/project_members/index.js +++ b/app/assets/javascripts/pages/projects/project_members/index.js @@ -21,7 +21,6 @@ const SHARED_FIELDS = ['account', 'maxRole', 'expiration', 'actions']; initMembersApp(document.querySelector('.js-project-members-list-app'), { [MEMBER_TYPES.user]: { tableFields: SHARED_FIELDS.concat(['source', 'activity']), - tableAttrs: { tr: { 'data-qa-selector': 'member_row' } }, tableSortableFields: [ 'account', 'granted', @@ -41,10 +40,6 @@ initMembersApp(document.querySelector('.js-project-members-list-app'), { }, [MEMBER_TYPES.group]: { tableFields: SHARED_FIELDS.concat(['source', 'granted']), - tableAttrs: { - table: { 'data-qa-selector': 'groups_list' }, - tr: { 'data-qa-selector': 'group_row' }, - }, requestFormatter: groupLinkRequestFormatter, filteredSearchBar: { show: true, 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 895c7d0a18e..964c6ca9792 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,7 +4,6 @@ import initSettingsPipelinesTriggers from '~/ci_settings_pipeline_triggers'; import initVariableList from '~/ci/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'; @@ -44,7 +43,5 @@ initArtifactsSettings(); initProjectRunners(); initSharedRunnersToggle(); initInstallRunner(); -initRunnerAwsDeployments(); - initTokenAccess(); initCiSecureFiles(); 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 5fa3288bbef..f2bc4796324 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 @@ -49,14 +49,10 @@ export default { infrastructureLabel: s__('ProjectSettings|Infrastructure'), infrastructureHelpText: s__('ProjectSettings|Configure your infrastructure.'), monitorLabel: s__('ProjectSettings|Monitor'), - 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.', - ), packageRegistryHelpText: s__('ProjectSettings|Publish, store, and view packages in a project.'), packageRegistryForEveryoneHelpText: s__( 'ProjectSettings|Anyone can pull packages with a package manager API.', ), - packagesLabel: s__('ProjectSettings|Packages'), packageRegistryLabel: s__('ProjectSettings|Package registry'), packageRegistryForEveryoneLabel: s__( 'ProjectSettings|Allow anyone to pull from Package Registry', @@ -355,9 +351,6 @@ export default { this.visibilityLevel < this.currentSettings.visibilityLevel ); }, - packageRegistryAccessLevelEnabled() { - return this.glFeatures.packageRegistryAccessLevel; - }, packageRegistryEnabled() { return this.packageRegistryAccessLevel > featureAccessLevel.NOT_ENABLED; }, @@ -392,14 +385,12 @@ export default { featureAccessLevel.PROJECT_MEMBERS, this.buildsAccessLevel, ); - if (this.packageRegistryAccessLevelEnabled) { - if ( - this.packageRegistryAccessLevel === featureAccessLevel.EVERYONE || - (this.packageRegistryAccessLevel > featureAccessLevel.EVERYONE && - oldValue === VISIBILITY_LEVEL_PUBLIC_INTEGER) - ) { - this.packageRegistryAccessLevel = featureAccessLevel.PROJECT_MEMBERS; - } + if ( + this.packageRegistryAccessLevel === featureAccessLevel.EVERYONE || + (this.packageRegistryAccessLevel > featureAccessLevel.EVERYONE && + oldValue === VISIBILITY_LEVEL_PUBLIC_INTEGER) + ) { + this.packageRegistryAccessLevel = featureAccessLevel.PROJECT_MEMBERS; } this.wikiAccessLevel = Math.min(featureAccessLevel.PROJECT_MEMBERS, this.wikiAccessLevel); this.snippetsAccessLevel = Math.min( @@ -459,10 +450,7 @@ export default { this.repositoryAccessLevel = featureAccessLevel.EVERYONE; if (this.mergeRequestsAccessLevel > featureAccessLevel.NOT_ENABLED) this.mergeRequestsAccessLevel = featureAccessLevel.EVERYONE; - if ( - this.packageRegistryAccessLevelEnabled && - this.packageRegistryAccessLevel === featureAccessLevel.PROJECT_MEMBERS - ) { + if (this.packageRegistryAccessLevel === featureAccessLevel.PROJECT_MEMBERS) { this.packageRegistryAccessLevel = PACKAGE_REGISTRY_ACCESS_LEVEL_DEFAULT_BY_PROJECT_VISIBILITY[value]; } @@ -488,19 +476,17 @@ export default { this.containerRegistryAccessLevel = featureAccessLevel.EVERYONE; this.highlightChanges(); - } else if (this.packageRegistryAccessLevelEnabled) { - if ( - value === VISIBILITY_LEVEL_PUBLIC_INTEGER && - this.packageRegistryAccessLevel === featureAccessLevel.EVERYONE - ) { - // eslint-disable-next-line prefer-destructuring - this.packageRegistryAccessLevel = FEATURE_ACCESS_LEVEL_ANONYMOUS[0]; - } else if ( - value === VISIBILITY_LEVEL_INTERNAL_INTEGER && - this.packageRegistryAccessLevel === FEATURE_ACCESS_LEVEL_ANONYMOUS[0] - ) { - this.packageRegistryAccessLevel = featureAccessLevel.EVERYONE; - } + } else if ( + value === VISIBILITY_LEVEL_PUBLIC_INTEGER && + this.packageRegistryAccessLevel === featureAccessLevel.EVERYONE + ) { + // eslint-disable-next-line prefer-destructuring + this.packageRegistryAccessLevel = FEATURE_ACCESS_LEVEL_ANONYMOUS[0]; + } else if ( + value === VISIBILITY_LEVEL_INTERNAL_INTEGER && + this.packageRegistryAccessLevel === FEATURE_ACCESS_LEVEL_ANONYMOUS[0] + ) { + this.packageRegistryAccessLevel = featureAccessLevel.EVERYONE; } }, @@ -770,22 +756,6 @@ export default { </p> </project-setting-row> <project-setting-row - v-if="packagesAvailable && !packageRegistryAccessLevelEnabled" - ref="package-settings" - :help-path="packagesHelpPath" - :label="$options.i18n.packagesLabel" - :help-text="$options.i18n.packagesHelpText" - > - <gl-toggle - v-model="packagesEnabled" - class="gl-my-2" - :disabled="!repositoryEnabled" - :label="$options.i18n.packagesLabel" - label-position="hidden" - name="project[packages_enabled]" - /> - </project-setting-row> - <project-setting-row ref="pipeline-settings" :label="$options.i18n.ciCdLabel" :help-text="s__('ProjectSettings|Build, test, and deploy your changes.')" @@ -889,7 +859,7 @@ export default { /> </project-setting-row> <project-setting-row - v-if="packageRegistryAccessLevelEnabled && packagesAvailable" + v-if="packagesAvailable" :help-path="packagesHelpPath" :label="$options.i18n.packageRegistryLabel" :help-text="$options.i18n.packageRegistryHelpText" diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js index 1de36f4a0fb..33d4090011f 100644 --- a/app/assets/javascripts/pages/projects/show/index.js +++ b/app/assets/javascripts/pages/projects/show/index.js @@ -6,6 +6,7 @@ import initClustersDeprecationAlert from '~/projects/clusters_deprecation_alert' import leaveByUrl from '~/namespaces/leave_by_url'; import initVueNotificationsDropdown from '~/notifications'; import Star from '~/projects/star'; +import initTerraformNotification from '~/projects/terraform_notification'; import { initUploadFileTrigger } from '~/projects/upload_file'; import initReadMore from '~/read_more'; @@ -44,6 +45,7 @@ initUploadFileTrigger(); initInviteMembersModal(); initInviteMembersTrigger(); initClustersDeprecationAlert(); +initTerraformNotification(); initReadMore(); new Star(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/search/show/refresh_counts.js b/app/assets/javascripts/pages/search/show/refresh_counts.js deleted file mode 100644 index f3f6312cb7c..00000000000 --- a/app/assets/javascripts/pages/search/show/refresh_counts.js +++ /dev/null @@ -1,24 +0,0 @@ -import axios from '~/lib/utils/axios_utils'; - -function showCount(el, count) { - el.textContent = count; - el.classList.remove('hidden'); -} - -function refreshCount(el) { - const { url } = el.dataset; - - return axios - .get(url) - .then(({ data }) => showCount(el, data.count)) - .catch((e) => { - // eslint-disable-next-line no-console - console.error(`Failed to fetch search count from '${url}'.`, e); - }); -} - -export default function refreshCounts() { - const elements = Array.from(document.querySelectorAll('.js-search-count')); - - return Promise.all(elements.map(refreshCount)); -} diff --git a/app/assets/javascripts/pages/shared/mount_runner_aws_deployments.js b/app/assets/javascripts/pages/shared/mount_runner_aws_deployments.js deleted file mode 100644 index f3807a33a2b..00000000000 --- a/app/assets/javascripts/pages/shared/mount_runner_aws_deployments.js +++ /dev/null @@ -1,17 +0,0 @@ -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/wikis/components/wiki_form.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue index 8e2f542aec0..0d2bbfbbc43 100644 --- a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue +++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue @@ -119,6 +119,12 @@ export default { isContentEditorActive: false, switchEditingControlDisabled: false, isFormDirty: getIsFormDirty(this.pageInfo), + formFieldProps: { + placeholder: this.$options.i18n.content.placeholder, + 'aria-label': this.$options.i18n.content.label, + id: 'wiki_content', + name: 'wiki[content]', + }, }; }, computed: { @@ -338,16 +344,13 @@ export default { <gl-form-group> <markdown-editor v-model="content" + :form-field-props="formFieldProps" :render-markdown-path="pageInfo.markdownPreviewPath" :markdown-docs-path="pageInfo.markdownHelpPath" :uploads-path="pageInfo.uploadsPath" :enable-content-editor="isMarkdownFormat" :enable-preview="isMarkdownFormat" :autofocus="pageInfo.persisted" - :form-field-placeholder="$options.i18n.content.placeholder" - :form-field-aria-label="$options.i18n.content.label" - form-field-id="wiki_content" - form-field-name="wiki[content]" @contentEditor="notifyContentEditorActive" @markdownField="notifyContentEditorInactive" @keydown.ctrl.enter="submitFormShortcut" diff --git a/app/assets/javascripts/pages/users/show/index.js b/app/assets/javascripts/pages/users/show/index.js new file mode 100644 index 00000000000..f1b4e00c810 --- /dev/null +++ b/app/assets/javascripts/pages/users/show/index.js @@ -0,0 +1,16 @@ +import { s__ } from '~/locale'; +import { createAlert } from '~/flash'; + +if (window.gon.features?.profileTabsVue) { + import('~/profile') + .then(({ initProfileTabs }) => { + initProfileTabs(); + }) + .catch(() => { + createAlert({ + message: s__( + 'UserProfile|An error occurred loading the profile. Please refresh the page to try again.', + ), + }); + }); +} |