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:
Krzysztof Kotlarek 2023-10-09 08:35:53 +11:00 committed by GitHub
parent 60e624e768
commit e94b553e9a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 330 additions and 9 deletions

View File

@ -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>

View File

@ -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}}

View File

@ -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)
)
);
}
},
},
});
}
}

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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 {

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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}")

View 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

View 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