Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/admin/topics')
-rw-r--r--app/assets/javascripts/admin/topics/components/merge_topics.vue141
-rw-r--r--app/assets/javascripts/admin/topics/components/topic_select.vue106
-rw-r--r--app/assets/javascripts/admin/topics/index.js32
3 files changed, 278 insertions, 1 deletions
diff --git a/app/assets/javascripts/admin/topics/components/merge_topics.vue b/app/assets/javascripts/admin/topics/components/merge_topics.vue
new file mode 100644
index 00000000000..921b762bbef
--- /dev/null
+++ b/app/assets/javascripts/admin/topics/components/merge_topics.vue
@@ -0,0 +1,141 @@
+<script>
+import { GlAlert, GlButton, GlModal, GlModalDirective, GlSprintf } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import csrf from '~/lib/utils/csrf';
+import TopicSelect from './topic_select.vue';
+
+export default {
+ components: {
+ GlAlert,
+ GlButton,
+ GlModal,
+ GlSprintf,
+ TopicSelect,
+ },
+ directives: {
+ GlModal: GlModalDirective,
+ },
+ inject: ['path'],
+ data() {
+ return {
+ sourceTopic: {},
+ targetTopic: {},
+ };
+ },
+ computed: {
+ sourceTopicId() {
+ return getIdFromGraphQLId(this.sourceTopic?.id);
+ },
+ targetTopicId() {
+ return getIdFromGraphQLId(this.targetTopic?.id);
+ },
+ validSelectedTopics() {
+ return (
+ Object.keys(this.sourceTopic).length &&
+ Object.keys(this.targetTopic).length &&
+ this.sourceTopic !== this.targetTopic
+ );
+ },
+ actionPrimary() {
+ return {
+ text: __('Merge'),
+ attributes: {
+ variant: 'danger',
+ disabled: !this.validSelectedTopics,
+ },
+ };
+ },
+ },
+ methods: {
+ selectSourceTopic(topic) {
+ this.sourceTopic = topic;
+ },
+ selectTargetTopic(topic) {
+ this.targetTopic = topic;
+ },
+ mergeTopics() {
+ this.$refs.mergeForm.submit();
+ },
+ },
+ i18n: {
+ title: s__('MergeTopics|Merge topics'),
+ body: s__(
+ 'MergeTopics|Move all assigned projects from the source topic to the target topic and remove the source topic.',
+ ),
+ sourceTopic: s__('MergeTopics|Source topic'),
+ targetTopic: s__('MergeTopics|Target topic'),
+ warningTitle: s__('MergeTopics|Merging topics will cause the following:'),
+ warningBody: s__('MergeTopics|This action cannot be undone.'),
+ warningRemoveTopic: s__('MergeTopics|%{sourceTopic} will be removed'),
+ warningMoveProjects: s__('MergeTopics|All assigned projects will be moved to %{targetTopic}'),
+ },
+ modal: {
+ id: 'merge-topics',
+ actionSecondary: {
+ text: __('Cancel'),
+ attributes: {
+ variant: 'default',
+ },
+ },
+ },
+ csrf,
+};
+</script>
+<template>
+ <div class="gl-mr-3">
+ <gl-button v-gl-modal="$options.modal.id" category="secondary">{{
+ $options.i18n.title
+ }}</gl-button>
+ <gl-modal
+ :title="$options.i18n.title"
+ :action-primary="actionPrimary"
+ :action-secondary="$options.modal.actionSecondary"
+ :modal-id="$options.modal.id"
+ size="sm"
+ @primary="mergeTopics"
+ >
+ <p>{{ $options.i18n.body }}</p>
+ <topic-select
+ :selected-topic="sourceTopic"
+ :label-text="$options.i18n.sourceTopic"
+ @click="selectSourceTopic"
+ />
+ <topic-select
+ :selected-topic="targetTopic"
+ :label-text="$options.i18n.targetTopic"
+ @click="selectTargetTopic"
+ />
+ <gl-alert
+ v-if="validSelectedTopics"
+ :title="$options.i18n.warningTitle"
+ :dismissible="false"
+ variant="danger"
+ >
+ <ul>
+ <li>
+ <gl-sprintf :message="$options.i18n.warningRemoveTopic">
+ <template #sourceTopic>
+ <strong>{{ sourceTopic.name }}</strong>
+ </template>
+ </gl-sprintf>
+ </li>
+ <li>
+ <gl-sprintf :message="$options.i18n.warningMoveProjects">
+ <template #targetTopic>
+ <strong>{{ targetTopic.name }}</strong>
+ </template>
+ </gl-sprintf>
+ </li>
+ </ul>
+ {{ $options.i18n.warningBody }}
+ </gl-alert>
+ <form ref="mergeForm" method="post" :action="path">
+ <input type="hidden" name="_method" value="post" />
+ <input type="hidden" name="authenticity_token" :value="$options.csrf.token" />
+ <input type="hidden" name="source_topic_id" :value="sourceTopicId" />
+ <input type="hidden" name="target_topic_id" :value="targetTopicId" />
+ </form>
+ </gl-modal>
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/topics/components/topic_select.vue b/app/assets/javascripts/admin/topics/components/topic_select.vue
new file mode 100644
index 00000000000..8bf5be1afd1
--- /dev/null
+++ b/app/assets/javascripts/admin/topics/components/topic_select.vue
@@ -0,0 +1,106 @@
+<script>
+import {
+ GlAvatarLabeled,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownText,
+ GlSearchBoxByType,
+} from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
+import searchProjectTopics from '~/graphql_shared/queries/project_topics_search.query.graphql';
+
+export default {
+ components: {
+ GlAvatarLabeled,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownText,
+ GlSearchBoxByType,
+ },
+ props: {
+ selectedTopic: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ labelText: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ apollo: {
+ topics: {
+ query: searchProjectTopics,
+ variables() {
+ return {
+ search: this.search,
+ };
+ },
+ update(data) {
+ return data.topics?.nodes || [];
+ },
+ debounce: 250,
+ },
+ },
+ data() {
+ return {
+ topics: [],
+ search: '',
+ };
+ },
+ computed: {
+ loading() {
+ return this.$apollo.queries.topics.loading;
+ },
+ isResultEmpty() {
+ return this.topics.length === 0;
+ },
+ dropdownText() {
+ if (Object.keys(this.selectedTopic).length) {
+ return this.selectedTopic.name;
+ }
+
+ return this.$options.i18n.dropdownText;
+ },
+ },
+ methods: {
+ selectTopic(topic) {
+ this.$emit('click', topic);
+ },
+ },
+ i18n: {
+ dropdownText: s__('TopicSelect|Select a topic'),
+ searchPlaceholder: s__('TopicSelect|Search topics'),
+ emptySearchResult: s__('TopicSelect|No matching results'),
+ },
+ AVATAR_SHAPE_OPTION_RECT,
+};
+</script>
+
+<template>
+ <div>
+ <label v-if="labelText">{{ labelText }}</label>
+ <gl-dropdown block :text="dropdownText">
+ <gl-search-box-by-type
+ v-model="search"
+ :is-loading="loading"
+ :placeholder="$options.i18n.searchPlaceholder"
+ />
+ <gl-dropdown-item v-for="topic in topics" :key="topic.id" @click="selectTopic(topic)">
+ <gl-avatar-labeled
+ :label="topic.title"
+ :sub-label="topic.name"
+ :src="topic.avatarUrl"
+ :entity-name="topic.name"
+ :size="32"
+ :shape="$options.AVATAR_SHAPE_OPTION_RECT"
+ />
+ </gl-dropdown-item>
+ <gl-dropdown-text v-if="isResultEmpty && !loading">
+ <span class="gl-text-gray-500">{{ $options.i18n.emptySearchResult }}</span>
+ </gl-dropdown-text>
+ </gl-dropdown>
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/topics/index.js b/app/assets/javascripts/admin/topics/index.js
index 09e9b20f220..d81690e8f4c 100644
--- a/app/assets/javascripts/admin/topics/index.js
+++ b/app/assets/javascripts/admin/topics/index.js
@@ -1,7 +1,20 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import showToast from '~/vue_shared/plugins/global_toast';
import RemoveAvatar from './components/remove_avatar.vue';
+import MergeTopics from './components/merge_topics.vue';
-export default () => {
+const toasts = document.querySelectorAll('.js-toast-message');
+toasts.forEach((toast) => showToast(toast.dataset.message));
+
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
+export const initRemoveAvatar = () => {
const el = document.querySelector('.js-remove-topic-avatar');
if (!el) {
@@ -21,3 +34,20 @@ export default () => {
},
});
};
+
+export const initMergeTopics = () => {
+ const el = document.querySelector('.js-merge-topics');
+
+ if (!el) return false;
+
+ const { path } = el.dataset;
+
+ return new Vue({
+ el,
+ apolloProvider,
+ provide: { path },
+ render(createElement) {
+ return createElement(MergeTopics);
+ },
+ });
+};