mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-23 14:14:36 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ebf4b8e0e7 | ||
|
|
0d44a075a6 |
No files matched your search
@@ -9,7 +9,7 @@ import {
|
||||
// The filters of the Marketplace tables. Search, sorting and grouping are kept
|
||||
// by the data table, see data-table-query-params.
|
||||
export const marketplaceFilterQueryParamConfig = {
|
||||
list: ["status", "type", "source"],
|
||||
list: ["status", "type", "source", "author"],
|
||||
} as const satisfies QueryParamConfig;
|
||||
|
||||
export type MarketplaceFilterQueryParams = QueryParamValues<
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { createRef, ref } from "lit/directives/ref";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
FilterPanelController,
|
||||
filterPanelStyles,
|
||||
} from "../../../common/controllers/filter-panel-controller";
|
||||
import {
|
||||
fireEvent,
|
||||
type HASSDomTargetEvent,
|
||||
} from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-check-list-item";
|
||||
import "../../../components/ha-expansion-panel";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-list";
|
||||
import "../../../components/input/ha-input-search";
|
||||
import type { HaInputSearch } from "../../../components/input/ha-input-search";
|
||||
import { haStyleScrollbar } from "../../../resources/styles";
|
||||
import { loadVirtualizer } from "../../../resources/virtualizer";
|
||||
import type { AuthorOption } from "../dashboards/dashboard-repositories";
|
||||
|
||||
// There can be thousands of authors, only the ones in view are rendered
|
||||
@customElement("ha-marketplace-filter-authors")
|
||||
export class HaMarketplaceFilterAuthors extends LitElement {
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
|
||||
@property({ attribute: false }) public authors: AuthorOption[] = [];
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public expanded = false;
|
||||
|
||||
@state() private _search = "";
|
||||
|
||||
private _content = createRef<HTMLElement>();
|
||||
|
||||
private _panel = new FilterPanelController(this, this._content);
|
||||
|
||||
public willUpdate(properties: PropertyValues<this>) {
|
||||
super.willUpdate(properties);
|
||||
|
||||
if (!this.hasUpdated) {
|
||||
loadVirtualizer();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-expansion-panel
|
||||
left-chevron
|
||||
.expanded=${this.expanded}
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
${this.label}
|
||||
${
|
||||
this.value?.length
|
||||
? html`<div class="badge">${this.value.length}</div>
|
||||
<ha-icon-button
|
||||
.path=${mdiFilterVariantRemove}
|
||||
@click=${this._clearFilter}
|
||||
></ha-icon-button>`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</ha-expansion-panel>
|
||||
${
|
||||
this._panel.showContent
|
||||
? html`
|
||||
<div class="content" ${ref(this._content)}>
|
||||
<ha-input-search
|
||||
appearance="outlined"
|
||||
.value=${this._search}
|
||||
@input=${this._handleSearchChange}
|
||||
>
|
||||
</ha-input-search>
|
||||
<ha-list class="ha-scrollbar" multi>
|
||||
<lit-virtualizer
|
||||
.items=${this._filteredAuthors(this.authors, this._search)}
|
||||
.keyFunction=${this._keyFunction}
|
||||
.renderItem=${this._renderItem}
|
||||
@click=${this._handleItemClick}
|
||||
@keydown=${this._handleItemKeydown}
|
||||
>
|
||||
</lit-virtualizer>
|
||||
</ha-list>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
private _filteredAuthors = memoizeOne(
|
||||
(authors: AuthorOption[], search: string): AuthorOption[] => {
|
||||
const query = search.trim().toLowerCase();
|
||||
|
||||
return query
|
||||
? authors.filter((author) => author.value.includes(query))
|
||||
: authors;
|
||||
}
|
||||
);
|
||||
|
||||
private _keyFunction = (author?: AuthorOption) => author?.value;
|
||||
|
||||
private _renderItem = (author?: AuthorOption) =>
|
||||
author
|
||||
? html`<ha-check-list-item
|
||||
tabindex="0"
|
||||
.value=${author.value}
|
||||
.selected=${this.value?.includes(author.value) ?? false}
|
||||
>
|
||||
${author.label}
|
||||
</ha-check-list-item>`
|
||||
: nothing;
|
||||
|
||||
private _handleItemKeydown(ev: KeyboardEvent) {
|
||||
if (ev.key === "Enter" || ev.key === " ") {
|
||||
ev.preventDefault();
|
||||
this._handleItemClick(ev);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleItemClick(ev: Event) {
|
||||
if (!(ev.target instanceof Element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const listItem = ev.target.closest("ha-check-list-item");
|
||||
|
||||
const author = listItem?.value;
|
||||
|
||||
if (!listItem || !author) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = this.value?.includes(author)
|
||||
? this.value.filter((picked) => picked !== author)
|
||||
: [...(this.value ?? []), author];
|
||||
|
||||
this.value = value;
|
||||
listItem.selected = value.includes(author);
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value,
|
||||
items: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private _expandedChanged(ev: CustomEvent<{ expanded: boolean }>) {
|
||||
this.expanded = ev.detail.expanded;
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: HASSDomTargetEvent<HaInputSearch>) {
|
||||
this._search = ev.target.value ?? "";
|
||||
}
|
||||
|
||||
private _clearFilter(ev: Event) {
|
||||
ev.preventDefault();
|
||||
this.value = undefined;
|
||||
fireEvent(this, "data-table-filter-changed", {
|
||||
value: undefined,
|
||||
items: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyleScrollbar,
|
||||
filterPanelStyles,
|
||||
css`
|
||||
ha-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.header ha-icon-button {
|
||||
margin-inline-start: auto;
|
||||
margin-inline-end: 8px;
|
||||
}
|
||||
.badge {
|
||||
display: inline-block;
|
||||
margin-inline-start: 8px;
|
||||
margin-inline-end: 0;
|
||||
min-width: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
background-color: var(--primary-color);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
text-align: center;
|
||||
padding: 0px 2px;
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
ha-check-list-item {
|
||||
width: 100%;
|
||||
}
|
||||
ha-input-search {
|
||||
display: block;
|
||||
padding: var(--ha-space-1) var(--ha-space-2) 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-marketplace-filter-authors": HaMarketplaceFilterAuthors;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { stringCompare } from "../../../common/string/compare";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import { dataTableQueryParamConfig } from "../../../common/url/data-table-query-params";
|
||||
import {
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
import type { DataTableRowData } from "../../../components/data-table/ha-data-table";
|
||||
import type { RepositoryBase } from "../../../data/marketplace/repository";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { repositoryMakers } from "../tools/authors";
|
||||
import type { MarketplaceTab } from "./ha-marketplace-dashboard";
|
||||
|
||||
export const STATUS_ORDER = [
|
||||
@@ -38,6 +40,8 @@ export const TYPE_FILTER = "type";
|
||||
|
||||
export const SOURCE_FILTER = "source";
|
||||
|
||||
export const AUTHOR_FILTER = "author";
|
||||
|
||||
// Where a repository comes from, the community catalog or added from a link
|
||||
export const SOURCE_ORDER = ["catalog", "custom"] as const;
|
||||
|
||||
@@ -46,10 +50,16 @@ type RepositorySource = (typeof SOURCE_ORDER)[number];
|
||||
const repositorySource = (repository: RepositoryBase): RepositorySource =>
|
||||
repository.custom ? "custom" : "catalog";
|
||||
|
||||
// GitHub names are the same person whatever their case
|
||||
const authorKey = (author: string): string => author.toLowerCase();
|
||||
|
||||
// What the filter panes picked, nothing picked in a pane shows everything
|
||||
export type RepositoryFilters = Partial<
|
||||
Record<
|
||||
typeof STATUS_FILTER | typeof TYPE_FILTER | typeof SOURCE_FILTER,
|
||||
| typeof STATUS_FILTER
|
||||
| typeof TYPE_FILTER
|
||||
| typeof SOURCE_FILTER
|
||||
| typeof AUTHOR_FILTER,
|
||||
string[]
|
||||
>
|
||||
>;
|
||||
@@ -185,9 +195,47 @@ const matchesFilters = (
|
||||
return false;
|
||||
}
|
||||
|
||||
const authors = filters[AUTHOR_FILTER];
|
||||
|
||||
if (
|
||||
authors?.length &&
|
||||
!repositoryMakers(repository).some((author) =>
|
||||
authors.includes(authorKey(author))
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export interface AuthorOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
// Everyone who made something, once each, in the order of their names
|
||||
export const repositoryAuthorOptions = (
|
||||
repositories: RepositoryBase[],
|
||||
language?: string
|
||||
): AuthorOption[] => {
|
||||
const options = new Map<string, AuthorOption>();
|
||||
|
||||
for (const repository of repositories) {
|
||||
for (const author of repositoryMakers(repository)) {
|
||||
const value = authorKey(author);
|
||||
|
||||
if (!options.has(value)) {
|
||||
options.set(value, { value, label: author });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...options.values()].sort((a, b) =>
|
||||
stringCompare(a.label, b.label, language)
|
||||
);
|
||||
};
|
||||
|
||||
// Downloaded first, then new ones, then the most starred.
|
||||
const compareRepositories = (a: RepositoryBase, b: RepositoryBase): number => {
|
||||
if (a.installed !== b.installed) {
|
||||
@@ -226,6 +274,7 @@ export const filterRepositories = (
|
||||
translated_source: localize(
|
||||
`ui.panel.marketplace.repository_source.${repositorySource(repository)}`
|
||||
),
|
||||
author: repositoryMakers(repository).join(", "),
|
||||
// A date as text or 0 without one, only numbers sort among each other
|
||||
last_updated_timestamp: timestamp(repository.last_updated),
|
||||
}));
|
||||
@@ -289,6 +338,7 @@ export const browseLinkFromUrl = (
|
||||
[STATUS_FILTER]: lists.status?.filter(Boolean),
|
||||
[TYPE_FILTER]: lists.type?.filter(Boolean),
|
||||
[SOURCE_FILTER]: lists.source?.filter(Boolean),
|
||||
[AUTHOR_FILTER]: lists.author?.filter(Boolean).map(authorKey),
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -57,6 +57,7 @@ import type { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-
|
||||
import type { HomeAssistantUI, Route } from "../../../types";
|
||||
import { showMarketplaceAddFromLink } from "../tools/add-from-link";
|
||||
import "../components/ha-marketplace-discover";
|
||||
import "../components/ha-marketplace-filter-authors";
|
||||
import type { MarketplaceRepositoryMenuItem } from "../components/ha-marketplace-repository-overflow-menu";
|
||||
import {
|
||||
renderRepositoryMenuEntry,
|
||||
@@ -88,10 +89,12 @@ import {
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import {
|
||||
applySkippedUpdates,
|
||||
AUTHOR_FILTER,
|
||||
browseLinkFromUrl,
|
||||
filterRepositories,
|
||||
filtersOfTab,
|
||||
marketplaceUpdateEntityIds,
|
||||
repositoryAuthorOptions,
|
||||
skippedUpdateRepositories,
|
||||
SOURCE_FILTER,
|
||||
SOURCE_ORDER,
|
||||
@@ -467,6 +470,17 @@ export class HaMarketplaceDashboard extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
@data-table-filter-changed=${this._sourceFilterChanged}
|
||||
></ha-filter-states>
|
||||
<ha-marketplace-filter-authors
|
||||
slot="filter-pane"
|
||||
.label=${this._i18n.localize("ui.panel.marketplace.filters.author")}
|
||||
.value=${this._filters[AUTHOR_FILTER]}
|
||||
.authors=${this._authorOptions(
|
||||
this.marketplace.repositories,
|
||||
this._i18n.locale.language
|
||||
)}
|
||||
.narrow=${this.narrow}
|
||||
@data-table-filter-changed=${this._authorFilterChanged}
|
||||
></ha-marketplace-filter-authors>
|
||||
</hass-tabs-subpage-data-table>
|
||||
<ha-dropdown
|
||||
id="repository-overflow-menu"
|
||||
@@ -599,6 +613,14 @@ export class HaMarketplaceDashboard extends LitElement {
|
||||
? html`<div class="secondary">${repository.description}</div>`
|
||||
: nothing,
|
||||
},
|
||||
author: {
|
||||
...defaultKeyData,
|
||||
title: localizeFunc("ui.panel.marketplace.column.author"),
|
||||
sortable: true,
|
||||
defaultHidden: true,
|
||||
hidden: false,
|
||||
flex: 2,
|
||||
},
|
||||
downloads: {
|
||||
...defaultKeyData,
|
||||
title: localizeFunc("ui.panel.marketplace.column.downloads"),
|
||||
@@ -698,7 +720,6 @@ export class HaMarketplaceDashboard extends LitElement {
|
||||
defaultHidden: true,
|
||||
},
|
||||
description: defaultKeyData,
|
||||
authors: defaultKeyData,
|
||||
domain: defaultKeyData,
|
||||
full_name: defaultKeyData,
|
||||
id: defaultKeyData,
|
||||
@@ -979,6 +1000,9 @@ export class HaMarketplaceDashboard extends LitElement {
|
||||
}))
|
||||
);
|
||||
|
||||
// From every repository, so an author picked on one tab can be unpicked on another
|
||||
private _authorOptions = memoize(repositoryAuthorOptions);
|
||||
|
||||
// Statuses and sources group in the order of their filters, not alphabetically
|
||||
private _groupOrder = memoize(
|
||||
(grouping: string | undefined, localize: LocalizeFunc) => {
|
||||
@@ -1025,6 +1049,11 @@ export class HaMarketplaceDashboard extends LitElement {
|
||||
this._writeFilters();
|
||||
}
|
||||
|
||||
private _authorFilterChanged(ev: CustomEvent<{ value?: string[] }>) {
|
||||
this._filters = { ...this._filters, [AUTHOR_FILTER]: ev.detail.value };
|
||||
this._writeFilters();
|
||||
}
|
||||
|
||||
private _handleSearchFilterChanged(ev: CustomEvent) {
|
||||
this._activeSearch = ev.detail.value;
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ import { marketplaceBrandsUrl } from "../../../util/brands-url";
|
||||
import { generateFrontendResourceURL } from "../tools/frontend-resource";
|
||||
import { installBlockedReason } from "../tools/install-blocked-reason";
|
||||
import { typeIcon } from "../tools/type-icon";
|
||||
import { isCommunityOrganization, repositoryAuthors } from "../tools/authors";
|
||||
import { repositoryMakers } from "../tools/authors";
|
||||
import {
|
||||
markdownWithRepositoryContext,
|
||||
repositoryUrl,
|
||||
@@ -429,11 +429,7 @@ export class HaMarketplaceRepositoryDashboard extends LitElement {
|
||||
// The same function while the repository stays, a new one renders it again
|
||||
private _readmeUrl = memoizeOne(repositoryUrl);
|
||||
|
||||
private _getAuthors = memoizeOne((repository: RepositoryInfo) =>
|
||||
repositoryAuthors(repository).filter(
|
||||
(author) => !isCommunityOrganization(author)
|
||||
)
|
||||
);
|
||||
private _getAuthors = memoizeOne(repositoryMakers);
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (this._error) {
|
||||
|
||||
@@ -18,3 +18,9 @@ export const repositoryAuthors = (repository: RepositoryBase): string[] => {
|
||||
|
||||
export const isCommunityOrganization = (author: string): boolean =>
|
||||
COMMUNITY_ORGANIZATIONS.includes(author);
|
||||
|
||||
// The people who made it, a community organization's name says nothing
|
||||
export const repositoryMakers = (repository: RepositoryBase): string[] =>
|
||||
repositoryAuthors(repository).filter(
|
||||
(author) => !isCommunityOrganization(author)
|
||||
);
|
||||
@@ -35,6 +35,7 @@ const marketplaceTableParams = {
|
||||
type: "string?",
|
||||
status: "string?",
|
||||
source: "string?",
|
||||
author: "string?",
|
||||
sort: "string?",
|
||||
direction: "string?",
|
||||
group: "string?",
|
||||
|
||||
@@ -12671,6 +12671,7 @@
|
||||
},
|
||||
"column": {
|
||||
"name": "Repository name",
|
||||
"author": "Author",
|
||||
"downloads": "Downloads",
|
||||
"stars": "Stars",
|
||||
"icon": "Icon",
|
||||
@@ -12693,7 +12694,8 @@
|
||||
"filters": {
|
||||
"status": "Status",
|
||||
"type": "Type",
|
||||
"source": "Source"
|
||||
"source": "Source",
|
||||
"author": "Author"
|
||||
},
|
||||
"dialog_custom_repositories": {
|
||||
"title": "Add from a GitHub link",
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
filterRepositories,
|
||||
filtersOfTab,
|
||||
marketplaceUpdateEntityIds,
|
||||
repositoryAuthorOptions,
|
||||
skippedUpdateRepositories,
|
||||
statusesOfTab,
|
||||
statusFilterOfTab,
|
||||
@@ -30,6 +31,8 @@ const repository = (
|
||||
({
|
||||
id: name,
|
||||
name,
|
||||
full_name: `owner/${name}`,
|
||||
authors: [],
|
||||
category: "integration",
|
||||
status: "default",
|
||||
installed: false,
|
||||
@@ -91,6 +94,29 @@ describe("filterRepositories", () => {
|
||||
).toEqual(expected);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{ author: ["alice"] }, ["Shared", "Theirs"]],
|
||||
[{ author: ["bob"] }, ["Shared"]],
|
||||
[{ author: ["owner"] }, ["Owned"]],
|
||||
[{ author: ["custom-cards"] }, []],
|
||||
[{ author: ["alice"], type: ["theme"] }, ["Theirs"]],
|
||||
])("keeps what is made by %j", (activeFilters, expected) => {
|
||||
expect(
|
||||
names(
|
||||
filterRepositories(
|
||||
[
|
||||
repository("Shared", { authors: ["@Alice", "bob"] }),
|
||||
repository("Theirs", { authors: ["alice"], category: "theme" }),
|
||||
repository("Owned"),
|
||||
repository("Community", { full_name: "custom-cards/card" }),
|
||||
],
|
||||
localize,
|
||||
activeFilters
|
||||
)
|
||||
).sort()
|
||||
).toEqual(expected);
|
||||
});
|
||||
|
||||
it("puts installed first, then new, then the most starred, then by name", () => {
|
||||
const sorted = filterRepositories(
|
||||
[
|
||||
@@ -159,6 +185,20 @@ describe("filterRepositories", () => {
|
||||
expect(row.translated_status).toBe("installed");
|
||||
});
|
||||
|
||||
it.each([
|
||||
[["@alice", "@bob"], "owner/Plain", "alice, bob"],
|
||||
[[], "owner/Plain", "owner"],
|
||||
[[], "custom-cards/Plain", ""],
|
||||
[["@custom-cards", "@alice"], "custom-cards/Plain", "alice"],
|
||||
])("shows %j of %s as made by %j", (authors, fullName, author) => {
|
||||
const [row] = filterRepositories(
|
||||
[repository("Plain", { authors, full_name: fullName })],
|
||||
localize
|
||||
);
|
||||
|
||||
expect(row.author).toBe(author);
|
||||
});
|
||||
|
||||
it("leaves the list it was handed alone", () => {
|
||||
const unsorted = [repository("Beta"), repository("Alpha")];
|
||||
|
||||
@@ -168,13 +208,38 @@ describe("filterRepositories", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("repositoryAuthorOptions", () => {
|
||||
it("lists everyone who made something once, by name", () => {
|
||||
expect(
|
||||
repositoryAuthorOptions(
|
||||
[
|
||||
repository("One", { authors: ["@bob", "Alice"] }),
|
||||
repository("Two", { authors: ["alice"] }),
|
||||
repository("Three", { full_name: "custom-cards/three" }),
|
||||
repository("Four", { full_name: "carol/four" }),
|
||||
],
|
||||
"en"
|
||||
)
|
||||
).toEqual([
|
||||
{ value: "alice", label: "Alice" },
|
||||
{ value: "bob", label: "bob" },
|
||||
{ value: "carol", label: "carol" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("browseUrl", () => {
|
||||
it("links to what browseLinkFromUrl and the table read back", () => {
|
||||
const url = new URL(
|
||||
browseUrl({
|
||||
search: "mushroom card",
|
||||
sorting: { column: "stars", direction: "desc" },
|
||||
filters: { status: ["new"], type: ["theme"], source: ["custom"] },
|
||||
filters: {
|
||||
status: ["new"],
|
||||
type: ["theme"],
|
||||
source: ["custom"],
|
||||
author: ["clooos"],
|
||||
},
|
||||
}),
|
||||
"http://localhost"
|
||||
);
|
||||
@@ -184,10 +249,21 @@ describe("browseUrl", () => {
|
||||
expect(url.searchParams.get("direction")).toBe("desc");
|
||||
expect(browseLinkFromUrl(url.search)).toEqual({
|
||||
search: "mushroom card",
|
||||
filters: { status: ["new"], type: ["theme"], source: ["custom"] },
|
||||
filters: {
|
||||
status: ["new"],
|
||||
type: ["theme"],
|
||||
source: ["custom"],
|
||||
author: ["clooos"],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("reads an author of a link whatever its case", () => {
|
||||
expect(browseLinkFromUrl("?author=Clooos")?.filters.author).toEqual([
|
||||
"clooos",
|
||||
]);
|
||||
});
|
||||
|
||||
it("leaves the direction out when sorting ascending", () => {
|
||||
expect(
|
||||
browseUrl({ sorting: { column: "name", direction: "asc" }, filters: {} })
|
||||
|
||||
@@ -79,8 +79,8 @@ const resetBulkUninstallMocks = () => {
|
||||
|
||||
beforeEach(resetBulkUninstallMocks);
|
||||
|
||||
const openDashboard = async (
|
||||
repositories: unknown[] = [],
|
||||
const openDashboard = async <T extends { id: string; full_name?: string }>(
|
||||
repositories: T[] = [],
|
||||
tab: "discover" | "browse" | "installed" = "browse"
|
||||
) => {
|
||||
host = document.createElement("div");
|
||||
@@ -88,6 +88,11 @@ const openDashboard = async (
|
||||
document.body.append(host);
|
||||
const dashboard = document.createElement("ha-marketplace-dashboard");
|
||||
dashboard.tab = tab;
|
||||
|
||||
// Every repository has a full name, the fixtures only set what they test
|
||||
for (const repository of repositories) {
|
||||
repository.full_name ??= "owner/repository";
|
||||
}
|
||||
dashboard.marketplace = {
|
||||
repositories,
|
||||
info: { categories: [] },
|
||||
|
||||
Reference in new issue
Block a user