From 3ad2fd032b6c3127f395aa5789ee3e18ac5b3b5c Mon Sep 17 00:00:00 2001 From: Jordan Vidrine <30537603+jordanvidrine@users.noreply.github.com> Date: Tue, 15 Oct 2024 10:54:38 -0500 Subject: [PATCH] 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 Co-authored-by: Joffrey JAFFEUX --- .../components/admin-config-area-card.gjs | 21 +- .../components/admin-config-areas/about.gjs | 36 +-- .../addon/components/admin-flags-form.gjs | 126 +++++----- .../components/dashboard-new-features.gjs | 8 +- .../addon/components/themes-grid-card.gjs | 216 ++++++++++++++++++ .../components/themes-grid-placeholder.gjs | 86 +++++++ .../admin/addon/components/themes-grid.gjs | 116 ++++++++++ .../admin-config-look-and-feel-themes.js | 13 ++ .../routes/admin-config-look-and-feel.js | 15 ++ .../admin/addon/routes/admin-route-map.js | 3 + .../templates/config-look-and-feel-themes.hbs | 8 + .../addon/templates/config-look-and-feel.hbs | 22 ++ .../common/admin/admin_config_area.scss | 13 ++ .../stylesheets/common/admin/admin_intro.scss | 1 - .../stylesheets/common/admin/customize.scss | 1 - .../stylesheets/common/components/_index.scss | 1 + .../common/components/theme-card.scss | 127 ++++++++++ .../admin/config/look_and_feel_controller.rb | 6 + config/locales/client.en.yml | 17 ++ config/routes.rb | 7 + 20 files changed, 760 insertions(+), 83 deletions(-) create mode 100644 app/assets/javascripts/admin/addon/components/themes-grid-card.gjs create mode 100644 app/assets/javascripts/admin/addon/components/themes-grid-placeholder.gjs create mode 100644 app/assets/javascripts/admin/addon/components/themes-grid.gjs create mode 100644 app/assets/javascripts/admin/addon/routes/admin-config-look-and-feel-themes.js create mode 100644 app/assets/javascripts/admin/addon/routes/admin-config-look-and-feel.js create mode 100644 app/assets/javascripts/admin/addon/templates/config-look-and-feel-themes.hbs create mode 100644 app/assets/javascripts/admin/addon/templates/config-look-and-feel.hbs create mode 100644 app/assets/stylesheets/common/components/theme-card.scss create mode 100644 app/controllers/admin/config/look_and_feel_controller.rb diff --git a/app/assets/javascripts/admin/addon/components/admin-config-area-card.gjs b/app/assets/javascripts/admin/addon/components/admin-config-area-card.gjs index d2b5e0af90d..63d7687c938 100644 --- a/app/assets/javascripts/admin/addon/components/admin-config-area-card.gjs +++ b/app/assets/javascripts/admin/addon/components/admin-config-area-card.gjs @@ -12,11 +12,28 @@ export default class AdminConfigAreaCard extends Component { return this.args.translatedHeading; } + get hasHeading() { + return this.args.heading || this.args.translatedHeading; + } + diff --git a/app/assets/javascripts/admin/addon/components/admin-config-areas/about.gjs b/app/assets/javascripts/admin/addon/components/admin-config-areas/about.gjs index e852a3def4b..50906d456f6 100644 --- a/app/assets/javascripts/admin/addon/components/admin-config-areas/about.gjs +++ b/app/assets/javascripts/admin/addon/components/admin-config-areas/about.gjs @@ -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" > - + <:content> + + - + <:content> + + - + <:content> + + diff --git a/app/assets/javascripts/admin/addon/components/admin-flags-form.gjs b/app/assets/javascripts/admin/addon/components/admin-flags-form.gjs index cb6d81bfde9..fa9a0b0d341 100644 --- a/app/assets/javascripts/admin/addon/components/admin-flags-form.gjs +++ b/app/assets/javascripts/admin/addon/components/admin-flags-form.gjs @@ -121,73 +121,77 @@ export default class AdminFlagsForm extends Component { />
-
- - - - - - - - - - - - - - - - + + - - {{i18n - "admin.config_areas.flags.form.require_message_description" + + + + + + + + + + + + + + + - + as |field| + > + + {{i18n + "admin.config_areas.flags.form.require_message_description" + }} + + - - - - + + + + - - {{i18n "admin.config_areas.flags.form.alert"}} - + + {{i18n "admin.config_areas.flags.form.alert"}} + - - + + +
diff --git a/app/assets/javascripts/admin/addon/components/dashboard-new-features.gjs b/app/assets/javascripts/admin/addon/components/dashboard-new-features.gjs index cff826158e6..7d48e7d1a74 100644 --- a/app/assets/javascripts/admin/addon/components/dashboard-new-features.gjs +++ b/app/assets/javascripts/admin/addon/components/dashboard-new-features.gjs @@ -52,9 +52,11 @@ export default class DashboardNewFeatures extends Component { {{#if this.groupedNewFeatures}} {{#each this.groupedNewFeatures as |groupedFeatures|}} - {{#each groupedFeatures.features as |feature|}} - - {{/each}} + <:content> + {{#each groupedFeatures.features as |feature|}} + + {{/each}} + {{/each}} {{else if this.isLoaded}} diff --git a/app/assets/javascripts/admin/addon/components/themes-grid-card.gjs b/app/assets/javascripts/admin/addon/components/themes-grid-card.gjs new file mode 100644 index 00000000000..3dfa4ece261 --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/themes-grid-card.gjs @@ -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"); + } + + +} diff --git a/app/assets/javascripts/admin/addon/components/themes-grid-placeholder.gjs b/app/assets/javascripts/admin/addon/components/themes-grid-placeholder.gjs new file mode 100644 index 00000000000..e5bb323c684 --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/themes-grid-placeholder.gjs @@ -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)", + }; + } + } + + +} diff --git a/app/assets/javascripts/admin/addon/components/themes-grid.gjs b/app/assets/javascripts/admin/addon/components/themes-grid.gjs new file mode 100644 index 00000000000..3ec57d870ce --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/themes-grid.gjs @@ -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() }, + }); + } + + +} diff --git a/app/assets/javascripts/admin/addon/routes/admin-config-look-and-feel-themes.js b/app/assets/javascripts/admin/addon/routes/admin-config-look-and-feel-themes.js new file mode 100644 index 00000000000..c7fed0b1b77 --- /dev/null +++ b/app/assets/javascripts/admin/addon/routes/admin-config-look-and-feel-themes.js @@ -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"); + } +} diff --git a/app/assets/javascripts/admin/addon/routes/admin-config-look-and-feel.js b/app/assets/javascripts/admin/addon/routes/admin-config-look-and-feel.js new file mode 100644 index 00000000000..01f998a1d1a --- /dev/null +++ b/app/assets/javascripts/admin/addon/routes/admin-config-look-and-feel.js @@ -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"); + } +} diff --git a/app/assets/javascripts/admin/addon/routes/admin-route-map.js b/app/assets/javascripts/admin/addon/routes/admin-route-map.js index 0715007bb69..facec60a4f4 100644 --- a/app/assets/javascripts/admin/addon/routes/admin-route-map.js +++ b/app/assets/javascripts/admin/addon/routes/admin-route-map.js @@ -218,6 +218,9 @@ export default function () { }); this.route("about"); + this.route("lookAndFeel", { path: "/look-and-feel" }, function () { + this.route("themes"); + }); } ); diff --git a/app/assets/javascripts/admin/addon/templates/config-look-and-feel-themes.hbs b/app/assets/javascripts/admin/addon/templates/config-look-and-feel-themes.hbs new file mode 100644 index 00000000000..7106093a3ba --- /dev/null +++ b/app/assets/javascripts/admin/addon/templates/config-look-and-feel-themes.hbs @@ -0,0 +1,8 @@ + + +
+ +
\ No newline at end of file diff --git a/app/assets/javascripts/admin/addon/templates/config-look-and-feel.hbs b/app/assets/javascripts/admin/addon/templates/config-look-and-feel.hbs new file mode 100644 index 00000000000..ad5dfcda6e4 --- /dev/null +++ b/app/assets/javascripts/admin/addon/templates/config-look-and-feel.hbs @@ -0,0 +1,22 @@ + + <:breadcrumbs> + + + + <:tabs> + + + + +
+ {{outlet}} +
\ No newline at end of file diff --git a/app/assets/stylesheets/common/admin/admin_config_area.scss b/app/assets/stylesheets/common/admin/admin_config_area.scss index bb8635fa5da..50ec6c821ad 100644 --- a/app/assets/stylesheets/common/admin/admin_config_area.scss +++ b/app/assets/stylesheets/common/admin/admin_config_area.scss @@ -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 { diff --git a/app/assets/stylesheets/common/admin/admin_intro.scss b/app/assets/stylesheets/common/admin/admin_intro.scss index 1ac8d0f724d..e503d9e7093 100644 --- a/app/assets/stylesheets/common/admin/admin_intro.scss +++ b/app/assets/stylesheets/common/admin/admin_intro.scss @@ -4,7 +4,6 @@ width: 51%; vertical-align: top; margin-left: 20%; - margin-top: 60px; img { display: inline-block; diff --git a/app/assets/stylesheets/common/admin/customize.scss b/app/assets/stylesheets/common/admin/customize.scss index e306399a2d1..6c4bc21f48e 100644 --- a/app/assets/stylesheets/common/admin/customize.scss +++ b/app/assets/stylesheets/common/admin/customize.scss @@ -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); diff --git a/app/assets/stylesheets/common/components/_index.scss b/app/assets/stylesheets/common/components/_index.scss index fa8b57d9dbd..b8d3ed7b2df 100644 --- a/app/assets/stylesheets/common/components/_index.scss +++ b/app/assets/stylesheets/common/components/_index.scss @@ -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"; diff --git a/app/assets/stylesheets/common/components/theme-card.scss b/app/assets/stylesheets/common/components/theme-card.scss new file mode 100644 index 00000000000..8a561dfd406 --- /dev/null +++ b/app/assets/stylesheets/common/components/theme-card.scss @@ -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; + } +} diff --git a/app/controllers/admin/config/look_and_feel_controller.rb b/app/controllers/admin/config/look_and_feel_controller.rb new file mode 100644 index 00000000000..306379e166a --- /dev/null +++ b/app/controllers/admin/config/look_and_feel_controller.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class Admin::Config::LookAndFeelController < Admin::AdminController + def themes + end +end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 4720a325d89..dafc1830bd2 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -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" diff --git a/config/routes.rb b/config/routes.rb index a1a734a2976..48902bd5095 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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