Files
frontend/test/layouts/partial-panel-resolver.test.ts
Franck Nijhof 1f98d6ef6b Add Marketplace panel (#54437)
* Add store panel as a copy of the HACS frontend

Verbatim copy of src/ from hacs/frontend at d97dce0 into src/panels/store. Imports still point at the HACS submodule layout and the iframe shell is still present; this is the baseline for the port.

* Rewrite store panel imports and drop the iframe shell

Every import that pointed at the old `homeassistant-frontend` submodule now
resolves inside this repository's `src/` tree. Five components were renamed or
removed upstream, so they point at their current equivalents: `ha-button-menu`
and `ha-menu` to `ha-dropdown`, `ha-md-menu-item` to `ha-dropdown-item`,
`ha-fab` to `ha-button`, and `ha-circular-progress` to `ha-spinner`. The
`@material/mwc-button` and `@material/mwc-linear-progress` side-effect imports
are gone with them; those packages are no longer dependencies here.

`main.ts` becomes `ha-panel-store.ts` and registers `ha-panel-store`. It now
receives `hass`, `narrow`, `route` and `panel` from the panel resolver, so
everything that only made sense inside an iframe is removed: applying themes to
the parent element, forwarding haptic and keydown events to the main window,
bridging `location-changed`, the document-level navigation click handler, the
dialog manager, the injected body stylesheet, and the redirect to the default
route. `entrypoint.ts`, `extra.ts`, `html/` and `version.ts` are deleted for the
same reason.

* Move store panel translations into the frontend translation tree

The panel's own translation bundle is gone. Its keys now live under
`ui.panel.store` in `src/translations/en.json`, and `panel.store` gives the
sidebar its "Community store" title. Every `hacs.localize("some.key")` call
became `hass.localize("ui.panel.store.some.key")`, which means the `localize`
member on the `Hacs` interface, the `HacsLocalizeKeys` type and the localize
bootstrap in `HacsElement` are no longer needed. `TranslationDict` is derived
straight from `en.json`, so the moved keys type-check without any casts.

`dialog_about.frontend_version` did not come along: its only consumer was the
frontend version constant that went away with the iframe shell.

* Hold store panel state in the panel instead of a HACS mixin

The panel element now owns the repository list and the info payload as
internal state, loads both on first update and refreshes them from the
store/subscribe signals through SubscribeMixin. That drops the
HacsElement base class and its logger, so the hacs object handed to the
child components is plain data. Children that used to patch that object
with an update-hacs event now fire store-refresh instead and let the
panel refetch.

Also moves every WebSocket command over to the store/ prefix and trims
the info payload to what the backend still returns.

* Update the store panel to current frontend components

Rebuilds the panel on the components the frontend ships today: the
dialogs get their heading from ha-dialog itself and their buttons from
ha-dialog-footer, the overflow menus become ha-dropdown with
ha-dropdown-item, the download action becomes a fab-slotted ha-button,
and the progress and spinner indicators move to ha-progress-bar and
ha-spinner. brandsUrl loses the dropped useFallback option.

Also clears the eslint backlog the panel came in with: no arrow
functions in templates, no console calls, alt text on the brand images,
and private members named consistently.

* Register the store panel and route old HACS paths

Add the store panel to the panel resolver so the backend can serve it,
and move every internal navigation path in the panel from /hacs to
/store. The /hacsfiles static prefix stays, the backend serves it.

Repoint the hacs_repository my-redirect at the store panel and add a
store_repository alias next to it. Both keep the same target, so the
owner, repository, and category parameters still reach the panel's
redirect handler. Existing README badges keep working.

* Rename HACS to Community store in the store panel

Bring the ported panel in line with the repository conventions: panel
parts are ha-*, dialogs are dialog-* with a matching show-dialog-*
helper, and the typo in the overflow menu filename is fixed.

Rename the code identifiers to match, so the panel talks about a store
instead of HACS: StoreData, StoreInfo, StoreDispatchEvent, a store
property on the child elements, storeStyles and --store-* custom
properties.

Reword every user-facing string that named HACS. Backend names stay put:
the hacs_dispatch_* signal values, the /hacsfiles static prefix, the
hacs_repository my-redirect key, and the hacs.xyz documentation and
catalog infrastructure.

The stale HACS-REPOSITORY-PANEL nodeName check now matches the element
it was always meant to match, so removing a repository from its own page
navigates back again.

* Show a message when the store panel is opened without the integration

The panel is registered on every install while the integration is only set up on some, so an empty store must not try to talk to a backend that is not there.

* Link the Community store from the Settings dashboard

The store panel is hidden from the sidebar, so reach it from the Settings dashboard instead and give its pages a way back.

* Fit the store pages into the main layout

The repository page still sized itself to the viewport and the router recoloured the header with sidebar colours, both leftovers from running inside an iframe. Next to the sidebar that made the page a sidebar-width too wide, pushing the toolbar menu off-screen and un-centering the card.

* Let the quick bar create My links for store repositories

The repository page listened for the "m" key on the document body and opened a My link itself, which fought the quick bar's own "m" shortcut once the panel stopped living in an iframe. The quick bar now resolves a store repository page to the hacs_repository redirect by looking up the repository name, and the panel's handler is gone. The reload after installing a dashboard resource no longer goes through the main window helper, and unused scrollbar styles are dropped.

* Remove the external links from the store menu

The About dialog, the issue tracker link and the link to the HACS GitHub organisation pointed at a project outside Home Assistant. The store is part of Home Assistant now, so they go. The documentation link stays until the documentation moves.

* Fix runtime bugs in the store panel

* Remove external HACS leftovers from the store panel

* Tidy types and conventions in the store panel

* Clean up the store panel translations

* Give the store redirect error screen its hass and narrow properties

Without them the screen's back and refresh buttons render without labels.

* Use the store signal names and local resource URLs

The backend renames these in lockstep: the dispatcher signals lose their
`hacs_dispatch_` prefix, and downloaded dashboard resources are served
from `/local/community/` with a `v` cache-busting parameter instead of
the store-owned `/hacsfiles/` route and `hacstag`.

* Drop the disabled reasons the store backend no longer reports

Setup failures now surface through the config entry state instead of a disabled store, so only the runtime reasons remain.

* Rename the Community store to Marketplace

The panel, its elements, dialogs, data types, WebSocket commands,
dispatcher signals and translations now use marketplace, matching the
renamed backend domain. The hacs_repository My link keeps its slug.

* Finish the Marketplace panel

* Confirm replacing a built-in integration with the download

* Fix README links, older versions and grouping in the Marketplace panel

* Handle list continuations and placeholder characters in Marketplace READMEs

- Indented lines inside a list item are treated as text, so their links and issue references get rewritten.
- A README that already contains the private placeholder characters is left alone.

* Keep indented code inside list items literal in Marketplace READMEs

* Fix the warning title, icon colors and dead subscriptions in the Marketplace panel

- The warning takes its title from the Marketplace translations, it is not empty on a direct visit.
- Icons keep the colors of the menu or chip they are in.
- Dismissing new repositories is offered whatever the filters show.
- The dialogs no longer subscribe to an error signal the backend never sends.
- The custom repositories list names the category in the language of the user.

* Handle the edges of the Marketplace panel more carefully

- The warning can be continued again when the panel did not take over.
- Only a query with an owner and a repository opens a repository as a My link.
- Links to /hacs inside Home Assistant open the Marketplace.
- A rate limit after the download dialog closed opens nothing.
- A removed entry takes its catalog with it.
- The quick bar says why it could not make a My link for a repository.

* Type the Marketplace panel against the backend contract

- The data types match what the backend sends.
- Errors are caught as unknown and read through one helper.
- Events are typed from their declarations.
- Spacing uses the theme tokens.

* Make the wording of Marketplace actions clearer

* Leave the way back from the Marketplace warning to the header

* Show the download progress in the Marketplace download dialog

* Offer to set up a newly downloaded integration right away

* Make the Marketplace download dialog plainer and move other versions to the menu

- One sentence says what the download or update does, no paths or technical details.
- Warnings span the top of the dialog, like in the more-info dialog.
- The progress shows where the update dialog shows it, and the button shows it is busy.
- A restart is only mentioned for updates of integrations.
- Choosing another version moved to the repository menu, it opens on its own when the newest version is too new.

* Use the consume decorator of the frontend in the Marketplace dialogs

* Save the Marketplace search, and keep search and filters per session

* Lay out the Marketplace repository page like the other Settings detail pages

* Bring the Marketplace list page in line with the other Settings lists

* Use the standard Settings dialogs, styles and strings in the Marketplace

* Name the Marketplace data helpers like the rest of the frontend and drop the global refresh event

* Center the Marketplace warning and say how long accepting it lasts

* Call it installing in the Marketplace, and set up right after installing

* Use the uninstall command of the Marketplace

* Fix the row menu, the failed setup screen and README links in the Marketplace panel

* Guide uninstalling an integration that is in use, and offer it after deleting the last entry

* Fix reinstalling, filters, stale pages and README handling in the Marketplace panel

* Explain why an integration in use can not be uninstalled

* Test that accepting the warning and uninstalling in use really happen

* Point HACS links and icons at the Marketplace, and clarify its copy

* Share the error message and menu rendering of the Marketplace panel

* Split the long renders of the Marketplace panel

* Treat Marketplace repository ids as strings

* Guide the user after installing or uninstalling from the Marketplace

* Count down before the Marketplace warning can be accepted, and show it only once

* Show what to do next and who made it on the Marketplace repository page

* Title the Marketplace repository page Marketplace and keep signals one line

* Say which Home Assistant a Marketplace repository requires

"Works with Home Assistant 2025.1 or newer" read as a promise it had been tried there. It is a minimum the repository asks for, so say it that way.

* Explain adding to the Marketplace from a GitHub link

The dialog was called "Custom repositories" and opened on a list of them. Someone adding their first one saw an empty list and two bare fields.

It now says what it is for, and that what is added this way is made by the community like everything else in the Marketplace. Both fields say what they want. The list of what was added from links comes after that, under its own heading, and only once there is any.

* Give the Marketplace Discover, Browse and Installed tabs

The Marketplace was one long table, grouped by status. Finding what you have installed, or what waits for an update, meant scrolling past everything else.

Discover opens with what is popular and what changed lately. Each section links to Browse, sorted and filtered the way the section picked from it. The sorting and filters are in that link, so it works in a new tab or when shared too. Browse is the full table, without the grouping. Installed lists only what is installed, and its tab says how many updates wait.

On the tables, the icon shows a check for what is installed and an arrow for what waits for an update. The search says how many items it looks through. Adding from a GitHub link also got its own button in the toolbar.

Tabs take an optional badge for the update count.

* Find out what is added from a link in the Marketplace

The dialog asks only for the link. What it holds is found out when it is added, and only when that does not settle it, the dialog asks for the type, from what it might be.

* Fix races and lost answers in the Marketplace panel

The add from link dialog could add another link than the one it looked at, a See all link followed a second time kept the filters of the first, Retry did not retry a My link, and a My link that finished late replaced the repository opened meanwhile. A /hacs link from a dashboard showed the Marketplace without its translations. Uninstalling after deleting the last entry could hang when it failed, and a failed removal from the list closed as if it worked. Code inside a quote in a README got issue links.

* Tidy up small things in the Marketplace panel

The README was prepared again for every change of an entity, a screen reader heard the name of a tab but not its badge, and four exports and a type member were left from before.

* Show Marketplace errors in the language of the user and refresh less

Errors from the backend are translated with their key and placeholders in
the panel and when offering to uninstall after deleting an entry, with the
message of Home Assistant as fallback. The repository page reloads after
updating its information, since the list does not carry everything it
shows. Repository signals share one list request, with one more after it
when anything changed meanwhile, and Discover parses each date once.

* Fix the Marketplace strings that showed placeholders and wrong plurals

The add repository question showed {repository} literally, the quotes
around it escaped the placeholder. Seconds, downloads, stars and issues
now say one instead of ones, the status names match across the list and
the repository page, and the GitHub and log messages read the same as
the rest.

* Handle the review of the Marketplace panel

Links to /hacs reach beforeRender again, a route alias was resolved
before it. A refetch on a signal no longer opens the GitHub dialog by
itself, a failed release list is told once, and a failed first load is
translated without logging to the console. Deleting an entry only offers
the uninstall for custom integrations, after the restart and credential
prompts. The YAML resource address carries the version and is encoded.
Applied links mark their history entry instead of listening on the
window, and the last updated column sorts on a number.

* Rewrite Marketplace README addresses once they are rendered

ha-markdown gets an optional rewriteUrl, which runs over every link and
image of the rendered HTML in an inert template, before anything loads.
The README of a repository uses it, so reference links, entities and
unquoted attributes no longer slip through as addresses on Home
Assistant. The regular expressions that set aside links and addresses
for issue references only start where those can start, a README with a
long word or many unclosed links no longer freezes the page.
2026-10-01 11:19:21 -04:00

80 lines
2.3 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
import { navigate } from "../../src/common/navigate";
import "../../src/layouts/partial-panel-resolver";
import type { Panels } from "../../src/types";
const panel = (name: string, urlPath = name) => ({
component_name: name,
url_path: urlPath,
icon: null,
title: null,
config: null,
require_admin: false,
config_panel_domain: undefined,
});
vi.mock("../../src/common/navigate", () => ({ navigate: vi.fn() }));
const routerOptions = (panels: Panels): Record<string, any> => {
const resolver = document.createElement(
"partial-panel-resolver"
) as unknown as Record<string, any>;
return resolver._getRoutes(panels);
};
const routes = (panels: Panels): Record<string, unknown> =>
routerOptions(panels).routes;
describe("partial-panel-resolver", () => {
afterEach(() => {
window.history.replaceState(null, "", "/");
vi.mocked(navigate).mockClear();
});
it("moves a link to the custom integration it replaces to the Marketplace", () => {
window.history.replaceState(
null,
"",
"/hacs/repository/1?category=theme#top"
);
routerOptions({
marketplace: panel("marketplace"),
} as unknown as Panels).beforeRender("hacs");
// The Marketplace loads its translations for the path it is at
expect(navigate).toHaveBeenCalledWith(
"/marketplace/repository/1?category=theme#top",
{ replace: true }
);
});
it("leaves a link to a panel that is really at /hacs alone", () => {
window.history.replaceState(null, "", "/hacs");
routerOptions({
hacs: panel("custom", "hacs"),
marketplace: panel("marketplace"),
} as unknown as Panels).beforeRender("hacs");
expect(navigate).not.toHaveBeenCalled();
});
it("leaves a link to the custom integration it replaces to beforeRender", () => {
// The router resolves a route alias before beforeRender, which would then
// never see the link to move it
expect(
routes({ marketplace: panel("marketplace") } as unknown as Panels).hacs
).toBeUndefined();
});
it("leaves a panel that is really at /hacs alone", () => {
expect(
routes({
hacs: panel("custom", "hacs"),
marketplace: panel("marketplace"),
} as unknown as Panels).hacs
).not.toBe("marketplace");
});
});