+ {{#if this.showSearch}}
+
{{d-icon "search"}}
{{/if}}
+
+
+ {{i18n "admin.customize.theme.filter_by"}}
+
+
+
{{#if this.hasThemes}}
- {{#if this.hasActiveThemes}}
+ {{#if (and this.hasActiveThemes (not this.inactiveFilter))}}
{{#each this.activeThemes as |theme|}}
{{/each}}
- {{#if this.hasInactiveThemes}}
-
+ {{#if (and this.hasInactiveThemes (not this.activeFilter))}}
+
{{#if this.selectInactiveMode}}
@@ -101,7 +111,7 @@
{{/if}}
{{/if}}
- {{#if this.hasInactiveThemes}}
+ {{#if (and this.hasInactiveThemes (not this.activeFilter))}}
{{#each this.inactiveThemes as |theme|}}
+
{{i18n "admin.customize.theme.empty"}}
{{/if}}
diff --git a/app/assets/javascripts/admin/addon/components/themes-list.js b/app/assets/javascripts/admin/addon/components/themes-list.js
index 32296a7c0ea..8bf160175b5 100644
--- a/app/assets/javascripts/admin/addon/components/themes-list.js
+++ b/app/assets/javascripts/admin/addon/components/themes-list.js
@@ -5,8 +5,42 @@ import { inject as service } from "@ember/service";
import { classNames } from "@ember-decorators/component";
import DeleteThemesConfirm from "discourse/components/modal/delete-themes-confirm";
import discourseComputed, { bind } from "discourse-common/utils/decorators";
+import I18n from "discourse-i18n";
import { COMPONENTS, THEMES } from "admin/models/theme";
+const ALL_FILTER = "all";
+const ACTIVE_FILTER = "active";
+const INACTIVE_FILTER = "inactive";
+const UPDATES_AVAILABLE_FILTER = "updates_available";
+
+const THEMES_FILTERS = [
+ { name: I18n.t("admin.customize.theme.all_filter"), id: ALL_FILTER },
+ { name: I18n.t("admin.customize.theme.active_filter"), id: ACTIVE_FILTER },
+ {
+ name: I18n.t("admin.customize.theme.inactive_filter"),
+ id: INACTIVE_FILTER,
+ },
+ {
+ name: I18n.t("admin.customize.theme.updates_available_filter"),
+ id: UPDATES_AVAILABLE_FILTER,
+ },
+];
+const COMPONENTS_FILTERS = [
+ { name: I18n.t("admin.customize.component.all_filter"), id: ALL_FILTER },
+ {
+ name: I18n.t("admin.customize.component.enabled_filter"),
+ id: ACTIVE_FILTER,
+ },
+ {
+ name: I18n.t("admin.customize.component.disabled_filter"),
+ id: INACTIVE_FILTER,
+ },
+ {
+ name: I18n.t("admin.customize.component.updates_available_filter"),
+ id: UPDATES_AVAILABLE_FILTER,
+ },
+];
+
@classNames("themes-list")
export default class ThemesList extends Component {
@service router;
@@ -14,7 +48,8 @@ export default class ThemesList extends Component {
THEMES = THEMES;
COMPONENTS = COMPONENTS;
- filterTerm = null;
+ searchTerm = null;
+ filter = ALL_FILTER;
selectInactiveMode = false;
@gt("themesList.length", 0) hasThemes;
@@ -23,12 +58,15 @@ export default class ThemesList extends Component {
@gt("inactiveThemes.length", 0) hasInactiveThemes;
- @gte("themesList.length", 10) showFilter;
+ @gte("themesList.length", 10) showSearch;
@equal("currentTab", THEMES) themesTabActive;
@equal("currentTab", COMPONENTS) componentsTabActive;
+ @equal("filter", ACTIVE_FILTER) activeFilter;
+ @equal("filter", INACTIVE_FILTER) inactiveFilter;
+
@discourseComputed("themes", "components", "currentTab")
themesList(themes, components) {
if (this.themesTabActive) {
@@ -38,13 +76,23 @@ export default class ThemesList extends Component {
}
}
+ @discourseComputed("currentTab")
+ selectableFilters() {
+ if (this.themesTabActive) {
+ return THEMES_FILTERS;
+ } else {
+ return COMPONENTS_FILTERS;
+ }
+ }
+
@discourseComputed(
"themesList",
"currentTab",
"themesList.@each.user_selectable",
"themesList.@each.default",
"themesList.@each.markedToDelete",
- "filterTerm"
+ "searchTerm",
+ "filter"
)
inactiveThemes(themes) {
let results;
@@ -57,7 +105,10 @@ export default class ThemesList extends Component {
(theme) => !theme.get("user_selectable") && !theme.get("default")
);
}
- return this._filterThemes(results, this.filterTerm);
+ if (this.filter === UPDATES_AVAILABLE_FILTER) {
+ results = results.filterBy("isPendingUpdates");
+ }
+ return this._searchThemes(results, this.searchTerm);
}
@discourseComputed("themesList.@each.markedToDelete")
@@ -75,7 +126,8 @@ export default class ThemesList extends Component {
"currentTab",
"themesList.@each.user_selectable",
"themesList.@each.default",
- "filterTerm"
+ "searchTerm",
+ "filter"
)
activeThemes(themes) {
let results;
@@ -96,7 +148,10 @@ export default class ThemesList extends Component {
.localeCompare(b.get("name").toLowerCase());
});
}
- return this._filterThemes(results, this.filterTerm);
+ if (this.filter === UPDATES_AVAILABLE_FILTER) {
+ results = results.filterBy("isPendingUpdates");
+ }
+ return this._searchThemes(results, this.searchTerm);
}
@discourseComputed("themesList.@each.markedToDelete")
someInactiveSelected() {
@@ -111,7 +166,7 @@ export default class ThemesList extends Component {
return this.selectedCount === this.inactiveThemes.length;
}
- _filterThemes(themes, term) {
+ _searchThemes(themes, term) {
term = term?.trim()?.toLowerCase();
if (!term) {
return themes;
@@ -131,8 +186,9 @@ export default class ThemesList extends Component {
if (newTab !== this.currentTab) {
this.set("selectInactiveMode", false);
this.set("currentTab", newTab);
- if (!this.showFilter) {
- this.set("filterTerm", null);
+ this.set("filter", ALL_FILTER);
+ if (!this.showSearch) {
+ this.set("searchTerm", null);
}
}
}
diff --git a/app/assets/javascripts/discourse/tests/acceptance/themes-test.js b/app/assets/javascripts/discourse/tests/acceptance/themes-test.js
index f972c5ee1fa..34acbde0e13 100644
--- a/app/assets/javascripts/discourse/tests/acceptance/themes-test.js
+++ b/app/assets/javascripts/discourse/tests/acceptance/themes-test.js
@@ -215,7 +215,7 @@ acceptance("Theme", function (needs) {
test("can continue installation", async function (assert) {
await visit("/admin/customize/themes");
- await click(".themes-list-container .themes-list-item");
+ await click(".themes-list-container__item .info");
assert.ok(
query(".control-unit .status-message").innerText.includes(
I18n.t("admin.customize.theme.last_attempt")
diff --git a/app/assets/javascripts/discourse/tests/integration/components/themes-list-test.js b/app/assets/javascripts/discourse/tests/integration/components/themes-list-test.js
index cbd23d8cdb8..ff7b25921c5 100644
--- a/app/assets/javascripts/discourse/tests/integration/components/themes-list-test.js
+++ b/app/assets/javascripts/discourse/tests/integration/components/themes-list-test.js
@@ -8,6 +8,7 @@ import {
query,
queryAll,
} from "discourse/tests/helpers/qunit-helpers";
+import selectKit from "discourse/tests/helpers/select-kit-helper";
import I18n from "discourse-i18n";
import Theme, { COMPONENTS, THEMES } from "admin/models/theme";
@@ -59,14 +60,18 @@ module("Integration | Component | themes-list", function (hooks) {
exists(".inactive-indicator"),
"there is no inactive themes separator when all themes are inactive"
);
- assert.strictEqual(count(".themes-list-item"), 5, "displays all themes");
+ assert.strictEqual(
+ count(".themes-list-container__item .info"),
+ 5,
+ "displays all themes"
+ );
[2, 3].forEach((num) => this.themes[num].set("user_selectable", true));
this.themes[4].set("default", true);
this.set("themes", this.themes);
const names = [4, 2, 3, 0, 1].map((num) => this.themes[num].get("name")); // default theme always on top, followed by user-selectable ones and then the rest
assert.deepEqual(
- Array.from(queryAll(".themes-list-item .name")).map((node) =>
+ [...queryAll(".themes-list-container__item .info .name")].map((node) =>
node.innerText.trim()
),
names,
@@ -74,7 +79,7 @@ module("Integration | Component | themes-list", function (hooks) {
);
assert.strictEqual(
queryAll(".inactive-indicator").index(),
- 3,
+ 4,
"the separator is in the right location"
);
@@ -87,12 +92,12 @@ module("Integration | Component | themes-list", function (hooks) {
this.set("themes", []);
assert.strictEqual(
- count(".themes-list-item"),
+ count(".themes-list-container__item .empty"),
1,
"shows one entry with a message when there is nothing to display"
);
assert.strictEqual(
- query(".themes-list-item span.empty").innerText.trim(),
+ query(".themes-list-container__item span.empty").innerText.trim(),
I18n.t("admin.customize.theme.empty"),
"displays the right message"
);
@@ -131,25 +136,25 @@ module("Integration | Component | themes-list", function (hooks) {
assert.notOk(exists(".inactive-indicator"), "there is no separator");
assert.strictEqual(
- count(".themes-list-item"),
+ count(".themes-list-container__item .info"),
5,
"displays all components"
);
this.set("components", []);
assert.strictEqual(
- count(".themes-list-item"),
+ count(".themes-list-container__item .empty"),
1,
"shows one entry with a message when there is nothing to display"
);
assert.strictEqual(
- query(".themes-list-item span.empty").innerText.trim(),
+ query(".themes-list-container__item span.empty").innerText.trim(),
I18n.t("admin.customize.theme.empty"),
"displays the right message"
);
});
- test("themes filter is not visible when there are less than 10 themes", async function (assert) {
+ test("themes search is not visible when there are less than 10 themes", async function (assert) {
const themes = createThemes(9);
this.setProperties({
themes,
@@ -161,12 +166,12 @@ module("Integration | Component | themes-list", function (hooks) {
);
assert.ok(
- !exists(".themes-list-filter"),
- "filter input not shown when we have fewer than 10 themes"
+ !exists(".themes-list-search"),
+ "search input not shown when we have fewer than 10 themes"
);
});
- test("themes filter keeps themes whose names include the filter term", async function (assert) {
+ test("themes search keeps themes whose names include the search term", async function (assert) {
const themes = ["osama", "OsAmaa", "osAMA 1234"]
.map((name) => Theme.create({ name: `Theme ${name}` }))
.concat(createThemes(7));
@@ -179,18 +184,90 @@ module("Integration | Component | themes-list", function (hooks) {
hbs``
);
- assert.ok(exists(".themes-list-filter"));
- await fillIn(".themes-list-filter .filter-input", " oSAma ");
+ assert.ok(exists(".themes-list-container__search-input"));
+ await fillIn(".themes-list-container__search-input", " oSAma ");
assert.deepEqual(
- Array.from(queryAll(".themes-list-item .name")).map((node) =>
+ [...queryAll(".themes-list-container__item .info .name")].map((node) =>
node.textContent.trim()
),
["Theme osama", "Theme OsAmaa", "Theme osAMA 1234"],
- "only themes whose names include the filter term are shown"
+ "only themes whose names include the search term are shown"
);
});
- test("switching between themes and components tabs keeps the filter visible only if both tabs have at least 10 items", async function (assert) {
+ test("themes filter", async function (assert) {
+ const themes = [
+ Theme.create({ name: "Theme enabled 1", user_selectable: true }),
+ Theme.create({ name: "Theme enabled 2", user_selectable: true }),
+ Theme.create({ name: "Theme disabled 1", user_selectable: false }),
+ Theme.create({
+ name: "Theme disabled 2",
+ user_selectable: false,
+ remote_theme: {
+ id: 42,
+ remote_url:
+ "git@github.com:discourse-org/discourse-incomplete-theme.git",
+ commits_behind: 1,
+ },
+ }),
+ ];
+ this.setProperties({
+ themes,
+ currentTab: THEMES,
+ });
+
+ await render(
+ hbs``
+ );
+
+ assert.ok(exists(".themes-list-container__filter-input"));
+ assert.deepEqual(
+ [...queryAll(".themes-list-container__item .info .name")].map((node) =>
+ node.textContent.trim()
+ ),
+ [
+ "Theme enabled 1",
+ "Theme enabled 2",
+ "Theme disabled 1",
+ "Theme disabled 2",
+ ]
+ );
+
+ await selectKit(".themes-list-container__filter-input").expand();
+ await selectKit(".themes-list-container__filter-input").selectRowByValue(
+ "active"
+ );
+ assert.deepEqual(
+ [...queryAll(".themes-list-container__item .info .name")].map((node) =>
+ node.textContent.trim()
+ ),
+ ["Theme enabled 1", "Theme enabled 2"]
+ );
+
+ await selectKit(".themes-list-container__filter-input").expand();
+ await selectKit(".themes-list-container__filter-input").selectRowByValue(
+ "inactive"
+ );
+ assert.deepEqual(
+ [...queryAll(".themes-list-container__item .info .name")].map((node) =>
+ node.textContent.trim()
+ ),
+ ["Theme disabled 1", "Theme disabled 2"]
+ );
+
+ await selectKit(".themes-list-container__filter-input").expand();
+ await selectKit(".themes-list-container__filter-input").selectRowByValue(
+ "updates_available"
+ );
+ assert.deepEqual(
+ [...queryAll(".themes-list-container__item .info .name")].map((node) =>
+ node.textContent.trim()
+ ),
+ ["Theme disabled 2"]
+ );
+ });
+
+ test("switching between themes and components tabs keeps the search visible only if both tabs have at least 10 items", async function (assert) {
const themes = createThemes(10, (n) => {
return { name: `Theme ${n}${n}` };
});
@@ -212,20 +289,20 @@ module("Integration | Component | themes-list", function (hooks) {
hbs``
);
- await fillIn(".themes-list-filter .filter-input", "11");
+ await fillIn(".themes-list-container__search-input", "11");
assert.strictEqual(
- query(".themes-list-container").textContent.trim(),
+ query(".themes-list-container__item .info").textContent.trim(),
"Theme 11",
"only 1 theme is shown"
);
await click(".themes-list-header .components-tab");
assert.ok(
- !exists(".themes-list-filter"),
- "filter input/term do not persist when we switch to the other" +
+ !exists(".themes-list-container__search-input"),
+ "search input/term do not persist when we switch to the other" +
" tab because it has fewer than 10 items"
);
assert.deepEqual(
- Array.from(queryAll(".themes-list-item .name")).map((node) =>
+ [...queryAll(".themes-list-container__item .info .name")].map((node) =>
node.textContent.trim()
),
[
@@ -253,22 +330,22 @@ module("Integration | Component | themes-list", function (hooks) {
)
);
assert.ok(
- exists(".themes-list-filter"),
- "filter is now shown for the components tab"
+ exists(".themes-list-container__search-input"),
+ "search is now shown for the components tab"
);
- await fillIn(".themes-list-filter .filter-input", "66");
+ await fillIn(".themes-list-container__search-input", "66");
assert.strictEqual(
- query(".themes-list-container").textContent.trim(),
+ query(".themes-list-container__item .info").textContent.trim(),
"Component 66",
"only 1 component is shown"
);
await click(".themes-list-header .themes-tab");
assert.strictEqual(
- query(".themes-list-container").textContent.trim(),
+ query(".themes-list-container__item .info").textContent.trim(),
"Theme 66",
- "filter term persisted between tabs because both have more than 10 items"
+ "search term persisted between tabs because both have more than 10 items"
);
});
});
diff --git a/app/assets/stylesheets/common/admin/customize.scss b/app/assets/stylesheets/common/admin/customize.scss
index 8d71216c03d..7c29ccd53f9 100644
--- a/app/assets/stylesheets/common/admin/customize.scss
+++ b/app/assets/stylesheets/common/admin/customize.scss
@@ -246,7 +246,6 @@
}
.themes-list-container {
- overflow-y: auto;
box-sizing: border-box;
max-height: 60vh;
border-bottom-right-radius: var(--d-border-radius);
@@ -262,10 +261,10 @@
border-left: 1px solid var(--primary-low);
width: 100%;
- .themes-list-item:last-child {
+ &__item:last-child {
border-bottom: none;
}
- .themes-list-item {
+ &__item {
color: var(--primary);
border-bottom: 1px solid var(--primary-low);
display: flex;
@@ -391,34 +390,52 @@
width: 100%;
}
}
+ &__filter {
+ padding-left: 0.67em;
+ display: flex;
+ height: 3em;
+ align-items: center;
+ background-color: var(--primary-very-low);
+ }
- .themes-list-filter {
+ &__filter-label {
+ white-space: nowrap;
+ margin-right: 1em;
+ }
+ &__filter-input {
+ margin-right: 0.5em;
+ summary {
+ width: auto;
+ }
+ }
+
+ &__search {
display: flex;
align-items: center;
position: sticky;
top: 0;
- background: var(--secondary);
z-index: z("base");
height: 3em;
+ background: var(--primary-very-low);
.d-icon {
position: absolute;
padding-left: 0.5em;
}
+ }
+ &__search-input {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ border: 0;
+ padding-left: 2em;
+ background-color: var(--primary-very-low);
- .filter-input {
- width: 100%;
- height: 100%;
- margin: 0;
- border: 0;
- padding-left: 2em;
+ &:focus {
+ outline: 0;
- &:focus {
- outline: 0;
-
- ~ .d-icon {
- color: var(--tertiary-hover);
- }
+ ~ .d-icon {
+ color: var(--tertiary-hover);
}
}
}
@@ -483,7 +500,7 @@
left: 0;
right: 0;
z-index: z("fullscreen");
- background-color: var(--secondary);
+ background: var(--secondary);
width: 100%;
padding: 0;
margin: 0;
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 544879944de..5c9589d7c69 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -5278,12 +5278,17 @@ en:
body: "Body"
revert: "Revert Changes"
revert_confirm: "Are you sure you want to revert your changes?"
-
+ component:
+ all_filter: "All"
+ enabled_filter: "Enabled"
+ disabled_filter: "Disabled"
+ updates_available_filter: "Updates Available"
theme:
+ filter_by: "Filter by"
theme: "Theme"
component: "Component"
components: "Components"
- filter_placeholder: "type to filter…"
+ search_placeholder: "type to search…"
theme_name: "Theme name"
component_name: "Component name"
themes_intro: "Select an existing theme or install a new one to get started"
@@ -5457,6 +5462,10 @@ en:
title: "Define theme settings in YAML format"
scss_color_variables_warning: 'Using core SCSS color variables in themes is deprecated. Please use CSS custom properties instead. See this guide for more details.'
scss_warning_inline: "Using core SCSS color variables in themes is deprecated."
+ all_filter: "All"
+ active_filter: "Active"
+ inactive_filter: "Inactive"
+ updates_available_filter: "Updates Available"
colors:
select_base:
title: "Select base color palette"
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 1885d5c984e..c3c880bbab0 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -70,6 +70,10 @@ en:
inline_oneboxer:
topic_page_title_post_number: "#%{post_number}"
topic_page_title_post_number_by_user: "#%{post_number} by %{username}"
+ components:
+ enabled_filter: "Enabled"
+ disabled_filter: "Disabled"
+ updates_available_filter: "Updates Available"
themes:
bad_color_scheme: "Can not update theme, invalid color palette"
other_error: "Something went wrong updating theme"