summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/projects
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-11-18 13:16:36 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-11-18 13:16:36 +0000
commit311b0269b4eb9839fa63f80c8d7a58f32b8138a0 (patch)
tree07e7870bca8aed6d61fdcc810731c50d2c40af47 /app/assets/javascripts/projects
parent27909cef6c4170ed9205afa7426b8d3de47cbb0c (diff)
downloadgitlab-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')
-rw-r--r--app/assets/javascripts/projects/commit/components/form_modal.vue10
-rw-r--r--app/assets/javascripts/projects/commit/init_cherry_pick_commit_modal.js3
-rw-r--r--app/assets/javascripts/projects/commit/init_revert_commit_modal.js3
-rw-r--r--app/assets/javascripts/projects/commit_box/info/init_commit_pipeline_mini_graph.js7
-rw-r--r--app/assets/javascripts/projects/components/project_delete_button.vue58
-rw-r--r--app/assets/javascripts/projects/default_project_templates.js4
-rw-r--r--app/assets/javascripts/projects/details/upload_button.vue7
-rw-r--r--app/assets/javascripts/projects/new/components/app.vue2
-rw-r--r--app/assets/javascripts/projects/new/components/new_project_url_select.vue27
-rw-r--r--app/assets/javascripts/projects/new/index.js2
-rw-r--r--app/assets/javascripts/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql3
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/app.vue12
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/index.js4
-rw-r--r--app/assets/javascripts/projects/project_delete_button.js16
-rw-r--r--app/assets/javascripts/projects/project_new.js7
-rw-r--r--app/assets/javascripts/projects/settings/topics/components/topics_token_selector.vue92
-rw-r--r--app/assets/javascripts/projects/settings/topics/index.js51
-rw-r--r--app/assets/javascripts/projects/settings/topics/queries/project_topics_search.query.graphql9
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue7
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue60
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_template_dropdown.vue115
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/index.js2
-rw-r--r--app/assets/javascripts/projects/storage_counter/components/storage_table.vue62
-rw-r--r--app/assets/javascripts/projects/storage_counter/components/storage_type_icon.vue35
-rw-r--r--app/assets/javascripts/projects/storage_counter/constants.js12
-rw-r--r--app/assets/javascripts/projects/storage_counter/index.js2
-rw-r--r--app/assets/javascripts/projects/storage_counter/utils.js4
-rw-r--r--app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue2
-rw-r--r--app/assets/javascripts/projects/upload_file.js (renamed from app/assets/javascripts/projects/upload_file_experiment.js)2
-rw-r--r--app/assets/javascripts/projects/upload_file_experiment_tracking.js9
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);
-};