FEATURE: Add filter box to the themes/components list (#13767)

This commit is contained in:
Osama Sayegh 2021-07-19 04:33:58 +03:00 committed by GitHub
parent 6d999fb087
commit 1c82989f77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 246 additions and 35 deletions

View File

@ -1,8 +1,9 @@
import { COMPONENTS, THEMES } from "admin/models/theme"; import { COMPONENTS, THEMES } from "admin/models/theme";
import { equal, gt } from "@ember/object/computed"; import { equal, gt, gte } from "@ember/object/computed";
import Component from "@ember/component"; import Component from "@ember/component";
import discourseComputed from "discourse-common/utils/decorators"; import discourseComputed from "discourse-common/utils/decorators";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import { action } from "@ember/object";
export default Component.extend({ export default Component.extend({
router: service(), router: service(),
@ -10,10 +11,12 @@ export default Component.extend({
COMPONENTS, COMPONENTS,
classNames: ["themes-list"], classNames: ["themes-list"],
filterTerm: null,
hasThemes: gt("themesList.length", 0), hasThemes: gt("themesList.length", 0),
hasActiveThemes: gt("activeThemes.length", 0), hasActiveThemes: gt("activeThemes.length", 0),
hasInactiveThemes: gt("inactiveThemes.length", 0), hasInactiveThemes: gt("inactiveThemes.length", 0),
showFilter: gte("themesList.length", 10),
themesTabActive: equal("currentTab", THEMES), themesTabActive: equal("currentTab", THEMES),
componentsTabActive: equal("currentTab", COMPONENTS), componentsTabActive: equal("currentTab", COMPONENTS),
@ -31,28 +34,36 @@ export default Component.extend({
"themesList", "themesList",
"currentTab", "currentTab",
"themesList.@each.user_selectable", "themesList.@each.user_selectable",
"themesList.@each.default" "themesList.@each.default",
"filterTerm"
) )
inactiveThemes(themes) { inactiveThemes(themes) {
let results;
if (this.componentsTabActive) { if (this.componentsTabActive) {
return themes.filter((theme) => theme.get("parent_themes.length") <= 0); results = themes.filter(
(theme) => theme.get("parent_themes.length") <= 0
);
} else {
results = themes.filter(
(theme) => !theme.get("user_selectable") && !theme.get("default")
);
} }
return themes.filter( return this._filterThemes(results, this.filterTerm);
(theme) => !theme.get("user_selectable") && !theme.get("default")
);
}, },
@discourseComputed( @discourseComputed(
"themesList", "themesList",
"currentTab", "currentTab",
"themesList.@each.user_selectable", "themesList.@each.user_selectable",
"themesList.@each.default" "themesList.@each.default",
"filterTerm"
) )
activeThemes(themes) { activeThemes(themes) {
let results;
if (this.componentsTabActive) { if (this.componentsTabActive) {
return themes.filter((theme) => theme.get("parent_themes.length") > 0); results = themes.filter((theme) => theme.get("parent_themes.length") > 0);
} else { } else {
return themes results = themes
.filter((theme) => theme.get("user_selectable") || theme.get("default")) .filter((theme) => theme.get("user_selectable") || theme.get("default"))
.sort((a, b) => { .sort((a, b) => {
if (a.get("default") && !b.get("default")) { if (a.get("default") && !b.get("default")) {
@ -66,16 +77,29 @@ export default Component.extend({
.localeCompare(b.get("name").toLowerCase()); .localeCompare(b.get("name").toLowerCase());
}); });
} }
return this._filterThemes(results, this.filterTerm);
}, },
actions: { _filterThemes(themes, term) {
changeView(newTab) { term = term?.trim()?.toLowerCase();
if (newTab !== this.currentTab) { if (!term) {
this.set("currentTab", newTab); return themes;
}
return themes.filter(({ name }) => name.toLowerCase().includes(term));
},
@action
changeView(newTab) {
if (newTab !== this.currentTab) {
this.set("currentTab", newTab);
if (!this.showFilter) {
this.set("filterTerm", null);
} }
}, }
navigateToTheme(theme) { },
this.router.transitionTo("adminCustomizeThemes.show", theme);
}, @action
navigateToTheme(theme) {
this.router.transitionTo("adminCustomizeThemes.show", theme);
}, },
}); });

View File

@ -15,6 +15,17 @@
</div> </div>
<div class="themes-list-container"> <div class="themes-list-container">
{{#if showFilter}}
<div class="themes-list-filter themes-list-item">
{{input
class="filter-input"
placeholder=(i18n "admin.customize.theme.filter_placeholder")
autocomplete="discourse"
value=(mut filterTerm)
}}
{{d-icon "search"}}
</div>
{{/if}}
{{#if hasThemes}} {{#if hasThemes}}
{{#if hasActiveThemes}} {{#if hasActiveThemes}}
{{#each activeThemes as |theme|}} {{#each activeThemes as |theme|}}

View File

@ -6,26 +6,37 @@ import componentTest, {
import { import {
count, count,
discourseModule, discourseModule,
exists,
query,
queryAll, queryAll,
} from "discourse/tests/helpers/qunit-helpers"; } from "discourse/tests/helpers/qunit-helpers";
import hbs from "htmlbars-inline-precompile"; import hbs from "htmlbars-inline-precompile";
import { click, fillIn } from "@ember/test-helpers";
function createThemes(itemsCount, customAttributesCallback) {
return [...Array(itemsCount)].map((_, i) => {
const attrs = { name: `Theme ${i + 1}` };
if (customAttributesCallback) {
Object.assign(attrs, customAttributesCallback(i + 1));
}
return Theme.create(attrs);
});
}
discourseModule("Integration | Component | themes-list", function (hooks) { discourseModule("Integration | Component | themes-list", function (hooks) {
setupRenderingTest(hooks); setupRenderingTest(hooks);
componentTest("current tab is themes", { componentTest("current tab is themes", {
template: hbs`{{themes-list themes=themes components=components currentTab=currentTab}}`, template: hbs`{{themes-list themes=themes components=components currentTab=currentTab}}`,
beforeEach() { beforeEach() {
this.themes = [1, 2, 3, 4, 5].map((num) => this.themes = createThemes(5);
Theme.create({ name: `Theme ${num}` }) this.components = createThemes(5, (n) => {
); return {
this.components = [1, 2, 3, 4, 5].map((num) => name: `Child ${n}`,
Theme.create({
name: `Child ${num}`,
component: true, component: true,
parentThemes: [this.themes[num - 1]], parentThemes: [this.themes[n - 1]],
parent_themes: [1, 2, 3, 4, 5], parent_themes: [1, 2, 3, 4, 5],
}) };
); });
this.setProperties({ this.setProperties({
themes: this.themes, themes: this.themes,
components: this.components, components: this.components,
@ -94,17 +105,15 @@ discourseModule("Integration | Component | themes-list", function (hooks) {
componentTest("current tab is components", { componentTest("current tab is components", {
template: hbs`{{themes-list themes=themes components=components currentTab=currentTab}}`, template: hbs`{{themes-list themes=themes components=components currentTab=currentTab}}`,
beforeEach() { beforeEach() {
this.themes = [1, 2, 3, 4, 5].map((num) => this.themes = createThemes(5);
Theme.create({ name: `Theme ${num}` }) this.components = createThemes(5, (n) => {
); return {
this.components = [1, 2, 3, 4, 5].map((num) => name: `Child ${n}`,
Theme.create({
name: `Child ${num}`,
component: true, component: true,
parentThemes: [this.themes[num - 1]], parentThemes: [this.themes[n - 1]],
parent_themes: [1, 2, 3, 4, 5], parent_themes: [1, 2, 3, 4, 5],
}) };
); });
this.setProperties({ this.setProperties({
themes: this.themes, themes: this.themes,
components: this.components, components: this.components,
@ -144,4 +153,139 @@ discourseModule("Integration | Component | themes-list", function (hooks) {
); );
}, },
}); });
componentTest(
"themes filter is not visible when there are less than 10 themes",
{
template: hbs`{{themes-list themes=themes components=[] currentTab=currentTab}}`,
beforeEach() {
const themes = createThemes(9);
this.setProperties({
themes,
currentTab: THEMES,
});
},
async test(assert) {
assert.ok(
!exists(".themes-list-filter"),
"filter input not shown when we have fewer than 10 themes"
);
},
}
);
componentTest(
"themes filter keeps themes whose names include the filter term",
{
template: hbs`{{themes-list themes=themes components=[] currentTab=currentTab}}`,
beforeEach() {
const themes = ["osama", "OsAmaa", "osAMA 1234"]
.map((name) => Theme.create({ name: `Theme ${name}` }))
.concat(createThemes(7));
this.setProperties({
themes,
currentTab: THEMES,
});
},
async test(assert) {
assert.ok(exists(".themes-list-filter"));
await fillIn(".themes-list-filter .filter-input", " oSAma ");
assert.deepEqual(
Array.from(queryAll(".themes-list-item .name")).map((node) =>
node.textContent.trim()
),
["Theme osama", "Theme OsAmaa", "Theme osAMA 1234"],
"only themes whose names include the filter term are shown"
);
},
}
);
componentTest(
"switching between themes and components tabs keeps the filter visible only if both tabs have at least 10 items",
{
template: hbs`{{themes-list themes=themes components=components currentTab=currentTab}}`,
beforeEach() {
const themes = createThemes(10, (n) => {
return { name: `Theme ${n}${n}` };
});
const components = createThemes(5, (n) => {
return {
name: `Component ${n}${n}`,
component: true,
parent_themes: [1],
parentThemes: [1],
};
});
this.setProperties({
themes,
components,
currentTab: THEMES,
});
},
async test(assert) {
await fillIn(".themes-list-filter .filter-input", "11");
assert.equal(
query(".themes-list-container").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" +
" tab because it has fewer than 10 items"
);
assert.deepEqual(
Array.from(queryAll(".themes-list-item .name")).map((node) =>
node.textContent.trim()
),
[
"Component 11",
"Component 22",
"Component 33",
"Component 44",
"Component 55",
],
"all components are shown"
);
this.set(
"components",
this.components.concat(
createThemes(5, (n) => {
n += 5;
return {
name: `Component ${n}${n}`,
component: true,
parent_themes: [1],
parentThemes: [1],
};
})
)
);
assert.ok(
exists(".themes-list-filter"),
"filter is now shown for the components tab"
);
await fillIn(".themes-list-filter .filter-input", "66");
assert.equal(
query(".themes-list-container").textContent.trim(),
"Component 66",
"only 1 component is shown"
);
await click(".themes-list-header .themes-tab");
assert.equal(
query(".themes-list-container").textContent.trim(),
"Theme 66",
"filter term persisted between tabs because both have more than 10 items"
);
},
}
);
}); });

View File

@ -295,6 +295,37 @@
width: 100%; width: 100%;
} }
} }
.themes-list-filter {
display: flex;
align-items: center;
position: sticky;
top: 0;
background: var(--secondary);
z-index: z("base");
height: 3em;
.d-icon {
position: absolute;
padding-left: 0.5em;
}
.filter-input {
width: 100%;
height: 100%;
margin: 0;
border: 0;
padding-left: 2em;
&:focus {
outline: 0;
~ .d-icon {
color: var(--tertiary-hover);
}
}
}
}
} }
.theme.settings { .theme.settings {

View File

@ -4230,6 +4230,7 @@ en:
theme: "Theme" theme: "Theme"
component: "Component" component: "Component"
components: "Components" components: "Components"
filter_placeholder: "type to filter…"
theme_name: "Theme name" theme_name: "Theme name"
component_name: "Component name" component_name: "Component name"
themes_intro: "Select an existing theme or install a new one to get started" themes_intro: "Select an existing theme or install a new one to get started"