UX: Multiple fixes to theme card rendering (#29225)

* Improvements, refactors, remove dead code

---------

Co-authored-by: Martin Brennan <martin@discourse.org>
This commit is contained in:
Jordan Vidrine
2024-10-16 11:13:36 -05:00
committed by GitHub
parent 6e6dbde898
commit f28f82f99e
6 changed files with 161 additions and 179 deletions

View File

@@ -24,7 +24,9 @@ export default class AdminConfigAreaCard extends Component {
class="admin-config-area-card__title" class="admin-config-area-card__title"
>{{this.computedHeading}}</h3> >{{this.computedHeading}}</h3>
{{else}} {{else}}
<h3 class="admin-config-area-card__title">{{yield to="header"}}</h3> {{#if (has-block "header")}}
<h3 class="admin-config-area-card__title">{{yield to="header"}}</h3>
{{/if}}
{{/if}} {{/if}}
{{#if (has-block "headerAction")}} {{#if (has-block "headerAction")}}
<div class="admin-config-area-card__header-action"> <div class="admin-config-area-card__header-action">

View File

@@ -1,6 +1,5 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { Input } from "@ember/component"; import { action } from "@ember/object";
import { action, computed } from "@ember/object";
import { service } from "@ember/service"; import { service } from "@ember/service";
import { htmlSafe } from "@ember/template"; import { htmlSafe } from "@ember/template";
import DButton from "discourse/components/d-button"; import DButton from "discourse/components/d-button";
@@ -21,54 +20,6 @@ export default class ThemeCard extends Component {
@service siteSettings; @service siteSettings;
@service toasts; @service toasts;
// NOTE: These 3 shouldn't need @computed, if we convert
// theme to a pure JS class with @tracked properties we
// won't need to do this.
@computed("args.theme.default")
get setDefaultButtonIcon() {
return this.args.theme.default ? "far-check-square" : "far-square";
}
@computed("args.theme.default")
get setDefaultButtonTitle() {
return this.args.theme.default
? "admin.customize.theme.default_theme"
: "admin.customize.theme.set_default_theme";
}
@computed("args.theme.default")
get setDefaultButtonClasses() {
return this.args.theme.default
? "btn-primary theme-card__button"
: "btn-default theme-card__button";
}
@computed(
"args.theme.default",
"args.theme.isBroken",
"args.theme.enabled",
"args.theme.isPendingUpdates"
)
get themeCardClasses() {
return this.args.theme.isBroken
? "--broken"
: !this.args.theme.enabled
? "--disabled"
: this.args.theme.isPendingUpdates
? "--updates"
: this.args.theme.default
? "--active"
: "";
}
get imageAlt() {
return this.args.theme.name;
}
get hasScreenshot() {
return this.args.theme.screenshot ? true : false;
}
get themeRouteModels() { get themeRouteModels() {
return ["themes", this.args.theme.id]; return ["themes", this.args.theme.id];
} }
@@ -92,91 +43,59 @@ export default class ThemeCard extends Component {
// Will also need some cleanup when refactoring other theme code. // Will also need some cleanup when refactoring other theme code.
@action @action
async setDefault() { async setDefault() {
this.args.theme.set("default", true); let oldDefaultThemeId;
this.args.theme.saveChanges("default").then(() => {
this.args.allThemes.forEach((theme) => {
if (theme.id !== this.args.theme.id) {
theme.set("default", !this.args.theme.get("default"));
}
});
this.toasts.success({
data: {
message: I18n.t("admin.customize.theme.set_default_success", {
theme: this.args.theme.name,
}),
},
duration: 2000,
});
});
}
@action this.args.theme.set("default", true);
async handleSubmit(event) { this.args.allThemes.forEach((theme) => {
this.args.theme.set("user_selectable", event.target.checked); if (theme.id !== this.args.theme.id) {
this.args.theme.saveChanges("user_selectable"); if (theme.get("default")) {
oldDefaultThemeId = theme.id;
}
theme.set("default", !this.args.theme.get("default"));
}
});
const changesSaved = await this.args.theme.saveChanges("default");
if (!changesSaved) {
this.args.allThemes
.find((theme) => theme.id === oldDefaultThemeId)
.set("default", true);
this.args.theme.set("default", false);
return;
}
this.toasts.success({
data: {
message: I18n.t("admin.customize.theme.set_default_success", {
theme: this.args.theme.name,
}),
},
duration: 2000,
});
} }
<template> <template>
<AdminConfigAreaCard <AdminConfigAreaCard
class={{concatClass "theme-card" this.themeCardClasses}} class={{concatClass "theme-card" (if @theme.default "-active")}}
@translatedHeading={{@theme.name}}
> >
<:header>
{{@theme.name}}
<span class="theme-card__icons">
{{#if @theme.isPendingUpdates}}
<DButton
@route="adminCustomizeThemes.show"
@routeModels={{this.themeRouteModels}}
@icon="sync"
@class="btn-flat theme-card__button"
@preventFocus={{true}}
/>
{{else}}
{{#if @theme.isBroken}}
{{icon
"exclamation-circle"
class="broken-indicator"
title="admin.customize.theme.broken_theme_tooltip"
}}
{{/if}}
{{#unless @theme.enabled}}
{{icon
"ban"
class="light-grey-icon"
title="admin.customize.theme.disabled_component_tooltip"
}}
{{/unless}}
{{/if}}
</span>
</:header>
<:headerAction>
<Input
@type="checkbox"
@checked={{@theme.user_selectable}}
id="user-select-theme-{{@theme.id}}"
onclick={{this.handleSubmit}}
/>
<label
class="theme-card__checkbox-label"
for="user-select-theme-{{@theme.id}}"
>
{{i18n "admin.config_areas.look_and_feel.themes.user_selectable"}}
</label>
</:headerAction>
<:content> <:content>
<div class="theme-card__image-wrapper"> <div class="theme-card__image-wrapper">
{{#if this.hasScreenshot}} {{#if @theme.screenshot}}
<img <img
class="theme-card__image" class="theme-card__image"
src={{htmlSafe @theme.screenshot}} src={{htmlSafe @theme.screenshot}}
alt={{this.imageAlt}} alt={{@theme.name}}
/> />
{{else}} {{else}}
<ThemesGridPlaceholder @theme={{@theme}} /> <ThemesGridPlaceholder @theme={{@theme}} />
{{/if}} {{/if}}
</div> </div>
<div class="theme-card__content"> <div class="theme-card__content">
<p class="theme-card__description">{{@theme.description}}</p> {{#if @theme.description}}
<p class="theme-card__description">{{@theme.description}}</p>
{{/if}}
{{#if @theme.childThemes}} {{#if @theme.childThemes}}
<span class="theme-card__components">{{i18n <span class="theme-card__components">{{i18n
"admin.customize.theme.components" "admin.customize.theme.components"
@@ -188,12 +107,52 @@ export default class ThemeCard extends Component {
<DButton <DButton
@action={{this.setDefault}} @action={{this.setDefault}}
@preventFocus={{true}} @preventFocus={{true}}
@icon={{this.setDefaultButtonIcon}} @icon={{if @theme.default "far-check-square" "far-square"}}
@class={{this.setDefaultButtonClasses}} @class={{concatClass
@translatedLabel={{i18n this.setDefaultButtonTitle}} "theme-card__button"
(if @theme.default "btn-primary" "btn-default")
}}
@translatedLabel={{i18n
(if
@theme.default
"admin.customize.theme.default_theme"
"admin.customize.theme.set_default_theme"
)
}}
@disabled={{@theme.default}} @disabled={{@theme.default}}
/> />
<div class="theme-card-footer__actions"> {{#if @theme.isPendingUpdates}}
<DButton
@route="adminCustomizeThemes.show"
@routeModels={{this.themeRouteModels}}
@icon="sync"
@class="btn btn-flat theme-card__button"
@title="admin.customize.theme.updates_available_tooltip"
@preventFocus={{true}}
/>
{{else}}
{{#if @theme.isBroken}}
<DButton
@route="adminCustomizeThemes.show"
@routeModels={{this.themeRouteModels}}
@icon="exclamation-circle"
@class="btn btn-flat theme-card__button broken-indicator"
@title="admin.customize.theme.broken_theme_tooltip"
@preventFocus={{true}}
/>
{{/if}}
{{#unless @theme.enabled}}
<DButton
@route="adminCustomizeThemes.show"
@routeModels={{this.themeRouteModels}}
@icon="ban"
@class="btn btn-flat theme-card__button broken-indicator light-grey-icon"
@title="admin.customize.theme.disabled_component_tooltip"
@preventFocus={{true}}
/>
{{/unless}}
{{/if}}
<div class="theme-card__footer-actions">
<a <a
href={{this.themePreviewUrl}} href={{this.themePreviewUrl}}
title={{i18n "admin.customize.explain_preview"}} title={{i18n "admin.customize.explain_preview"}}

View File

@@ -31,12 +31,12 @@ export default class ThemesGrid extends Component {
}, },
]; ];
// Always show the default theme first in the list
get sortedThemes() { get sortedThemes() {
// Always show currently set default theme first
return this.args.themes.sort((a, b) => { return this.args.themes.sort((a, b) => {
if (a.default) { if (a.get("default")) {
return -1; return -1;
} else if (b.default) { } else if (b.get("default")) {
return 1; return 1;
} }
}); });
@@ -78,39 +78,42 @@ export default class ThemesGrid extends Component {
<template> <template>
<div class="themes-cards-container"> <div class="themes-cards-container">
{{#each this.sortedThemes as |theme|}} <div class="themes-cards-container__main">
<ThemesGridCard @theme={{theme}} @allThemes={{@themes}} /> {{#each this.sortedThemes as |theme|}}
{{/each}} <ThemesGridCard @theme={{theme}} @allThemes={{@themes}} />
{{/each}}
<AdminConfigAreaCard class="theme-card"> </div>
<:content> <div class="themes-cards-container__helper">
<h2 class="theme-card__title">{{i18n <AdminConfigAreaCard
"admin.config_areas.look_and_feel.themes.new_theme" class="theme-card"
}}</h2> @heading="admin.config_areas.look_and_feel.themes.new_theme"
<p class="theme-card__description">{{i18n >
"admin.customize.theme.themes_intro_new" <:content>
}}</p> <p class="theme-card__description">{{i18n
<div class="external-resources"> "admin.customize.theme.themes_intro_new"
{{#each this.externalResources as |resource|}} }}</p>
<a <div class="external-resources">
href={{resource.link}} {{#each this.externalResources as |resource|}}
class="external-link" <a
rel="noopener noreferrer" href={{resource.link}}
target="_blank" class="external-link"
> rel="noopener noreferrer"
{{i18n resource.key}} target="_blank"
{{icon "external-link-alt"}} >
</a> {{i18n resource.key}}
{{/each}} {{icon "external-link-alt"}}
</div> </a>
<DButton {{/each}}
@action={{this.installModal}} </div>
@icon="upload" <DButton
@label="admin.customize.install" @action={{this.installModal}}
class="btn-primary theme-card__button" @icon="upload"
/> @label="admin.customize.install"
</:content> class="btn-primary theme-card__install-button"
</AdminConfigAreaCard> />
</:content>
</AdminConfigAreaCard>
</div>
</div> </div>
</template> </template>
} }

View File

@@ -317,6 +317,7 @@ class Theme extends RestModel {
saveChanges() { saveChanges() {
const hash = this.getProperties.apply(this, arguments); const hash = this.getProperties.apply(this, arguments);
return this.save(hash) return this.save(hash)
.then(() => true)
.finally(() => this.set("changed", false)) .finally(() => this.set("changed", false))
.catch(popupAjaxError); .catch(popupAjaxError);
} }

View File

@@ -5,8 +5,19 @@
.themes-cards-container { .themes-cards-container {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); grid-template-columns: calc(66.66% - 0.5em) calc(33.33% - 0.5em);
gap: 1em; gap: 1em;
@media screen and (max-width: 1300px) {
display: flex;
flex-direction: column-reverse;
}
&__main {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(calc(325px - 1em), 1fr));
gap: 1em;
}
} }
.theme-card { .theme-card {
@@ -19,22 +30,18 @@
flex-grow: 1; flex-grow: 1;
} }
&.--active { &.-active {
@include theme-card-border(tertiary); @include theme-card-border(tertiary);
} }
&.--disabled { &.-disabled {
@include theme-card-border(primary); @include theme-card-border(primary);
} }
&.--broken { &.-broken {
@include theme-card-border(danger); @include theme-card-border(danger);
} }
&.--updates {
@include theme-card-border(success);
}
.broken-indicator { .broken-indicator {
color: var(--danger); color: var(--danger);
} }
@@ -43,10 +50,6 @@
display: flex; display: flex;
} }
&__icons .btn-flat {
padding: 0;
}
&__image-wrapper { &__image-wrapper {
width: 100%; width: 100%;
height: 160px; height: 160px;
@@ -76,21 +79,34 @@
font-weight: 400; font-weight: 400;
} }
&__components { &__content {
display: block; padding: 10px 0;
font-size: var(--font-down-1);
margin-bottom: 10px;
color: var(--primary-high);
} }
&__button { &__description {
margin: 0 0 10px 0;
}
&__components {
display: -webkit-box;
font-size: var(--font-down-1);
color: var(--primary-high);
-webkit-box-orient: vertical;
overflow: hidden;
-webkit-line-clamp: 3;
}
&__install-button {
margin-top: auto; margin-top: auto;
} }
&__footer { &__footer {
margin-top: auto; margin-top: auto;
display: flex; display: flex;
justify-content: space-between; }
&__footer-actions {
margin-left: auto;
} }
.admin-config-area-card__header-action { .admin-config-area-card__header-action {
@@ -110,6 +126,7 @@
justify-content: space-between; justify-content: space-between;
flex-direction: column; flex-direction: column;
font-size: var(--font-down-1); font-size: var(--font-down-1);
margin-bottom: 10px;
.external-link { .external-link {
margin-bottom: 0.25em; margin-bottom: 0.25em;

View File

@@ -5668,7 +5668,7 @@ en:
move_up: "Move up" move_up: "Move up"
move_down: "Move down" move_down: "Move down"
look_and_feel: look_and_feel:
title: "Look & Feel" title: "Look & feel"
description: "Themes, components, and color schemes can be used to customise and brand your Discourse site, giving it a distinctive style." description: "Themes, components, and color schemes can be used to customise and brand your Discourse site, giving it a distinctive style."
themes: themes:
title: "Themes" title: "Themes"