mirror of
https://github.com/discourse/discourse.git
synced 2025-02-20 13:49:29 +08:00
FEATURE: delete multiple inactive themes/components (#23788)
Ability to select multiple inactive themes or components and delete them all together
This commit is contained in:
parent
60e624e768
commit
e94b553e9a
|
@ -8,6 +8,13 @@
|
|||
</span>
|
||||
|
||||
<div class="info">
|
||||
{{#if @selectInactiveMode}}
|
||||
<Input
|
||||
@checked={{this.theme.markedToDelete}}
|
||||
id={{this.theme.id}}
|
||||
@type="checkbox"
|
||||
/>
|
||||
{{/if}}
|
||||
<span class="name">
|
||||
{{this.theme.name}}
|
||||
</span>
|
||||
|
|
|
@ -41,11 +41,61 @@
|
|||
{{#if this.hasInactiveThemes}}
|
||||
<div class="themes-list-item inactive-indicator">
|
||||
<span class="empty">
|
||||
{{#if this.themesTabActive}}
|
||||
{{i18n "admin.customize.theme.inactive_themes"}}
|
||||
{{else}}
|
||||
{{i18n "admin.customize.theme.inactive_components"}}
|
||||
{{/if}}
|
||||
<div class="info">
|
||||
{{#if this.selectInactiveMode}}
|
||||
<Input
|
||||
@type="checkbox"
|
||||
@checked={{(or
|
||||
(eq this.allInactiveSelected true)
|
||||
(eq this.someInactiveSelected true)
|
||||
)}}
|
||||
class="toggle-all-inactive"
|
||||
indeterminate={{this.someInactiveSelected}}
|
||||
{{on "click" this.toggleAllInactive}}
|
||||
/>
|
||||
{{else}}
|
||||
<DButton
|
||||
class="btn-flat select-inactive-mode"
|
||||
@action={{this.toggleInactiveMode}}
|
||||
>
|
||||
{{d-icon "list"}}
|
||||
</DButton>
|
||||
{{/if}}
|
||||
{{#if this.selectInactiveMode}}
|
||||
<span class="select-inactive-mode-label">
|
||||
{{i18n
|
||||
"admin.customize.theme.selected"
|
||||
count=this.selectedCount
|
||||
}}
|
||||
</span>
|
||||
{{else if this.themesTabActive}}
|
||||
<span class="header">
|
||||
{{i18n "admin.customize.theme.inactive_themes"}}
|
||||
</span>
|
||||
{{else}}
|
||||
<span class="header">
|
||||
{{i18n "admin.customize.theme.inactive_components"}}
|
||||
</span>
|
||||
{{/if}}
|
||||
|
||||
{{#if this.selectInactiveMode}}
|
||||
<a
|
||||
href
|
||||
{{on "click" this.toggleInactiveMode}}
|
||||
class="cancel-select-inactive-mode"
|
||||
>
|
||||
{{i18n "admin.customize.theme.cancel"}}
|
||||
</a>
|
||||
<DButton
|
||||
class="btn btn-delete"
|
||||
@action={{this.deleteConfirmation}}
|
||||
@disabled={{(eq this.selectedCount 0)}}
|
||||
>
|
||||
{{d-icon "trash-alt"}}
|
||||
Delete
|
||||
</DButton>
|
||||
{{/if}}
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
@ -57,6 +107,7 @@
|
|||
@classNames="inactive-theme"
|
||||
@theme={{theme}}
|
||||
@navigateToTheme={{action "navigateToTheme" theme}}
|
||||
@selectInactiveMode={{this.selectInactiveMode}}
|
||||
/>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
|
|
@ -3,16 +3,19 @@ import { inject as service } from "@ember/service";
|
|||
import { equal, gt, gte } from "@ember/object/computed";
|
||||
import { COMPONENTS, THEMES } from "admin/models/theme";
|
||||
import Component from "@ember/component";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import discourseComputed, { bind } from "discourse-common/utils/decorators";
|
||||
import { action } from "@ember/object";
|
||||
import DeleteThemesConfirm from "discourse/components/modal/delete-themes-confirm";
|
||||
|
||||
@classNames("themes-list")
|
||||
export default class ThemesList extends Component {
|
||||
@service router;
|
||||
@service modal;
|
||||
|
||||
THEMES = THEMES;
|
||||
COMPONENTS = COMPONENTS;
|
||||
filterTerm = null;
|
||||
selectInactiveMode = false;
|
||||
|
||||
@gt("themesList.length", 0) hasThemes;
|
||||
|
||||
|
@ -40,6 +43,7 @@ export default class ThemesList extends Component {
|
|||
"currentTab",
|
||||
"themesList.@each.user_selectable",
|
||||
"themesList.@each.default",
|
||||
"themesList.@each.markedToDelete",
|
||||
"filterTerm"
|
||||
)
|
||||
inactiveThemes(themes) {
|
||||
|
@ -56,6 +60,16 @@ export default class ThemesList extends Component {
|
|||
return this._filterThemes(results, this.filterTerm);
|
||||
}
|
||||
|
||||
@discourseComputed("themesList.@each.markedToDelete")
|
||||
selectedThemesOrComponents() {
|
||||
return this.themesList.filter((theme) => theme.markedToDelete);
|
||||
}
|
||||
|
||||
@discourseComputed("themesList.@each.markedToDelete")
|
||||
selectedCount() {
|
||||
return this.selectedThemesOrComponents.length;
|
||||
}
|
||||
|
||||
@discourseComputed(
|
||||
"themesList",
|
||||
"currentTab",
|
||||
|
@ -84,6 +98,18 @@ export default class ThemesList extends Component {
|
|||
}
|
||||
return this._filterThemes(results, this.filterTerm);
|
||||
}
|
||||
@discourseComputed("themesList.@each.markedToDelete")
|
||||
someInactiveSelected() {
|
||||
return (
|
||||
this.selectedCount > 0 &&
|
||||
this.selectedCount !== this.inactiveThemes.length
|
||||
);
|
||||
}
|
||||
|
||||
@discourseComputed("themesList.@each.markedToDelete")
|
||||
allInactiveSelected() {
|
||||
return this.selectedCount === this.inactiveThemes.length;
|
||||
}
|
||||
|
||||
_filterThemes(themes, term) {
|
||||
term = term?.trim()?.toLowerCase();
|
||||
|
@ -93,9 +119,17 @@ export default class ThemesList extends Component {
|
|||
return themes.filter(({ name }) => name.toLowerCase().includes(term));
|
||||
}
|
||||
|
||||
@bind
|
||||
toggleInactiveMode(event) {
|
||||
event?.preventDefault();
|
||||
this.inactiveThemes.forEach((theme) => theme.set("markedToDelete", false));
|
||||
this.toggleProperty("selectInactiveMode");
|
||||
}
|
||||
|
||||
@action
|
||||
changeView(newTab) {
|
||||
if (newTab !== this.currentTab) {
|
||||
this.set("selectInactiveMode", false);
|
||||
this.set("currentTab", newTab);
|
||||
if (!this.showFilter) {
|
||||
this.set("filterTerm", null);
|
||||
|
@ -107,4 +141,41 @@ export default class ThemesList extends Component {
|
|||
navigateToTheme(theme) {
|
||||
this.router.transitionTo("adminCustomizeThemes.show", theme);
|
||||
}
|
||||
|
||||
@action
|
||||
toggleAllInactive() {
|
||||
const markedToDelete = this.selectedCount === 0;
|
||||
this.inactiveThemes.forEach((theme) =>
|
||||
theme.set("markedToDelete", markedToDelete)
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
deleteConfirmation() {
|
||||
this.modal.show(DeleteThemesConfirm, {
|
||||
model: {
|
||||
selectedThemesOrComponents: this.selectedThemesOrComponents,
|
||||
type: this.themesTabActive ? "themes" : "components",
|
||||
refreshAfterDelete: () => {
|
||||
this.set("selectInactiveMode", false);
|
||||
if (this.themesTabActive) {
|
||||
this.set(
|
||||
"themes",
|
||||
this.themes.filter(
|
||||
(theme) => !this.selectedThemesOrComponents.includes(theme)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
this.set(
|
||||
"components",
|
||||
this.components.filter(
|
||||
(component) =>
|
||||
!this.selectedThemesOrComponents.includes(component)
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
<DModal
|
||||
@closeModal={{@closeModal}}
|
||||
@title={{i18n "admin.customize.bulk_delete"}}
|
||||
>
|
||||
<:body>
|
||||
{{i18n (concat "admin.customize.bulk_" @model.type "_delete_confirm")}}
|
||||
<ul>
|
||||
{{#each @model.selectedThemesOrComponents as |theme|}}
|
||||
<li>{{theme.name}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
|
||||
</:body>
|
||||
<:footer>
|
||||
<DButton class="btn-primary" @action={{this.delete}} @label="yes_value" />
|
||||
<DModalCancel @close={{@closeModal}} />
|
||||
</:footer>
|
||||
</DModal>
|
|
@ -0,0 +1,21 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { action } from "@ember/object";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
|
||||
export default class DeleteThemesConfirmComponent extends Component {
|
||||
@action
|
||||
delete() {
|
||||
ajax(`/admin/themes/bulk_destroy.json`, {
|
||||
type: "DELETE",
|
||||
data: {
|
||||
theme_ids: this.args.model.selectedThemesOrComponents.mapBy("id"),
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.args.model.refreshAfterDelete();
|
||||
this.args.closeModal();
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
}
|
||||
}
|
|
@ -266,10 +266,14 @@
|
|||
border-bottom: 1px solid var(--primary-low);
|
||||
display: flex;
|
||||
|
||||
.select-inactive-mode-label {
|
||||
color: var(--tertiary);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&.inactive-theme {
|
||||
color: var(--primary-high);
|
||||
background: var(--primary-very-low);
|
||||
font-size: var(--font-down-1);
|
||||
&:not(.selected):hover {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
@ -285,6 +289,11 @@
|
|||
padding-left: 0.33em;
|
||||
padding-top: 1em;
|
||||
}
|
||||
.btn.select-inactive-mode {
|
||||
padding-left: 0;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
&:not(.inactive-indicator):not(.selected):hover {
|
||||
background-color: var(--primary-very-low);
|
||||
|
@ -319,11 +328,25 @@
|
|||
.info {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
font-size: var(--font-up-1);
|
||||
|
||||
align-items: center;
|
||||
height: 2em;
|
||||
.icons {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.cancel-select-inactive-mode {
|
||||
margin-left: auto;
|
||||
}
|
||||
.btn-delete {
|
||||
font-size: var(--font-down-1);
|
||||
margin-left: 0.5em;
|
||||
svg {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
}
|
||||
input {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.components-list {
|
||||
|
|
|
@ -265,6 +265,18 @@ class Admin::ThemesController < Admin::AdminController
|
|||
respond_to { |format| format.json { head :no_content } }
|
||||
end
|
||||
|
||||
def bulk_destroy
|
||||
themes = Theme.where(id: params[:theme_ids])
|
||||
raise Discourse::InvalidParameters.new(:id) unless themes.present?
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
themes.each { |theme| StaffActionLogger.new(current_user).log_theme_destroy(theme) }
|
||||
themes.destroy_all
|
||||
end
|
||||
|
||||
respond_to { |format| format.json { head :no_content } }
|
||||
end
|
||||
|
||||
def show
|
||||
@theme = Theme.include_relations.find_by(id: params[:id])
|
||||
raise Discourse::InvalidParameters.new(:id) unless @theme
|
||||
|
|
|
@ -5112,6 +5112,9 @@ en:
|
|||
install: "Install"
|
||||
delete: "Delete"
|
||||
delete_confirm: 'Are you sure you want to delete "%{theme_name}"?'
|
||||
bulk_delete: 'Are you sure?'
|
||||
bulk_themes_delete_confirm: "This will uninstall the following themes, they will no longer be useable by any users on your site:"
|
||||
bulk_components_delete_confirm: "This will uninstall the following components, they will no longer be useable by any users on your site:"
|
||||
color: "Color"
|
||||
opacity: "Opacity"
|
||||
copy: "Duplicate"
|
||||
|
@ -5182,6 +5185,8 @@ en:
|
|||
convert_theme_tooltip: "Convert this theme to component"
|
||||
inactive_themes: "Inactive themes:"
|
||||
inactive_components: "Unused components:"
|
||||
selected: "%{count} selected"
|
||||
cancel: "Cancel"
|
||||
broken_theme_tooltip: "This theme has errors in its CSS, HTML or YAML"
|
||||
disabled_component_tooltip: "This component has been disabled"
|
||||
default_theme_tooltip: "This theme is the site's default theme"
|
||||
|
|
|
@ -230,6 +230,7 @@ Discourse::Application.routes.draw do
|
|||
post "import" => "themes#import"
|
||||
post "upload_asset" => "themes#upload_asset"
|
||||
post "generate_key_pair" => "themes#generate_key_pair"
|
||||
delete "bulk_destroy" => "themes#bulk_destroy"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1052,4 +1052,23 @@ RSpec.describe Admin::ThemesController do
|
|||
include_examples "theme update not allowed"
|
||||
end
|
||||
end
|
||||
|
||||
describe "#bulk_destroy" do
|
||||
fab!(:theme) { Fabricate(:theme, name: "Awesome Theme") }
|
||||
fab!(:theme_2) { Fabricate(:theme, name: "Another awesome Theme") }
|
||||
let(:theme_ids) { [theme.id, theme_2.id] }
|
||||
|
||||
before { sign_in(admin) }
|
||||
|
||||
it "destroys all selected the themes" do
|
||||
expect do
|
||||
delete "/admin/themes/bulk_destroy.json", params: { theme_ids: theme_ids }
|
||||
end.to change { Theme.count }.by(-2)
|
||||
end
|
||||
|
||||
it "logs the theme destroy action for each theme" do
|
||||
StaffActionLogger.any_instance.expects(:log_theme_destroy).twice
|
||||
delete "/admin/themes/bulk_destroy.json", params: { theme_ids: theme_ids }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,6 +7,41 @@ describe "Admin Customize Themes", type: :system do
|
|||
|
||||
before { sign_in(admin) }
|
||||
|
||||
describe "when visiting the page to customize themes" do
|
||||
fab!(:theme_2) { Fabricate(:theme) }
|
||||
fab!(:theme_3) { Fabricate(:theme) }
|
||||
let(:admin_customize_themes_page) { PageObjects::Pages::AdminCustomizeThemes.new }
|
||||
let(:delete_themes_confirm_modal) { PageObjects::Modals::DeleteThemesConfirm.new }
|
||||
|
||||
it "should allow admin to bulk delete inactive themes" do
|
||||
visit("/admin/customize/themes")
|
||||
|
||||
expect(admin_customize_themes_page).to have_inactive_themes
|
||||
|
||||
admin_customize_themes_page.click_select_inactive_mode
|
||||
expect(admin_customize_themes_page).to have_inactive_themes_selected(count: 0)
|
||||
admin_customize_themes_page.toggle_all_inactive
|
||||
expect(admin_customize_themes_page).to have_inactive_themes_selected(count: 3)
|
||||
|
||||
admin_customize_themes_page.cancel_select_inactive_mode
|
||||
expect(admin_customize_themes_page).to have_select_inactive_mode_button
|
||||
|
||||
admin_customize_themes_page.click_select_inactive_mode
|
||||
expect(admin_customize_themes_page).to have_disabled_delete_theme_button
|
||||
|
||||
admin_customize_themes_page.toggle_all_inactive
|
||||
|
||||
admin_customize_themes_page.click_delete_themes_button
|
||||
|
||||
expect(delete_themes_confirm_modal).to have_theme("Cool theme 1")
|
||||
expect(delete_themes_confirm_modal).to have_theme("Cool theme 2")
|
||||
expect(delete_themes_confirm_modal).to have_theme("Cool theme 3")
|
||||
delete_themes_confirm_modal.confirm
|
||||
|
||||
expect(admin_customize_themes_page).to have_no_inactive_themes
|
||||
end
|
||||
end
|
||||
|
||||
describe "when visiting the page to customize the theme" do
|
||||
it "should allow admin to update the color scheme of the theme" do
|
||||
visit("/admin/customize/themes/#{theme.id}")
|
||||
|
|
15
spec/system/page_objects/modals/delete_themes_confirm.rb
Normal file
15
spec/system/page_objects/modals/delete_themes_confirm.rb
Normal file
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module PageObjects
|
||||
module Modals
|
||||
class DeleteThemesConfirm < PageObjects::Pages::Base
|
||||
def has_theme?(name)
|
||||
has_css?(".modal li", text: name)
|
||||
end
|
||||
|
||||
def confirm
|
||||
find(".modal-footer .btn-primary").click
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
43
spec/system/page_objects/pages/admin_customize_themes.rb
Normal file
43
spec/system/page_objects/pages/admin_customize_themes.rb
Normal file
|
@ -0,0 +1,43 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module PageObjects
|
||||
module Pages
|
||||
class AdminCustomizeThemes < PageObjects::Pages::Base
|
||||
def has_inactive_themes?
|
||||
has_css?(".inactive-indicator")
|
||||
end
|
||||
|
||||
def has_no_inactive_themes?
|
||||
has_no_css?(".inactive-indicator")
|
||||
end
|
||||
|
||||
def has_select_inactive_mode_button?
|
||||
has_css?(".select-inactive-mode")
|
||||
end
|
||||
|
||||
def click_select_inactive_mode
|
||||
find(".select-inactive-mode").click
|
||||
end
|
||||
|
||||
def cancel_select_inactive_mode
|
||||
find(".cancel-select-inactive-mode").click
|
||||
end
|
||||
|
||||
def has_inactive_themes_selected?(count:)
|
||||
has_css?(".inactive-theme input:checked", count: count)
|
||||
end
|
||||
|
||||
def toggle_all_inactive
|
||||
find(".toggle-all-inactive").click
|
||||
end
|
||||
|
||||
def has_disabled_delete_theme_button?
|
||||
find_button("Delete", disabled: true)
|
||||
end
|
||||
|
||||
def click_delete_themes_button
|
||||
find(".btn-delete").click
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue
Block a user