mirror of
https://github.com/discourse/discourse.git
synced 2025-03-23 06:15:41 +08:00
WIP: Experiemental Bulk Topic Actions Dropdown (#25245)
* WIP: Experiemental Bulk Topic Actions Dropdown Creates a new dropdown that can be used for topic bulk actions.
This commit is contained in:
parent
7b173e883f
commit
89883b2f51
@ -0,0 +1,160 @@
|
|||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { action } from "@ember/object";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
import { Promise } from "rsvp";
|
||||||
|
import DButton from "discourse/components/d-button";
|
||||||
|
import DModal from "discourse/components/d-modal";
|
||||||
|
import Topic from "discourse/models/topic";
|
||||||
|
import htmlSafe from "discourse-common/helpers/html-safe";
|
||||||
|
import i18n from "discourse-common/helpers/i18n";
|
||||||
|
//import AppendTags from "../bulk-actions/append-tags";
|
||||||
|
//import ChangeCategory from "../bulk-actions/change-category";
|
||||||
|
//import ChangeTags from "../bulk-actions/change-tags";
|
||||||
|
//import NotificationLevel from "../bulk-actions/notification-level";
|
||||||
|
|
||||||
|
export default class BulkTopicActions extends Component {
|
||||||
|
@service router;
|
||||||
|
|
||||||
|
async perform(operation) {
|
||||||
|
this.loading = true;
|
||||||
|
|
||||||
|
if (this.args.model.bulkSelectHelper.selected.length > 20) {
|
||||||
|
this.showProgress = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return this._processChunks(operation);
|
||||||
|
} catch {
|
||||||
|
this.dialog.alert(i18n.t("generic_error"));
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
this.processedTopicCount = 0;
|
||||||
|
this.showProgress = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_generateTopicChunks(allTopics) {
|
||||||
|
let startIndex = 0;
|
||||||
|
const chunkSize = 30;
|
||||||
|
const chunks = [];
|
||||||
|
|
||||||
|
while (startIndex < allTopics.length) {
|
||||||
|
chunks.push(allTopics.slice(startIndex, startIndex + chunkSize));
|
||||||
|
startIndex += chunkSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
_processChunks(operation) {
|
||||||
|
const allTopics = this.args.model.bulkSelectHelper.selected;
|
||||||
|
const topicChunks = this._generateTopicChunks(allTopics);
|
||||||
|
const topicIds = [];
|
||||||
|
const options = {};
|
||||||
|
|
||||||
|
if (this.args.model.allowSilent === true) {
|
||||||
|
options.silent = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tasks = topicChunks.map((topics) => async () => {
|
||||||
|
const result = await Topic.bulkOperation(topics, operation, options);
|
||||||
|
this.processedTopicCount += topics.length;
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const resolveNextTask = async () => {
|
||||||
|
if (tasks.length === 0) {
|
||||||
|
const topics = topicIds.map((id) => allTopics.findBy("id", id));
|
||||||
|
return resolve(topics);
|
||||||
|
}
|
||||||
|
|
||||||
|
const task = tasks.shift();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await task();
|
||||||
|
if (result?.topic_ids) {
|
||||||
|
topicIds.push(...result.topic_ids);
|
||||||
|
}
|
||||||
|
resolveNextTask();
|
||||||
|
} catch {
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
resolveNextTask();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
setComponent(component) {
|
||||||
|
this.activeComponent = component;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
performAction() {
|
||||||
|
switch (this.args.model.action) {
|
||||||
|
case "close":
|
||||||
|
this.forEachPerformed({ type: "close" }, (t) => t.set("closed", true));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async forEachPerformed(operation, cb) {
|
||||||
|
const topics = await this.perform(operation);
|
||||||
|
|
||||||
|
if (topics) {
|
||||||
|
topics.forEach(cb);
|
||||||
|
this.args.model.refreshClosure?.();
|
||||||
|
this.args.closeModal();
|
||||||
|
this.args.model.bulkSelectHelper.toggleBulkSelect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action
|
||||||
|
async performAndRefresh(operation) {
|
||||||
|
await this.perform(operation);
|
||||||
|
|
||||||
|
this.args.model.refreshClosure?.();
|
||||||
|
this.args.closeModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DModal
|
||||||
|
@title={{@model.title}}
|
||||||
|
@closeModal={{@closeModal}}
|
||||||
|
class="topic-bulk-actions-modal -large"
|
||||||
|
>
|
||||||
|
<:body>
|
||||||
|
<div>
|
||||||
|
{{htmlSafe (i18n "topics.bulk.selected" count=@model.topics.length)}}
|
||||||
|
</div>
|
||||||
|
</:body>
|
||||||
|
|
||||||
|
<:footer>
|
||||||
|
{{#if @model.allowSilent}}
|
||||||
|
<div class="topic-bulk-actions-options">
|
||||||
|
<label
|
||||||
|
for="topic-bulk-action-options__silent"
|
||||||
|
class="checkbox-label"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
class=""
|
||||||
|
id="topic-bulk-action-options__silent"
|
||||||
|
type="checkbox"
|
||||||
|
/>{{i18n "topics.bulk.silent"}}</label>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
<DButton
|
||||||
|
@action={{this.performAction}}
|
||||||
|
@icon="check"
|
||||||
|
@label="topics.bulk.confirm"
|
||||||
|
id="bulk-topics-confirm"
|
||||||
|
class="btn-primary"
|
||||||
|
/>
|
||||||
|
</:footer>
|
||||||
|
|
||||||
|
</DModal>
|
||||||
|
</template>
|
||||||
|
}
|
@ -196,6 +196,14 @@ export default class TopicBulkActions extends Component {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
|
||||||
|
if (this.args.model.initialAction === "set-component") {
|
||||||
|
this.setComponent(this.args.model.initialComponent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get buttons() {
|
get buttons() {
|
||||||
return [...this.defaultButtons, ..._customButtons].filter(({ visible }) => {
|
return [...this.defaultButtons, ..._customButtons].filter(({ visible }) => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
|
@ -12,6 +12,8 @@
|
|||||||
sortable=this.sortable
|
sortable=this.sortable
|
||||||
listTitle=this.listTitle
|
listTitle=this.listTitle
|
||||||
bulkSelectEnabled=this.bulkSelectEnabled
|
bulkSelectEnabled=this.bulkSelectEnabled
|
||||||
|
bulkSelectHelper=this.bulkSelectHelper
|
||||||
|
experimentalTopicBulkActionsEnabled=this.experimentalTopicBulkActionsEnabled
|
||||||
canDoBulkActions=this.canDoBulkActions
|
canDoBulkActions=this.canDoBulkActions
|
||||||
showTopicsAndRepliesToggle=this.showTopicsAndRepliesToggle
|
showTopicsAndRepliesToggle=this.showTopicsAndRepliesToggle
|
||||||
newListSubset=this.newListSubset
|
newListSubset=this.newListSubset
|
||||||
|
@ -48,6 +48,11 @@ export default Component.extend(LoadMore, {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@discourseComputed
|
||||||
|
experimentalTopicBulkActionsEnabled() {
|
||||||
|
return this.currentUser?.use_experimental_topic_bulk_actions;
|
||||||
|
},
|
||||||
|
|
||||||
@discourseComputed
|
@discourseComputed
|
||||||
sortable() {
|
sortable() {
|
||||||
return !!this.changeSort;
|
return !!this.changeSort;
|
||||||
|
@ -49,7 +49,7 @@ export default class BulkSelectHelper {
|
|||||||
this.router.currentRoute.queryParams["filter"]) === "tracked";
|
this.router.currentRoute.queryParams["filter"]) === "tracked";
|
||||||
|
|
||||||
const promise = this.selected.length
|
const promise = this.selected.length
|
||||||
? Topic.bulkOperation(this.selected, operation, isTracked)
|
? Topic.bulkOperation(this.selected, operation, {}, isTracked)
|
||||||
: Topic.bulkOperationByFilter("unread", operation, options, isTracked);
|
: Topic.bulkOperationByFilter("unread", operation, options, isTracked);
|
||||||
|
|
||||||
promise.then((result) => {
|
promise.then((result) => {
|
||||||
|
@ -806,13 +806,19 @@ Topic.reopenClass({
|
|||||||
return promise;
|
return promise;
|
||||||
},
|
},
|
||||||
|
|
||||||
bulkOperation(topics, operation, tracked) {
|
bulkOperation(topics, operation, options, tracked) {
|
||||||
const data = {
|
const data = {
|
||||||
topic_ids: topics.mapBy("id"),
|
topic_ids: topics.mapBy("id"),
|
||||||
operation,
|
operation,
|
||||||
tracked,
|
tracked,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (options) {
|
||||||
|
if (options.select) {
|
||||||
|
data.silent = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return ajax("/topics/bulk", {
|
return ajax("/topics/bulk", {
|
||||||
type: "PUT",
|
type: "PUT",
|
||||||
data,
|
data,
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
{{{view.html}}}
|
@ -1,12 +1,22 @@
|
|||||||
<th data-sort-order='{{order}}' class='{{view.className}} topic-list-data' scope="col" {{#if ariaLabel}}aria-label='{{ariaLabel}}'{{/if}} {{#if sortable}}tabindex="0" role="button" aria-pressed='{{view.ariaPressed}}' {{#if view.ariaSort}}aria-sort='{{view.ariaSort}}'{{/if}} {{/if}}>
|
<th data-sort-order='{{order}}' class='{{view.className}} topic-list-data' scope="col" {{#if ariaLabel}}aria-label='{{ariaLabel}}'{{/if}} {{#if sortable}}tabindex="0" role="button" aria-pressed='{{view.ariaPressed}}' {{#if view.ariaSort}}aria-sort='{{view.ariaSort}}'{{/if}} {{/if}}>
|
||||||
{{~#if canBulkSelect}}
|
{{~#if canBulkSelect}}
|
||||||
{{~#if showBulkToggle}}
|
{{~#if showBulkToggle}}
|
||||||
{{raw "flat-button" class="bulk-select" icon="list" title="topics.bulk.toggle"}}
|
{{~#if experimentalTopicBulkActionsEnabled }}
|
||||||
|
{{raw "flat-button" class="bulk-select" icon="tasks" title="topics.bulk.toggle"}}
|
||||||
|
{{else}}
|
||||||
|
{{raw "flat-button" class="bulk-select" icon="list" title="topics.bulk.toggle"}}
|
||||||
|
{{/if ~}}
|
||||||
{{/if ~}}
|
{{/if ~}}
|
||||||
{{~#if bulkSelectEnabled}}
|
{{~#if bulkSelectEnabled}}
|
||||||
<span class='bulk-select-topics'>
|
<span class='bulk-select-topics'>
|
||||||
{{~#if canDoBulkActions}}
|
{{~#if canDoBulkActions}}
|
||||||
<button class='btn btn-default btn-icon no-text bulk-select-actions'>{{d-icon "cog"}}​</button>
|
{{~#if experimentalTopicBulkActionsEnabled }}
|
||||||
|
{{raw "topic-bulk-select-dropdown" bulkSelectHelper=bulkSelectHelper}}
|
||||||
|
{{! Just showing both buttons for now for development}}
|
||||||
|
<button class='btn btn-icon no-text bulk-select-actions'>{{d-icon "cog"}}​</button>
|
||||||
|
{{else}}
|
||||||
|
<button class='btn btn-icon no-text bulk-select-actions'>{{d-icon "cog"}}​</button>
|
||||||
|
{{/if ~}}
|
||||||
{{/if ~}}
|
{{/if ~}}
|
||||||
<button class='btn btn-default bulk-select-all'>{{i18n "topics.bulk.select_all"}}</button>
|
<button class='btn btn-default bulk-select-all'>{{i18n "topics.bulk.select_all"}}</button>
|
||||||
<button class='btn btn-default bulk-clear-all'>{{i18n "topics.bulk.clear_all"}}</button>
|
<button class='btn btn-default bulk-clear-all'>{{i18n "topics.bulk.clear_all"}}</button>
|
||||||
|
@ -2,11 +2,15 @@
|
|||||||
{{#if bulkSelectEnabled}}
|
{{#if bulkSelectEnabled}}
|
||||||
<th class="bulk-select topic-list-data">
|
<th class="bulk-select topic-list-data">
|
||||||
{{#if canBulkSelect}}
|
{{#if canBulkSelect}}
|
||||||
{{raw "flat-button" class="bulk-select" icon="list" title="topics.bulk.toggle"}}
|
{{#if experimentalTopicBulkActionsEnabled }}
|
||||||
|
{{raw "flat-button" class="bulk-select" icon="tasks" title="topics.bulk.toggle"}}
|
||||||
|
{{else}}
|
||||||
|
{{raw "flat-button" class="bulk-select" icon="list" title="topics.bulk.toggle"}}
|
||||||
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</th>
|
</th>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{raw "topic-list-header-column" order='default' name=listTitle bulkSelectEnabled=bulkSelectEnabled showBulkToggle=toggleInTitle canBulkSelect=canBulkSelect canDoBulkActions=canDoBulkActions showTopicsAndRepliesToggle=showTopicsAndRepliesToggle newListSubset=newListSubset newRepliesCount=newRepliesCount newTopicsCount=newTopicsCount}}
|
{{raw "topic-list-header-column" order='default' name=listTitle bulkSelectEnabled=bulkSelectEnabled showBulkToggle=toggleInTitle canBulkSelect=canBulkSelect canDoBulkActions=canDoBulkActions showTopicsAndRepliesToggle=showTopicsAndRepliesToggle newListSubset=newListSubset newRepliesCount=newRepliesCount newTopicsCount=newTopicsCount experimentalTopicBulkActionsEnabled=experimentalTopicBulkActionsEnabled bulkSelectHelper=bulkSelectHelper }}
|
||||||
{{raw-plugin-outlet name="topic-list-header-after-main-link"}}
|
{{raw-plugin-outlet name="topic-list-header-after-main-link"}}
|
||||||
{{#if showPosters}}
|
{{#if showPosters}}
|
||||||
{{raw "topic-list-header-column" order='posters' ariaLabel=(i18n "category.sort_options.posters")}}
|
{{raw "topic-list-header-column" order='posters' ariaLabel=(i18n "category.sort_options.posters")}}
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
import EmberObject from "@ember/object";
|
||||||
|
import rawRenderGlimmer from "discourse/lib/raw-render-glimmer";
|
||||||
|
import BulkSelectTopicsDropdown from "select-kit/components/bulk-select-topics-dropdown";
|
||||||
|
|
||||||
|
export default class extends EmberObject {
|
||||||
|
get selectedCount() {
|
||||||
|
return this.bulkSelectHelper.selected.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
get html() {
|
||||||
|
return rawRenderGlimmer(
|
||||||
|
this,
|
||||||
|
"div.bulk-select-topics-dropdown",
|
||||||
|
<template>
|
||||||
|
<span>{{@data.selectedCount}} selected</span>
|
||||||
|
<BulkSelectTopicsDropdown
|
||||||
|
@bulkSelectHelper={{@data.bulkSelectHelper}}
|
||||||
|
/>
|
||||||
|
</template>,
|
||||||
|
{
|
||||||
|
bulkSelectHelper: this.bulkSelectHelper,
|
||||||
|
selectedCount: this.selectedCount,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
import { action } from "@ember/object";
|
||||||
|
import { inject as service } from "@ember/service";
|
||||||
|
import ChangeCategory from "discourse/components/bulk-actions/change-category";
|
||||||
|
import BulkTopicActions from "discourse/components/modal/bulk-topic-actions";
|
||||||
|
import TopicBulkActions from "discourse/components/modal/topic-bulk-actions";
|
||||||
|
import i18n from "discourse-common/helpers/i18n";
|
||||||
|
import DropdownSelectBoxComponent from "select-kit/components/dropdown-select-box";
|
||||||
|
|
||||||
|
export default DropdownSelectBoxComponent.extend({
|
||||||
|
classNames: ["bulk-select-topics-dropdown"],
|
||||||
|
headerIcon: null,
|
||||||
|
showFullTitle: true,
|
||||||
|
selectKitOptions: {
|
||||||
|
showCaret: true,
|
||||||
|
showFullTitle: true,
|
||||||
|
none: "select_kit.components.bulk_select_topics_dropdown.title",
|
||||||
|
},
|
||||||
|
|
||||||
|
modal: service(),
|
||||||
|
router: service(),
|
||||||
|
|
||||||
|
computeContent() {
|
||||||
|
let options = [];
|
||||||
|
options = options.concat([
|
||||||
|
{
|
||||||
|
id: "update-category",
|
||||||
|
icon: "pencil-alt",
|
||||||
|
name: i18n("topic_bulk_actions.update_category.name"),
|
||||||
|
description: i18n("topic_bulk_actions.update_category.description"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "close-topics",
|
||||||
|
icon: "lock",
|
||||||
|
name: i18n("topic_bulk_actions.close_topics.name"),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
return options;
|
||||||
|
},
|
||||||
|
|
||||||
|
@action
|
||||||
|
onSelect(id) {
|
||||||
|
switch (id) {
|
||||||
|
case "update-category":
|
||||||
|
// Temporary: just use the existing modal & action
|
||||||
|
this.modal.show(TopicBulkActions, {
|
||||||
|
model: {
|
||||||
|
topics: this.bulkSelectHelper.selected,
|
||||||
|
category: this.category,
|
||||||
|
refreshClosure: () => this.router.refresh(),
|
||||||
|
initialAction: "set-component",
|
||||||
|
initialComponent: ChangeCategory,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "close-topics":
|
||||||
|
this.modal.show(BulkTopicActions, {
|
||||||
|
model: {
|
||||||
|
action: "close",
|
||||||
|
title: i18n("topics.bulk.close_topics"),
|
||||||
|
bulkSelectHelper: this.bulkSelectHelper,
|
||||||
|
refreshClosure: () => this.router.refresh(),
|
||||||
|
allowSilent: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
@ -218,6 +218,20 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.d-modal.topic-bulk-actions-modal {
|
||||||
|
.d-modal {
|
||||||
|
&__container {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
#bulk-topics-confirm {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.d-modal.edit-slow-mode-modal {
|
.d-modal.edit-slow-mode-modal {
|
||||||
.slow-mode-label {
|
.slow-mode-label {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
@ -200,6 +200,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
border: solid white;
|
||||||
|
border-width: 0 3px 3px 0;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.down {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
-webkit-transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-select-actions {
|
||||||
|
}
|
||||||
|
|
||||||
.dismiss-container-top {
|
.dismiss-container-top {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
@ -66,7 +66,8 @@ class CurrentUserSerializer < BasicUserSerializer
|
|||||||
:sidebar_tags,
|
:sidebar_tags,
|
||||||
:sidebar_category_ids,
|
:sidebar_category_ids,
|
||||||
:sidebar_sections,
|
:sidebar_sections,
|
||||||
:new_new_view_enabled?
|
:new_new_view_enabled?,
|
||||||
|
:use_experimental_topic_bulk_actions?
|
||||||
|
|
||||||
delegate :user_stat, to: :object, private: true
|
delegate :user_stat, to: :object, private: true
|
||||||
delegate :any_posts, :draft_count, :pending_posts_count, :read_faq?, to: :user_stat
|
delegate :any_posts, :draft_count, :pending_posts_count, :read_faq?, to: :user_stat
|
||||||
@ -277,4 +278,8 @@ class CurrentUserSerializer < BasicUserSerializer
|
|||||||
def unseen_reviewable_count
|
def unseen_reviewable_count
|
||||||
Reviewable.unseen_reviewable_count(object)
|
Reviewable.unseen_reviewable_count(object)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def use_experimental_topic_bulk_actions?
|
||||||
|
scope.user.in_any_groups?(SiteSetting.experimental_topic_bulk_actions_enabled_groups_map)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
@ -2349,6 +2349,8 @@ en:
|
|||||||
filter_for_more: Filter for more…
|
filter_for_more: Filter for more…
|
||||||
categories_admin_dropdown:
|
categories_admin_dropdown:
|
||||||
title: "Manage categories"
|
title: "Manage categories"
|
||||||
|
bulk_select_topics_dropdown:
|
||||||
|
title: "Bulk Actions"
|
||||||
|
|
||||||
date_time_picker:
|
date_time_picker:
|
||||||
from: From
|
from: From
|
||||||
@ -2886,6 +2888,7 @@ en:
|
|||||||
topics:
|
topics:
|
||||||
new_messages_marker: "last visit"
|
new_messages_marker: "last visit"
|
||||||
bulk:
|
bulk:
|
||||||
|
confirm: "Confirm"
|
||||||
select_all: "Select All"
|
select_all: "Select All"
|
||||||
clear_all: "Clear All"
|
clear_all: "Clear All"
|
||||||
unlist_topics: "Unlist Topics"
|
unlist_topics: "Unlist Topics"
|
||||||
@ -2944,6 +2947,7 @@ en:
|
|||||||
progress:
|
progress:
|
||||||
one: "Progress: <strong>%{count}</strong> topic"
|
one: "Progress: <strong>%{count}</strong> topic"
|
||||||
other: "Progress: <strong>%{count}</strong> topics"
|
other: "Progress: <strong>%{count}</strong> topics"
|
||||||
|
silent: "Perform this action silently."
|
||||||
|
|
||||||
none:
|
none:
|
||||||
unread: "You have no unread topics."
|
unread: "You have no unread topics."
|
||||||
@ -2975,6 +2979,13 @@ en:
|
|||||||
bookmarks: "There are no more bookmarked topics."
|
bookmarks: "There are no more bookmarked topics."
|
||||||
filter: "There are no more topics."
|
filter: "There are no more topics."
|
||||||
|
|
||||||
|
topic_bulk_actions:
|
||||||
|
close_topics:
|
||||||
|
name: "Close Topics"
|
||||||
|
update_category:
|
||||||
|
name: "Update Category"
|
||||||
|
description: "Choose the new category for the selected topics"
|
||||||
|
|
||||||
topic:
|
topic:
|
||||||
filter_to:
|
filter_to:
|
||||||
one: "%{count} post in topic"
|
one: "%{count} post in topic"
|
||||||
|
@ -2355,6 +2355,13 @@ developer:
|
|||||||
default: ""
|
default: ""
|
||||||
client: true
|
client: true
|
||||||
hidden: true
|
hidden: true
|
||||||
|
experimental_topic_bulk_actions_enabled_groups:
|
||||||
|
default: ""
|
||||||
|
hidden: true
|
||||||
|
type: group_list
|
||||||
|
list_type: compact
|
||||||
|
allow_any: false
|
||||||
|
refresh: true
|
||||||
|
|
||||||
navigation:
|
navigation:
|
||||||
navigation_menu:
|
navigation_menu:
|
||||||
|
@ -30,6 +30,18 @@ module PageObjects
|
|||||||
page.has_no_css?(topic_list_item_class(topic))
|
page.has_no_css?(topic_list_item_class(topic))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def has_topic_checkbox?(topic)
|
||||||
|
page.has_css?("#{topic_list_item_class(topic)} input#bulk-select-#{topic.id}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_closed_status?(topic)
|
||||||
|
page.has_css?("#{topic_list_item_closed(topic)}")
|
||||||
|
end
|
||||||
|
|
||||||
|
def click_topic_checkbox(topic)
|
||||||
|
find("#{topic_list_item_class(topic)} input#bulk-select-#{topic.id}").click
|
||||||
|
end
|
||||||
|
|
||||||
def visit_topic_with_title(title)
|
def visit_topic_with_title(title)
|
||||||
find("#{TOPIC_LIST_BODY_SELECTOR} a", text: title).click
|
find("#{TOPIC_LIST_BODY_SELECTOR} a", text: title).click
|
||||||
end
|
end
|
||||||
@ -52,6 +64,10 @@ module PageObjects
|
|||||||
def topic_list_item_class(topic)
|
def topic_list_item_class(topic)
|
||||||
"#{TOPIC_LIST_ITEM_SELECTOR}[data-topic-id='#{topic.id}']"
|
"#{TOPIC_LIST_ITEM_SELECTOR}[data-topic-id='#{topic.id}']"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def topic_list_item_closed(topic)
|
||||||
|
"#{topic_list_item_class(topic)} .topic-statuses .topic-status svg.locked"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
56
spec/system/page_objects/components/topic_list_header.rb
Normal file
56
spec/system/page_objects/components/topic_list_header.rb
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module PageObjects
|
||||||
|
module Components
|
||||||
|
class TopicListHeader < PageObjects::Components::Base
|
||||||
|
TOPIC_LIST_HEADER_SELECTOR = ".topic-list .topic-list-header"
|
||||||
|
TOPIC_LIST_DATA_SELECTOR = "#{TOPIC_LIST_HEADER_SELECTOR} .topic-list-data"
|
||||||
|
|
||||||
|
def topic_list_header
|
||||||
|
TOPIC_LIST_HEADER_SELECTOR
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_bulk_select_button?
|
||||||
|
page.has_css?("#{TOPIC_LIST_HEADER_SELECTOR} button.bulk-select")
|
||||||
|
end
|
||||||
|
|
||||||
|
def click_bulk_select_button
|
||||||
|
find("#{TOPIC_LIST_HEADER_SELECTOR} button.bulk-select").click
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_bulk_select_topics_dropdown?
|
||||||
|
page.has_css?(
|
||||||
|
"#{TOPIC_LIST_HEADER_SELECTOR} .bulk-select-topics div.bulk-select-topics-dropdown",
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def click_bulk_select_topics_dropdown
|
||||||
|
find(
|
||||||
|
"#{TOPIC_LIST_HEADER_SELECTOR} .bulk-select-topics div.bulk-select-topics-dropdown",
|
||||||
|
).click
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_close_topics_button?
|
||||||
|
page.has_css?(bulk_select_dropdown_item("close-topics"))
|
||||||
|
end
|
||||||
|
|
||||||
|
def click_close_topics_button
|
||||||
|
find(bulk_select_dropdown_item("close-topics")).click
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_bulk_select_modal?
|
||||||
|
page.has_css?("#discourse-modal-title")
|
||||||
|
end
|
||||||
|
|
||||||
|
def click_bulk_topics_confirm
|
||||||
|
find("#bulk-topics-confirm").click
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def bulk_select_dropdown_item(name)
|
||||||
|
"#{TOPIC_LIST_HEADER_SELECTOR} .bulk-select-topics div.bulk-select-topics-dropdown li[data-value='#{name}']"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
41
spec/system/topic_bulk_select_spec.rb
Normal file
41
spec/system/topic_bulk_select_spec.rb
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
describe "Topic bulk select", type: :system do
|
||||||
|
before { SiteSetting.experimental_topic_bulk_actions_enabled_groups = "1" }
|
||||||
|
fab!(:topics) { Fabricate.times(10, :post).map(&:topic) }
|
||||||
|
let(:topic_list_header) { PageObjects::Components::TopicListHeader.new }
|
||||||
|
let(:topic_list) { PageObjects::Components::TopicList.new }
|
||||||
|
|
||||||
|
context "when in topic" do
|
||||||
|
fab!(:admin)
|
||||||
|
|
||||||
|
before { sign_in(admin) }
|
||||||
|
|
||||||
|
it "closes multiple topics" do
|
||||||
|
visit("/latest")
|
||||||
|
expect(page).to have_css(".topic-list button.bulk-select")
|
||||||
|
expect(topic_list_header).to have_bulk_select_button
|
||||||
|
|
||||||
|
# Click bulk select button
|
||||||
|
topic_list_header.click_bulk_select_button
|
||||||
|
expect(topic_list).to have_topic_checkbox(topics.first)
|
||||||
|
|
||||||
|
# Select Topics
|
||||||
|
topic_list.click_topic_checkbox(topics.first)
|
||||||
|
topic_list.click_topic_checkbox(topics.second)
|
||||||
|
|
||||||
|
# Has Dropdown
|
||||||
|
expect(topic_list_header).to have_bulk_select_topics_dropdown
|
||||||
|
topic_list_header.click_bulk_select_topics_dropdown
|
||||||
|
|
||||||
|
# Clicking the close button opens up the modal
|
||||||
|
expect(topic_list_header).to have_close_topics_button
|
||||||
|
topic_list_header.click_close_topics_button
|
||||||
|
expect(topic_list_header).to have_bulk_select_modal
|
||||||
|
|
||||||
|
# Closes the selected topics
|
||||||
|
topic_list_header.click_bulk_topics_confirm
|
||||||
|
expect(topic_list).to have_closed_status(topics.first)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
x
Reference in New Issue
Block a user