the plugins screen becomes /marketplace
`/plugins` redirects rather than 404s: the word is in the dock, in the docs and in whatever anyone bookmarked, and a route that worked yesterday returning nothing reads as the feature having been removed. The API keeps its name — only the screen moved — so the permission entry lists both paths, because a client-side redirect is still a route that has to resolve before it can send anyone anywhere. Also: the fetch stubs in marketplace.test.ts needed `as unknown as typeof fetch`. Bun's `typeof fetch` carries `preconnect`, so a bare stub is not assignable — tests passed while tsgo did not, which is the wrong way round to find out. tsgo clean, frontend builds, 873 pass / 7 fail unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -69,7 +69,11 @@ export function App() {
|
||||
<Route path="/photos" element={<Dashboard.PhotosScreen />} />
|
||||
<Route path="/photos/:section" element={<Dashboard.PhotosScreen />} />
|
||||
<Route path="/app-store" element={<Dashboard.AppStoreScreen />} />
|
||||
<Route path="/plugins" element={<Dashboard.PluginsScreen />} />
|
||||
<Route path="/marketplace" element={<Dashboard.PluginsScreen />} />
|
||||
{/* The screen was `/plugins` until marketplace.officer.dev existed. Redirected rather than
|
||||
dropped: the word appears in the dock, in docs, and in whatever anyone bookmarked, and a
|
||||
404 on a route that worked yesterday reads as the feature having been removed. */}
|
||||
<Route path="/plugins" element={<Navigate to="/marketplace" replace />} />
|
||||
{/* Installed plugins. Core routes above stay hand-written; everything below is generated from
|
||||
what is installed, because a bundler cannot follow a runtime import specifier. The wildcard
|
||||
hands the whole subtree to the plugin's own router, which react-router nests natively. */}
|
||||
|
||||
@@ -186,7 +186,7 @@ export const CORE_DOCK_ITEMS: DockItem[] = [
|
||||
// things that disappears when uninstalled.
|
||||
{ label: 'App store', to: '/app-store', icon: Store, color: '#64748b' },
|
||||
// Core, not contributed by a plugin: this is the screen that installs them, so it cannot arrive with one.
|
||||
{ label: 'Plugins', to: '/plugins', icon: Puzzle, color: '#94a3b8' },
|
||||
{ label: 'Marketplace', to: '/marketplace', icon: Puzzle, color: '#94a3b8' },
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,7 +22,8 @@ const RULES: TitleRule[] = [
|
||||
{ match: (p) => p.startsWith('/calendar'), title: 'Calendar' },
|
||||
{ match: (p) => p.startsWith('/contacts'), title: 'Contacts' },
|
||||
{ match: (p) => p.startsWith('/app-store'), title: 'App store' },
|
||||
{ match: (p) => p.startsWith('/plugins'), title: 'Plugins' },
|
||||
{ match: (p) => p.startsWith('/marketplace'), title: 'Marketplace' },
|
||||
{ match: (p) => p.startsWith('/plugins'), title: 'Marketplace' },
|
||||
{ match: (p) => p.startsWith('/photos'), title: 'Photos' },
|
||||
{ match: (p) => p.startsWith('/jellyfin'), title: 'Video' },
|
||||
{ match: (p) => p.startsWith('/soulseek'), title: 'Soulseek' },
|
||||
|
||||
@@ -385,7 +385,9 @@ const CORE_REGISTRY: Permission[] = [
|
||||
// `offscale` at read without being able to install or remove anything.
|
||||
kind: 'admin',
|
||||
api: ['/plugins'],
|
||||
routes: ['/plugins'],
|
||||
// The API kept its name; only the screen was renamed. `/plugins` stays listed because the redirect
|
||||
// is a real route — a client-side redirect still has to resolve before it can send anyone anywhere.
|
||||
routes: ['/marketplace', '/plugins'],
|
||||
},
|
||||
{
|
||||
key: 'server-admin',
|
||||
|
||||
@@ -24,13 +24,16 @@ const entry = (appName: string) => ({
|
||||
|
||||
const respondWith = (body: unknown, status = 200) => {
|
||||
globalThis.fetch = (async () =>
|
||||
new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } })) as typeof fetch;
|
||||
new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
})) as unknown as typeof fetch;
|
||||
};
|
||||
|
||||
const failWith = (message: string) => {
|
||||
globalThis.fetch = (async () => {
|
||||
throw new Error(message);
|
||||
}) as typeof fetch;
|
||||
}) as unknown as typeof fetch;
|
||||
};
|
||||
|
||||
beforeEach(() => clearCatalogueCache());
|
||||
@@ -71,7 +74,8 @@ describe('fetchCatalogue', () => {
|
||||
});
|
||||
|
||||
test('a proxy serving HTML instead of JSON does not empty the store', async () => {
|
||||
globalThis.fetch = (async () => new Response('<html>502 Bad Gateway</html>', { status: 200 })) as typeof fetch;
|
||||
globalThis.fetch = (async () =>
|
||||
new Response('<html>502 Bad Gateway</html>', { status: 200 })) as unknown as typeof fetch;
|
||||
const entries = await fetchCatalogue();
|
||||
expect(entries.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user