Открыть боковую панель
GitLab.org
Gitlab
Коммиты
c57c261a
Коммит
c57c261a
создал
Авг 19, 2022
по автору
Jonas Wälter
Зафиксировано автором
Heinrich Lee Yu
Авг 19, 2022
Просмотр файлов
Allow admins to merge topics [frontend]
Changelog: added
владелец
c5ea92bd
Изменения
12
Скрыть пробелы
Построчно
Рядом
app/assets/javascripts/admin/topics/components/merge_topics.vue
0 → 100644
Просмотр файла @
c57c261a
<
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>
app/assets/javascripts/admin/topics/components/topic_select.vue
0 → 100644
Просмотр файла @
c57c261a
<
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
>
app/assets/javascripts/admin/topics/index.js
Просмотр файла @
c57c261a
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
);
},
});
};
app/assets/javascripts/
projects/settings/topics
/queries/project_topics_search.query.graphql
→
app/assets/javascripts/
graphql_shared
/queries/project_topics_search.query.graphql
Просмотр файла @
c57c261a
Файл перемещен
app/assets/javascripts/pages/admin/topics/edit/index.js
Просмотр файла @
c57c261a
...
...
@@ -2,7 +2,7 @@ import $ from 'jquery';
import
GLForm
from
'
~/gl_form
'
;
import
initFilePickers
from
'
~/file_pickers
'
;
import
ZenMode
from
'
~/zen_mode
'
;
import
initRemoveAvatar
from
'
~/admin/topics
'
;
import
{
initRemoveAvatar
}
from
'
~/admin/topics
'
;
new
GLForm
(
$
(
'
.js-project-topic-form
'
));
// eslint-disable-line no-new
initFilePickers
();
...
...
app/assets/javascripts/pages/admin/topics/index.js
0 → 100644
Просмотр файла @
c57c261a
import
{
initMergeTopics
}
from
'
~/admin/topics
'
;
initMergeTopics
();
app/assets/javascripts/projects/settings/topics/components/topics_token_selector.vue
Просмотр файла @
c57c261a
...
...
@@ -2,7 +2,7 @@
import
{
GlTokenSelector
,
GlAvatarLabeled
}
from
'
@gitlab/ui
'
;
import
{
s__
}
from
'
~/locale
'
;
import
{
AVATAR_SHAPE_OPTION_RECT
}
from
'
~/vue_shared/constants
'
;
import
searchProjectTopics
from
'
..
/queries/project_topics_search.query.graphql
'
;
import
searchProjectTopics
from
'
~/graphql_shared
/queries/project_topics_search.query.graphql
'
;
export
default
{
components
:
{
...
...
app/controllers/admin/topics_controller.rb
Просмотр файла @
c57c261a
...
...
@@ -56,9 +56,8 @@ def merge
end
message
=
_
(
'Topic %{source_topic} was successfully merged into topic %{target_topic}.'
)
redirect_to
admin_topics_path
,
status: :found
,
notice:
message
%
{
source_topic:
source_topic
.
name
,
target_topic:
target_topic
.
name
}
flash
[
:toast
]
=
message
%
{
source_topic:
source_topic
.
name
,
target_topic:
target_topic
.
name
}
redirect_to
admin_topics_path
,
status: :found
end
private
...
...
app/views/admin/topics/index.html.haml
Просмотр файла @
c57c261a
-
page_title
_
(
"Topics"
)
=
form_tag
admin_topics_path
,
method: :get
do
|
f
|
.
gl-py-3.gl-display-flex.gl-flex-direction-column-reverse.gl-md-flex-direction-row.gl-border-b-solid.gl-border-gray-100.gl-border-
b-
1
.gl-flex-grow-1.gl-mt-3.gl-md-mt-0
.inline.gl-w-full.gl-md-w-auto
-
search
=
params
.
fetch
(
:search
,
nil
)
.
search
-
field
-holder
=
s
earch_field_tag
:search
,
search
,
class:
"form-control gl-form-input search-text-input js-search-input"
,
autofocus:
true
,
spellcheck:
false
,
placeholder:
_
(
'Search by name'
),
data:
{
qa_selector:
'topic_search_field'
}
=
sprite_icon
(
'search'
,
css_class:
'search-icon'
)
.
nav-controls
=
link_to
new_admin_topic_path
,
class:
"gl-button btn btn-confirm gl-w-full gl-md-w-auto"
do
=
_
(
'New topic'
)
.top-area
.
nav-controls.gl-w-full.gl-mt-3.gl-m
b-
3
=
form_tag
admin_topics_path
,
method: :get
do
|
f
|
-
search
=
params
.
fetch
(
:search
,
nil
)
.
search
-field-holder
=
search
_
field
_tag
:search
,
search
,
class:
"form-control gl-form-input search-text-input js-search-input"
,
autofocus:
true
,
spellcheck:
false
,
placeholder:
_
(
'Search by name'
),
data:
{
qa_selector:
'topic_search_field'
}
=
s
prite_icon
(
'
search
'
,
css_
class:
'search-icon'
)
.gl-flex-grow-1
.
js-merge-topics
{
data:
{
path:
merge_admin_topics_path
}
}
=
link_to
new_admin_topic_path
,
class:
"gl-button btn btn-confirm gl-w-full gl-md-w-auto"
do
=
_
(
'New topic'
)
%ul
.content-list
=
render
partial:
'topic'
,
collection:
@topics
...
...
doc/user/admin_area/index.md
Просмотр файла @
c57c261a
...
...
@@ -288,6 +288,8 @@ To edit a topic, select **Edit** in that topic's row.
To remove a topic, select
**Remove**
in that topic's row.
To remove a topic and move all assigned projects to another topic, select
**Merge topics**
.
To search for topics by name, enter your criteria in the search box. The topic search is case
insensitive and applies partial matching.
...
...
locale/gitlab.pot
Просмотр файла @
c57c261a
...
...
@@ -24887,6 +24887,30 @@ msgstr ""
msgid "MergeRequest|Search (e.g. *.vue) (%{modifier_key}P)"
msgstr ""
msgid "MergeTopics|%{sourceTopic} will be removed"
msgstr ""
msgid "MergeTopics|All assigned projects will be moved to %{targetTopic}"
msgstr ""
msgid "MergeTopics|Merge topics"
msgstr ""
msgid "MergeTopics|Merging topics will cause the following:"
msgstr ""
msgid "MergeTopics|Move all assigned projects from the source topic to the target topic and remove the source topic."
msgstr ""
msgid "MergeTopics|Source topic"
msgstr ""
msgid "MergeTopics|Target topic"
msgstr ""
msgid "MergeTopics|This action cannot be undone."
msgstr ""
msgid "Merged"
msgstr ""
...
...
@@ -41086,6 +41110,15 @@ msgstr ""
msgid "Topic was successfully updated."
msgstr ""
msgid "TopicSelect|No matching results"
msgstr ""
msgid "TopicSelect|Search topics"
msgstr ""
msgid "TopicSelect|Select a topic"
msgstr ""
msgid "Topics"
msgstr ""
spec/frontend/admin/topics/components/topic_select_spec.js
0 → 100644
Просмотр файла @
c57c261a
import
{
GlAvatarLabeled
,
GlDropdown
,
GlDropdownItem
}
from
'
@gitlab/ui
'
;
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
TopicSelect
from
'
~/admin/topics/components/topic_select.vue
'
;
const
mockTopics
=
[
{
id
:
1
,
name
:
'
topic1
'
,
title
:
'
Topic 1
'
,
avatarUrl
:
'
avatar.com/topic1.png
'
},
{
id
:
2
,
name
:
'
GitLab
'
,
title
:
'
GitLab
'
,
avatarUrl
:
'
avatar.com/GitLab.png
'
},
];
describe
(
'
TopicSelect
'
,
()
=>
{
let
wrapper
;
const
findDropdown
=
()
=>
wrapper
.
findComponent
(
GlDropdown
);
const
findAllDropdownItems
=
()
=>
wrapper
.
findAllComponents
(
GlDropdownItem
);
function
createComponent
(
props
=
{})
{
wrapper
=
shallowMount
(
TopicSelect
,
{
propsData
:
props
,
data
()
{
return
{
topics
:
mockTopics
,
search
:
''
,
};
},
mocks
:
{
$apollo
:
{
queries
:
{
topics
:
{
loading
:
false
},
},
},
},
});
}
afterEach
(()
=>
{
wrapper
.
destroy
();
});
it
(
'
mounts
'
,
()
=>
{
createComponent
();
expect
(
wrapper
.
exists
()).
toBe
(
true
);
});
it
(
'
`selectedTopic` prop defaults to `{}`
'
,
()
=>
{
createComponent
();
expect
(
wrapper
.
props
(
'
selectedTopic
'
)).
toEqual
({});
});
it
(
'
`labelText` prop defaults to `null`
'
,
()
=>
{
createComponent
();
expect
(
wrapper
.
props
(
'
labelText
'
)).
toBe
(
null
);
});
it
(
'
renders default text if no selected topic
'
,
()
=>
{
createComponent
();
expect
(
findDropdown
().
props
(
'
text
'
)).
toBe
(
'
Select a topic
'
);
});
it
(
'
renders selected topic
'
,
()
=>
{
createComponent
({
selectedTopic
:
mockTopics
[
0
]
});
expect
(
findDropdown
().
props
(
'
text
'
)).
toBe
(
'
topic1
'
);
});
it
(
'
renders label
'
,
()
=>
{
createComponent
({
labelText
:
'
my label
'
});
expect
(
wrapper
.
find
(
'
label
'
).
text
()).
toBe
(
'
my label
'
);
});
it
(
'
renders dropdown items
'
,
()
=>
{
createComponent
();
const
dropdownItems
=
findAllDropdownItems
();
expect
(
dropdownItems
.
at
(
0
).
find
(
GlAvatarLabeled
).
props
(
'
label
'
)).
toBe
(
'
Topic 1
'
);
expect
(
dropdownItems
.
at
(
1
).
find
(
GlAvatarLabeled
).
props
(
'
label
'
)).
toBe
(
'
GitLab
'
);
});
it
(
'
emits `click` event when topic selected
'
,
()
=>
{
createComponent
();
findAllDropdownItems
().
at
(
0
).
vm
.
$emit
(
'
click
'
);
expect
(
wrapper
.
emitted
(
'
click
'
)).
toEqual
([[
mockTopics
[
0
]]]);
});
});
Редактирование
Предварительный просмотр
Поддерживает Markdown
0%
Попробовать снова
или
прикрепить новый файл
.
Отмена
You are about to add
0
people
to the discussion. Proceed with caution.
Сначала завершите редактирование этого сообщения!
Отмена
Пожалуйста,
зарегистрируйтесь
или
войдите
чтобы прокомментировать