UX: Tweaks to the theme/component pages when using admin sidebar (#30953)

There are a number of minor changes in this commit :

1. Combine the "Themes" and "Components" links in the admin sidebar into
a single tab labelled "Themes and components"
2. The combined tab links to the `/admin/config/customize/themes` page
(titled as "Themes and components")
3. Add a new "Components" tab to the "Themes and components" page.
There's already an existing "Themes" tab
4. Add a "back to" link at the top of individual theme/component page to
navigate back to the respective tab in the "Themes and components" page
5. Remove the themes/components list/sidebar that currently serves for
navigating between themes/components
6. Remove the header in the theme/component page

Changes 4–6 apply only if the admin sidebar is enabled; they have no
effect otherwise.

Internal topic: t/146006.
This commit is contained in:
Osama Sayegh
2025-03-13 15:34:17 +03:00
committed by GitHub
parent 7b5213cc8d
commit f87e5aab0b
33 changed files with 507 additions and 230 deletions

View File

@ -0,0 +1,76 @@
import Component from "@glimmer/component";
import DButton from "discourse/components/d-button";
import icon from "discourse/helpers/d-icon";
import { i18n } from "discourse-i18n";
import AdminConfigAreaCard from "admin/components/admin-config-area-card";
export default class InstallThemeCard extends Component {
externalResources = [
{
key: "admin.customize.theme.beginners_guide_title",
link: "https://meta.discourse.org/t/91966",
},
{
key: "admin.customize.theme.developers_guide_title",
link: "https://meta.discourse.org/t/93648",
},
{
key: "admin.customize.theme.browse_themes",
link: "https://meta.discourse.org/c/theme",
},
];
get heading() {
if (this.args.component) {
return i18n(
"admin.config_areas.themes_and_components.components.new_component"
);
} else {
return i18n("admin.config_areas.themes_and_components.themes.new_theme");
}
}
get intro() {
if (this.args.component) {
return i18n(
"admin.config_areas.themes_and_components.components.components_intro"
);
} else {
return i18n(
"admin.config_areas.themes_and_components.themes.themes_intro"
);
}
}
<template>
<AdminConfigAreaCard
class="theme-install-card"
@translatedHeading={{this.heading}}
>
<:content>
<p>{{this.intro}}</p>
<div class="theme-install-card__external-links">
{{#each this.externalResources as |resource|}}
<a
href={{resource.link}}
class="external-link"
rel="noopener noreferrer"
target="_blank"
>
{{i18n resource.key}}
{{icon "up-right-from-square"}}
</a>
{{/each}}
</div>
<DButton
class="btn-primary theme-install-card__install-button"
@translatedLabel={{i18n
"admin.config_areas.themes_and_components.install"
}}
@icon="upload"
@action={{@openModal}}
/>
</:content>
</AdminConfigAreaCard>
</template>
}

View File

@ -0,0 +1,63 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { service } from "@ember/service";
import { i18n } from "discourse-i18n";
import InstallThemeCard from "admin/components/admin-config-area-cards/install-theme-card";
import InstallComponentModal from "admin/components/modal/install-theme";
import ThemesGrid from "admin/components/themes-grid";
import { COMPONENTS } from "admin/models/theme";
export default class AdminConfigAreasComponents extends Component {
@service modal;
@service router;
@service toasts;
@action
installModal() {
this.modal.show(InstallComponentModal, {
model: { ...this.installOptions() },
});
}
// TODO (martin) These install methods may not belong here and they
// are incomplete or have stubbed or omitted properties. We may want
// to move this to the new config route or a dedicated component
// that sits in the route.
installOptions() {
return {
selectedType: COMPONENTS,
userId: null,
content: [],
installedThemes: this.args.components,
addTheme: this.addComponent,
updateSelectedType: () => {},
showComponentsOnly: true,
};
}
@action
addComponent(component) {
this.toasts.success({
data: {
message: i18n("admin.customize.theme.install_success", {
theme: component.name,
}),
},
duration: 2000,
});
this.router.refresh();
}
<template>
<div class="admin-detail">
<ThemesGrid @themes={{@components}}>
<:specialCard>
<InstallThemeCard
@component={{true}}
@openModal={{this.installModal}}
/>
</:specialCard>
</ThemesGrid>
</div>
</template>
}

View File

@ -1,12 +1,13 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { action } from "@ember/object"; import { action } from "@ember/object";
import { service } from "@ember/service"; import { service } from "@ember/service";
import DPageSubheader from "discourse/components/d-page-subheader";
import { i18n } from "discourse-i18n"; import { i18n } from "discourse-i18n";
import InstallThemeCard from "admin/components/admin-config-area-cards/install-theme-card";
import InstallThemeModal from "admin/components/modal/install-theme"; import InstallThemeModal from "admin/components/modal/install-theme";
import ThemesGrid from "admin/components/themes-grid"; import ThemesGrid from "admin/components/themes-grid";
import { THEMES } from "admin/models/theme";
export default class AdminConfigAreasLookAndFeelThemes extends Component { export default class AdminConfigAreasThemes extends Component {
@service modal; @service modal;
@service router; @service router;
@service toasts; @service toasts;
@ -24,12 +25,13 @@ export default class AdminConfigAreasLookAndFeelThemes extends Component {
// that sits in the route. // that sits in the route.
installThemeOptions() { installThemeOptions() {
return { return {
selectedType: "theme", selectedType: THEMES,
userId: null, userId: null,
content: [], content: [],
installedThemes: this.args.themes, installedThemes: this.args.themes,
addTheme: this.addTheme, addTheme: this.addTheme,
updateSelectedType: () => {}, updateSelectedType: () => {},
showThemesOnly: true,
}; };
} }
@ -47,23 +49,12 @@ export default class AdminConfigAreasLookAndFeelThemes extends Component {
} }
<template> <template>
<DPageSubheader
@titleLabel={{i18n "admin.config_areas.look_and_feel.themes.title"}}
@descriptionLabel={{i18n "admin.customize.theme.themes_intro_new"}}
@learnMoreUrl="https://meta.discourse.org/t/93648"
>
<:actions as |actions|>
<actions.Primary
@action={{this.installModal}}
@label="admin.customize.install"
@icon="upload"
class="admin-look-and-feel__install-theme"
/>
</:actions>
</DPageSubheader>
<div class="admin-detail"> <div class="admin-detail">
<ThemesGrid @themes={{@themes}} /> <ThemesGrid @themes={{@themes}}>
<:specialCard>
<InstallThemeCard @openModal={{this.installModal}} />
</:specialCard>
</ThemesGrid>
</div> </div>
</template> </template>
} }

View File

@ -111,7 +111,8 @@ export default class InstallThemeModal extends Component {
} }
get themes() { get themes() {
return POPULAR_THEMES.map((popularTheme) => { const list = [];
POPULAR_THEMES.forEach((popularTheme) => {
if ( if (
this.args.model.installedThemes.some((installedTheme) => this.args.model.installedThemes.some((installedTheme) =>
this.themeHasSameUrl(installedTheme, popularTheme.value) this.themeHasSameUrl(installedTheme, popularTheme.value)
@ -119,8 +120,20 @@ export default class InstallThemeModal extends Component {
) { ) {
popularTheme.installed = true; popularTheme.installed = true;
} }
return popularTheme;
if (this.args.model.showComponentsOnly) {
if (popularTheme.component) {
list.push(popularTheme);
}
} else if (this.args.model.showThemesOnly) {
if (!popularTheme.component) {
list.push(popularTheme);
}
} else {
list.push(popularTheme);
}
}); });
return list;
} }
get installingMessage() { get installingMessage() {

View File

@ -12,21 +12,6 @@ export default class ThemesGrid extends Component {
sortedThemes; sortedThemes;
externalResources = [
{
key: "admin.customize.theme.beginners_guide_title",
link: "https://meta.discourse.org/t/91966",
},
{
key: "admin.customize.theme.developers_guide_title",
link: "https://meta.discourse.org/t/93648",
},
{
key: "admin.customize.theme.browse_themes",
link: "https://meta.discourse.org/c/theme",
},
];
constructor() { constructor() {
super(...arguments); super(...arguments);
@ -51,6 +36,9 @@ export default class ThemesGrid extends Component {
{{#each @themes as |theme|}} {{#each @themes as |theme|}}
<ThemesGridCard @theme={{theme}} @allThemes={{@themes}} /> <ThemesGridCard @theme={{theme}} @allThemes={{@themes}} />
{{/each}} {{/each}}
{{#if (has-block "specialCard")}}
{{yield to="specialCard"}}
{{/if}}
</div> </div>
</template> </template>
} }

View File

@ -0,0 +1,13 @@
import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n";
export default class AdminConfigThemesAndComponentsComponentsRoute extends DiscourseRoute {
async model() {
const components = await this.store.findAll("theme");
return components.reject((t) => !t.component);
}
titleToken() {
return i18n("admin.config_areas.themes_and_components.components.title");
}
}

View File

@ -0,0 +1,10 @@
import { service } from "@ember/service";
import DiscourseRoute from "discourse/routes/discourse";
export default class AdminConfigThemesAndComponentsIndexRoute extends DiscourseRoute {
@service router;
beforeModel() {
this.router.replaceWith("adminConfig.customize.themes");
}
}

View File

@ -1,13 +1,13 @@
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n"; import { i18n } from "discourse-i18n";
export default class AdminConfigLookAndFeelThemesRoute extends DiscourseRoute { export default class AdminConfigThemesAndComponentsThemesRoute extends DiscourseRoute {
async model() { async model() {
const themes = await this.store.findAll("theme"); const themes = await this.store.findAll("theme");
return themes.reject((t) => t.component); return themes.reject((t) => t.component);
} }
titleToken() { titleToken() {
return i18n("admin.config_areas.look_and_feel.themes.title"); return i18n("admin.config_areas.themes_and_components.themes.title");
} }
} }

View File

@ -2,10 +2,10 @@ import { service } from "@ember/service";
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
import { i18n } from "discourse-i18n"; import { i18n } from "discourse-i18n";
export default class AdminConfigLookAndFeelRoute extends DiscourseRoute { export default class AdminConfigThemesAndComponentsRoute extends DiscourseRoute {
@service router; @service router;
titleToken() { titleToken() {
return i18n("admin.config_areas.look_and_feel.title"); return i18n("admin.config_areas.themes_and_components.title");
} }
} }

View File

@ -1,10 +0,0 @@
import { service } from "@ember/service";
import AdminConfigWithSettingsRoute from "./admin-config-with-settings-route";
export default class AdminConfigLookAndFeelIndexRoute extends AdminConfigWithSettingsRoute {
@service router;
beforeModel() {
this.router.replaceWith("adminConfig.lookAndFeel.themes");
}
}

View File

@ -276,8 +276,9 @@ export default function () {
path: "/", path: "/",
}); });
}); });
this.route("lookAndFeel", { path: "/look-and-feel" }, function () { this.route("customize", function () {
this.route("themes"); this.route("themes");
this.route("components");
}); });
this.route( this.route(
"adminPermalinks", "adminPermalinks",

View File

@ -0,0 +1,6 @@
<DBreadcrumbsItem
@path="/admin/config/customize/components"
@label={{i18n "admin.config_areas.themes_and_components.components.title"}}
/>
<AdminConfigAreas::Components @components={{this.model}} />

View File

@ -0,0 +1,6 @@
<DBreadcrumbsItem
@path="/admin/config/customize/themes"
@label={{i18n "admin.config_areas.themes_and_components.themes.title"}}
/>
<AdminConfigAreas::Themes @themes={{this.model}} />

View File

@ -0,0 +1,28 @@
<DPageHeader
@titleLabel={{i18n "admin.config_areas.themes_and_components.title"}}
@descriptionLabel={{i18n
"admin.config_areas.themes_and_components.description"
}}
@learnMoreUrl="https://meta.discourse.org/t/beginners-guide-to-using-discourse-themes/91966"
>
<:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
</:breadcrumbs>
<:tabs>
<NavItem
@route="adminConfig.customize.themes"
@label="admin.config_areas.themes_and_components.themes.title"
/>
<NavItem
@route="adminConfig.customize.components"
@label="admin.config_areas.themes_and_components.components.title"
/>
</:tabs>
</DPageHeader>
<div class="admin-container admin-config-page__main-area">
<PluginOutlet @name="admin-config-customize">
{{outlet}}
</PluginOutlet>
</div>

View File

@ -1,6 +0,0 @@
<DBreadcrumbsItem
@path="/admin/config/look-and-feel/themes"
@label={{i18n "admin.config_areas.look_and_feel.themes.title"}}
/>
<AdminConfigAreas::LookAndFeelThemes @themes={{this.model}} />

View File

@ -1,24 +0,0 @@
<DPageHeader
@titleLabel={{i18n "admin.config_areas.look_and_feel.title"}}
@descriptionLabel={{i18n "admin.config_areas.look_and_feel.description"}}
@learnMoreUrl="https://meta.discourse.org/t/beginners-guide-to-using-discourse-themes/91966"
>
<:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem
@path="/admin/config/look-and-feel"
@label={{i18n "admin.config_areas.look_and_feel.title"}}
/>
</:breadcrumbs>
<:tabs>
<NavItem
@route="adminConfig.lookAndFeel.themes"
@label="admin.config_areas.look_and_feel.themes.title"
/>
</:tabs>
</DPageHeader>
<div class="admin-container admin-config-page__main-area">
{{outlet}}
</div>

View File

@ -1,6 +1,24 @@
{{#if this.editingThemeSetting}} {{#if this.editingThemeSetting}}
{{outlet}} {{outlet}}
{{else}} {{else}}
<div class="back-to-themes-and-components">
<LinkTo
@route={{if
this.model.component
"adminConfig.customize.components"
"adminConfig.customize.themes"
}}
>
{{dIcon "angle-left"}}
{{i18n
(if
this.model.component
"admin.config_areas.themes_and_components.components.back"
"admin.config_areas.themes_and_components.themes.back"
)
}}
</LinkTo>
</div>
<div class="show-current-style"> <div class="show-current-style">
<span> <span>
<PluginOutlet <PluginOutlet

View File

@ -1,43 +1,3 @@
{{#if (eq this.currentTab "themes")}}
<DPageHeader
@titleLabel={{i18n "admin.config.themes.title"}}
@descriptionLabel={{i18n "admin.config.themes.header_description"}}
@learnMoreUrl="https://meta.discourse.org/t/91966"
@hideTabs={{true}}
>
<:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem
@path="/admin/customize/themes"
@label={{i18n "admin.config.themes.title"}}
/>
</:breadcrumbs>
</DPageHeader>
{{else}}
<DPageHeader
@titleLabel={{i18n "admin.config.components.title"}}
@descriptionLabel={{i18n "admin.config.components.header_description"}}
@hideTabs={{true}}
>
<:breadcrumbs>
<DBreadcrumbsItem @path="/admin" @label={{i18n "admin_title"}} />
<DBreadcrumbsItem
@path="/admin/customize/components"
@label={{i18n "admin.config.components.title"}}
/>
</:breadcrumbs>
</DPageHeader>
{{/if}}
<PluginOutlet @name="admin-customize-themes"> <PluginOutlet @name="admin-customize-themes">
{{#unless this.editingTheme}}
<ThemesList
@themes={{this.fullThemes}}
@components={{this.childThemes}}
@currentTab={{this.currentTab}}
@installModal={{route-action "installModal"}}
/>
{{/unless}}
{{outlet}} {{outlet}}
</PluginOutlet> </PluginOutlet>

View File

@ -240,22 +240,14 @@ export const ADMIN_NAV_MAP = [
settings_area: "navigation", settings_area: "navigation",
}, },
{ {
name: "admin_themes", name: "admin_themes_and_components",
route: "adminCustomizeThemes", route: "adminConfig.customize.themes",
routeModels: ["themes"], currentWhen:
model: "themes", "adminConfig.customize.themes adminConfig.customize.components",
label: "admin.config.themes.title", label: "admin.appearance.sidebar_link.themes_and_components.title",
description: "admin.config.themes.header_description",
icon: "paintbrush", icon: "paintbrush",
}, keywords:
{ "admin.appearance.sidebar_link.themes_and_components.keywords",
name: "admin_components",
route: "adminCustomizeThemes",
routeModels: ["components"],
label: "admin.config.components.title",
description: "admin.config.components.header_description",
icon: "puzzle-piece",
keywords: "admin.config.components.keywords",
}, },
{ {
name: "admin_customize_site_texts", name: "admin_customize_site_texts",

View File

@ -93,6 +93,9 @@ class SidebarAdminSectionLink extends BaseCustomSidebarSectionLink {
return this.router.currentRoute.name; return this.router.currentRoute.name;
} }
} }
if (this.adminSidebarNavLink.currentWhen) {
return this.adminSidebarNavLink.currentWhen;
}
} }
get keywords() { get keywords() {
@ -352,11 +355,11 @@ export default class AdminSidebarPanel extends BaseCustomSidebarPanel {
store.findAll("theme").then((themes) => { store.findAll("theme").then((themes) => {
this.adminSidebarStateManager.setLinkKeywords( this.adminSidebarStateManager.setLinkKeywords(
"admin_themes", "admin_themes_and_components",
themes.content.rejectBy("component").mapBy("name") themes.content.rejectBy("component").mapBy("name")
); );
this.adminSidebarStateManager.setLinkKeywords( this.adminSidebarStateManager.setLinkKeywords(
"admin_components", "admin_themes_and_components",
themes.content.filterBy("component").mapBy("name") themes.content.filterBy("component").mapBy("name")
); );
}); });

View File

@ -175,9 +175,9 @@ acceptance("Theme", function (needs) {
}); });
test("can force install themes", async function (assert) { test("can force install themes", async function (assert) {
await visit("/admin/customize/themes"); await visit("/admin/config/customize/themes");
await click(".themes-list .create-actions button"); await click(".theme-install-card .btn-primary");
await click(".install-theme-items #remote"); await click(".install-theme-items #remote");
await fillIn( await fillIn(
".install-theme-content .repo input", ".install-theme-content .repo input",
@ -200,9 +200,9 @@ acceptance("Theme", function (needs) {
}); });
test("can continue installation", async function (assert) { test("can continue installation", async function (assert) {
await visit("/admin/customize/themes"); await visit("/admin/config/customize/themes");
await click(".themes-list-container__item .info"); await click(".theme-card .btn-primary");
assert assert
.dom(".control-unit .status-message") .dom(".control-unit .status-message")
.includesText( .includesText(

View File

@ -853,6 +853,18 @@
} }
} }
.theme-install-card {
&__external-links {
display: flex;
margin-bottom: 1em;
flex-direction: column;
}
&__install-button {
width: 100%;
}
}
.add-upload-modal { .add-upload-modal {
input[type="file"] { input[type="file"] {
display: block; display: block;
@ -1191,7 +1203,7 @@
.desktop-view .admin-customize { .desktop-view .admin-customize {
.show-current-style { .show-current-style {
padding-left: 2%; padding-left: 0;
width: 68%; width: 68%;
.title { .title {
@ -1204,6 +1216,10 @@
.themes-list { .themes-list {
width: 28%; width: 28%;
} }
.back-to-themes-and-components {
margin-bottom: 1em;
}
} }
.mobile-view .admin-customize { .mobile-view .admin-customize {

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class Admin::Config::CustomizeController < Admin::AdminController
def themes
end
def components
end
end

View File

@ -1,6 +0,0 @@
# frozen_string_literal: true
class Admin::Config::LookAndFeelController < Admin::AdminController
def themes
end
end

View File

@ -5958,6 +5958,72 @@ en:
failed: "Failed" failed: "Failed"
home: home:
title: "Home" title: "Home"
account:
title: "Account"
sidebar_link:
backups: "Backups"
whats_new:
title: "What's new?"
keywords: "changelog|feature|release"
community:
title: "Community"
sidebar_link:
about_your_site: "About your site"
badges: "Badges"
login_and_authentication: "Login & authentication"
notifications: "Notifications"
permalinks: "Permalinks"
trust_levels: "Trust levels"
group_permissions: "Group permissions"
users: "Users"
groups: "Groups"
user_fields: "User fields"
watched_words: "Watched words"
legal: "Legal"
moderation_flags:
title: "Moderation"
keywords: "flag|review"
appearance:
title: "Appearance"
sidebar_link:
font_style: "Font style"
site_logo: "Site logo"
color_schemes: "Color palettes"
emoji: "Emoji"
navigation: "Navigation"
themes_and_components:
title: "Themes and components"
keywords: "extension"
site_texts: "Site texts"
email_settings:
title: "Email Settings"
sidebar_link:
appearance: "Appearance"
server_setup:
title: "Server setup & logs"
keywords: "email|smtp|mailgun|sendgrid|sent|skipped|bounced|received|rejected|email logs|preview summary"
security:
title: "Security"
sidebar_link:
security: "Security settings"
spam: "Spam settings"
staff_action_logs:
title: "Logs & screening"
keywords: "error logs|staff action|screened emails|screened ips|screened urls|search logs"
section_landing_pages:
account:
title: "Account"
backups:
title: "Backups"
description: "Take a backup of your site's data"
whats_new:
title: "What's new?"
description: "Discover new releases and improvements to Discourse"
config_areas: config_areas:
about: about:
@ -6054,16 +6120,21 @@ en:
delete: "Delete" delete: "Delete"
more_options: more_options:
title: "More options" title: "More options"
look_and_feel: themes_and_components:
title: "Themes and components"
description: "Personalize your Discourse site with themes and components to match your needs."
breadcrumb_title: "Customize"
install: "Install"
themes: themes:
title: "Themes" title: "Themes"
themes_intro: "Install a new theme to get started, or create your own from scratch using these resources." themes_intro: "Install a new theme to get started, or create your own from scratch using these resources."
themes_intro_img_alt: "New theme placeholder"
set_default_theme: "Set default"
default_theme: "Default theme"
themes_description: "Themes are expansive customizations that change multiple elements of the style of your forum design, and often also include additional front-end features."
new_theme: "New theme" new_theme: "New theme"
user_selectable: "User selectable" back: "Back to themes"
components:
title: "Components"
components_intro: "Install a new component to get started, or create your own from scratch using these resources."
new_component: "New component"
back: "Back to components"
user_fields: user_fields:
field: "Field" field: "Field"
type: "Type" type: "Type"
@ -6240,6 +6311,7 @@ en:
component_name: "Component name" component_name: "Component name"
themes_intro: "Select an existing theme or install a new one to get started" themes_intro: "Select an existing theme or install a new one to get started"
themes_intro_new: "Install a new theme to get started, or create your own from scratch using these resources." themes_intro_new: "Install a new theme to get started, or create your own from scratch using these resources."
components_intro_new: "Install a new component to get started, or create your own from scratch using these resources."
themes_intro_img_alt: "New theme placeholder" themes_intro_img_alt: "New theme placeholder"
beginners_guide_title: "Beginners guide to using Discourse Themes" beginners_guide_title: "Beginners guide to using Discourse Themes"
developers_guide_title: "Developers guide to Discourse Themes" developers_guide_title: "Developers guide to Discourse Themes"

View File

@ -426,11 +426,14 @@ Discourse::Application.routes.draw do
collection { put "/" => "about#update" } collection { put "/" => "about#update" }
end end
resources :look_and_feel, resources :customize,
path: "look-and-feel", path: "customize",
constraints: AdminConstraint.new, constraints: AdminConstraint.new,
only: %i[index] do only: %i[index] do
collection { get "/themes" => "look_and_feel#themes" } collection do
get "/themes" => "customize#themes"
get "/components" => "customize#components"
end
end end
end end

View File

@ -10,6 +10,7 @@ module SvgSprite
align-left align-left
anchor anchor
angle-down angle-down
angle-left
angle-right angle-right
angle-up angle-up
angles-down angles-down

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
describe "Admin Customize Config Area Page", type: :system do
fab!(:admin)
let(:config_area) { PageObjects::Pages::AdminCustomizeConfigArea.new }
let(:install_modal) { PageObjects::Modals::InstallTheme.new }
before { sign_in(admin) }
context "when in the themes tab" do
it "has a special card for installing new themes" do
config_area.visit
expect(config_area.install_card).to have_text(
I18n.t("admin_js.admin.config_areas.themes_and_components.themes.new_theme"),
)
config_area.install_card.find(".btn-primary").click
expect(install_modal).to be_open
expect(install_modal.popular_options.first).to have_text("Air")
end
end
context "when in the components tab" do
it "has a special card for installing new components" do
config_area.visit_components
expect(config_area.install_card).to have_text(
I18n.t("admin_js.admin.config_areas.themes_and_components.components.new_component"),
)
config_area.install_card.find(".btn-primary").click
expect(install_modal).to be_open
expect(install_modal.popular_options.first).to have_text("Brand Header")
end
end
end

View File

@ -9,76 +9,6 @@ describe "Admin Customize Themes", type: :system do
before { sign_in(admin) } before { sign_in(admin) }
describe "when visiting the page to customize themes" do
fab!(:theme_2) { Fabricate(:theme, name: "Cool theme 2") }
fab!(:theme_3) { Fabricate(:theme, name: "Cool theme 3") }
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(theme.name)
expect(delete_themes_confirm_modal).to have_theme(theme_2.name)
expect(delete_themes_confirm_modal).to have_theme(theme_3.name)
delete_themes_confirm_modal.confirm
expect(admin_customize_themes_page).to have_no_inactive_themes
end
it "selects the themes tab by default" do
visit("/admin/customize/themes")
expect(find(".themes-list-header")).to have_css(".themes-tab.active")
end
it "selects the component tab when visiting the theme-components route" do
visit("/admin/customize/components")
expect(find(".themes-list-header")).to have_css(".components-tab.active")
end
it "switching between themes and components tabs keeps the search visible only if both tabs have at least 10 items" do
(1..6).each { |number| Fabricate(:theme, component: false, name: "Cool theme #{number}") }
(1..5).each { |number| Fabricate(:theme, component: true, name: "Cool component #{number}") }
visit("/admin/customize/themes")
expect(admin_customize_themes_page).to have_themes(count: 11)
admin_customize_themes_page.search("5")
expect(admin_customize_themes_page).to have_themes(count: 1)
admin_customize_themes_page.switch_to_components
expect(admin_customize_themes_page).to have_no_search
expect(admin_customize_themes_page).to have_themes(count: 5)
(6..11).each { |number| Fabricate(:theme, component: true, name: "Cool component #{number}") }
visit("/admin/customize/components")
expect(admin_customize_themes_page).to have_themes(count: 11)
admin_customize_themes_page.search("5")
expect(admin_customize_themes_page).to have_themes(count: 1)
admin_customize_themes_page.switch_to_themes
expect(admin_customize_themes_page).to have_themes(count: 1)
end
end
describe "when visiting the page to customize a single theme" do describe "when visiting the page to customize a single theme" do
it "should allow admin to update the color scheme of the theme" do it "should allow admin to update the color scheme of the theme" do
visit("/admin/customize/themes/#{theme.id}") visit("/admin/customize/themes/#{theme.id}")
@ -213,4 +143,20 @@ describe "Admin Customize Themes", type: :system do
expect(theme_translations_picker.component.text).to eq("English (US)") expect(theme_translations_picker.component.text).to eq("English (US)")
end end
end end
context "when visting a theme's page" do
it "has a link to the themes page" do
visit("/admin/customize/themes/#{theme.id}")
expect(admin_customize_themes_page).to have_back_button_to_themes_page
end
end
context "when visting a component's page" do
fab!(:component) { Fabricate(:theme, component: true, name: "Cool component 493") }
it "has a link to the components page" do
visit("/admin/customize/themes/#{component.id}")
expect(admin_customize_themes_page).to have_back_button_to_components_page
end
end
end end

View File

@ -293,12 +293,26 @@ describe "Admin | Sidebar Navigation", type: :system do
filter.filter("air") filter.filter("air")
links = page.all(".sidebar-section-link-content-text") links = page.all(".sidebar-section-link-content-text")
expect(links.count).to eq(1) expect(links.count).to eq(1)
expect(links.map(&:text)).to eq(["Themes"]) expect(links.map(&:text)).to eq(["Themes and components"])
filter.filter("kanban") filter.filter("kanban")
links = page.all(".sidebar-section-link-content-text") links = page.all(".sidebar-section-link-content-text")
expect(links.count).to eq(1) expect(links.count).to eq(1)
expect(links.map(&:text)).to eq(["Components"]) expect(links.map(&:text)).to eq(["Themes and components"])
end
it "highlights the 'Themes and components' link when the themes page is visited" do
visit("/admin/config/customize/themes")
expect(page).to have_css(
'.sidebar-section-link-wrapper[data-list-item-name="admin_themes_and_components"] a.active',
)
end
it "highlights the 'Themes and components' link when the components page is visited" do
visit("/admin/config/customize/components")
expect(page).to have_css(
'.sidebar-section-link-wrapper[data-list-item-name="admin_themes_and_components"] a.active',
)
end end
it "does not show the button to customize sidebar sections, that is only supported in the main panel" do it "does not show the button to customize sidebar sections, that is only supported in the main panel" do

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module PageObjects
module Modals
class InstallTheme < PageObjects::Modals::Base
def modal
find(".admin-install-theme-modal")
end
def popular_options
all(".popular-theme-item")
end
end
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
module PageObjects
module Pages
class AdminCustomizeConfigArea < PageObjects::Pages::Base
def visit
page.visit("/admin/config/customize")
end
def visit_components
page.visit("/admin/config/customize/components")
end
def install_card
find(".theme-install-card")
end
end
end
end

View File

@ -27,6 +27,28 @@ module PageObjects
has_css?("#{setting_selector(setting_name)} .desc", exact_text: description) has_css?("#{setting_selector(setting_name)} .desc", exact_text: description)
end end
def has_no_themes_list?
has_no_css?(".themes-list-header")
end
def has_back_button_to_themes_page?
has_css?(
'.back-to-themes-and-components a[href="/admin/config/customize/themes"]',
text: I18n.t("admin_js.admin.config_areas.themes_and_components.themes.back"),
)
end
def has_back_button_to_components_page?
has_css?(
'.back-to-themes-and-components a[href="/admin/config/customize/components"]',
text: I18n.t("admin_js.admin.config_areas.themes_and_components.components.back"),
)
end
def has_no_page_header?
has_no_css?(".d-page-header")
end
def reset_overridden_setting(setting_name) def reset_overridden_setting(setting_name)
setting_section = find("section.theme.settings .setting[data-setting=\"#{setting_name}\"]") setting_section = find("section.theme.settings .setting[data-setting=\"#{setting_name}\"]")
setting_section.click_button(I18n.t("admin_js.admin.settings.reset")) setting_section.click_button(I18n.t("admin_js.admin.settings.reset"))