feat(extensions): Render plugin-described UI via extension slots#98
Conversation
Code Review SummaryThis PR introduces a robust extension slot system that allows backend-driven UI contributions. It removes hardcoded logic in onboarding and settings, replacing it with a flexible primitive-based rendering system and a component registry 'escape hatch'. 🚀 Key Improvements
💡 Minor Suggestions
|
| slot, | ||
| integration: integration as SlotContribution['integration'], | ||
| ui, | ||
| order: |
There was a problem hiding this comment.
The logic for calculating the order is specific to onboarding. To keep this composable generic, it's better to default to the specific UI order if available, or fall back to the global default. This makes it easier to add ordering to other slots later.
| order: | |
| order: | |
| ui.onboarding?.order ?? ui.card?.order ?? DEFAULT_ORDER, |
| const card = computed(() => ui.value.card); | ||
| const onboarding = computed(() => ui.value.onboarding); | ||
| const variant = computed(() => props.contribution.slot); | ||
| const href = computed(() => onboarding.value?.href || card.value?.href || null); |
There was a problem hiding this comment.
The priority of href resolution is slightly inconsistent. In the template (line 18-19), it checks href (which includes onboarding), but for the card variant (line 39) it uses href || undefined. Centralizing this logic in the computed property prevents potential dead links.
| const href = computed(() => onboarding.value?.href || card.value?.href || null); | |
| const href = computed(() => ui.value.onboarding?.href || ui.value.card?.href || null); |
| const { contributionsFor } = useExtensionSlots(); | ||
| const onboardingContributions = contributionsFor('onboarding.steps'); | ||
|
|
||
| const builtInSteps = [ |
There was a problem hiding this comment.
Hardcoded step IDs and orders should ideally be defined in a constant or configuration file to avoid magic numbers and make the onboarding flow easier to modify without touching the main page logic.
| const builtInSteps = [ | |
| const BUILT_IN_STEPS = [ | |
| { id: 'language', order: 10 }, | |
| { id: 'wallet', order: 20 }, | |
| { id: 'categories', order: 30 }, | |
| { id: 'complete', order: 1000 } | |
| ]; |
The client now renders UI that backend integrations describe, with no per-plugin code for the common cases. Installed integrations are loaded after login and surfaced into named slots (settings integrations panel and tab bar, onboarding steps, dashboard widgets, sidebar nav). A descriptor renderer draws the common cases (card, connect, gated panel, onboarding step) from a fixed catalog; when a descriptor names a component, a plugin Nuxt layer can register a real one for that slot as an escape hatch. Onboarding is now a data-driven step list: built-in steps and plugin steps merge by order instead of hardcoded branches. Contributions are gated on the server's entitlement decision and shown with a needs-setup state when not yet configured.
2d6212e to
76fe528
Compare
Deploying trakli-dev with
|
| Latest commit: |
76fe528
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://b7c7fb5d.trakli-dev.pages.dev |
| Branch Preview URL: | https://feat-plugin-extension-slots.trakli-dev.pages.dev |
| const icon = computed<Component>(() => { | ||
| const name = integration.value.icon; | ||
| if (name) { | ||
| const pascal = name.replace(/(^\w|[-_ ]\w)/g, (m) => m.replace(/[-_ ]/, '').toUpperCase()); |
There was a problem hiding this comment.
The regex used for PascalCase conversion is slightly inefficient. While functional for short strings like icon names, a more standard approach or utility would be cleaner. More importantly, ensure that lucide[name] check accounts for the case where name might be kebab-case while Lucide exports are PascalCase.
| const pascal = name.replace(/(^\w|[-_ ]\w)/g, (m) => m.replace(/[-_ ]/, '').toUpperCase()); | |
| const pascal = name.split(/[-_ ]/).map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(''); |
| // cookie has settled (fresh login writes it asynchronously) and never let | ||
| // a failure here disrupt the session. | ||
| const { load: loadIntegrations } = useIntegrations(); | ||
| loadIntegrations().catch(() => []); |
There was a problem hiding this comment.
Silencing the error with an empty array in .catch(() => []) might make debugging difficult if the integrations API fails for reasons other than auth (e.g., 500 error). It is better to let useIntegrations handle the error logging (which it does) but perhaps not suppress the promise rejection entirely if upstream needs to know.
| loadIntegrations().catch(() => []); | |
| loadIntegrations(); |
Deploying webui with
|
| Latest commit: |
76fe528
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://2fb9d93f.webui-9fh.pages.dev |
| Branch Preview URL: | https://feat-plugin-extension-slots.webui-9fh.pages.dev |
Closes #95. Part of the plugin-UI extensibility work.
Onboarding steps, settings tabs, dashboard widgets, and sidebar nav were all hardcoded, and there was no integrations client, so nothing a backend plugin contributes could appear without editing core UI. This loads each integration's
uidescriptor from the integrations endpoint and renders it into named slots, so a plugin lands in the right place with no per-plugin UI code.settings.integrations,settings.tabs,onboarding.steps,dashboard.widgets,sidebar.nav. A registry composable collects contributions per slot, ordered and gated.trakli/ui-kitlayer mechanism) for custom UI.Consumes the backend contract in trakli/webservice#270. Host adoption of the ui-kit primitives stays out of scope.