diff options
Diffstat (limited to 'app/assets/javascripts/pages')
37 files changed, 605 insertions, 265 deletions
diff --git a/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js b/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js index 674b807edbe..da7f81759ea 100644 --- a/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js +++ b/app/assets/javascripts/pages/admin/abuse_reports/abuse_reports.js @@ -32,7 +32,7 @@ export default class AbuseReports { $messageCellElement.text(originalMessage); } else { $messageCellElement.data('messageTruncated', 'true'); - $messageCellElement.text(`${originalMessage.substr(0, MAX_MESSAGE_LENGTH - 3)}...`); + $messageCellElement.text(truncate(originalMessage, MAX_MESSAGE_LENGTH)); } } } diff --git a/app/assets/javascripts/pages/admin/clusters/show/index.js b/app/assets/javascripts/pages/admin/clusters/show/index.js index 8001d2dd1da..ccf631b2c53 100644 --- a/app/assets/javascripts/pages/admin/clusters/show/index.js +++ b/app/assets/javascripts/pages/admin/clusters/show/index.js @@ -1,5 +1,7 @@ import ClustersBundle from '~/clusters/clusters_bundle'; +import initClusterHealth from '~/pages/projects/clusters/show/cluster_health'; document.addEventListener('DOMContentLoaded', () => { new ClustersBundle(); // eslint-disable-line no-new + initClusterHealth(); }); diff --git a/app/assets/javascripts/pages/admin/groups/show/index.js b/app/assets/javascripts/pages/admin/groups/show/index.js index b0cdad627a6..69d219d29f7 100644 --- a/app/assets/javascripts/pages/admin/groups/show/index.js +++ b/app/assets/javascripts/pages/admin/groups/show/index.js @@ -1,3 +1,23 @@ -import UsersSelect from '../../../../users_select'; +import Vue from 'vue'; +import UsersSelect from '~/users_select'; +import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue'; -document.addEventListener('DOMContentLoaded', () => new UsersSelect()); +function mountRemoveMemberModal() { + const el = document.querySelector('.js-remove-member-modal'); + if (!el) { + return false; + } + + return new Vue({ + el, + render(createComponent) { + return createComponent(RemoveMemberModal); + }, + }); +} + +document.addEventListener('DOMContentLoaded', () => { + mountRemoveMemberModal(); + + new UsersSelect(); // eslint-disable-line no-new +}); diff --git a/app/assets/javascripts/pages/admin/projects/index.js b/app/assets/javascripts/pages/admin/projects/index.js index d6b1e747aec..d86c5e2ddb8 100644 --- a/app/assets/javascripts/pages/admin/projects/index.js +++ b/app/assets/javascripts/pages/admin/projects/index.js @@ -1,7 +1,25 @@ -import ProjectsList from '../../../projects_list'; -import NamespaceSelect from '../../../namespace_select'; +import Vue from 'vue'; +import ProjectsList from '~/projects_list'; +import NamespaceSelect from '~/namespace_select'; +import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue'; + +function mountRemoveMemberModal() { + const el = document.querySelector('.js-remove-member-modal'); + if (!el) { + return false; + } + + return new Vue({ + el, + render(createComponent) { + return createComponent(RemoveMemberModal); + }, + }); +} document.addEventListener('DOMContentLoaded', () => { + mountRemoveMemberModal(); + new ProjectsList(); // eslint-disable-line no-new document diff --git a/app/assets/javascripts/pages/constants.js b/app/assets/javascripts/pages/constants.js index 5e119454ce1..35c67190b62 100644 --- a/app/assets/javascripts/pages/constants.js +++ b/app/assets/javascripts/pages/constants.js @@ -4,4 +4,5 @@ export const FILTERED_SEARCH = { MERGE_REQUESTS: 'merge_requests', ISSUES: 'issues', ADMIN_RUNNERS: 'admin/runners', + GROUP_RUNNERS_ANCHOR: 'runners-settings', }; diff --git a/app/assets/javascripts/pages/groups/clusters/show/index.js b/app/assets/javascripts/pages/groups/clusters/show/index.js index 8001d2dd1da..ccf631b2c53 100644 --- a/app/assets/javascripts/pages/groups/clusters/show/index.js +++ b/app/assets/javascripts/pages/groups/clusters/show/index.js @@ -1,5 +1,7 @@ import ClustersBundle from '~/clusters/clusters_bundle'; +import initClusterHealth from '~/pages/projects/clusters/show/cluster_health'; document.addEventListener('DOMContentLoaded', () => { new ClustersBundle(); // eslint-disable-line no-new + initClusterHealth(); }); diff --git a/app/assets/javascripts/pages/groups/group_members/index.js b/app/assets/javascripts/pages/groups/group_members/index.js new file mode 100644 index 00000000000..e146592e134 --- /dev/null +++ b/app/assets/javascripts/pages/groups/group_members/index.js @@ -0,0 +1,30 @@ +import Vue from 'vue'; +import Members from 'ee_else_ce/members'; +import memberExpirationDate from '~/member_expiration_date'; +import UsersSelect from '~/users_select'; +import groupsSelect from '~/groups_select'; +import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue'; + +function mountRemoveMemberModal() { + const el = document.querySelector('.js-remove-member-modal'); + if (!el) { + return false; + } + + return new Vue({ + el, + render(createComponent) { + return createComponent(RemoveMemberModal); + }, + }); +} + +document.addEventListener('DOMContentLoaded', () => { + groupsSelect(); + memberExpirationDate(); + memberExpirationDate('.js-access-expiration-date-groups'); + mountRemoveMemberModal(); + + new Members(); // eslint-disable-line no-new + new UsersSelect(); // eslint-disable-line no-new +}); diff --git a/app/assets/javascripts/pages/groups/group_members/index/index.js b/app/assets/javascripts/pages/groups/group_members/index/index.js deleted file mode 100644 index 0c732922e81..00000000000 --- a/app/assets/javascripts/pages/groups/group_members/index/index.js +++ /dev/null @@ -1,14 +0,0 @@ -/* eslint-disable no-new */ - -import Members from 'ee_else_ce/members'; -import memberExpirationDate from '~/member_expiration_date'; -import UsersSelect from '~/users_select'; -import groupsSelect from '~/groups_select'; - -document.addEventListener('DOMContentLoaded', () => { - memberExpirationDate(); - memberExpirationDate('.js-access-expiration-date-groups'); - new Members(); - groupsSelect(); - new UsersSelect(); -}); diff --git a/app/assets/javascripts/pages/groups/new/group_path_validator.js b/app/assets/javascripts/pages/groups/new/group_path_validator.js index eeaa6527431..d2684b6af59 100644 --- a/app/assets/javascripts/pages/groups/new/group_path_validator.js +++ b/app/assets/javascripts/pages/groups/new/group_path_validator.js @@ -12,6 +12,7 @@ const successMessageSelector = '.validation-success'; const pendingMessageSelector = '.validation-pending'; const unavailableMessageSelector = '.validation-error'; const suggestionsMessageSelector = '.gl-path-suggestions'; +const inputGroupSelector = '.input-group'; export default class GroupPathValidator extends InputValidator { constructor(opts = {}) { @@ -39,7 +40,7 @@ export default class GroupPathValidator extends InputValidator { static validateGroupPathInput(inputDomElement) { const groupPath = inputDomElement.value; - if (inputDomElement.checkValidity() && groupPath.length > 0) { + if (inputDomElement.checkValidity() && groupPath.length > 1) { GroupPathValidator.setMessageVisibility(inputDomElement, pendingMessageSelector); fetchGroupPathAvailability(groupPath) @@ -69,9 +70,10 @@ export default class GroupPathValidator extends InputValidator { } static setMessageVisibility(inputDomElement, messageSelector, isVisible = true) { - const messageElement = inputDomElement.parentElement.parentElement.querySelector( - messageSelector, - ); + const messageElement = inputDomElement + .closest(inputGroupSelector) + .parentElement.querySelector(messageSelector); + messageElement.classList.toggle('hide', !isVisible); } 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 479c82265f2..23283f46a5d 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 @@ -1,11 +1,20 @@ import initSettingsPanels from '~/settings_panels'; import AjaxVariableList from '~/ci_variable_list/ajax_variable_list'; import initVariableList from '~/ci_variable_list'; +import initFilteredSearch from '~/pages/search/init_filtered_search'; +import AdminRunnersFilteredSearchTokenKeys from '~/filtered_search/admin_runners_filtered_search_token_keys'; +import { FILTERED_SEARCH } from '~/pages/constants'; document.addEventListener('DOMContentLoaded', () => { // Initialize expandable settings panels initSettingsPanels(); + initFilteredSearch({ + page: FILTERED_SEARCH.ADMIN_RUNNERS, + filteredSearchTokenKeys: AdminRunnersFilteredSearchTokenKeys, + anchor: FILTERED_SEARCH.GROUP_RUNNERS_ANCHOR, + }); + if (gon.features.newVariablesUi) { initVariableList(); } else { diff --git a/app/assets/javascripts/pages/groups/shared/group_details.js b/app/assets/javascripts/pages/groups/shared/group_details.js index 85daff3f60f..37b253d7c48 100644 --- a/app/assets/javascripts/pages/groups/shared/group_details.js +++ b/app/assets/javascripts/pages/groups/shared/group_details.js @@ -8,7 +8,6 @@ import NotificationsForm from '~/notifications_form'; import ProjectsList from '~/projects_list'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import GroupTabs from './group_tabs'; -import initNamespaceStorageLimitAlert from '~/namespace_storage_limit_alert'; export default function initGroupDetails(actionName = 'show') { const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup'); @@ -28,6 +27,4 @@ export default function initGroupDetails(actionName = 'show') { if (newGroupChildWrapper) { new NewGroupChild(newGroupChildWrapper); } - - initNamespaceStorageLimitAlert(); } diff --git a/app/assets/javascripts/pages/profiles/show/index.js b/app/assets/javascripts/pages/profiles/show/index.js index ad003181728..74ab1bc13a9 100644 --- a/app/assets/javascripts/pages/profiles/show/index.js +++ b/app/assets/javascripts/pages/profiles/show/index.js @@ -4,6 +4,7 @@ import emojiRegex from 'emoji-regex'; import createFlash from '~/flash'; import EmojiMenu from './emoji_menu'; import { __ } from '~/locale'; +import * as Emoji from '~/emoji'; const defaultStatusEmoji = 'speech_balloon'; @@ -55,8 +56,8 @@ document.addEventListener('DOMContentLoaded', () => { } }); - import(/* webpackChunkName: 'emoji' */ '~/emoji') - .then(Emoji => { + Emoji.initEmojiMap() + .then(() => { const emojiMenu = new EmojiMenu( Emoji, toggleEmojiMenuButtonSelector, diff --git a/app/assets/javascripts/pages/projects/clusters/show/cluster_health.js b/app/assets/javascripts/pages/projects/clusters/show/cluster_health.js new file mode 100644 index 00000000000..382d39645a9 --- /dev/null +++ b/app/assets/javascripts/pages/projects/clusters/show/cluster_health.js @@ -0,0 +1,18 @@ +import monitoringApp from '~/monitoring/monitoring_app'; + +export default () => { + const el = document.getElementById('prometheus-graphs'); + + if (el && el.dataset) { + monitoringApp({ + ...el.dataset, + showLegend: false, + showHeader: false, + showPanels: false, + forceSmallGraph: true, + smallEmptyState: true, + currentEnvironmentName: '', + hasMetrics: true, + }); + } +}; diff --git a/app/assets/javascripts/pages/projects/clusters/show/index.js b/app/assets/javascripts/pages/projects/clusters/show/index.js index 397f9faf6fe..d20e2c19583 100644 --- a/app/assets/javascripts/pages/projects/clusters/show/index.js +++ b/app/assets/javascripts/pages/projects/clusters/show/index.js @@ -1,7 +1,9 @@ import ClustersBundle from '~/clusters/clusters_bundle'; import initGkeNamespace from '~/create_cluster/gke_cluster_namespace'; +import initClusterHealth from './cluster_health'; document.addEventListener('DOMContentLoaded', () => { new ClustersBundle(); // eslint-disable-line no-new initGkeNamespace(); + initClusterHealth(); }); diff --git a/app/assets/javascripts/pages/projects/commit/pipelines/index.js b/app/assets/javascripts/pages/projects/commit/pipelines/index.js index 9f08260c3d6..1415a6f60c8 100644 --- a/app/assets/javascripts/pages/projects/commit/pipelines/index.js +++ b/app/assets/javascripts/pages/projects/commit/pipelines/index.js @@ -1,6 +1,7 @@ import $ from 'jquery'; import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown'; import initPipelines from '~/commit/pipelines/pipelines_bundle'; +import { fetchCommitMergeRequests } from '~/commit_merge_requests'; document.addEventListener('DOMContentLoaded', () => { new MiniPipelineGraph({ @@ -8,5 +9,6 @@ document.addEventListener('DOMContentLoaded', () => { }).bindEvents(); // eslint-disable-next-line no-jquery/no-load $('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath); + fetchCommitMergeRequests(); initPipelines(); }); diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js index 9fb07917f9b..e65c18c07a9 100644 --- a/app/assets/javascripts/pages/projects/edit/index.js +++ b/app/assets/javascripts/pages/projects/edit/index.js @@ -7,13 +7,20 @@ import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory'; import initFilePickers from '~/file_pickers'; import initProjectLoadingSpinner from '../shared/save_project_loader'; import initProjectPermissionsSettings from '../shared/permissions'; +import initProjectRemoveModal from '~/projects/project_remove_modal'; +import UserCallout from '~/user_callout'; +import initServiceDesk from '~/projects/settings_service_desk'; document.addEventListener('DOMContentLoaded', () => { initFilePickers(); initConfirmDangerModal(); initSettingsPanels(); + initProjectRemoveModal(); mountBadgeSettings(PROJECT_BADGE); + new UserCallout({ className: 'js-service-desk-callout' }); // eslint-disable-line no-new + initServiceDesk(); + initProjectLoadingSpinner(); initProjectPermissionsSettings(); setupTransferEdit('.js-project-transfer-form', 'select.select2'); 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 new file mode 100644 index 00000000000..77753521342 --- /dev/null +++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list.vue @@ -0,0 +1,91 @@ +<script> +import { GlTabs, GlTab, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui'; +import axios from '~/lib/utils/axios_utils'; +import { __ } from '~/locale'; +import createFlash from '~/flash'; +import ForkGroupsListItem from './fork_groups_list_item.vue'; + +export default { + components: { + GlTabs, + GlTab, + GlLoadingIcon, + GlSearchBoxByType, + ForkGroupsListItem, + }, + props: { + hasReachedProjectLimit: { + type: Boolean, + required: true, + }, + endpoint: { + type: String, + required: true, + }, + }, + data() { + return { + namespaces: null, + filter: '', + }; + }, + computed: { + filteredNamespaces() { + return this.namespaces.filter(n => n.name.toLowerCase().includes(this.filter.toLowerCase())); + }, + }, + + mounted() { + this.loadGroups(); + }, + + methods: { + loadGroups() { + axios + .get(this.endpoint) + .then(response => { + this.namespaces = response.data.namespaces; + }) + .catch(() => createFlash(__('There was a problem fetching groups.'))); + }, + }, + + i18n: { + searchPlaceholder: __('Search by name'), + }, +}; +</script> +<template> + <gl-tabs class="fork-groups"> + <gl-tab :title="__('Groups and subgroups')"> + <gl-loading-icon v-if="!namespaces" size="md" class="gl-mt-3" /> + <template v-else-if="namespaces.length === 0"> + <div class="gl-text-center"> + <div class="h5">{{ __('No available groups to fork the project.') }}</div> + <p class="gl-mt-5"> + {{ __('You must have permission to create a project in a group before forking.') }} + </p> + </div> + </template> + <div v-else-if="filteredNamespaces.length === 0" class="gl-text-center gl-mt-3"> + {{ s__('GroupsTree|No groups matched your search') }} + </div> + <ul v-else class="groups-list group-list-tree"> + <fork-groups-list-item + v-for="(namespace, index) in filteredNamespaces" + :key="index" + :group="namespace" + :has-reached-project-limit="hasReachedProjectLimit" + /> + </ul> + </gl-tab> + <template #tabs-end> + <gl-search-box-by-type + v-if="namespaces && namespaces.length" + v-model="filter" + :placeholder="$options.i18n.searchPlaceholder" + class="gl-align-self-center gl-ml-auto fork-filtered-search" + /> + </template> + </gl-tabs> +</template> diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue new file mode 100644 index 00000000000..792c2f3db34 --- /dev/null +++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue @@ -0,0 +1,147 @@ +<script> +import { + GlLink, + GlButton, + GlIcon, + GlAvatar, + GlTooltipDirective, + GlTooltip, + GlBadge, +} from '@gitlab/ui'; +import { VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE } from '~/groups/constants'; +import { __ } from '~/locale'; +import csrf from '~/lib/utils/csrf'; + +export default { + components: { + GlIcon, + GlAvatar, + GlBadge, + GlButton, + GlTooltip, + GlLink, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + group: { + type: Object, + required: true, + }, + hasReachedProjectLimit: { + type: Boolean, + required: true, + }, + }, + data() { + return { namespaces: null }; + }, + + computed: { + rowClass() { + return { + 'has-description': this.group.description, + 'being-removed': this.isGroupPendingRemoval, + }; + }, + isGroupPendingRemoval() { + return this.group.marked_for_deletion; + }, + hasForkedProject() { + return Boolean(this.group.forked_project_path); + }, + visibilityIcon() { + return VISIBILITY_TYPE_ICON[this.group.visibility]; + }, + visibilityTooltip() { + return GROUP_VISIBILITY_TYPE[this.group.visibility]; + }, + isSelectButtonDisabled() { + return this.hasReachedProjectLimit || !this.group.can_create_project; + }, + selectButtonDisabledTooltip() { + return this.hasReachedProjectLimit + ? this.$options.i18n.hasReachedProjectLimitMessage + : this.$options.i18n.insufficientPermissionsMessage; + }, + }, + + i18n: { + hasReachedProjectLimitMessage: __('You have reached your project limit'), + insufficientPermissionsMessage: __( + 'You must have permission to create a project in a namespace before forking.', + ), + }, + + csrf, +}; +</script> +<template> + <li :class="rowClass" class="group-row"> + <div class="group-row-contents gl-display-flex gl-align-items-center gl-py-3 gl-pr-5"> + <div class="folder-toggle-wrap gl-mr-2 gl-display-flex gl-align-items-center"> + <gl-icon name="folder-o" /> + </div> + <gl-link + :href="group.relative_path" + class="gl-display-none gl-flex-shrink-0 gl-display-sm-flex gl-mr-3" + > + <gl-avatar :size="32" shape="rect" :entity-name="group.name" :src="group.avatarUrl" /> + </gl-link> + <div class="gl-min-w-0 gl-display-flex gl-flex-grow-1 gl-flex-shrink-1 gl-align-items-center"> + <div class="gl-min-w-0 gl-flex-grow-1 flex-shrink-1"> + <div class="title gl-display-flex gl-align-items-center gl-flex-wrap gl-mr-3"> + <gl-link :href="group.relative_path" class="gl-mt-3 gl-mr-3 gl-text-gray-900!">{{ + group.full_name + }}</gl-link> + <gl-icon + v-gl-tooltip.hover.bottom + class="gl-mr-0 gl-inline-flex gl-mt-3 text-secondary" + :name="visibilityIcon" + :title="visibilityTooltip" + /> + <gl-badge + v-if="isGroupPendingRemoval" + variant="warning" + class="gl-display-none gl-display-sm-flex gl-mt-3 gl-mr-1" + >{{ __('pending removal') }}</gl-badge + > + <span v-if="group.permission" class="user-access-role gl-mt-3"> + {{ group.permission }} + </span> + </div> + <div v-if="group.description" class="description"> + <span v-html="group.markdown_description"> </span> + </div> + </div> + <div class="gl-display-flex gl-flex-shrink-0"> + <gl-button + v-if="hasForkedProject" + class="gl-h-7 gl-text-decoration-none!" + :href="group.forked_project_path" + >{{ __('Go to fork') }}</gl-button + > + <template v-else> + <div ref="selectButtonWrapper"> + <form method="POST" :action="group.fork_path"> + <input type="hidden" name="authenticity_token" :value="$options.csrf.token" /> + <gl-button + type="submit" + class="gl-h-7 gl-text-decoration-none!" + :data-qa-name="group.full_name" + variant="success" + :disabled="isSelectButtonDisabled" + >{{ __('Select') }}</gl-button + > + </form> + </div> + <gl-tooltip v-if="isSelectButtonDisabled" :target="() => $refs.selectButtonWrapper"> + {{ selectButtonDisabledTooltip }} + </gl-tooltip> + </template> + </div> + </div> + </div> + </li> +</template> diff --git a/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue b/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue index af8fb032c22..39d6df33a85 100644 --- a/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue +++ b/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue @@ -63,17 +63,19 @@ export default { selectedDailyCoverageName() { return this.selectedDailyCoverage?.group_name; }, - formattedData() { - if (this.selectedDailyCoverage?.data) { - return this.selectedDailyCoverage.data.map(value => [ - dateFormat(value.date, 'mmm dd'), - value.coverage, - ]); - } - + sortedData() { // If the fetching failed, we return an empty array which // allow the graph to render while empty - return []; + if (!this.selectedDailyCoverage?.data) { + return []; + } + + return [...this.selectedDailyCoverage.data].sort( + (a, b) => new Date(a.date) - new Date(b.date), + ); + }, + formattedData() { + return this.sortedData.map(value => [dateFormat(value.date, 'mmm dd'), value.coverage]); }, chartData() { return [ diff --git a/app/assets/javascripts/pages/projects/integrations/jira/issues/index/index.js b/app/assets/javascripts/pages/projects/integrations/jira/issues/index/index.js new file mode 100644 index 00000000000..260ba69b4bc --- /dev/null +++ b/app/assets/javascripts/pages/projects/integrations/jira/issues/index/index.js @@ -0,0 +1,5 @@ +import initIssuablesList from '~/issuables_list'; + +document.addEventListener('DOMContentLoaded', () => { + initIssuablesList(); +}); diff --git a/app/assets/javascripts/pages/projects/issues/service_desk/filtered_search.js b/app/assets/javascripts/pages/projects/issues/service_desk/filtered_search.js new file mode 100644 index 00000000000..72003b61c8a --- /dev/null +++ b/app/assets/javascripts/pages/projects/issues/service_desk/filtered_search.js @@ -0,0 +1,30 @@ +/* eslint-disable class-methods-use-this */ +import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys'; +import FilteredSearchManager from 'ee_else_ce/filtered_search/filtered_search_manager'; + +const AUTHOR_PARAM_KEY = 'author_username'; + +export default class FilteredSearchServiceDesk extends FilteredSearchManager { + constructor(supportBotData) { + super({ + page: 'service_desk', + filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys, + }); + + this.supportBotData = supportBotData; + } + + canEdit(tokenName) { + return tokenName !== 'author'; + } + + modifyUrlParams(paramsArray) { + const supportBotParamPair = `${AUTHOR_PARAM_KEY}=${this.supportBotData.username}`; + const onlyValidParams = paramsArray.filter(param => param.indexOf(AUTHOR_PARAM_KEY) === -1); + + // unshift ensures author param is always first token element + onlyValidParams.unshift(supportBotParamPair); + + return onlyValidParams; + } +} diff --git a/app/assets/javascripts/pages/projects/issues/service_desk/index.js b/app/assets/javascripts/pages/projects/issues/service_desk/index.js new file mode 100644 index 00000000000..56054f5fc80 --- /dev/null +++ b/app/assets/javascripts/pages/projects/issues/service_desk/index.js @@ -0,0 +1,11 @@ +import FilteredSearchServiceDesk from './filtered_search'; + +document.addEventListener('DOMContentLoaded', () => { + const supportBotData = JSON.parse( + document.querySelector('.js-service-desk-issues').dataset.supportBot, + ); + + const filteredSearchManager = new FilteredSearchServiceDesk(supportBotData); + + filteredSearchManager.setup(); +}); diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js index 46c9b2fe0af..32f77465347 100644 --- a/app/assets/javascripts/pages/projects/issues/show.js +++ b/app/assets/javascripts/pages/projects/issues/show.js @@ -3,7 +3,7 @@ import Issue from '~/issue'; import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; import ZenMode from '~/zen_mode'; import '~/notes/index'; -import initIssueableApp from '~/issue_show'; +import initIssueableApp, { issuableHeaderWarnings } from '~/issue_show'; import initSentryErrorStackTraceApp from '~/sentry_error_stack_trace'; import initRelatedMergeRequestsApp from '~/related_merge_requests'; import initVueIssuableSidebarApp from '~/issuable_sidebar/sidebar_bundle'; @@ -12,15 +12,17 @@ export default function() { initIssueableApp(); initSentryErrorStackTraceApp(); initRelatedMergeRequestsApp(); + issuableHeaderWarnings(); - // .js-design-management is currently EE-only. - // This will be moved to CE as part of https://gitlab.com/gitlab-org/gitlab/-/issues/212566#frontend - // at which point this conditional can be removed. - if (document.querySelector('.js-design-management')) { - import(/* webpackChunkName: 'design_management' */ '~/design_management') - .then(module => module.default()) - .catch(() => {}); - } + import(/* webpackChunkName: 'design_management' */ '~/design_management') + .then(module => module.default()) + .catch(() => {}); + + // This will be removed when we remove the `design_management_moved` feature flag + // See https://gitlab.com/gitlab-org/gitlab/-/issues/223197 + import(/* webpackChunkName: 'design_management' */ '~/design_management_new') + .then(module => module.default()) + .catch(() => {}); new Issue(); // eslint-disable-line no-new new ShortcutsIssuable(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/projects/metrics_dashboard/index.js b/app/assets/javascripts/pages/projects/metrics_dashboard/index.js new file mode 100644 index 00000000000..d3028aec313 --- /dev/null +++ b/app/assets/javascripts/pages/projects/metrics_dashboard/index.js @@ -0,0 +1,3 @@ +import monitoringApp from '~/monitoring/monitoring_app'; + +document.addEventListener('DOMContentLoaded', monitoringApp); 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 4efabcb7df3..5ef1f959b2c 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,12 +1,19 @@ <script> -import { GlSprintf, GlLink } from '@gitlab/ui'; +import { GlFormRadio, GlFormRadioGroup, GlLink, GlSprintf } from '@gitlab/ui'; import { s__, sprintf } from '~/locale'; import { getWeekdayNames } from '~/lib/utils/datetime_utility'; +const KEY_EVERY_DAY = 'everyDay'; +const KEY_EVERY_WEEK = 'everyWeek'; +const KEY_EVERY_MONTH = 'everyMonth'; +const KEY_CUSTOM = 'custom'; + export default { components: { - GlSprintf, + GlFormRadio, + GlFormRadioGroup, GlLink, + GlSprintf, }, props: { initialCronInterval: { @@ -22,6 +29,7 @@ export default { randomWeekDayIndex: this.generateRandomWeekDayIndex(), randomDay: this.generateRandomDay(), inputNameAttribute: 'schedule[cron]', + radioValue: this.initialCronInterval ? KEY_CUSTOM : KEY_EVERY_DAY, cronInterval: this.initialCronInterval, cronSyntaxUrl: 'https://en.wikipedia.org/wiki/Cron', }; @@ -29,14 +37,11 @@ export default { computed: { cronIntervalPresets() { return { - everyDay: `0 ${this.randomHour} * * *`, - everyWeek: `0 ${this.randomHour} * * ${this.randomWeekDayIndex}`, - everyMonth: `0 ${this.randomHour} ${this.randomDay} * *`, + [KEY_EVERY_DAY]: `0 ${this.randomHour} * * *`, + [KEY_EVERY_WEEK]: `0 ${this.randomHour} * * ${this.randomWeekDayIndex}`, + [KEY_EVERY_MONTH]: `0 ${this.randomHour} ${this.randomDay} * *`, }; }, - intervalIsPreset() { - return Object.values(this.cronIntervalPresets).includes(this.cronInterval); - }, formattedTime() { if (this.randomHour > 12) { return `${this.randomHour - 12}:00pm`; @@ -45,24 +50,36 @@ export default { } return `${this.randomHour}:00am`; }, + radioOptions() { + return [ + { + value: KEY_EVERY_DAY, + text: sprintf(s__(`Every day (at %{time})`), { time: this.formattedTime }), + }, + { + value: KEY_EVERY_WEEK, + text: sprintf(s__('Every week (%{weekday} at %{time})'), { + weekday: this.weekday, + time: this.formattedTime, + }), + }, + { + value: KEY_EVERY_MONTH, + text: sprintf(s__('Every month (Day %{day} at %{time})'), { + day: this.randomDay, + time: this.formattedTime, + }), + }, + { + value: KEY_CUSTOM, + text: s__('PipelineScheduleIntervalPattern|Custom (%{linkStart}Cron syntax%{linkEnd})'), + link: this.cronSyntaxUrl, + }, + ]; + }, weekday() { return getWeekdayNames()[this.randomWeekDayIndex]; }, - everyDayText() { - return sprintf(s__(`Every day (at %{time})`), { time: this.formattedTime }); - }, - everyWeekText() { - return sprintf(s__('Every week (%{weekday} at %{time})'), { - weekday: this.weekday, - time: this.formattedTime, - }); - }, - everyMonthText() { - return sprintf(s__('Every month (Day %{day} at %{time})'), { - day: this.randomDay, - time: this.formattedTime, - }); - }, }, watch: { cronInterval() { @@ -72,38 +89,18 @@ export default { gl.pipelineScheduleFieldErrors.updateFormValidityState(); }); }, - }, - // If at the mounting stage the default is still an empty string, we - // know we are not editing an existing field so we update it so - // that the default is the first radio option - mounted() { - if (this.cronInterval === '') { - this.cronInterval = this.cronIntervalPresets.everyDay; - } + radioValue: { + immediate: true, + handler(val) { + if (val !== KEY_CUSTOM) { + this.cronInterval = this.cronIntervalPresets[val]; + } + }, + }, }, methods: { - setCustomInput(e) { - if (!this.isEditingCustom) { - this.isEditingCustom = true; - this.$refs.customInput.click(); - // Because we need to manually trigger the click on the radio btn, - // it will add a space to update the v-model. If the user is typing - // and the space is added, it will feel very unituitive so we reset - // the value to the original - this.cronInterval = e.target.value; - } - if (this.intervalIsPreset) { - this.isEditingCustom = false; - } - }, - toggleCustomInput(shouldEnable) { - this.isEditingCustom = shouldEnable; - - if (shouldEnable) { - // We need to change the value so other radios don't remain selected - // because the model (cronInterval) hasn't changed. The server trims it. - this.cronInterval = `${this.cronInterval} `; - } + onCustomInput() { + this.radioValue = KEY_CUSTOM; }, generateRandomHour() { return Math.floor(Math.random() * 23); @@ -119,89 +116,33 @@ export default { </script> <template> - <div class="interval-pattern-form-group"> - <div class="cron-preset-radio-input"> - <input - id="every-day" - v-model="cronInterval" - :name="inputNameAttribute" - :value="cronIntervalPresets.everyDay" - class="label-bold" - type="radio" - @click="toggleCustomInput(false)" - /> - - <label class="label-bold" for="every-day"> - {{ everyDayText }} - </label> - </div> - - <div class="cron-preset-radio-input"> - <input - id="every-week" - v-model="cronInterval" - :name="inputNameAttribute" - :value="cronIntervalPresets.everyWeek" - class="label-bold" - type="radio" - @click="toggleCustomInput(false)" - /> - - <label class="label-bold" for="every-week"> - {{ everyWeekText }} - </label> - </div> - - <div class="cron-preset-radio-input"> - <input - id="every-month" - v-model="cronInterval" - :name="inputNameAttribute" - :value="cronIntervalPresets.everyMonth" - class="label-bold" - type="radio" - @click="toggleCustomInput(false)" - /> - - <label class="label-bold" for="every-month"> - {{ everyMonthText }} - </label> - </div> - - <div class="cron-preset-radio-input"> - <input - id="custom" - ref="customInput" - v-model="cronInterval" - :name="inputNameAttribute" - :value="cronInterval" - class="label-bold" - type="radio" - @click="toggleCustomInput(true)" - /> - - <label for="custom"> {{ s__('PipelineSheduleIntervalPattern|Custom') }} </label> - - <gl-sprintf :message="__('(%{linkStart}Cron syntax%{linkEnd})')"> - <template #link="{content}"> - <gl-link :href="cronSyntaxUrl" target="_blank" class="gl-font-sm"> - {{ content }} - </gl-link> - </template> - </gl-sprintf> - </div> - - <div class="cron-interval-input-wrapper"> - <input - id="schedule_cron" - v-model="cronInterval" - :placeholder="__('Define a custom pattern with cron syntax')" - :name="inputNameAttribute" - class="form-control inline cron-interval-input" - type="text" - required="true" - @input="setCustomInput" - /> - </div> + <div> + <gl-form-radio-group v-model="radioValue" :name="inputNameAttribute"> + <gl-form-radio + v-for="option in radioOptions" + :key="option.value" + :value="option.value" + :data-testid="option.value" + > + <gl-sprintf v-if="option.link" :message="option.text"> + <template #link="{content}"> + <gl-link :href="option.link" target="_blank" class="gl-font-sm"> + {{ content }} + </gl-link> + </template> + </gl-sprintf> + <template v-else>{{ option.text }}</template> + </gl-form-radio> + </gl-form-radio-group> + <input + id="schedule_cron" + v-model="cronInterval" + :placeholder="__('Define a custom pattern with cron syntax')" + :name="inputNameAttribute" + class="form-control inline cron-interval-input" + type="text" + required="true" + @input="onCustomInput" + /> </div> </template> diff --git a/app/assets/javascripts/pages/projects/pipelines/index/index.js b/app/assets/javascripts/pages/projects/pipelines/index/index.js index 2c37d7da4a7..bed9a751d4c 100644 --- a/app/assets/javascripts/pages/projects/pipelines/index/index.js +++ b/app/assets/javascripts/pages/projects/pipelines/index/index.js @@ -8,7 +8,7 @@ import { } from '~/lib/utils/common_utils'; import { __ } from '~/locale'; import PipelinesStore from '../../../../pipelines/stores/pipelines_store'; -import pipelinesComponent from '../../../../pipelines/components/pipelines.vue'; +import pipelinesComponent from '../../../../pipelines/components/pipelines_list/pipelines.vue'; import Translate from '../../../../vue_shared/translate'; Vue.use(Translate); @@ -40,6 +40,7 @@ document.addEventListener( props: { store: this.store, endpoint: this.dataset.endpoint, + pipelineScheduleUrl: this.dataset.pipelineScheduleUrl, helpPagePath: this.dataset.helpPagePath, emptyStateSvgPath: this.dataset.emptyStateSvgPath, errorStateSvgPath: this.dataset.errorStateSvgPath, diff --git a/app/assets/javascripts/pages/projects/project_members/index.js b/app/assets/javascripts/pages/projects/project_members/index.js index f39765818e7..e146592e134 100644 --- a/app/assets/javascripts/pages/projects/project_members/index.js +++ b/app/assets/javascripts/pages/projects/project_members/index.js @@ -1,12 +1,30 @@ +import Vue from 'vue'; import Members from 'ee_else_ce/members'; -import memberExpirationDate from '../../../member_expiration_date'; -import UsersSelect from '../../../users_select'; -import groupsSelect from '../../../groups_select'; +import memberExpirationDate from '~/member_expiration_date'; +import UsersSelect from '~/users_select'; +import groupsSelect from '~/groups_select'; +import RemoveMemberModal from '~/vue_shared/components/remove_member_modal.vue'; + +function mountRemoveMemberModal() { + const el = document.querySelector('.js-remove-member-modal'); + if (!el) { + return false; + } + + return new Vue({ + el, + render(createComponent) { + return createComponent(RemoveMemberModal); + }, + }); +} document.addEventListener('DOMContentLoaded', () => { - memberExpirationDate('.js-access-expiration-date-groups'); groupsSelect(); memberExpirationDate(); + memberExpirationDate('.js-access-expiration-date-groups'); + mountRemoveMemberModal(); + new Members(); // eslint-disable-line no-new new UsersSelect(); // eslint-disable-line no-new }); diff --git a/app/assets/javascripts/pages/projects/releases/new/index.js b/app/assets/javascripts/pages/projects/releases/new/index.js new file mode 100644 index 00000000000..0e314aacf8a --- /dev/null +++ b/app/assets/javascripts/pages/projects/releases/new/index.js @@ -0,0 +1,7 @@ +import ZenMode from '~/zen_mode'; +import initNewRelease from '~/releases/mount_new'; + +document.addEventListener('DOMContentLoaded', () => { + new ZenMode(); // eslint-disable-line no-new + initNewRelease(); +}); diff --git a/app/assets/javascripts/pages/projects/settings/operations/show/index.js b/app/assets/javascripts/pages/projects/settings/operations/show/index.js index 721d4a31fe4..1b9ec44ed4a 100644 --- a/app/assets/javascripts/pages/projects/settings/operations/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/operations/show/index.js @@ -1,13 +1,17 @@ import mountErrorTrackingForm from '~/error_tracking_settings'; +import mountAlertsSettings from '~/alerts_settings'; import mountOperationSettings from '~/operation_settings'; import mountGrafanaIntegration from '~/grafana_integration'; import initSettingsPanels from '~/settings_panels'; +import initIncidentsSettings from '~/incidents_settings'; document.addEventListener('DOMContentLoaded', () => { + initIncidentsSettings(); mountErrorTrackingForm(); mountOperationSettings(); mountGrafanaIntegration(); if (!IS_EE) { initSettingsPanels(); } + mountAlertsSettings(document.querySelector('.js-alerts-settings')); }); 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 7181332a1d6..a95f0af46cd 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 @@ -426,7 +426,7 @@ export default { v-if="lfsAvailable" ref="git-lfs-settings" :help-path="lfsHelpPath" - :label="s__('ProjectSettings|Git Large File Storage')" + :label="s__('ProjectSettings|Git Large File Storage (LFS)')" :help-text=" s__('ProjectSettings|Manages large files such as audio, video, and graphics files') " diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js index 3c44053e2b2..c65cc3e4c57 100644 --- a/app/assets/javascripts/pages/projects/show/index.js +++ b/app/assets/javascripts/pages/projects/show/index.js @@ -1,25 +1,18 @@ -import $ from 'jquery'; -import 'jquery.waitforimages'; - import initBlob from '~/blob_edit/blob_bundle'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import NotificationsForm from '~/notifications_form'; import UserCallout from '~/user_callout'; -import TreeView from '~/tree'; import BlobViewer from '~/blob/viewer/index'; import Activities from '~/activities'; -import { ajaxGet } from '~/lib/utils/common_utils'; -import GpgBadges from '~/gpg_badges'; import initReadMore from '~/read_more'; import leaveByUrl from '~/namespaces/leave_by_url'; import Star from '../../../star'; import notificationsDropdown from '../../../notifications_dropdown'; -import initNamespaceStorageLimitAlert from '~/namespace_storage_limit_alert'; import { showLearnGitLabProjectPopover } from '~/onboarding_issues'; +import initTree from 'ee_else_ce/repository'; document.addEventListener('DOMContentLoaded', () => { initReadMore(); - initNamespaceStorageLimitAlert(); new Star(); // eslint-disable-line no-new notificationsDropdown(); new ShortcutsNavigation(); // eslint-disable-line no-new @@ -31,10 +24,10 @@ document.addEventListener('DOMContentLoaded', () => { }); // Project show page loads different overview content based on user preferences - const treeSlider = document.querySelector('#tree-slider'); + const treeSlider = document.getElementById('js-tree-list'); if (treeSlider) { - new TreeView(); // eslint-disable-line no-new initBlob(); + initTree(); } if (document.querySelector('.blob-viewer')) { @@ -45,21 +38,7 @@ document.addEventListener('DOMContentLoaded', () => { new Activities(); // eslint-disable-line no-new } - $(treeSlider).waitForImages(() => { - ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath); - }); - - GpgBadges.fetch(); leaveByUrl('project'); - if (document.getElementById('js-tree-list')) { - initBlob(); - import('ee_else_ce/repository') - .then(m => m.default()) - .catch(e => { - throw e; - }); - } - showLearnGitLabProjectPopover(); }); diff --git a/app/assets/javascripts/pages/projects/tree/show/index.js b/app/assets/javascripts/pages/projects/tree/show/index.js index 0d1d32317fe..78a4ea23f1a 100644 --- a/app/assets/javascripts/pages/projects/tree/show/index.js +++ b/app/assets/javascripts/pages/projects/tree/show/index.js @@ -1,53 +1,12 @@ import $ from 'jquery'; -import 'jquery.waitforimages'; - -import Vue from 'vue'; import initBlob from '~/blob_edit/blob_bundle'; -import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue'; -import GpgBadges from '~/gpg_badges'; -import TreeView from '../../../../tree'; import ShortcutsNavigation from '../../../../behaviors/shortcuts/shortcuts_navigation'; -import BlobViewer from '../../../../blob/viewer'; import NewCommitForm from '../../../../new_commit_form'; -import { ajaxGet } from '../../../../lib/utils/common_utils'; +import initTree from 'ee_else_ce/repository'; document.addEventListener('DOMContentLoaded', () => { new ShortcutsNavigation(); // eslint-disable-line no-new - new TreeView(); // eslint-disable-line no-new - new BlobViewer(); // eslint-disable-line no-new new NewCommitForm($('.js-create-dir-form')); // eslint-disable-line no-new - $('#tree-slider').waitForImages(() => - ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath), - ); - initBlob(); - const commitPipelineStatusEl = document.querySelector('.js-commit-pipeline-status'); - const statusLink = document.querySelector('.commit-actions .ci-status-link'); - if (statusLink != null) { - statusLink.remove(); - // eslint-disable-next-line no-new - new Vue({ - el: commitPipelineStatusEl, - components: { - commitPipelineStatus, - }, - render(createElement) { - return createElement('commit-pipeline-status', { - props: { - endpoint: commitPipelineStatusEl.dataset.endpoint, - }, - }); - }, - }); - } - - GpgBadges.fetch(); - - if (document.getElementById('js-tree-list')) { - import('ee_else_ce/repository') - .then(m => m.default()) - .catch(e => { - throw e; - }); - } + initTree(); }); diff --git a/app/assets/javascripts/pages/search/init_filtered_search.js b/app/assets/javascripts/pages/search/init_filtered_search.js index e54e32199f0..b331a2bee6a 100644 --- a/app/assets/javascripts/pages/search/init_filtered_search.js +++ b/app/assets/javascripts/pages/search/init_filtered_search.js @@ -7,6 +7,7 @@ export default ({ isGroupAncestor, isGroupDecendent, stateFiltersSelector, + anchor, }) => { const filteredSearchEnabled = FilteredSearchManager && document.querySelector('.filtered-search'); if (filteredSearchEnabled) { @@ -17,6 +18,7 @@ export default ({ isGroupDecendent, filteredSearchTokenKeys, stateFiltersSelector, + anchor, }); filteredSearchManager.setup(); } diff --git a/app/assets/javascripts/pages/sessions/new/length_validator.js b/app/assets/javascripts/pages/sessions/new/length_validator.js index 3d687ca08cc..92482c81f3c 100644 --- a/app/assets/javascripts/pages/sessions/new/length_validator.js +++ b/app/assets/javascripts/pages/sessions/new/length_validator.js @@ -21,11 +21,24 @@ export default class LengthValidator extends InputValidator { ); const { value } = this.inputDomElement; - const { maxLengthMessage, maxLength } = this.inputDomElement.dataset; - - this.errorMessage = maxLengthMessage; - - this.invalidInput = value.length > parseInt(maxLength, 10); + const { + minLength, + minLengthMessage, + maxLengthMessage, + maxLength, + } = this.inputDomElement.dataset; + + this.invalidInput = false; + + if (value.length > parseInt(maxLength, 10)) { + this.invalidInput = true; + this.errorMessage = maxLengthMessage; + } + + if (value.length < parseInt(minLength, 10)) { + this.invalidInput = true; + this.errorMessage = minLengthMessage; + } this.setValidationStateAndMessage(); } diff --git a/app/assets/javascripts/pages/sessions/new/username_validator.js b/app/assets/javascripts/pages/sessions/new/username_validator.js index 1048e3b4548..ecb5e677290 100644 --- a/app/assets/javascripts/pages/sessions/new/username_validator.js +++ b/app/assets/javascripts/pages/sessions/new/username_validator.js @@ -39,7 +39,7 @@ export default class UsernameValidator extends InputValidator { static validateUsernameInput(inputDomElement) { const username = inputDomElement.value; - if (inputDomElement.checkValidity() && username.length > 0) { + if (inputDomElement.checkValidity() && username.length > 1) { UsernameValidator.setMessageVisibility(inputDomElement, pendingMessageSelector); UsernameValidator.fetchUsernameAvailability(username) .then(usernameTaken => { diff --git a/app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue b/app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue index 580cca49b5e..a7b7d597fb7 100644 --- a/app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue +++ b/app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue @@ -55,13 +55,22 @@ export default { <template> <div class="d-inline-block"> - <button v-gl-modal="modalId" type="button" class="btn btn-danger">{{ __('Delete') }}</button> + <button + v-gl-modal="modalId" + type="button" + class="btn btn-danger" + data-qa-selector="delete_button" + > + {{ __('Delete') }} + </button> <gl-modal :title="title" - :ok-title="s__('WikiPageConfirmDelete|Delete page')" + :action-primary="{ + text: s__('WikiPageConfirmDelete|Delete page'), + attributes: { variant: 'danger', 'data-qa-selector': 'confirm_deletion_button' }, + }" :modal-id="modalId" title-tag="h4" - ok-variant="danger" @ok="onSubmit" > {{ message }} diff --git a/app/assets/javascripts/pages/shared/wikis/wikis.js b/app/assets/javascripts/pages/shared/wikis/wikis.js index ed67219383b..41d43812b5d 100644 --- a/app/assets/javascripts/pages/shared/wikis/wikis.js +++ b/app/assets/javascripts/pages/shared/wikis/wikis.js @@ -1,5 +1,6 @@ import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; import { s__, sprintf } from '~/locale'; +import Tracking from '~/tracking'; const MARKDOWN_LINK_TEXT = { markdown: '[Link Title](page-slug)', @@ -8,6 +9,9 @@ const MARKDOWN_LINK_TEXT = { org: '[[page-slug]]', }; +const TRACKING_EVENT_NAME = 'view_wiki_page'; +const TRACKING_CONTEXT_SCHEMA = 'iglu:com.gitlab/wiki_page_context/jsonschema/1-0-0'; + export default class Wikis { constructor() { this.sidebarEl = document.querySelector('.js-wiki-sidebar'); @@ -57,6 +61,8 @@ export default class Wikis { window.onbeforeunload = null; }); } + + Wikis.trackPageView(); } handleWikiTitleChange(e) { @@ -97,4 +103,17 @@ export default class Wikis { classList.remove('right-sidebar-expanded'); } } + + static trackPageView() { + const wikiPageContent = document.querySelector('.js-wiki-page-content[data-tracking-context]'); + if (!wikiPageContent) return; + + Tracking.event(document.body.dataset.page, TRACKING_EVENT_NAME, { + label: TRACKING_EVENT_NAME, + context: { + schema: TRACKING_CONTEXT_SCHEMA, + data: JSON.parse(wikiPageContent.dataset.trackingContext), + }, + }); + } } |