mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
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:
@@ -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">
|
||||||
|
|||||||
@@ -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"}}
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user