Skip to content

feat(frontend): render backend-declared plugin metadata - #567

Merged
hamza-56 merged 12 commits into
hamza/feat/plugin-frontend-metadatafrom
hamza/feat/plugin-frontend-consumption
Aug 6, 2026
Merged

feat(frontend): render backend-declared plugin metadata#567
hamza-56 merged 12 commits into
hamza/feat/plugin-frontend-metadatafrom
hamza/feat/plugin-frontend-consumption

Conversation

@hamza-56

@hamza-56 hamza-56 commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Part of: Plugin identity and display metadata are split across disconnected backend and frontend registries

Second PR of the sequence proposed in #372, stacked on #566 (hooks + API exposure). The drift gate follows in #568.

What

The frontend stops keeping its own copy of plugin display metadata and renders what the backend declares: PluginDefinition shrinks to the plugin name plus component loaders, and display name, description, icon, and sidebar config come from the user-plugins API.

Changes

  • feat(frontend): PluginDefinition shrinks to what only the frontend can own (name, loadComponent, loadSettingsComponent); displayName, description, icon, category, and the sidebar fields are removed
  • feat(frontend): UserPluginState carries display, sidebar, and has_frontend, typed from the generated OpenAPI schema
  • feat(frontend): sidebar, dashboard cards, settings list, and plugin pages read the backend-declared metadata; backend-only plugins (canvas, open-edx) now show a proper name and description in settings
  • feat(frontend): icons resolve from declared lucide names via lib/plugins/icons.ts; plugins register custom brand icons under their name (slack)
  • fix(frontend): add google-drive to generateStaticParams (was missing)
  • test(frontend): metadata helpers, icon resolution, and settings list rendering

How to Test

  1. cd frontend && bun run test (new suites: lib/tests/plugin-metadata.test.ts, lib/tests/plugin-icons.test.ts, components/settings/ListItem.test.tsx)
  2. Run the app, open the dashboard: sidebar entries (Create Course, Slack TA Bot) and cards come from the API metadata
  3. Open Settings > My Plugins: canvas and open-edx show display names and descriptions instead of slugs and blanks

Notes

  • Stacked on feat(plugins): backend-declared plugin names and frontend metadata #566 (requires the backend hooks and API fields).
  • Removed frontend registry APIs that existed only for the frontend-side metadata copy: getSidebarPlugins, getPluginsByCategory, getCorePlugins, searchPlugins, useSidebarPlugins (all unused outside the registry).
  • No migrations, no env vars, no dependency changes.

This PR description was written with the assistance of an LLM (Claude).

@hamza-56 hamza-56 self-assigned this Jul 31, 2026
@hamza-56
hamza-56 marked this pull request as ready for review August 3, 2026 07:28
@hamza-56

hamza-56 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

@claude-review

@claude

claude Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Claude finished @hamza-56's task in 3m 8s —— View job


Review: render backend-declared plugin metadata

  • Gather context (read changed files & diff)
  • Review metadata/icons helpers & types
  • Review components (sidebar, dashboard, settings, renderer)
  • Review plugin registrations & test coverage
  • Post review findings

Solid, well-scoped refactor. The frontend genuinely stops being a second source of truth for display metadata — PluginDefinition now holds only what the frontend can own (name + loaders), and everything user-facing is read off the API response. The registry/hook cleanup (getSidebarPlugins, getPluginsByCategory, getCorePlugins, searchPlugins, useSidebarPlugins) is a real reduction in surface area, and I confirmed no stale references to any removed field/function remain in app/, components/, lib/, or plugins/. No unused imports left behind (useMemo still used in usePlugins.ts, ComponentType fully removed from AppSidebar.tsx). Docs updated in the same PR. 👍

Things done well

  • sidebarItemsFrom filters before sorting, so .sort() mutates the throwaway array, not the userPlugins prop — correct.
  • Graceful fallbacks throughout: displayNameOf → plugin name, dashboard card → letter avatar when no icon resolves, sidebar titleundefined.
  • The slack import-cycle note (importing registerPluginIcon from the leaf module, not the barrel) is a good catch and well-commented.
  • Helper unit tests (plugin-metadata, plugin-icons) and the ListItem render test cover the core new logic, including the fallback paths.

Suggestions (non-blocking)

Medium — UserPluginState duplicates the generated schema. lib/api/generated.ts already exposes the full response as UserPluginResponse (with display, sidebar, has_frontend, config, …). Since you already source DisplayInfo/SidebarEntry from the schema, typing the whole state as Schema<"UserPluginResponse"> would eliminate the hand-maintained copy and two drifts it introduces: config: Record<string, string> vs generated { [key: string]: unknown }, and has_frontend?: boolean (optional) vs generated required boolean. The optional has_frontend? slightly weakens the dashboard/page.tsx filter typing. Left an inline note. (If #568's drift gate already covers this, ignore.)

Low — unmapped icon names fail silently. icons.ts maps only "plus"; any other backend-declared lucide name resolves to undefined and silently degrades. A dev-only console.warn (or a "register the lucide name in icons.ts" step in the plugin guide) would make the gap visible to plugin authors. Inline suggestion posted.

Low — test coverage gap on behavioral changes. The two highest-regression-risk changes have no test: dashboard filtering on has_frontend, and generateStaticParams gaining google-drive. The helper tests are good; a small render assertion for the dashboard card / sidebar mapping would lock in the resolvePluginIcon + displayNameOf wiring. Not blocking.

Notes

  • Correctly stacked on feat(plugins): backend-declared plugin names and frontend metadata #566 — the display/sidebar/has_frontend fields and the DisplayInfo/SidebarEntry/UserPluginResponse schemas are present in generated.ts on this branch, so the type imports resolve.
  • I did not run bun run test — the frontend node_modules isn't installed in this checkout. The new suites read correctly on inspection; please confirm they pass locally / in CI.

No blocking issues. Nice consolidation.
· hamza/feat/plugin-frontend-consumption

@hamza-56
hamza-56 force-pushed the hamza/feat/plugin-frontend-consumption branch from eb50e7b to 8f4dead Compare August 3, 2026 07:31
Comment thread frontend/lib/plugins/types.ts Outdated
Comment thread frontend/lib/plugins/icons.ts
@hamza-56
hamza-56 force-pushed the hamza/feat/plugin-frontend-consumption branch 2 times, most recently from 7cbe193 to 446c764 Compare August 3, 2026 20:04

@abdulrafey1 abdulrafey1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few small comments:

useEnabledPlugins and the provider's enabledPlugins state are now dead, and they are the frontend-side copy this PR set out to retire. Files: frontend/lib/plugins/context.tsx:27, 146, 318-325

Comment thread frontend/app/dashboard/[pluginName]/page-client.tsx
Comment thread frontend/lib/plugins/metadata.ts Outdated
Comment thread frontend/app/dashboard/[pluginName]/page.tsx Outdated
Comment thread frontend/app/dashboard/page.tsx Outdated
Comment thread frontend/lib/plugins/metadata.ts Outdated
@hamza-56
hamza-56 force-pushed the hamza/feat/plugin-frontend-consumption branch from 446c764 to b35b777 Compare August 4, 2026 11:41
@hamza-56

hamza-56 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

Addressed @abdulrafey1's review comment about useEnabledPlugins: the hook and the provider's enabledPlugins state are removed in c214408 (their only remaining reference was a stale mock in layout.test.tsx). The inline comments are fixed in cb8a30d, e0ec1b7, d4c9a31, and 3cbb82e. Type check, lint, and the full frontend suite (231 tests) pass.

This comment was written with the assistance of an LLM (Claude).

hamza-56 added 11 commits August 6, 2026 12:46
The frontend kept its own copy of every plugin's display name,
description, icon, and sidebar config, drifting from the backend
(#565, item 4 of #372).

- PluginDefinition shrinks to what only the frontend can own: the
  plugin name and the component loaders. Display metadata comes from
  the user-plugins API (display, sidebar, has_frontend), typed via the
  generated OpenAPI schema.
- Sidebar, dashboard cards, settings list, and plugin pages read the
  backend-declared metadata; backend-only plugins (canvas, open-edx)
  now show a proper name and description in settings.
- Icons resolve from declared lucide names via a small registry;
  plugins register custom brand icons (slack) under their name.
- google-drive added to generateStaticParams (was missing).
The AppSidebar test came from main mocking useEnabledPlugins, but the
sidebar now reads userPlugins via usePluginContext, so the mock left the
hook undefined and every case failed to render.
The hand-written interface duplicated UserPluginResponse and had already
drifted from it: config was typed Record<string, string> though the
backend stores JSONB, and has_frontend was optional though the response
always carries it (weakening the dashboard filter).

Typing config values as unknown makes the JSONB reality visible at the
read sites, so add lib/plugins/config helpers to narrow there instead of
asserting: configString for a single text field, stringConfigOf for the
generic editor that renders every field as an input.
sidebarItemsFrom filtered on enabled && sidebar, so a backend plugin
declaring a SidebarEntry without a frontend app still got a nav link,
which 404s under output: export. The registry lookup used to be this
guard before the list moved to backend metadata.

A type-guard filter also lets TS narrow sidebar as non-null through the
sort, removing the unreachable ?? DEFAULT_SIDEBAR_ORDER fallback (order
is required in the generated schema).
Before the user-plugins fetch resolves, userPlugins is empty, so
isEnabled is false and displayName falls back to the slug. The plugin
page and PluginRenderer rendered a 'not enabled'/'disabled' verdict with
the slug in it on every load. Render a spinner while the context is
loading instead.
useEnabledPlugins and the provider's enabledPlugins state had no
consumers left; they were the frontend-side registry projection this
branch set out to retire.
The hardcoded list was a third place a plugin name had to be written
before its page existed under output: export; the registry already holds
exactly the set of renderable plugin pages.
Every other consumer imports displayNameOf from @/lib/plugins; the
dashboard page reached into the leaf modules.
…onent

An unmapped lucide name silently degraded to the icon-less fallback,
invisible to a plugin author who declared the icon backend-side. Outside
production the resolver now logs which name is missing and where to
register it.
Main centralized frontend unit tests under frontend/tests/ while this
branch was in flight; this test was added co-located and the relocation
on main could not know about it.
@hamza-56
hamza-56 force-pushed the hamza/feat/plugin-frontend-consumption branch from 0425dad to c10dcfd Compare August 6, 2026 07:52
@hamza-56
hamza-56 requested a review from abdulrafey1 August 6, 2026 07:54
generateStaticParams in the plugin page (a server component) now
imports from the @/lib/plugins barrel, which re-exports this hooks
module; without the directive the static-export build fails.
@hamza-56
hamza-56 merged commit 2888e93 into main Aug 6, 2026
7 of 8 checks passed
@hamza-56
hamza-56 deleted the hamza/feat/plugin-frontend-consumption branch August 6, 2026 08:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants