diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-11-18 13:16:36 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-11-18 13:16:36 +0000 |
commit | 311b0269b4eb9839fa63f80c8d7a58f32b8138a0 (patch) | |
tree | 07e7870bca8aed6d61fdcc810731c50d2c40af47 /app/assets/javascripts/projects | |
parent | 27909cef6c4170ed9205afa7426b8d3de47cbb0c (diff) | |
download | gitlab-ce-311b0269b4eb9839fa63f80c8d7a58f32b8138a0.tar.gz |
Add latest changes from gitlab-org/gitlab@14-5-stable-eev14.5.0-rc42
Diffstat (limited to 'app/assets/javascripts/projects')
30 files changed, 534 insertions, 95 deletions
diff --git a/app/assets/javascripts/projects/commit/components/form_modal.vue b/app/assets/javascripts/projects/commit/components/form_modal.vue index ec7d37644a8..f9dd72119d1 100644 --- a/app/assets/javascripts/projects/commit/components/form_modal.vue +++ b/app/assets/javascripts/projects/commit/components/form_modal.vue @@ -1,6 +1,7 @@ <script> import { GlModal, GlForm, GlFormCheckbox, GlSprintf, GlFormGroup } from '@gitlab/ui'; import { mapActions, mapState } from 'vuex'; +import api from '~/api'; import { BV_SHOW_MODAL } from '~/lib/utils/constants'; import csrf from '~/lib/utils/csrf'; import eventHub from '../event_hub'; @@ -40,6 +41,11 @@ export default { required: false, default: false, }, + primaryActionEventName: { + type: String, + required: false, + default: null, + }, }, data() { return { @@ -83,6 +89,10 @@ export default { this.$root.$emit(BV_SHOW_MODAL, this.modalId); }, handlePrimary() { + if (this.primaryActionEventName) { + api.trackRedisHllUserEvent(this.primaryActionEventName); + } + this.$refs.form.$el.submit(); }, resetModalHandler() { diff --git a/app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js b/app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js index 47ee8237fea..b21fd1a74de 100644 --- a/app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js +++ b/app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js @@ -9,7 +9,7 @@ import { } from './constants'; import createStore from './store'; -export default function initInviteMembersModal() { +export default function initInviteMembersModal(primaryActionEventName) { const el = document.querySelector('.js-cherry-pick-commit-modal'); if (!el) { return false; @@ -52,6 +52,7 @@ export default function initInviteMembersModal() { openModal: OPEN_CHERRY_PICK_MODAL, modalId: CHERRY_PICK_MODAL_ID, isCherryPick: true, + primaryActionEventName, }, }), }); diff --git a/app/assets/javascripts/projects/commit/init_revert_commit_modal.js b/app/assets/javascripts/projects/commit/init_revert_commit_modal.js index df26aa3c830..849b2f4858c 100644 --- a/app/assets/javascripts/projects/commit/init_revert_commit_modal.js +++ b/app/assets/javascripts/projects/commit/init_revert_commit_modal.js @@ -10,7 +10,7 @@ import { } from './constants'; import createStore from './store'; -export default function initInviteMembersModal() { +export default function initInviteMembersModal(primaryActionEventName) { const el = document.querySelector('.js-revert-commit-modal'); if (!el) { return false; @@ -49,6 +49,7 @@ export default function initInviteMembersModal() { i18n: { ...I18N_REVERT_MODAL, ...I18N_MODAL }, openModal: OPEN_REVERT_MODAL, modalId: REVERT_MODAL_ID, + primaryActionEventName, }, }), }); diff --git a/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js b/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js index 2505c47147f..1d4ec4c110b 100644 --- a/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js +++ b/app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js @@ -5,12 +5,7 @@ import createDefaultClient from '~/lib/graphql'; Vue.use(VueApollo); const apolloProvider = new VueApollo({ - defaultClient: createDefaultClient( - {}, - { - assumeImmutableResults: true, - }, - ), + defaultClient: createDefaultClient(), }); export const initCommitPipelineMiniGraph = async (selector = '.js-commit-pipeline-mini-graph') => { diff --git a/app/assets/javascripts/projects/components/project_delete_button.vue b/app/assets/javascripts/projects/components/project_delete_button.vue index 06711e4025a..eaf93e2da4f 100644 --- a/app/assets/javascripts/projects/components/project_delete_button.vue +++ b/app/assets/javascripts/projects/components/project_delete_button.vue @@ -18,12 +18,36 @@ export default { type: String, required: true, }, + isFork: { + type: Boolean, + required: true, + }, + issuesCount: { + type: Number, + required: true, + }, + mergeRequestsCount: { + type: Number, + required: true, + }, + forksCount: { + type: Number, + required: true, + }, + starsCount: { + type: Number, + required: true, + }, }, strings: { alertTitle: __('You are about to permanently delete this project'), alertBody: __( - 'Once a project is permanently deleted, it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd}, including issues, merge requests etc.', + 'After a project is permanently deleted, it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd}, including issues, merge requests etc.', + ), + isNotForkMessage: __( + 'This project is %{strongStart}NOT%{strongEnd} a fork, and has the following:', ), + isForkMessage: __('This forked project has the following:'), }, }; </script> @@ -37,6 +61,38 @@ export default { :title="$options.strings.alertTitle" :dismissible="false" > + <p> + <gl-sprintf v-if="isFork" :message="$options.strings.isForkMessage" /> + <gl-sprintf v-else :message="$options.strings.isNotForkMessage"> + <template #strong="{ content }"> + <strong>{{ content }}</strong> + </template> + </gl-sprintf> + </p> + <ul> + <li> + <gl-sprintf :message="n__('%d issue', '%d issues', issuesCount)"> + <template #issuesCount>{{ issuesCount }}</template> + </gl-sprintf> + </li> + <li> + <gl-sprintf + :message="n__('%d merge requests', '%d merge requests', mergeRequestsCount)" + > + <template #mergeRequestsCount>{{ mergeRequestsCount }}</template> + </gl-sprintf> + </li> + <li> + <gl-sprintf :message="n__('%d fork', '%d forks', forksCount)"> + <template #forksCount>{{ forksCount }}</template> + </gl-sprintf> + </li> + <li> + <gl-sprintf :message="n__('%d star', '%d stars', starsCount)"> + <template #starsCount>{{ starsCount }}</template> + </gl-sprintf> + </li> + </ul> <gl-sprintf :message="$options.strings.alertBody"> <template #strong="{ content }"> <strong>{{ content }}</strong> diff --git a/app/assets/javascripts/projects/default_project_templates.js b/app/assets/javascripts/projects/default_project_templates.js index 2da9449d24e..0393d82ca36 100644 --- a/app/assets/javascripts/projects/default_project_templates.js +++ b/app/assets/javascripts/projects/default_project_templates.js @@ -93,6 +93,10 @@ export default { text: s__('ProjectTemplates|Serverless Framework/JS'), icon: '.template-option .icon-serverless_framework', }, + tencent_serverless_framework: { + text: s__('ProjectTemplates|Tencent Serverless Framework/NextjsSSR'), + icon: '.template-option .icon-tencent_serverless_framework', + }, cluster_management: { text: s__('ProjectTemplates|GitLab Cluster Management'), icon: '.template-option .icon-cluster_management', diff --git a/app/assets/javascripts/projects/details/upload_button.vue b/app/assets/javascripts/projects/details/upload_button.vue index 5b19f15c233..e1c8c66a214 100644 --- a/app/assets/javascripts/projects/details/upload_button.vue +++ b/app/assets/javascripts/projects/details/upload_button.vue @@ -1,7 +1,6 @@ <script> import { GlButton, GlModalDirective } from '@gitlab/ui'; import UploadBlobModal from '~/repository/components/upload_blob_modal.vue'; -import { trackFileUploadEvent } from '../upload_file_experiment_tracking'; const UPLOAD_BLOB_MODAL_ID = 'details-modal-upload-blob'; @@ -30,11 +29,6 @@ export default { default: '', }, }, - methods: { - trackOpenModal() { - trackFileUploadEvent('click_upload_modal_trigger'); - }, - }, uploadBlobModalId: UPLOAD_BLOB_MODAL_ID, }; </script> @@ -44,7 +38,6 @@ export default { v-gl-modal="$options.uploadBlobModalId" icon="upload" data-testid="upload-file-button" - @click="trackOpenModal" >{{ __('Upload File') }}</gl-button > <upload-blob-modal diff --git a/app/assets/javascripts/projects/new/components/app.vue b/app/assets/javascripts/projects/new/components/app.vue index 6e9efc50be8..476d6466cbb 100644 --- a/app/assets/javascripts/projects/new/components/app.vue +++ b/app/assets/javascripts/projects/new/components/app.vue @@ -95,7 +95,7 @@ export default { <template> <new-namespace-page - :initial-breadcrumb="s__('New project')" + :initial-breadcrumb="__('New project')" :panels="availablePanels" :jump-to-last-persisted-panel="hasErrors" :title="s__('ProjectsNew|Create new project')" diff --git a/app/assets/javascripts/projects/new/components/new_project_url_select.vue b/app/assets/javascripts/projects/new/components/new_project_url_select.vue index bf44ff70562..e0ba60074af 100644 --- a/app/assets/javascripts/projects/new/components/new_project_url_select.vue +++ b/app/assets/javascripts/projects/new/components/new_project_url_select.vue @@ -6,9 +6,9 @@ import { GlDropdownItem, GlDropdownText, GlDropdownSectionHeader, - GlLoadingIcon, GlSearchBoxByType, } from '@gitlab/ui'; +import { joinPaths } from '~/lib/utils/url_utility'; import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import Tracking from '~/tracking'; @@ -24,7 +24,6 @@ export default { GlDropdownItem, GlDropdownText, GlDropdownSectionHeader, - GlLoadingIcon, GlSearchBoxByType, }, mixins: [Tracking.mixin()], @@ -103,6 +102,15 @@ export default { focusInput() { this.$refs.search.focusInput(); }, + handleDropdownItemClick(namespace) { + eventHub.$emit('update-visibility', { + name: namespace.name, + visibility: namespace.visibility, + showPath: namespace.webUrl, + editPath: joinPaths(namespace.webUrl, '-', 'edit'), + }); + this.setNamespace(namespace); + }, handleSelectTemplate(groupId) { this.groupToFilterBy = this.userGroups.find( (group) => getIdFromGraphQLId(group.id) === groupId, @@ -134,23 +142,23 @@ export default { <gl-search-box-by-type ref="search" v-model.trim="search" + :is-loading="$apollo.queries.currentUser.loading" data-qa-selector="select_namespace_dropdown_search_field" /> - <gl-loading-icon v-if="$apollo.queries.currentUser.loading" /> - <template v-else> + <template v-if="!$apollo.queries.currentUser.loading"> <template v-if="hasGroupMatches"> <gl-dropdown-section-header>{{ __('Groups') }}</gl-dropdown-section-header> <gl-dropdown-item v-for="group of filteredGroups" :key="group.id" - @click="setNamespace(group)" + @click="handleDropdownItemClick(group)" > {{ group.fullPath }} </gl-dropdown-item> </template> <template v-if="hasNamespaceMatches"> <gl-dropdown-section-header>{{ __('Users') }}</gl-dropdown-section-header> - <gl-dropdown-item @click="setNamespace(userNamespace)"> + <gl-dropdown-item @click="handleDropdownItemClick(userNamespace)"> {{ userNamespace.fullPath }} </gl-dropdown-item> </template> @@ -158,6 +166,11 @@ export default { </template> </gl-dropdown> - <input type="hidden" name="project[namespace_id]" :value="selectedNamespace.id" /> + <input + id="project_namespace_id" + type="hidden" + name="project[namespace_id]" + :value="selectedNamespace.id" + /> </gl-button-group> </template> diff --git a/app/assets/javascripts/projects/new/index.js b/app/assets/javascripts/projects/new/index.js index 572d3276e4f..010c6a29ae3 100644 --- a/app/assets/javascripts/projects/new/index.js +++ b/app/assets/javascripts/projects/new/index.js @@ -50,7 +50,7 @@ export function initNewProjectUrlSelect() { new Vue({ el, apolloProvider: new VueApollo({ - defaultClient: createDefaultClient({}, { assumeImmutableResults: true }), + defaultClient: createDefaultClient(), }), provide: { namespaceFullPath: el.dataset.namespaceFullPath, diff --git a/app/assets/javascripts/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql b/app/assets/javascripts/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql index e16fe5dde49..74febec5a51 100644 --- a/app/assets/javascripts/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql +++ b/app/assets/javascripts/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql @@ -4,6 +4,9 @@ query searchNamespacesWhereUserCanCreateProjects($search: String) { nodes { id fullPath + name + visibility + webUrl } } namespace { diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app.vue b/app/assets/javascripts/projects/pipelines/charts/components/app.vue index 25bacc1cc4a..7379d5caed7 100644 --- a/app/assets/javascripts/projects/pipelines/charts/components/app.vue +++ b/app/assets/javascripts/projects/pipelines/charts/components/app.vue @@ -11,12 +11,17 @@ export default { DeploymentFrequencyCharts: () => import('ee_component/dora/components/deployment_frequency_charts.vue'), LeadTimeCharts: () => import('ee_component/dora/components/lead_time_charts.vue'), + ProjectQualitySummary: () => import('ee_component/project_quality_summary/app.vue'), }, inject: { shouldRenderDoraCharts: { type: Boolean, default: false, }, + shouldRenderQualitySummary: { + type: Boolean, + default: false, + }, }, data() { return { @@ -31,6 +36,10 @@ export default { chartsToShow.push('deployment-frequency', 'lead-time'); } + if (this.shouldRenderQualitySummary) { + chartsToShow.push('project-quality'); + } + return chartsToShow; }, }, @@ -68,6 +77,9 @@ export default { <lead-time-charts /> </gl-tab> </template> + <gl-tab v-if="shouldRenderQualitySummary" :title="s__('QualitySummary|Project quality')"> + <project-quality-summary /> + </gl-tab> </gl-tabs> <pipeline-charts v-else /> </div> diff --git a/app/assets/javascripts/projects/pipelines/charts/index.js b/app/assets/javascripts/projects/pipelines/charts/index.js index f7ea89068a0..003b61d94b1 100644 --- a/app/assets/javascripts/projects/pipelines/charts/index.js +++ b/app/assets/javascripts/projects/pipelines/charts/index.js @@ -7,13 +7,14 @@ import ProjectPipelinesCharts from './components/app.vue'; Vue.use(VueApollo); const apolloProvider = new VueApollo({ - defaultClient: createDefaultClient({}, { assumeImmutableResults: true }), + defaultClient: createDefaultClient(), }); const mountPipelineChartsApp = (el) => { const { projectPath } = el.dataset; const shouldRenderDoraCharts = parseBoolean(el.dataset.shouldRenderDoraCharts); + const shouldRenderQualitySummary = parseBoolean(el.dataset.shouldRenderQualitySummary); return new Vue({ el, @@ -25,6 +26,7 @@ const mountPipelineChartsApp = (el) => { provide: { projectPath, shouldRenderDoraCharts, + shouldRenderQualitySummary, }, render: (createElement) => createElement(ProjectPipelinesCharts, {}), }); diff --git a/app/assets/javascripts/projects/project_delete_button.js b/app/assets/javascripts/projects/project_delete_button.js index aa7fc31d307..b4d388eda3a 100644 --- a/app/assets/javascripts/projects/project_delete_button.js +++ b/app/assets/javascripts/projects/project_delete_button.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import { parseBoolean } from '~/lib/utils/common_utils'; import ProjectDeleteButton from './components/project_delete_button.vue'; export default (selector = '#js-project-delete-button') => { @@ -6,7 +7,15 @@ export default (selector = '#js-project-delete-button') => { if (!el) return; - const { confirmPhrase, formPath } = el.dataset; + const { + confirmPhrase, + formPath, + isFork, + issuesCount, + mergeRequestsCount, + forksCount, + starsCount, + } = el.dataset; // eslint-disable-next-line no-new new Vue({ @@ -16,6 +25,11 @@ export default (selector = '#js-project-delete-button') => { props: { confirmPhrase, formPath, + isFork: parseBoolean(isFork), + issuesCount: parseInt(issuesCount, 10), + mergeRequestsCount: parseInt(mergeRequestsCount, 10), + forksCount: parseInt(forksCount, 10), + starsCount: parseInt(starsCount, 10), }, }); }, diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index b350db0c838..8d71a3dab68 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -43,6 +43,8 @@ const onProjectPathChange = ($projectNameInput, $projectPathInput, hasExistingPr }; const setProjectNamePathHandlers = ($projectNameInput, $projectPathInput) => { + const specialRepo = document.querySelector('.js-user-readme-repo'); + // eslint-disable-next-line @gitlab/no-global-event-off $projectNameInput.off('keyup change').on('keyup change', () => { onProjectNameChange($projectNameInput, $projectPathInput); @@ -54,6 +56,11 @@ const setProjectNamePathHandlers = ($projectNameInput, $projectPathInput) => { $projectPathInput.off('keyup change').on('keyup change', () => { onProjectPathChange($projectNameInput, $projectPathInput, hasUserDefinedProjectName); hasUserDefinedProjectPath = $projectPathInput.val().trim().length > 0; + + specialRepo.classList.toggle( + 'gl-display-none', + $projectPathInput.val() !== $projectPathInput.data('username'), + ); }); }; diff --git a/app/assets/javascripts/projects/settings/topics/components/topics_token_selector.vue b/app/assets/javascripts/projects/settings/topics/components/topics_token_selector.vue new file mode 100644 index 00000000000..e8b0e95b142 --- /dev/null +++ b/app/assets/javascripts/projects/settings/topics/components/topics_token_selector.vue @@ -0,0 +1,92 @@ +<script> +import { GlTokenSelector, GlAvatarLabeled } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import searchProjectTopics from '../queries/project_topics_search.query.graphql'; + +export default { + components: { + GlTokenSelector, + GlAvatarLabeled, + }, + i18n: { + placeholder: s__('ProjectSettings|Search for topic'), + }, + props: { + selected: { + type: Array, + required: false, + default: () => [], + }, + }, + apollo: { + topics: { + query: searchProjectTopics, + variables() { + return { + search: this.search, + }; + }, + update(data) { + return ( + data.topics?.nodes.filter( + (topic) => !this.selectedTokens.some((token) => token.name === topic.name), + ) || [] + ); + }, + debounce: 250, + }, + }, + data() { + return { + topics: [], + selectedTokens: this.selected, + search: '', + }; + }, + computed: { + loading() { + return this.$apollo.queries.topics.loading; + }, + placeholderText() { + return this.selectedTokens.length ? '' : this.$options.i18n.placeholder; + }, + }, + methods: { + handleEnter(event) { + // Prevent form from submitting when adding a token + if (event.target.value !== '') { + event.preventDefault(); + } + }, + filterTopics(searchTerm) { + this.search = searchTerm; + }, + onTokensUpdate(tokens) { + this.$emit('update', tokens); + }, + }, +}; +</script> +<template> + <gl-token-selector + ref="tokenSelector" + v-model="selectedTokens" + :dropdown-items="topics" + :loading="loading" + allow-user-defined-tokens + :placeholder="placeholderText" + @keydown.enter="handleEnter" + @text-input="filterTopics" + @input="onTokensUpdate" + > + <template #dropdown-item-content="{ dropdownItem }"> + <gl-avatar-labeled + :src="dropdownItem.avatarUrl" + :entity-name="dropdownItem.name" + :label="dropdownItem.name" + :size="32" + shape="rect" + /> + </template> + </gl-token-selector> +</template> diff --git a/app/assets/javascripts/projects/settings/topics/index.js b/app/assets/javascripts/projects/settings/topics/index.js new file mode 100644 index 00000000000..3fbd1a61abe --- /dev/null +++ b/app/assets/javascripts/projects/settings/topics/index.js @@ -0,0 +1,51 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import TopicsTokenSelector from './components/topics_token_selector.vue'; + +Vue.use(VueApollo); + +const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), +}); + +export default () => { + const el = document.querySelector('.js-topics-selector'); + + if (!el) return null; + + const { hiddenInputId } = el.dataset; + const hiddenInput = document.getElementById(hiddenInputId); + + const selected = hiddenInput.value + ? hiddenInput.value.split(/,\s*/).map((token, index) => ({ + id: index, + name: token, + })) + : []; + + return new Vue({ + el, + apolloProvider, + render(createElement) { + return createElement(TopicsTokenSelector, { + props: { + selected, + }, + on: { + update(tokens) { + const value = tokens.map(({ name }) => name).join(', '); + hiddenInput.value = value; + // Dispatch `input` event so form submit button becomes active + hiddenInput.dispatchEvent( + new Event('input', { + bubbles: true, + cancelable: true, + }), + ); + }, + }, + }); + }, + }); +}; diff --git a/app/assets/javascripts/projects/settings/topics/queries/project_topics_search.query.graphql b/app/assets/javascripts/projects/settings/topics/queries/project_topics_search.query.graphql new file mode 100644 index 00000000000..b193165062a --- /dev/null +++ b/app/assets/javascripts/projects/settings/topics/queries/project_topics_search.query.graphql @@ -0,0 +1,9 @@ +query searchProjectTopics($search: String) { + topics(search: $search) { + nodes { + id + name + avatarUrl + } + } +} diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue index 4c083ed5496..14c8c53dd19 100644 --- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue +++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue @@ -31,6 +31,9 @@ export default { selectedTemplate: { default: '', }, + selectedFileTemplateProjectId: { + default: null, + }, outgoingName: { default: '', }, @@ -80,7 +83,7 @@ export default { }); }, - onSaveTemplate({ selectedTemplate, outgoingName, projectKey }) { + onSaveTemplate({ selectedTemplate, fileTemplateProjectId, outgoingName, projectKey }) { this.isTemplateSaving = true; const body = { @@ -88,6 +91,7 @@ export default { outgoing_name: outgoingName, project_key: projectKey, service_desk_enabled: this.isEnabled, + file_template_project_id: fileTemplateProjectId, }; return axios @@ -132,6 +136,7 @@ export default { :custom-email="updatedCustomEmail" :custom-email-enabled="customEmailEnabled" :initial-selected-template="selectedTemplate" + :initial-selected-file-template-project-id="selectedFileTemplateProjectId" :initial-outgoing-name="outgoingName" :initial-project-key="projectKey" :templates="templates" diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue index fe2d376f1da..b8053bf9ab5 100644 --- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue +++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue @@ -1,15 +1,8 @@ <script> -import { - GlButton, - GlFormSelect, - GlToggle, - GlLoadingIcon, - GlSprintf, - GlFormInput, - GlLink, -} from '@gitlab/ui'; +import { GlButton, GlToggle, GlLoadingIcon, GlSprintf, GlFormInput, GlLink } from '@gitlab/ui'; import { __ } from '~/locale'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import ServiceDeskTemplateDropdown from './service_desk_template_dropdown.vue'; export default { i18n: { @@ -18,12 +11,12 @@ export default { components: { ClipboardButton, GlButton, - GlFormSelect, GlToggle, GlLoadingIcon, GlSprintf, GlFormInput, GlLink, + ServiceDeskTemplateDropdown, }, props: { isEnabled: { @@ -49,6 +42,11 @@ export default { required: false, default: '', }, + initialSelectedFileTemplateProjectId: { + type: Number, + required: false, + default: null, + }, initialOutgoingName: { type: String, required: false, @@ -73,14 +71,14 @@ export default { data() { return { selectedTemplate: this.initialSelectedTemplate, + selectedFileTemplateProjectId: this.initialSelectedFileTemplateProjectId, outgoingName: this.initialOutgoingName || __('GitLab Support Bot'), projectKey: this.initialProjectKey, + searchTerm: '', + projectKeyError: null, }; }, computed: { - templateOptions() { - return [''].concat(this.templates); - }, hasProjectKeySupport() { return Boolean(this.customEmailEnabled); }, @@ -100,8 +98,21 @@ export default { selectedTemplate: this.selectedTemplate, outgoingName: this.outgoingName, projectKey: this.projectKey, + fileTemplateProjectId: this.selectedFileTemplateProjectId, }); }, + templateChange({ selectedFileTemplateProjectId, selectedTemplate }) { + this.selectedFileTemplateProjectId = selectedFileTemplateProjectId; + this.selectedTemplate = selectedTemplate; + }, + validateProjectKey() { + if (this.projectKey && !new RegExp(/^[a-z0-9_]+$/).test(this.projectKey)) { + this.projectKeyError = __('Only use lowercase letters, numbers, and underscores.'); + return; + } + + this.projectKeyError = null; + }, }, }; </script> @@ -167,8 +178,17 @@ export default { v-model.trim="projectKey" data-testid="project-suffix" class="form-control" + :state="!projectKeyError" + @blur="validateProjectKey" /> - <span v-if="hasProjectKeySupport" class="form-text text-muted"> + <span v-if="hasProjectKeySupport && projectKeyError" class="form-text text-danger"> + {{ projectKeyError }} + </span> + <span + v-if="hasProjectKeySupport" + class="form-text text-muted" + :class="{ 'gl-mt-2!': hasProjectKeySupport && projectKeyError }" + > {{ __('A string appended to the project path to form the Service Desk email address.') }} </span> <span v-else class="form-text text-muted"> @@ -193,12 +213,13 @@ export default { <label for="service-desk-template-select" class="mt-3"> {{ __('Template to append to all Service Desk issues') }} </label> - <gl-form-select - id="service-desk-template-select" - v-model="selectedTemplate" - data-qa-selector="service_desk_template_dropdown" - :options="templateOptions" + <service-desk-template-dropdown + :selected-template="selectedTemplate" + :selected-file-template-project-id="selectedFileTemplateProjectId" + :templates="templates" + @change="templateChange" /> + <label for="service-desk-email-from-name" class="mt-3"> {{ __('Email display name') }} </label> @@ -210,6 +231,7 @@ export default { <gl-button variant="success" class="gl-mt-5" + data-testid="save_service_desk_settings_button" data-qa-selector="save_service_desk_settings_button" :disabled="isTemplateSaving" @click="onSaveTemplate" diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_template_dropdown.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_template_dropdown.vue new file mode 100644 index 00000000000..bdd9f940d79 --- /dev/null +++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_template_dropdown.vue @@ -0,0 +1,115 @@ +<script> +import fuzzaldrinPlus from 'fuzzaldrin-plus'; +import { GlDropdown, GlDropdownSectionHeader, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui'; +import { __ } from '~/locale'; + +export default { + components: { + GlDropdown, + GlDropdownSectionHeader, + GlDropdownItem, + GlSearchBoxByType, + }, + props: { + selectedTemplate: { + type: String, + required: false, + default: '', + }, + templates: { + type: Array, + required: true, + }, + selectedFileTemplateProjectId: { + type: Number, + required: false, + default: null, + }, + }, + data() { + return { + searchTerm: '', + }; + }, + computed: { + templateOptions() { + if (this.searchTerm) { + const filteredTemplates = []; + for (let i = 0; i < this.templates.length; i += 2) { + const sectionName = this.templates[i]; + const availableTemplates = this.templates[i + 1]; + + const matchedTemplates = fuzzaldrinPlus.filter(availableTemplates, this.searchTerm, { + key: 'name', + }); + + if (matchedTemplates.length > 0) { + filteredTemplates.push(sectionName, matchedTemplates); + } + } + + return filteredTemplates; + } + + return this.templates; + }, + }, + methods: { + templateClick(template) { + // Clicking on the same template should unselect it + if ( + template.name === this.selectedTemplate && + template.project_id === this.selectedFileTemplateProjectId + ) { + this.$emit('change', { + selectedFileTemplateProjectId: null, + selectedTemplate: null, + }); + return; + } + + this.$emit('change', { + selectedFileTemplateProjectId: template.project_id, + selectedTemplate: template.key, + }); + }, + }, + i18n: { + defaultDropdownText: __('Choose a template'), + }, +}; +</script> +<template> + <gl-dropdown + id="service-desk-template-select" + :text="selectedTemplate || $options.i18n.defaultDropdownText" + :header-text="$options.i18n.defaultDropdownText" + data-qa-selector="service_desk_template_dropdown" + :block="true" + class="service-desk-template-select" + toggle-class="gl-m-0" + > + <template #header> + <gl-search-box-by-type v-model.trim="searchTerm" /> + </template> + <template v-for="item in templateOptions"> + <gl-dropdown-section-header v-if="!Array.isArray(item)" :key="item"> + {{ item }} + </gl-dropdown-section-header> + <template v-else> + <gl-dropdown-item + v-for="template in item" + :key="template.key" + :is-check-item="true" + :is-checked=" + template.project_id === selectedFileTemplateProjectId && + template.name === selectedTemplate + " + @click="() => templateClick(template)" + > + {{ template.name }} + </gl-dropdown-item> + </template> + </template> + </gl-dropdown> +</template> diff --git a/app/assets/javascripts/projects/settings_service_desk/index.js b/app/assets/javascripts/projects/settings_service_desk/index.js index f842ffaaa2b..e14cdee17ce 100644 --- a/app/assets/javascripts/projects/settings_service_desk/index.js +++ b/app/assets/javascripts/projects/settings_service_desk/index.js @@ -18,6 +18,7 @@ export default () => { outgoingName, projectKey, selectedTemplate, + selectedFileTemplateProjectId, templates, } = el.dataset; @@ -32,6 +33,7 @@ export default () => { outgoingName, projectKey, selectedTemplate, + selectedFileTemplateProjectId: parseInt(selectedFileTemplateProjectId, 10) || null, templates: JSON.parse(templates), }, render: (createElement) => createElement(ServiceDeskRoot), diff --git a/app/assets/javascripts/projects/storage_counter/components/storage_table.vue b/app/assets/javascripts/projects/storage_counter/components/storage_table.vue index 7047fd925fb..a42a9711572 100644 --- a/app/assets/javascripts/projects/storage_counter/components/storage_table.vue +++ b/app/assets/javascripts/projects/storage_counter/components/storage_table.vue @@ -1,9 +1,10 @@ <script> -import { GlLink, GlIcon, GlTable, GlSprintf } from '@gitlab/ui'; +import { GlLink, GlIcon, GlTableLite as GlTable, GlSprintf } from '@gitlab/ui'; import { numberToHumanSize } from '~/lib/utils/number_utils'; import { thWidthClass } from '~/lib/utils/table_utility'; import { sprintf } from '~/locale'; import { PROJECT_TABLE_LABELS, HELP_LINK_ARIA_LABEL } from '../constants'; +import StorageTypeIcon from './storage_type_icon.vue'; export default { name: 'StorageTable', @@ -12,6 +13,7 @@ export default { GlIcon, GlTable, GlSprintf, + StorageTypeIcon, }, props: { storageTypes: { @@ -48,31 +50,39 @@ export default { <template> <gl-table :items="storageTypes" :fields="$options.projectTableFields"> <template #cell(storageType)="{ item }"> - <p class="gl-font-weight-bold gl-mb-0" :data-testid="`${item.storageType.id}-name`"> - {{ item.storageType.name }} - <gl-link - v-if="item.storageType.helpPath" - :href="item.storageType.helpPath" - target="_blank" - :aria-label="helpLinkAriaLabel(item.storageType.name)" - :data-testid="`${item.storageType.id}-help-link`" - > - <gl-icon name="question" :size="12" /> - </gl-link> - </p> - <p class="gl-mb-0" :data-testid="`${item.storageType.id}-description`"> - {{ item.storageType.description }} - </p> - <p v-if="item.storageType.warningMessage" class="gl-mb-0 gl-font-sm"> - <gl-icon name="warning" :size="12" /> - <gl-sprintf :message="item.storageType.warningMessage"> - <template #warningLink="{ content }"> - <gl-link :href="item.storageType.warningLink" target="_blank" class="gl-font-sm">{{ - content - }}</gl-link> - </template> - </gl-sprintf> - </p> + <div class="gl-display-flex gl-flex-direction-row"> + <storage-type-icon + :name="item.storageType.id" + :data-testid="`${item.storageType.id}-icon`" + /> + <div> + <p class="gl-font-weight-bold gl-mb-0" :data-testid="`${item.storageType.id}-name`"> + {{ item.storageType.name }} + <gl-link + v-if="item.storageType.helpPath" + :href="item.storageType.helpPath" + target="_blank" + :aria-label="helpLinkAriaLabel(item.storageType.name)" + :data-testid="`${item.storageType.id}-help-link`" + > + <gl-icon name="question" :size="12" /> + </gl-link> + </p> + <p class="gl-mb-0" :data-testid="`${item.storageType.id}-description`"> + {{ item.storageType.description }} + </p> + <p v-if="item.storageType.warningMessage" class="gl-mb-0 gl-font-sm"> + <gl-icon name="warning" :size="12" /> + <gl-sprintf :message="item.storageType.warningMessage"> + <template #warningLink="{ content }"> + <gl-link :href="item.storageType.warningLink" target="_blank" class="gl-font-sm">{{ + content + }}</gl-link> + </template> + </gl-sprintf> + </p> + </div> + </div> </template> </gl-table> </template> diff --git a/app/assets/javascripts/projects/storage_counter/components/storage_type_icon.vue b/app/assets/javascripts/projects/storage_counter/components/storage_type_icon.vue new file mode 100644 index 00000000000..bc7cd42df1e --- /dev/null +++ b/app/assets/javascripts/projects/storage_counter/components/storage_type_icon.vue @@ -0,0 +1,35 @@ +<script> +import { GlIcon } from '@gitlab/ui'; + +export default { + components: { GlIcon }, + props: { + name: { + type: String, + required: false, + default: '', + }, + }, + methods: { + iconName(storageTypeName) { + const defaultStorageTypeIcon = 'disk'; + const storageTypeIconMap = { + lfsObjectsSize: 'doc-image', + snippetsSize: 'snippet', + uploadsSize: 'upload', + repositorySize: 'infrastructure-registry', + packagesSize: 'package', + }; + + return storageTypeIconMap[`${storageTypeName}`] ?? defaultStorageTypeIcon; + }, + }, +}; +</script> +<template> + <span + class="gl-display-inline-flex gl-align-items-flex-start gl-justify-content-center gl-min-w-8 gl-pr-2 gl-pt-1" + > + <gl-icon :name="iconName(name)" :size="16" class="gl-mt-1" /> + </span> +</template> diff --git a/app/assets/javascripts/projects/storage_counter/constants.js b/app/assets/javascripts/projects/storage_counter/constants.js index d9b28abfbe7..df4b1800dff 100644 --- a/app/assets/javascripts/projects/storage_counter/constants.js +++ b/app/assets/javascripts/projects/storage_counter/constants.js @@ -6,13 +6,13 @@ export const PROJECT_STORAGE_TYPES = [ name: s__('UsageQuota|Artifacts'), description: s__('UsageQuota|Pipeline artifacts and job artifacts, created with CI/CD.'), warningMessage: s__( - 'UsageQuota|There is a known issue with Artifact storage where the total could be incorrect for some projects. More details and progress are available in %{warningLinkStart}the epic%{warningLinkEnd}.', + 'UsageQuota|Because of a known issue, the artifact total for some projects may be incorrect. For more details, read %{warningLinkStart}the epic%{warningLinkEnd}.', ), warningLink: 'https://gitlab.com/groups/gitlab-org/-/epics/5380', }, { id: 'lfsObjectsSize', - name: s__('UsageQuota|LFS Storage'), + name: s__('UsageQuota|LFS storage'), description: s__('UsageQuota|Audio samples, videos, datasets, and graphics.'), }, { @@ -23,7 +23,7 @@ export const PROJECT_STORAGE_TYPES = [ { id: 'repositorySize', name: s__('UsageQuota|Repository'), - description: s__('UsageQuota|Git repository, managed by the Gitaly service.'), + description: s__('UsageQuota|Git repository.'), }, { id: 'snippetsSize', @@ -51,11 +51,11 @@ export const ERROR_MESSAGE = s__( 'UsageQuota|Something went wrong while fetching project storage statistics', ); -export const LEARN_MORE_LABEL = s__('Learn more.'); +export const LEARN_MORE_LABEL = __('Learn more.'); export const USAGE_QUOTAS_LABEL = s__('UsageQuota|Usage Quotas'); export const HELP_LINK_ARIA_LABEL = s__('UsageQuota|%{linkTitle} help link'); export const TOTAL_USAGE_DEFAULT_TEXT = __('N/A'); -export const TOTAL_USAGE_TITLE = s__('UsageQuota|Usage Breakdown'); +export const TOTAL_USAGE_TITLE = s__('UsageQuota|Usage breakdown'); export const TOTAL_USAGE_SUBTITLE = s__( - 'UsageQuota|Includes project registry, artifacts, packages, wiki, uploads and other items.', + 'UsageQuota|Includes artifacts, repositories, wiki, uploads, and other items.', ); diff --git a/app/assets/javascripts/projects/storage_counter/index.js b/app/assets/javascripts/projects/storage_counter/index.js index 10668f08402..15796bc1870 100644 --- a/app/assets/javascripts/projects/storage_counter/index.js +++ b/app/assets/javascripts/projects/storage_counter/index.js @@ -25,7 +25,7 @@ export default (containerId = 'js-project-storage-count-app') => { } = el.dataset; const apolloProvider = new VueApollo({ - defaultClient: createDefaultClient({}, { assumeImmutableResults: true }), + defaultClient: createDefaultClient(), }); return new Vue({ diff --git a/app/assets/javascripts/projects/storage_counter/utils.js b/app/assets/javascripts/projects/storage_counter/utils.js index cb26603fff5..9fca9d88f46 100644 --- a/app/assets/javascripts/projects/storage_counter/utils.js +++ b/app/assets/javascripts/projects/storage_counter/utils.js @@ -14,10 +14,6 @@ export const parseGetProjectStorageResults = (data, helpLinks) => { } const { storageSize, ...storageStatistics } = projectStatistics; const storageTypes = PROJECT_STORAGE_TYPES.reduce((types, currentType) => { - if (!storageStatistics[currentType.id]) { - return types; - } - const helpPathKey = currentType.id.replace(`Size`, `HelpPagePath`); const helpPath = helpLinks[helpPathKey]; diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue index f6f409873c8..a79da00de43 100644 --- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue +++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue @@ -58,7 +58,7 @@ export default { }; this.isLoading = false; createFlash({ - message: s__('Something went wrong on our end'), + message: __('Something went wrong on our end'), }); }, initPolling() { diff --git a/app/assets/javascripts/projects/upload_file_experiment.js b/app/assets/javascripts/projects/upload_file.js index a7519f2bce8..597965eabfc 100644 --- a/app/assets/javascripts/projects/upload_file_experiment.js +++ b/app/assets/javascripts/projects/upload_file.js @@ -4,7 +4,7 @@ import createRouter from '~/repository/router'; import UploadButton from './details/upload_button.vue'; export const initUploadFileTrigger = () => { - const uploadFileTriggerEl = document.querySelector('.js-upload-file-experiment-trigger'); + const uploadFileTriggerEl = document.querySelector('.js-upload-file-trigger'); if (!uploadFileTriggerEl) return false; diff --git a/app/assets/javascripts/projects/upload_file_experiment_tracking.js b/app/assets/javascripts/projects/upload_file_experiment_tracking.js deleted file mode 100644 index c5e93f19b32..00000000000 --- a/app/assets/javascripts/projects/upload_file_experiment_tracking.js +++ /dev/null @@ -1,9 +0,0 @@ -import ExperimentTracking from '~/experimentation/experiment_tracking'; - -export const trackFileUploadEvent = (eventName) => { - const isEmpty = Boolean(document.querySelector('.project-home-panel.empty-project')); - const property = isEmpty ? 'empty' : 'nonempty'; - const label = 'blob-upload-modal'; - const FileUploadTracking = new ExperimentTracking('empty_repo_upload', { label, property }); - FileUploadTracking.event(eventName); -}; |