mirror of
https://github.com/discourse/discourse.git
synced 2024-11-22 09:42:02 +08:00
FEATURE: Initial themes config area with grid (#28828)
* UX: More additions * UX: more * DEV: Add admin/config/themes route * UX: Use admin config card * syntax merge fixes * cleanup * cleanup * checkbox * more * error * save on click * more * fix setter * DEV: Implement vanilla checkbox * cleanup * UX: save themes as default * DEV: Add component list to card * DEV: Add placeholder for no screenshots * DEV: Fix default theme reactivity Also add content/optionalAction yields to config area card and put the theme user selectable checkbox there, along with adding styles. * DEV: Change to generic "look and feel" config area * DEV: Auto redirect to themes on base look and feel route * UX: Remove computed from sorted themes * linting * UX: Turn update icon into button that routes to settings * DEV: remove unused function * UX: center icons with title * DEV: Lint * UX: Hook up theme preview button * DEV: Minor fixes --------- Co-authored-by: Martin Brennan <martin@discourse.org> Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com>
This commit is contained in:
parent
688f65a39d
commit
3ad2fd032b
|
@ -12,11 +12,28 @@ export default class AdminConfigAreaCard extends Component {
|
|||
return this.args.translatedHeading;
|
||||
}
|
||||
|
||||
get hasHeading() {
|
||||
return this.args.heading || this.args.translatedHeading;
|
||||
}
|
||||
|
||||
<template>
|
||||
<section class="admin-config-area-card" ...attributes>
|
||||
<h3 class="admin-config-area-card__title">{{this.computedHeading}}</h3>
|
||||
<div class="admin-config-area-card__header-wrapper">
|
||||
{{#if this.hasHeading}}
|
||||
<h3
|
||||
class="admin-config-area-card__title"
|
||||
>{{this.computedHeading}}</h3>
|
||||
{{else}}
|
||||
<h3 class="admin-config-area-card__title">{{yield to="header"}}</h3>
|
||||
{{/if}}
|
||||
{{#if (has-block "headerAction")}}
|
||||
<div class="admin-config-area-card__header-action">
|
||||
{{yield to="headerAction"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="admin-config-area-card__content">
|
||||
{{yield}}
|
||||
{{yield to="content"}}
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
|
|
@ -54,31 +54,37 @@ export default class AdminConfigAreasAbout extends Component {
|
|||
@heading="admin.config_areas.about.general_settings"
|
||||
class="admin-config-area-about__general-settings-section"
|
||||
>
|
||||
<AdminConfigAreasAboutGeneralSettings
|
||||
@generalSettings={{this.generalSettings}}
|
||||
@setGlobalSavingStatus={{this.setSavingStatus}}
|
||||
@globalSavingStatus={{this.saving}}
|
||||
/>
|
||||
<:content>
|
||||
<AdminConfigAreasAboutGeneralSettings
|
||||
@generalSettings={{this.generalSettings}}
|
||||
@setGlobalSavingStatus={{this.setSavingStatus}}
|
||||
@globalSavingStatus={{this.saving}}
|
||||
/>
|
||||
</:content>
|
||||
</AdminConfigAreaCard>
|
||||
<AdminConfigAreaCard
|
||||
@heading="admin.config_areas.about.contact_information"
|
||||
class="admin-config-area-about__contact-information-section"
|
||||
>
|
||||
<AdminConfigAreasAboutContactInformation
|
||||
@contactInformation={{this.contactInformation}}
|
||||
@setGlobalSavingStatus={{this.setSavingStatus}}
|
||||
@globalSavingStatus={{this.saving}}
|
||||
/>
|
||||
<:content>
|
||||
<AdminConfigAreasAboutContactInformation
|
||||
@contactInformation={{this.contactInformation}}
|
||||
@setGlobalSavingStatus={{this.setSavingStatus}}
|
||||
@globalSavingStatus={{this.saving}}
|
||||
/>
|
||||
</:content>
|
||||
</AdminConfigAreaCard>
|
||||
<AdminConfigAreaCard
|
||||
@heading="admin.config_areas.about.your_organization"
|
||||
class="admin-config-area-about__your-organization-section"
|
||||
>
|
||||
<AdminConfigAreasAboutYourOrganization
|
||||
@yourOrganization={{this.yourOrganization}}
|
||||
@setGlobalSavingStatus={{this.setSavingStatus}}
|
||||
@globalSavingStatus={{this.saving}}
|
||||
/>
|
||||
<:content>
|
||||
<AdminConfigAreasAboutYourOrganization
|
||||
@yourOrganization={{this.yourOrganization}}
|
||||
@setGlobalSavingStatus={{this.setSavingStatus}}
|
||||
@globalSavingStatus={{this.saving}}
|
||||
/>
|
||||
</:content>
|
||||
</AdminConfigAreaCard>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -121,73 +121,77 @@ export default class AdminFlagsForm extends Component {
|
|||
/>
|
||||
<div class="admin-config-area__primary-content admin-flag-form">
|
||||
<AdminConfigAreaCard @heading={{this.header}}>
|
||||
<Form @onSubmit={{this.save}} @data={{this.formData}} as |form|>
|
||||
<form.Field
|
||||
@name="name"
|
||||
@title={{i18n "admin.config_areas.flags.form.name"}}
|
||||
@validation="required|length:3,200"
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Input />
|
||||
</form.Field>
|
||||
|
||||
<form.Field
|
||||
@name="description"
|
||||
@title={{i18n "admin.config_areas.flags.form.description"}}
|
||||
@validation="required|length:3,1000"
|
||||
as |field|
|
||||
>
|
||||
<field.Textarea @height={{60}} />
|
||||
</form.Field>
|
||||
|
||||
<form.Field
|
||||
@name="appliesTo"
|
||||
@title={{i18n "admin.config_areas.flags.form.applies_to"}}
|
||||
@validation="required"
|
||||
@validate={{this.validateAppliesTo}}
|
||||
as |field|
|
||||
>
|
||||
<field.Custom>
|
||||
<MultiSelect
|
||||
@id={{field.id}}
|
||||
@value={{field.value}}
|
||||
@onChange={{field.set}}
|
||||
@content={{this.appliesToValues}}
|
||||
@options={{hash allowAny=false}}
|
||||
class="admin-flag-form__applies-to"
|
||||
/>
|
||||
</field.Custom>
|
||||
</form.Field>
|
||||
|
||||
<form.CheckboxGroup as |checkboxGroup|>
|
||||
<checkboxGroup.Field
|
||||
@name="requireMessage"
|
||||
@title={{i18n "admin.config_areas.flags.form.require_message"}}
|
||||
<:content>
|
||||
<Form @onSubmit={{this.save}} @data={{this.formData}} as |form|>
|
||||
<form.Field
|
||||
@name="name"
|
||||
@title={{i18n "admin.config_areas.flags.form.name"}}
|
||||
@validation="required|length:3,200"
|
||||
@format="large"
|
||||
as |field|
|
||||
>
|
||||
<field.Checkbox>
|
||||
{{i18n
|
||||
"admin.config_areas.flags.form.require_message_description"
|
||||
<field.Input />
|
||||
</form.Field>
|
||||
|
||||
<form.Field
|
||||
@name="description"
|
||||
@title={{i18n "admin.config_areas.flags.form.description"}}
|
||||
@validation="required|length:3,1000"
|
||||
as |field|
|
||||
>
|
||||
<field.Textarea @height={{60}} />
|
||||
</form.Field>
|
||||
|
||||
<form.Field
|
||||
@name="appliesTo"
|
||||
@title={{i18n "admin.config_areas.flags.form.applies_to"}}
|
||||
@validation="required"
|
||||
@validate={{this.validateAppliesTo}}
|
||||
as |field|
|
||||
>
|
||||
<field.Custom>
|
||||
<MultiSelect
|
||||
@id={{field.id}}
|
||||
@value={{field.value}}
|
||||
@onChange={{field.set}}
|
||||
@content={{this.appliesToValues}}
|
||||
@options={{hash allowAny=false}}
|
||||
class="admin-flag-form__applies-to"
|
||||
/>
|
||||
</field.Custom>
|
||||
</form.Field>
|
||||
|
||||
<form.CheckboxGroup as |checkboxGroup|>
|
||||
<checkboxGroup.Field
|
||||
@name="requireMessage"
|
||||
@title={{i18n
|
||||
"admin.config_areas.flags.form.require_message"
|
||||
}}
|
||||
</field.Checkbox>
|
||||
</checkboxGroup.Field>
|
||||
as |field|
|
||||
>
|
||||
<field.Checkbox>
|
||||
{{i18n
|
||||
"admin.config_areas.flags.form.require_message_description"
|
||||
}}
|
||||
</field.Checkbox>
|
||||
</checkboxGroup.Field>
|
||||
|
||||
<checkboxGroup.Field
|
||||
@name="enabled"
|
||||
@title={{i18n "admin.config_areas.flags.form.enabled"}}
|
||||
as |field|
|
||||
>
|
||||
<field.Checkbox />
|
||||
</checkboxGroup.Field>
|
||||
</form.CheckboxGroup>
|
||||
<checkboxGroup.Field
|
||||
@name="enabled"
|
||||
@title={{i18n "admin.config_areas.flags.form.enabled"}}
|
||||
as |field|
|
||||
>
|
||||
<field.Checkbox />
|
||||
</checkboxGroup.Field>
|
||||
</form.CheckboxGroup>
|
||||
|
||||
<form.Alert @icon="circle-info">
|
||||
{{i18n "admin.config_areas.flags.form.alert"}}
|
||||
</form.Alert>
|
||||
<form.Alert @icon="info-circle">
|
||||
{{i18n "admin.config_areas.flags.form.alert"}}
|
||||
</form.Alert>
|
||||
|
||||
<form.Submit @label="admin.config_areas.flags.form.save" />
|
||||
</Form>
|
||||
<form.Submit @label="admin.config_areas.flags.form.save" />
|
||||
</Form>
|
||||
</:content>
|
||||
</AdminConfigAreaCard>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -52,9 +52,11 @@ export default class DashboardNewFeatures extends Component {
|
|||
{{#if this.groupedNewFeatures}}
|
||||
{{#each this.groupedNewFeatures as |groupedFeatures|}}
|
||||
<AdminConfigAreaCard @translatedHeading={{groupedFeatures.date}}>
|
||||
{{#each groupedFeatures.features as |feature|}}
|
||||
<DashboardNewFeatureItem @item={{feature}} />
|
||||
{{/each}}
|
||||
<:content>
|
||||
{{#each groupedFeatures.features as |feature|}}
|
||||
<DashboardNewFeatureItem @item={{feature}} />
|
||||
{{/each}}
|
||||
</:content>
|
||||
</AdminConfigAreaCard>
|
||||
{{/each}}
|
||||
{{else if this.isLoaded}}
|
||||
|
|
|
@ -0,0 +1,216 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { Input } from "@ember/component";
|
||||
import { action, computed } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import I18n from "discourse-i18n";
|
||||
import AdminConfigAreaCard from "admin/components/admin-config-area-card";
|
||||
import ThemesGridPlaceholder from "./themes-grid-placeholder";
|
||||
|
||||
// NOTE (martin): We will need to revisit and improve this component
|
||||
// over time.
|
||||
//
|
||||
// Much of the existing theme logic in /admin/customize/themes has old patterns
|
||||
// and technical debt, so anything copied from there to here is subject
|
||||
// to change as we improve this incrementally.
|
||||
export default class ThemeCard extends Component {
|
||||
@service siteSettings;
|
||||
@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() {
|
||||
return ["themes", this.args.theme.id];
|
||||
}
|
||||
|
||||
get childrenString() {
|
||||
return this.args.theme.childThemes.reduce((acc, theme, idx) => {
|
||||
if (idx === this.args.theme.childThemes.length - 1) {
|
||||
return acc + theme.name;
|
||||
} else {
|
||||
return acc + theme.name + ", ";
|
||||
}
|
||||
}, "");
|
||||
}
|
||||
|
||||
get themePreviewUrl() {
|
||||
return `/admin/themes/${this.args.theme.id}/preview`;
|
||||
}
|
||||
|
||||
// NOTE: inspired by -> https://github.com/discourse/discourse/blob/24caa36eef826bcdaed88aebfa7df154413fb349/app/assets/javascripts/admin/addon/controllers/admin-customize-themes-show.js#L366
|
||||
//
|
||||
// Will also need some cleanup when refactoring other theme code.
|
||||
@action
|
||||
async setDefault() {
|
||||
this.args.theme.set("default", true);
|
||||
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
|
||||
async handleSubmit(event) {
|
||||
this.args.theme.set("user_selectable", event.target.checked);
|
||||
this.args.theme.saveChanges("user_selectable");
|
||||
}
|
||||
|
||||
<template>
|
||||
<AdminConfigAreaCard
|
||||
class={{concatClass "theme-card" this.themeCardClasses}}
|
||||
>
|
||||
<: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>
|
||||
<div class="theme-card__image-wrapper">
|
||||
{{#if this.hasScreenshot}}
|
||||
<img
|
||||
class="theme-card__image"
|
||||
src={{htmlSafe @theme.screenshot}}
|
||||
alt={{this.imageAlt}}
|
||||
/>
|
||||
{{else}}
|
||||
<ThemesGridPlaceholder @theme={{@theme}} />
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="theme-card__content">
|
||||
<p class="theme-card__description">{{@theme.description}}</p>
|
||||
{{#if @theme.childThemes}}
|
||||
<span class="theme-card__components">{{i18n
|
||||
"admin.customize.theme.components"
|
||||
}}:
|
||||
{{htmlSafe this.childrenString}}</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="theme-card__footer">
|
||||
<DButton
|
||||
@action={{this.setDefault}}
|
||||
@preventFocus={{true}}
|
||||
@icon={{this.setDefaultButtonIcon}}
|
||||
@class={{this.setDefaultButtonClasses}}
|
||||
@translatedLabel={{i18n this.setDefaultButtonTitle}}
|
||||
@disabled={{@theme.default}}
|
||||
/>
|
||||
<div class="theme-card-footer__actions">
|
||||
<a
|
||||
href={{this.themePreviewUrl}}
|
||||
title={{i18n "admin.customize.explain_preview"}}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
class="btn btn-flat theme-card__button"
|
||||
>{{icon "eye"}}</a>
|
||||
<DButton
|
||||
@route="adminCustomizeThemes.show"
|
||||
@routeModels={{this.themeRouteModels}}
|
||||
@icon="cog"
|
||||
@class="btn-flat theme-card__button"
|
||||
@preventFocus={{true}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</:content>
|
||||
</AdminConfigAreaCard>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
|
||||
export default class ThemesGridPlaceholder extends Component {
|
||||
get themeColors() {
|
||||
if (this.args.theme.color_scheme) {
|
||||
return {
|
||||
primary: `#${this.args.theme.color_scheme.colors[0].hex}`,
|
||||
secondary: `#${this.args.theme.color_scheme.colors[1].hex}`,
|
||||
tertiary: `#${this.args.theme.color_scheme.colors[2].hex}`,
|
||||
quaternary: `#${this.args.theme.color_scheme.colors[3].hex}`,
|
||||
highlight: `#${this.args.theme.color_scheme.colors[6].hex}`,
|
||||
danger: `#${this.args.theme.color_scheme.colors[7].hex}`,
|
||||
success: `#${this.args.theme.color_scheme.colors[8].hex}`,
|
||||
love: `#${this.args.theme.color_scheme.colors[9].hex}`,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
primary: "var(--primary)",
|
||||
secondary: "var(--secondary)",
|
||||
tertiary: "var(--tertiary)",
|
||||
quaternary: "var(--quaternary)",
|
||||
highlight: "var(--highlight)",
|
||||
danger: "var(--danger)",
|
||||
success: "var(--success)",
|
||||
love: "var(--love)",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<svg viewBox="0 0 636 347" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect
|
||||
width="635.115"
|
||||
height="347"
|
||||
fill={{htmlSafe this.themeColors.secondary}}
|
||||
/>
|
||||
<path
|
||||
d="M54.9766 79.1039C55.9198 76.0065 59.8623 75.0916 62.0732 77.4571L121.448 140.986C123.659 143.351 122.48 147.223 119.326 147.955L34.621 167.611C31.467 168.343 28.7034 165.386 29.6466 162.288L54.9766 79.1039Z"
|
||||
fill={{htmlSafe this.themeColors.primary}}
|
||||
/>
|
||||
<path
|
||||
d="M398.487 211.02C400.651 208.611 404.611 209.448 405.615 212.527L432.579 295.196C433.584 298.274 430.879 301.285 427.711 300.615L342.635 282.633C339.467 281.963 338.212 278.115 340.376 275.707L398.487 211.02Z"
|
||||
fill={{htmlSafe this.themeColors.tertiary}}
|
||||
/>
|
||||
<circle cx="109.357" cy="262.879" r="44.1636" fill="#D1F0FF" />
|
||||
<circle cx="365.927" cy="103.048" r="44.1636" fill="#E45735" />
|
||||
<rect
|
||||
x="166.139"
|
||||
y="68.751"
|
||||
width="81.9226"
|
||||
height="81.9226"
|
||||
rx="4.20606"
|
||||
transform="rotate(-15.9297 166.139 68.751)"
|
||||
fill={{htmlSafe this.themeColors.danger}}
|
||||
/>
|
||||
<rect
|
||||
x="500.521"
|
||||
y="100.296"
|
||||
width="81.9226"
|
||||
height="81.9226"
|
||||
rx="4.20606"
|
||||
transform="rotate(-15.9297 500.521 100.296)"
|
||||
fill={{htmlSafe this.themeColors.success}}
|
||||
/>
|
||||
<rect
|
||||
x="481.857"
|
||||
y="222.921"
|
||||
width="121.976"
|
||||
height="54.6788"
|
||||
rx="4.20606"
|
||||
transform="rotate(9.12857 481.857 222.921)"
|
||||
fill={{htmlSafe this.themeColors.love}}
|
||||
/>
|
||||
<rect
|
||||
x="176.654"
|
||||
y="240.608"
|
||||
width="121.976"
|
||||
height="54.6788"
|
||||
rx="4.20606"
|
||||
transform="rotate(-22.7296 176.654 240.608)"
|
||||
fill={{htmlSafe this.themeColors.primary}}
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
}
|
116
app/assets/javascripts/admin/addon/components/themes-grid.gjs
Normal file
116
app/assets/javascripts/admin/addon/components/themes-grid.gjs
Normal file
|
@ -0,0 +1,116 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { action } from "@ember/object";
|
||||
import { service } from "@ember/service";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import AdminConfigAreaCard from "admin/components/admin-config-area-card";
|
||||
import InstallThemeModal from "../components/modal/install-theme";
|
||||
import ThemesGridCard from "./themes-grid-card";
|
||||
|
||||
// NOTE (martin): Much of the JS code in this component is placeholder code. Much
|
||||
// of the existing theme logic in /admin/customize/themes has old patterns
|
||||
// and technical debt, so anything copied from there to here is subject
|
||||
// to change as we improve this incrementally.
|
||||
export default class ThemesGrid extends Component {
|
||||
@service modal;
|
||||
@service router;
|
||||
|
||||
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 sortedThemes() {
|
||||
// Always show currently set default theme first
|
||||
return this.args.themes.sort((a, b) => {
|
||||
if (a.default) {
|
||||
return -1;
|
||||
} else if (b.default) {
|
||||
return 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 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.
|
||||
installThemeOptions() {
|
||||
return {
|
||||
selectedType: "theme",
|
||||
userId: null,
|
||||
content: null,
|
||||
installedThemes: this.args.themes,
|
||||
addTheme: this.addTheme,
|
||||
updateSelectedType: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
@action
|
||||
addTheme(theme) {
|
||||
this.refresh();
|
||||
theme.setProperties({ recentlyInstalled: true });
|
||||
this.router.transitionTo("adminCustomizeThemes.show", theme.get("id"), {
|
||||
queryParams: {
|
||||
repoName: null,
|
||||
repoUrl: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
installModal() {
|
||||
this.modal.show(InstallThemeModal, {
|
||||
model: { ...this.installThemeOptions() },
|
||||
});
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="themes-cards-container">
|
||||
{{#each this.sortedThemes as |theme|}}
|
||||
<ThemesGridCard @theme={{theme}} @allThemes={{@themes}} />
|
||||
{{/each}}
|
||||
|
||||
<AdminConfigAreaCard class="theme-card">
|
||||
<:content>
|
||||
<h2 class="theme-card__title">{{i18n
|
||||
"admin.config_areas.look_and_feel.themes.new_theme"
|
||||
}}</h2>
|
||||
<p class="theme-card__description">{{i18n
|
||||
"admin.customize.theme.themes_intro_new"
|
||||
}}</p>
|
||||
<div class="external-resources">
|
||||
{{#each this.externalResources as |resource|}}
|
||||
<a
|
||||
href={{resource.link}}
|
||||
class="external-link"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{{i18n resource.key}}
|
||||
{{icon "external-link-alt"}}
|
||||
</a>
|
||||
{{/each}}
|
||||
</div>
|
||||
<DButton
|
||||
@action={{this.installModal}}
|
||||
@icon="upload"
|
||||
@label="admin.customize.install"
|
||||
class="btn-primary theme-card__button"
|
||||
/>
|
||||
</:content>
|
||||
</AdminConfigAreaCard>
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
export default class AdminConfigLookAndFeelThemesRoute extends DiscourseRoute {
|
||||
async model() {
|
||||
const themes = await this.store.findAll("theme");
|
||||
return themes.reject((t) => t.component);
|
||||
}
|
||||
|
||||
titleToken() {
|
||||
return I18n.t("admin.config_areas.look_and_feel.themes.title");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { service } from "@ember/service";
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
import I18n from "discourse-i18n";
|
||||
|
||||
export default class AdminConfigLookAndFeelRoute extends DiscourseRoute {
|
||||
@service router;
|
||||
|
||||
beforeModel() {
|
||||
this.router.replaceWith("adminConfig.lookAndFeel.themes");
|
||||
}
|
||||
|
||||
titleToken() {
|
||||
return I18n.t("admin.config_areas.look_and_feel.title");
|
||||
}
|
||||
}
|
|
@ -218,6 +218,9 @@ export default function () {
|
|||
});
|
||||
|
||||
this.route("about");
|
||||
this.route("lookAndFeel", { path: "/look-and-feel" }, function () {
|
||||
this.route("themes");
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
<DBreadcrumbsItem
|
||||
@path="/admin/config/look-and-feel/themes"
|
||||
@label={{i18n "admin.config_areas.look_and_feel.themes.title"}}
|
||||
/>
|
||||
|
||||
<div class="admin-detail">
|
||||
<ThemesGrid @themes={{this.model}} />
|
||||
</div>
|
|
@ -0,0 +1,22 @@
|
|||
<AdminPageHeader
|
||||
@titleLabel="admin.config_areas.look_and_feel.title"
|
||||
@descriptionLabel="admin.config_areas.look_and_feel.description"
|
||||
>
|
||||
<:breadcrumbs>
|
||||
<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>
|
||||
</AdminPageHeader>
|
||||
|
||||
<div class="admin-container admin-config-page__main-area">
|
||||
{{outlet}}
|
||||
</div>
|
|
@ -27,6 +27,7 @@
|
|||
font-size: var(--font-down-1);
|
||||
padding: 10px 10px;
|
||||
}
|
||||
|
||||
&__control-group-horizontal {
|
||||
display: flex;
|
||||
margin-bottom: 18px;
|
||||
|
@ -34,6 +35,18 @@
|
|||
margin-right: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
&__title {
|
||||
flex: 2;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&__header-wrapper {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
flex: 1;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-config-page {
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
width: 51%;
|
||||
vertical-align: top;
|
||||
margin-left: 20%;
|
||||
margin-top: 60px;
|
||||
|
||||
img {
|
||||
display: inline-block;
|
||||
|
|
|
@ -337,7 +337,6 @@
|
|||
|
||||
.themes-list-container {
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
max-height: 60vh;
|
||||
border-bottom-right-radius: var(--d-border-radius);
|
||||
border-bottom-left-radius: var(--d-border-radius);
|
||||
|
|
|
@ -40,6 +40,7 @@
|
|||
@import "signup-progress-bar";
|
||||
@import "svg";
|
||||
@import "tap-tile";
|
||||
@import "theme-card";
|
||||
@import "time-input";
|
||||
@import "time-shortcut-picker";
|
||||
@import "topic-map";
|
||||
|
|
127
app/assets/stylesheets/common/components/theme-card.scss
Normal file
127
app/assets/stylesheets/common/components/theme-card.scss
Normal file
|
@ -0,0 +1,127 @@
|
|||
@mixin theme-card-border($color) {
|
||||
border-color: var(--#{$color}-medium);
|
||||
box-shadow: 0px 0px 0px 3px var(--#{$color}-low);
|
||||
}
|
||||
|
||||
.themes-cards-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
.theme-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
.admin-config-area-card__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
&.--active {
|
||||
@include theme-card-border(tertiary);
|
||||
}
|
||||
|
||||
&.--disabled {
|
||||
@include theme-card-border(primary);
|
||||
}
|
||||
|
||||
&.--broken {
|
||||
@include theme-card-border(danger);
|
||||
}
|
||||
|
||||
&.--updates {
|
||||
@include theme-card-border(success);
|
||||
}
|
||||
|
||||
.broken-indicator {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
&__icons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__icons .btn-flat {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&__image-wrapper {
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
overflow: hidden;
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: top left;
|
||||
border-radius: calc(var(--d-border-radius) + 1px);
|
||||
}
|
||||
|
||||
.ember-checkbox {
|
||||
margin: 0 5px 0 0;
|
||||
}
|
||||
|
||||
&__checkbox-label {
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&__components {
|
||||
display: block;
|
||||
font-size: var(--font-down-1);
|
||||
margin-bottom: 10px;
|
||||
color: var(--primary-high);
|
||||
}
|
||||
|
||||
&__button {
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
&__footer {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.admin-config-area-card__header-action {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--secondary);
|
||||
right: 20px;
|
||||
top: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
.admin-config-area-card.theme-card
|
||||
.admin-config-area-card__content
|
||||
.external-resources {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-direction: column;
|
||||
font-size: var(--font-down-1);
|
||||
|
||||
.external-link {
|
||||
margin-bottom: 0.25em;
|
||||
color: var(--primary);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-card .admin-config-area-card {
|
||||
&__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
}
|
||||
}
|
6
app/controllers/admin/config/look_and_feel_controller.rb
Normal file
6
app/controllers/admin/config/look_and_feel_controller.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Admin::Config::LookAndFeelController < Admin::AdminController
|
||||
def themes
|
||||
end
|
||||
end
|
|
@ -5665,6 +5665,18 @@ en:
|
|||
title: "More options"
|
||||
move_up: "Move up"
|
||||
move_down: "Move down"
|
||||
look_and_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."
|
||||
themes:
|
||||
title: "Themes"
|
||||
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"
|
||||
user_selectable: "User selectable"
|
||||
plugins:
|
||||
title: "Plugins"
|
||||
installed: "Installed plugins"
|
||||
|
@ -5837,6 +5849,8 @@ en:
|
|||
theme_name: "Theme name"
|
||||
component_name: "Component name"
|
||||
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_img_alt: "New theme placeholder"
|
||||
beginners_guide_title: "Beginner’s guide to using Discourse Themes"
|
||||
developers_guide_title: "Developer’s guide to Discourse Themes"
|
||||
browse_themes: "Browse community themes"
|
||||
|
@ -5884,6 +5898,9 @@ en:
|
|||
convert_theme_alert_generic: "Are you sure you want to convert this theme to component?"
|
||||
convert_theme_tooltip: "Convert this theme to component"
|
||||
inactive_themes: "Inactive themes:"
|
||||
set_default_theme: "Set default"
|
||||
default_theme: "Default theme"
|
||||
set_default_success: "Default theme set to %{theme}"
|
||||
inactive_components: "Unused components:"
|
||||
selected:
|
||||
one: "%{count} selected"
|
||||
|
|
|
@ -401,6 +401,13 @@ Discourse::Application.routes.draw do
|
|||
resources :about, constraints: AdminConstraint.new, only: %i[index] do
|
||||
collection { put "/" => "about#update" }
|
||||
end
|
||||
|
||||
resources :look_and_feel,
|
||||
path: "look-and-feel",
|
||||
constraints: AdminConstraint.new,
|
||||
only: %i[index] do
|
||||
collection { get "/themes" => "look_and_feel#themes" }
|
||||
end
|
||||
end
|
||||
|
||||
get "section/:section_id" => "section#show", :constraints => AdminConstraint.new
|
||||
|
|
Loading…
Reference in New Issue
Block a user