Skip to content

Commit 1f19f12

Browse files
refactor(cmsui): drop the marketplace catalog, keep the add-on search
1 parent 41c8d5d commit 1f19f12

9 files changed

Lines changed: 37 additions & 506 deletions

File tree

packages/cmsui/locales/de/common.json

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,6 @@
6565
"install": "Installieren",
6666
"uninstall": "Deinstallieren",
6767
"upgrade": "Aktualisieren",
68-
"details": "Details",
69-
"catalog": "Katalog",
7068
"updatesAvailable": "Updates verfügbar",
7169
"upgradeFromTo": "Update {{from}} → {{to}}",
7270
"prev": "Zurück",
@@ -75,8 +73,6 @@
7573
"searchPlaceholder": "Erweiterungen filtern…",
7674
"installHint": "Damit neue Add-ons hier erscheinen, füge sie zur Konfiguration hinzu, baue das Projekt neu und starte den Server-Prozess neu. Eine ausführliche Anleitung findest du unter",
7775
"installHintLink": "Ein Add-on eines Drittanbieters installieren",
78-
"pairsWith": "Passt zu",
79-
"opensInNewTab": "(öffnet in neuem Tab)",
8076
"resultsCount": "{{total}} Add-ons gefunden",
8177
"actionError": "Aktion fehlgeschlagen. Bitte erneut versuchen."
8278
},

packages/cmsui/locales/en/common.json

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,6 @@
113113
"install": "Install",
114114
"uninstall": "Uninstall",
115115
"upgrade": "Upgrade",
116-
"details": "Details",
117-
"catalog": "Catalog",
118116
"updatesAvailable": "Updates available",
119117
"upgradeFromTo": "Update {{from}} → {{to}}",
120118
"prev": "Previous",
@@ -123,8 +121,6 @@
123121
"searchPlaceholder": "Filter add-ons…",
124122
"installHint": "To make new add-ons show up here, add them to your configuration, build, and restart the server process. For detailed instructions see",
125123
"installHintLink": "Installing a third party add-on",
126-
"pairsWith": "Pairs with",
127-
"opensInNewTab": "(opens in a new tab)",
128124
"resultsCount": "{{total}} add-ons found",
129125
"actionError": "Action failed. Please try again."
130126
},

packages/cmsui/locales/it/common.json

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,6 @@
103103
"install": "Installa",
104104
"uninstall": "Disinstalla",
105105
"upgrade": "Aggiorna",
106-
"details": "Dettagli",
107-
"catalog": "Catalogo",
108106
"updatesAvailable": "Aggiornamenti disponibili",
109107
"upgradeFromTo": "Aggiorna {{from}} → {{to}}",
110108
"prev": "Precedente",
@@ -113,8 +111,6 @@
113111
"searchPlaceholder": "Filtra i prodotti aggiuntivi…",
114112
"installHint": "Per visualizzare qui nuovi add-on, aggiungili alla configurazione, ricompila e riavvia il processo del server. Per istruzioni dettagliate consulta",
115113
"installHintLink": "Installare un add-on di terze parti",
116-
"pairsWith": "Si abbina a",
117-
"opensInNewTab": "(si apre in una nuova scheda)",
118114
"resultsCount": "{{total}} add-on trovati",
119115
"actionError": "Operazione non riuscita. Riprova."
120116
},
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
Add the add-ons marketplace control panel at `/controlpanel/addons`. @redram404, @veyxos, @richardbenedikt
1+
Add a control panel to search and manage the site's add-ons at `/controlpanel/addons`. @redram404, @veyxos, @richardbenedikt

packages/cmsui/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,7 @@
8383
"tailwind-variants": "catalog:",
8484
"tailwindcss": "catalog:",
8585
"tailwindcss-react-aria-components": "^2.0.0",
86-
"usehooks-ts": "^3.1.1",
87-
"zod": "^3.22.4"
86+
"usehooks-ts": "^3.1.1"
8887
},
8988
"devDependencies": {
9089
"@plone/types": "workspace:*",

packages/cmsui/routes/controlpanelAddons.stories.tsx

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,6 @@ import AddonsControlPanel from './controlpanelAddons';
77

88
type LoaderData = {
99
siteAddons: any[];
10-
query: string;
11-
catalog: any[];
12-
catalogTotal: number;
13-
catalogStart: number;
14-
catalogSize: number;
1510
};
1611

1712
const translations = {
@@ -24,14 +19,10 @@ const translations = {
2419
'cmsui.addons.install': 'Install',
2520
'cmsui.addons.uninstall': 'Uninstall',
2621
'cmsui.addons.upgrade': 'Upgrade',
27-
'cmsui.addons.details': 'Details',
28-
'cmsui.addons.catalog': 'Catalog',
2922
'cmsui.addons.prev': 'Previous',
3023
'cmsui.addons.next': 'Next',
3124
'cmsui.addons.page': '{{current}} / {{total}}',
3225
'cmsui.addons.searchPlaceholder': 'Filter add-ons…',
33-
'cmsui.addons.pairsWith': 'Pairs with',
34-
'cmsui.addons.opensInNewTab': '(opens in a new tab)',
3526
'cmsui.addons.resultsCount': '{{total}} add-ons found',
3627
'cmsui.addons.actionError': 'Action failed. Please try again.',
3728
} as const;
@@ -117,53 +108,12 @@ const SITE_ADDONS = [
117108
},
118109
];
119110

120-
const CATALOG = [
121-
{
122-
id: 'pypi:collective.volto.formsupport',
123-
name: 'collective.volto.formsupport',
124-
title: 'Volto Form Support',
125-
summary: 'Backend support for Volto forms.',
126-
kind: 'backend',
127-
source: 'pypi',
128-
latest_version: '5.0.0',
129-
trust: 'community',
130-
status: 'available',
131-
repo_url: 'https://github.com/collective/collective.volto.formsupport',
132-
homepage: 'https://github.com/collective/collective.volto.formsupport',
133-
pairs_with: ['npm:@kitconcept/volto-form-block'],
134-
},
135-
{
136-
id: 'pypi:eea.facetednavigation',
137-
name: 'eea.facetednavigation',
138-
title: 'EEA Faceted Navigation',
139-
summary: 'Faceted search and navigation for Plone.',
140-
kind: 'backend',
141-
source: 'pypi',
142-
latest_version: '20.0',
143-
trust: 'community',
144-
status: 'available',
145-
repo_url: 'https://github.com/eea/eea.facetednavigation',
146-
homepage: 'https://github.com/eea/eea.facetednavigation',
147-
pairs_with: [],
148-
},
149-
];
150-
151111
const POPULATED: LoaderData = {
152112
siteAddons: SITE_ADDONS,
153-
query: '',
154-
catalog: CATALOG,
155-
catalogTotal: CATALOG.length,
156-
catalogStart: 0,
157-
catalogSize: 24,
158113
};
159114

160115
const EMPTY: LoaderData = {
161116
siteAddons: [],
162-
query: '',
163-
catalog: [],
164-
catalogTotal: 0,
165-
catalogStart: 0,
166-
catalogSize: 24,
167117
};
168118

169119
const createAddonsRouter = (data: LoaderData) =>

packages/cmsui/routes/controlpanelAddons.test.tsx

Lines changed: 27 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -33,34 +33,6 @@ const mockSiteAddons = {
3333
],
3434
};
3535

36-
const mockCatalogResponse = {
37-
query: '',
38-
kind: null,
39-
plone_version: '6.1.0',
40-
volto_version: '19',
41-
compatible: true,
42-
total: 1,
43-
b_start: 0,
44-
b_size: 24,
45-
count: 1,
46-
results: [
47-
{
48-
id: 'pypi:collective.catalog',
49-
name: 'collective.catalog',
50-
title: 'Catalog Add-on',
51-
summary: 'From the marketplace',
52-
kind: 'backend',
53-
source: 'pypi',
54-
latest_version: '3.0.0',
55-
trust: 'community',
56-
status: 'available',
57-
repo_url: 'https://example.com/repo',
58-
homepage: 'https://example.com',
59-
pairs_with: ['npm:@collective/catalog'],
60-
},
61-
],
62-
};
63-
6436
function makeClient(overrides: Record<string, unknown> = {}) {
6537
return {
6638
config: { apiPath, token: 'fake-token' },
@@ -97,107 +69,16 @@ describe('Addons control panel route', () => {
9769
});
9870

9971
describe('loader', () => {
100-
it('fetches site add-ons and queries the marketplace catalog endpoint', async () => {
101-
const fetchMock = vi.fn().mockResolvedValue(
102-
new Response(JSON.stringify(mockCatalogResponse), {
103-
status: 200,
104-
headers: { 'Content-Type': 'application/json' },
105-
}),
106-
);
107-
vi.stubGlobal('fetch', fetchMock);
108-
72+
it('returns the site add-ons from getAddons', async () => {
10973
const client = makeClient();
11074
const result = await callLoader(
11175
client,
112-
'http://example.com/controlpanel/addons?q=catalog',
113-
);
114-
115-
expect(client.getAddons).toHaveBeenCalledOnce();
116-
117-
// The full catalog is always requested (no compatibility filtering).
118-
expect(fetchMock).toHaveBeenCalledOnce();
119-
const [requestedUrl, requestInit] = fetchMock.mock.calls[0];
120-
expect(requestedUrl).toContain(`${apiPath}/@addon-marketplace-search?`);
121-
expect(requestedUrl).toContain('q=catalog');
122-
expect(requestedUrl).toContain('b_start=0');
123-
expect(requestedUrl).toContain('b_size=24');
124-
expect(requestedUrl).toContain('compatible=0');
125-
expect(requestInit.headers.Authorization).toBe('Bearer fake-token');
126-
127-
expect(result.siteAddons).toEqual(mockSiteAddons.items);
128-
expect(result.query).toBe('catalog');
129-
expect(result.catalog).toHaveLength(1);
130-
expect(result.catalog[0].id).toBe('pypi:collective.catalog');
131-
expect(result.catalogTotal).toBe(1);
132-
});
133-
134-
it('clamps a negative b_start to 0 and always requests the full catalog', async () => {
135-
const fetchMock = vi.fn().mockResolvedValue(
136-
new Response(JSON.stringify(mockCatalogResponse), {
137-
status: 200,
138-
headers: { 'Content-Type': 'application/json' },
139-
}),
140-
);
141-
vi.stubGlobal('fetch', fetchMock);
142-
143-
await callLoader(
144-
makeClient(),
145-
'http://example.com/controlpanel/addons?b_start=-5',
146-
);
147-
148-
const [requestedUrl] = fetchMock.mock.calls[0];
149-
expect(requestedUrl).toContain('b_start=0');
150-
expect(requestedUrl).toContain('compatible=0');
151-
expect(requestedUrl).not.toContain('compatible=1');
152-
});
153-
154-
it('falls back to the empty catalog when the endpoint responds non-ok', async () => {
155-
const fetchMock = vi
156-
.fn()
157-
.mockResolvedValue(new Response('nope', { status: 500 }));
158-
vi.stubGlobal('fetch', fetchMock);
159-
160-
const result = await callLoader(
161-
makeClient(),
16276
'http://example.com/controlpanel/addons',
16377
);
16478

165-
expect(result.catalog).toEqual([]);
166-
expect(result.catalogTotal).toBe(0);
167-
// Site add-ons are still returned independently of the catalog failure.
79+
expect(client.getAddons).toHaveBeenCalledOnce();
16880
expect(result.siteAddons).toEqual(mockSiteAddons.items);
16981
});
170-
171-
it('falls back to the empty catalog when the response fails zod validation', async () => {
172-
const fetchMock = vi.fn().mockResolvedValue(
173-
new Response(JSON.stringify({ totally: 'wrong shape' }), {
174-
status: 200,
175-
headers: { 'Content-Type': 'application/json' },
176-
}),
177-
);
178-
vi.stubGlobal('fetch', fetchMock);
179-
180-
const result = await callLoader(
181-
makeClient(),
182-
'http://example.com/controlpanel/addons',
183-
);
184-
185-
expect(result.catalog).toEqual([]);
186-
expect(result.catalogTotal).toBe(0);
187-
});
188-
189-
it('falls back to the empty catalog when fetch throws', async () => {
190-
const fetchMock = vi.fn().mockRejectedValue(new Error('network down'));
191-
vi.stubGlobal('fetch', fetchMock);
192-
193-
const result = await callLoader(
194-
makeClient(),
195-
'http://example.com/controlpanel/addons',
196-
);
197-
198-
expect(result.catalog).toEqual([]);
199-
expect(result.catalogTotal).toBe(0);
200-
});
20182
});
20283

20384
describe('action', () => {
@@ -269,14 +150,7 @@ describe('Addons control panel route', () => {
269150
});
270151

271152
describe('AddonsControlPanel component', () => {
272-
const loaderData = {
273-
siteAddons: mockSiteAddons.items,
274-
query: '',
275-
catalog: mockCatalogResponse.results,
276-
catalogTotal: 1,
277-
catalogStart: 0,
278-
catalogSize: 24,
279-
};
153+
const loaderData = { siteAddons: mockSiteAddons.items };
280154

281155
afterEach(() => {
282156
vi.resetModules();
@@ -286,6 +160,9 @@ describe('Addons control panel route', () => {
286160
// the default component can render in isolation (no router/provider tree).
287161
const renderPanel = async (fetcherData?: unknown, data = loaderData) => {
288162
vi.resetModules();
163+
// resetModules defeats Testing Library's auto-cleanup, so clear any DOM
164+
// left by a previous render to keep each render isolated.
165+
document.body.innerHTML = '';
289166

290167
vi.doMock('@plone/react-router', () => ({
291168
requireAuthCookie: vi.fn().mockResolvedValue('fake-token'),
@@ -311,8 +188,6 @@ describe('Addons control panel route', () => {
311188
...actual,
312189
useLoaderData: () => data,
313190
useNavigate: () => vi.fn(),
314-
useNavigation: () => ({ state: 'idle', location: null }),
315-
useSearchParams: () => [new URLSearchParams(), vi.fn()],
316191
useFetcher: () => ({
317192
state: 'idle',
318193
data: fetcherData,
@@ -327,12 +202,12 @@ describe('Addons control panel route', () => {
327202
'./controlpanelAddons'
328203
);
329204

330-
render(<AddonsControlPanel />);
331-
return screen;
205+
const result = render(<AddonsControlPanel />);
206+
return { screen, ...result };
332207
};
333208

334-
it('groups installed/available site add-ons and renders the catalog', async () => {
335-
const screen = await renderPanel();
209+
it('groups installed and available site add-ons', async () => {
210+
const { screen } = await renderPanel();
336211

337212
// Section headings (i18n falls back to the raw key in tests).
338213
expect(
@@ -341,36 +216,39 @@ describe('Addons control panel route', () => {
341216
expect(
342217
screen.getByRole('heading', { name: 'cmsui.addons.available' }),
343218
).toBeInTheDocument();
344-
expect(
345-
screen.getByRole('heading', { name: 'cmsui.addons.catalog' }),
346-
).toBeInTheDocument();
347219

348220
expect(screen.getByText('Installed Add-on')).toBeInTheDocument();
349221
expect(screen.getByText('Available Add-on')).toBeInTheDocument();
350-
expect(screen.getByText('Catalog Add-on')).toBeInTheDocument();
222+
});
223+
224+
it('filters the list as you type in the search box', async () => {
225+
const { screen } = await renderPanel();
226+
const { fireEvent } = await import('@testing-library/react');
227+
228+
expect(screen.getByText('Installed Add-on')).toBeInTheDocument();
229+
230+
fireEvent.change(screen.getByRole('searchbox'), {
231+
target: { value: 'Available' },
232+
});
233+
234+
expect(screen.getByText('Available Add-on')).toBeInTheDocument();
235+
expect(screen.queryByText('Installed Add-on')).not.toBeInTheDocument();
351236
});
352237

353238
it('shows the action error alert when fetcher.data.ok is false', async () => {
354-
const screen = await renderPanel({ ok: false, error: 'boom' });
239+
const { screen } = await renderPanel({ ok: false, error: 'boom' });
355240

356241
const alerts = screen.getAllByRole('alert');
357242
expect(alerts.length).toBeGreaterThan(0);
358-
// Site-add-on action errors surface the actionError message; locate it by text.
359243
const actionAlert = alerts.find((alert) =>
360244
alert.textContent?.includes('cmsui.addons.actionError'),
361245
);
362246
expect(actionAlert).toBeDefined();
363-
// Raw error detail is surfaced via the title attribute.
364247
expect(actionAlert).toHaveAttribute('title', 'boom');
365248
});
366249

367-
it('shows the empty message when there are no add-ons at all', async () => {
368-
const screen = await renderPanel(undefined, {
369-
...loaderData,
370-
siteAddons: [],
371-
catalog: [],
372-
catalogTotal: 0,
373-
});
250+
it('shows the empty message when there are no add-ons', async () => {
251+
const { screen } = await renderPanel(undefined, { siteAddons: [] });
374252

375253
expect(screen.getByText('cmsui.addons.empty')).toBeInTheDocument();
376254
});

0 commit comments

Comments
 (0)