@webiny/mcp 6.0.0 → 6.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/claude.d.ts +2 -2
- package/agents/claude.js.map +1 -1
- package/agents/cline.d.ts +2 -2
- package/agents/cline.js.map +1 -1
- package/agents/copilot.d.ts +2 -2
- package/agents/copilot.js +1 -1
- package/agents/copilot.js.map +1 -1
- package/agents/cursor.d.ts +2 -2
- package/agents/cursor.js.map +1 -1
- package/agents/instructions.js +1 -1
- package/agents/instructions.js.map +1 -1
- package/agents/kiro.d.ts +15 -0
- package/agents/kiro.js +33 -0
- package/agents/kiro.js.map +1 -0
- package/agents/opencode.d.ts +22 -0
- package/agents/opencode.js +76 -0
- package/agents/opencode.js.map +1 -0
- package/agents/shared.d.ts +5 -5
- package/agents/shared.js +3 -3
- package/agents/shared.js.map +1 -1
- package/agents/windsurf.d.ts +2 -2
- package/agents/windsurf.js.map +1 -1
- package/bin.d.ts +2 -0
- package/bin.js +4 -0
- package/bin.js.map +1 -0
- package/cli/ConfigureMcp.d.ts +7 -14
- package/cli/ConfigureMcp.js +24 -52
- package/cli/ConfigureMcp.js.map +1 -1
- package/cli/McpServer.d.ts +3 -10
- package/cli/McpServer.js +83 -109
- package/cli/McpServer.js.map +1 -1
- package/cli.d.ts +1 -0
- package/cli.js +60 -0
- package/cli.js.map +1 -0
- package/index.d.ts +6 -1
- package/index.js +7 -1
- package/index.js.map +1 -1
- package/package.json +11 -23
- package/skills/admin/admin-architect/SKILL.md +389 -0
- package/skills/admin/ui-extensions/SKILL.md +268 -0
- package/skills/api/api-architect/SKILL.md +189 -0
- package/skills/api/custom-field-type/SKILL.md +263 -0
- package/skills/api/event-handler-pattern/SKILL.md +131 -0
- package/skills/{custom-graphql-api → api/graphql-api}/SKILL.md +3 -3
- package/skills/api/use-case-pattern/SKILL.md +102 -0
- package/skills/cli-extensions/SKILL.md +45 -47
- package/skills/configure-auth0/SKILL.md +4 -4
- package/skills/configure-okta/SKILL.md +3 -3
- package/skills/content-models/SKILL.md +197 -196
- package/skills/dependency-injection/SKILL.md +9 -219
- package/skills/full-stack-architect/SKILL.md +195 -0
- package/skills/generated/admin/SKILL.md +119 -0
- package/skills/generated/admin/aco/SKILL.md +28 -0
- package/skills/generated/admin/build-params/SKILL.md +33 -0
- package/skills/generated/admin/cms/SKILL.md +342 -0
- package/skills/generated/admin/configs/SKILL.md +23 -0
- package/skills/generated/admin/env-config/SKILL.md +30 -0
- package/skills/generated/admin/form/SKILL.md +88 -0
- package/skills/generated/admin/graphql-client/SKILL.md +23 -0
- package/skills/generated/admin/lexical/SKILL.md +105 -0
- package/skills/generated/admin/local-storage/SKILL.md +42 -0
- package/skills/generated/admin/router/SKILL.md +48 -0
- package/skills/generated/admin/security/SKILL.md +63 -0
- package/skills/generated/admin/tenancy/SKILL.md +64 -0
- package/skills/generated/admin/ui/SKILL.md +468 -0
- package/skills/generated/admin/website-builder/SKILL.md +318 -0
- package/skills/generated/api/SKILL.md +40 -0
- package/skills/generated/api/aco/SKILL.md +202 -0
- package/skills/generated/api/build-params/SKILL.md +31 -0
- package/skills/generated/api/cms/SKILL.md +646 -0
- package/skills/generated/api/event-publisher/SKILL.md +31 -0
- package/skills/generated/api/file-manager/SKILL.md +189 -0
- package/skills/generated/api/graphql/SKILL.md +61 -0
- package/skills/generated/api/key-value-store/SKILL.md +31 -0
- package/skills/generated/api/logger/SKILL.md +25 -0
- package/skills/generated/api/opensearch/SKILL.md +39 -0
- package/skills/generated/api/scheduler/SKILL.md +112 -0
- package/skills/generated/api/security/SKILL.md +317 -0
- package/skills/generated/api/system/SKILL.md +34 -0
- package/skills/generated/api/tasks/SKILL.md +31 -0
- package/skills/generated/api/tenancy/SKILL.md +124 -0
- package/skills/generated/api/tenant-manager/SKILL.md +34 -0
- package/skills/generated/api/website-builder/SKILL.md +356 -0
- package/skills/generated/cli/SKILL.md +28 -0
- package/skills/generated/cli/command/SKILL.md +24 -0
- package/skills/generated/extensions/SKILL.md +43 -0
- package/skills/generated/infra/SKILL.md +190 -0
- package/skills/infrastructure-extensions/SKILL.md +3 -2
- package/skills/local-development/SKILL.md +2 -28
- package/skills/project-structure/SKILL.md +78 -56
- package/skills/webiny-sdk/SKILL.md +77 -76
- package/skills/website-builder/SKILL.md +143 -149
- package/ui.d.ts +24 -0
- package/ui.js +31 -0
- package/ui.js.map +1 -0
- package/Extension.d.ts +0 -2
- package/Extension.js +0 -11
- package/Extension.js.map +0 -1
- package/skills/admin-ui-extensions/SKILL.md +0 -267
- package/skills/api-custom-feature/SKILL.md +0 -195
- package/skills/lifecycle-events/SKILL.md +0 -348
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: webiny-admin-architect
|
|
3
|
+
context: webiny-extensions
|
|
4
|
+
description: >
|
|
5
|
+
Admin-side architecture patterns for Webiny extensions. Use this skill when building
|
|
6
|
+
frontend features with headless features (UseCase/Repository/Gateway), presentation
|
|
7
|
+
features (Presenter/ViewModel/hooks/components), MobX-based presenters, RegisterFeature,
|
|
8
|
+
and Admin BuildParams. Covers the admin/ directory structure for both features/ and
|
|
9
|
+
presentation/ layers.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Admin Architecture Patterns
|
|
13
|
+
|
|
14
|
+
## TL;DR
|
|
15
|
+
|
|
16
|
+
Admin extensions are React components that register headless features (business logic with no UI) and presentation features (MobX presenters, React hooks, components). Headless features live in `admin/features/` and follow **UseCase → Repository → Gateway** layering. Presentation features live in `admin/presentation/` and add a **Presenter** (MobX view model) layer on top. Both use `createFeature` and `createAbstraction` from `webiny/admin`.
|
|
17
|
+
|
|
18
|
+
**All features — both headless and presentation — MUST provide a `resolve` function** in `createFeature`. This is how the `useFeature` hook accesses resolved instances from the DI container. Without `resolve`, the feature cannot be consumed from React.
|
|
19
|
+
|
|
20
|
+
## Admin Directory Structure
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
admin/
|
|
24
|
+
├── Extension.tsx # Admin entry point (React component)
|
|
25
|
+
├── features/ # Headless features (business logic, no UI)
|
|
26
|
+
│ └── EnableThing/
|
|
27
|
+
│ ├── abstractions.ts
|
|
28
|
+
│ ├── EnableThingUseCase.ts
|
|
29
|
+
│ ├── EnableThingRepository.ts
|
|
30
|
+
│ ├── EnableThingGateway.ts
|
|
31
|
+
│ └── feature.ts
|
|
32
|
+
└── presentation/ # Presentation layer (hooks, components, presenters)
|
|
33
|
+
└── CurrentThing/
|
|
34
|
+
├── abstractions.ts # Presenter + ViewModel interfaces
|
|
35
|
+
├── CurrentThingPresenter.ts # MobX-based view model
|
|
36
|
+
├── useCurrentThing.ts # React hook for consumers
|
|
37
|
+
├── feature.ts # createFeature registration
|
|
38
|
+
└── components/ # React UI components
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Admin Extension Entry Point
|
|
42
|
+
|
|
43
|
+
The Admin entry point is a React component that registers features, providers, UI decorators, and config:
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
// src/admin/Extension.tsx
|
|
47
|
+
import React from "react";
|
|
48
|
+
import { AdminConfig, RegisterFeature } from "webiny/admin";
|
|
49
|
+
import { CurrentThingFeature } from "./presentation/CurrentThing/feature.js";
|
|
50
|
+
import { EnableThingFeature } from "./features/EnableThing/index.js";
|
|
51
|
+
import { CurrentThingProvider } from "./presentation/CurrentThing/CurrentThingProvider.js";
|
|
52
|
+
import { ThingListView } from "./presentation/ThingListView/ThingListView.js";
|
|
53
|
+
|
|
54
|
+
export const Extension = () => {
|
|
55
|
+
return (
|
|
56
|
+
<>
|
|
57
|
+
{/* Register headless features (use cases, repositories, gateways) */}
|
|
58
|
+
<RegisterFeature feature={EnableThingFeature} />
|
|
59
|
+
|
|
60
|
+
{/* Register presentation features (presenters, view models) */}
|
|
61
|
+
<RegisterFeature feature={CurrentThingFeature} />
|
|
62
|
+
|
|
63
|
+
{/* Providers and UI components */}
|
|
64
|
+
<CurrentThingProvider />
|
|
65
|
+
<ThingListView />
|
|
66
|
+
|
|
67
|
+
{/* Admin config (menus, routes, etc.) */}
|
|
68
|
+
<AdminConfig>{/* Menu items, route definitions, etc. */}</AdminConfig>
|
|
69
|
+
</>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Headless Features (`features/`)
|
|
75
|
+
|
|
76
|
+
Headless features contain business logic with no UI — use cases, repositories, and gateways. They follow the same layering as API features but use `webiny/admin` imports.
|
|
77
|
+
|
|
78
|
+
### Abstractions
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// src/admin/features/EnableThing/abstractions.ts
|
|
82
|
+
import { createAbstraction } from "webiny/admin";
|
|
83
|
+
|
|
84
|
+
export interface IEnableThingUseCase {
|
|
85
|
+
execute(id: string): Promise<void>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const EnableThingUseCase = createAbstraction<IEnableThingUseCase>("EnableThingUseCase");
|
|
89
|
+
|
|
90
|
+
export namespace EnableThingUseCase {
|
|
91
|
+
export type Interface = IEnableThingUseCase;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface IEnableThingRepository {
|
|
95
|
+
execute(id: string): Promise<void>;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const EnableThingRepository =
|
|
99
|
+
createAbstraction<IEnableThingRepository>("EnableThingRepository");
|
|
100
|
+
|
|
101
|
+
export namespace EnableThingRepository {
|
|
102
|
+
export type Interface = IEnableThingRepository;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface IEnableThingGateway {
|
|
106
|
+
enableThing(id: string): Promise<boolean>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export const EnableThingGateway = createAbstraction<IEnableThingGateway>("EnableThingGateway");
|
|
110
|
+
|
|
111
|
+
export namespace EnableThingGateway {
|
|
112
|
+
export type Interface = IEnableThingGateway;
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Feature Registration
|
|
117
|
+
|
|
118
|
+
Headless features **must** provide a `resolve` function that returns the resolved use case (or multiple exports). This is what makes the feature consumable via `useFeature()` in the presentation layer:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
// src/admin/features/EnableThing/feature.ts
|
|
122
|
+
import { createFeature } from "webiny/admin";
|
|
123
|
+
import { EnableThingUseCase as UseCase } from "./abstractions.js";
|
|
124
|
+
import { EnableThingUseCase } from "./EnableThingUseCase.js";
|
|
125
|
+
import { EnableThingRepository } from "./EnableThingRepository.js";
|
|
126
|
+
import { EnableThingGateway } from "./EnableThingGateway.js";
|
|
127
|
+
|
|
128
|
+
export const EnableThingFeature = createFeature({
|
|
129
|
+
name: "EnableThing",
|
|
130
|
+
register(container) {
|
|
131
|
+
container.register(EnableThingUseCase);
|
|
132
|
+
container.register(EnableThingRepository).inSingletonScope();
|
|
133
|
+
container.register(EnableThingGateway).inSingletonScope();
|
|
134
|
+
},
|
|
135
|
+
resolve(container) {
|
|
136
|
+
return {
|
|
137
|
+
useCase: container.resolve(UseCase)
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## The `useFeature` Hook — Bridging DI and React
|
|
144
|
+
|
|
145
|
+
`useFeature` is the standard way to access headless features from React. It resolves the feature from the DI container and returns whatever `resolve` returned:
|
|
146
|
+
|
|
147
|
+
```tsx
|
|
148
|
+
import { useFeature } from "webiny/admin";
|
|
149
|
+
import { EnableThingFeature } from "~/features/EnableThing/feature.js";
|
|
150
|
+
|
|
151
|
+
// In a presentation hook or component:
|
|
152
|
+
const { useCase } = useFeature(EnableThingFeature);
|
|
153
|
+
await useCase.execute(id);
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Presentation hooks wrap `useFeature` to provide a clean, typed API to React components:
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
// src/admin/presentation/EnableThing/useEnableThing.ts
|
|
160
|
+
import { useFeature } from "webiny/admin";
|
|
161
|
+
import { EnableThingFeature } from "~/features/EnableThing/feature.js";
|
|
162
|
+
|
|
163
|
+
export function useEnableThing() {
|
|
164
|
+
const { useCase } = useFeature(EnableThingFeature);
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
enableThing: useCase.execute.bind(useCase)
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Common `useFeature` patterns
|
|
173
|
+
|
|
174
|
+
**Multiple features in one hook:**
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
export function useAuthentication() {
|
|
178
|
+
const { useCase: logInUseCase } = useFeature(LogInFeature);
|
|
179
|
+
const { useCase: logOutUseCase } = useFeature(LogOutFeature);
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
login: logInUseCase.execute.bind(logInUseCase),
|
|
183
|
+
logout: logOutUseCase.execute.bind(logOutUseCase)
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**MobX reactive state synced to React:**
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
export function useIdentity() {
|
|
192
|
+
const { identityContext } = useFeature(IdentityContextFeature);
|
|
193
|
+
|
|
194
|
+
const [identity, setIdentity] = useState(identityContext.getIdentity());
|
|
195
|
+
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
return autorun(() => {
|
|
198
|
+
setIdentity(identityContext.getIdentity());
|
|
199
|
+
});
|
|
200
|
+
}, [identityContext]);
|
|
201
|
+
|
|
202
|
+
return { identity, isAuthenticated: identity.isAuthenticated };
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Presentation Features (`presentation/`)
|
|
207
|
+
|
|
208
|
+
Presentation features contain the UI layer — a **Presenter** (MobX view model) that produces a ViewModel for React, plus hooks and components. The Presenter typically depends on a **headless feature abstraction** (use case or service) for data and actions — it does NOT duplicate the repository/gateway layering.
|
|
209
|
+
|
|
210
|
+
### Typical Pattern: Presenter depends on a headless feature
|
|
211
|
+
|
|
212
|
+
The Presenter injects a headless feature's use case or service as a DI dependency:
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
// src/admin/presentation/CurrentThing/abstractions.ts
|
|
216
|
+
import { createAbstraction } from "webiny/admin";
|
|
217
|
+
import type { MyEntity } from "~/shared/MyEntity.js";
|
|
218
|
+
|
|
219
|
+
export interface ICurrentThingVm {
|
|
220
|
+
loading: boolean;
|
|
221
|
+
entity: MyEntity | undefined;
|
|
222
|
+
error: Error | undefined;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface ICurrentThingPresenter {
|
|
226
|
+
vm: ICurrentThingVm;
|
|
227
|
+
init(): void;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export const CurrentThingPresenter =
|
|
231
|
+
createAbstraction<ICurrentThingPresenter>("CurrentThingPresenter");
|
|
232
|
+
|
|
233
|
+
export namespace CurrentThingPresenter {
|
|
234
|
+
export type Interface = ICurrentThingPresenter;
|
|
235
|
+
export type ViewModel = ICurrentThingVm;
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
// src/admin/presentation/CurrentThing/feature.ts
|
|
241
|
+
import { createFeature } from "webiny/admin";
|
|
242
|
+
import { CurrentThingPresenter as PresenterAbstraction } from "./abstractions.js";
|
|
243
|
+
import { CurrentThingPresenter } from "./CurrentThingPresenter.js";
|
|
244
|
+
|
|
245
|
+
export const CurrentThingFeature = createFeature({
|
|
246
|
+
name: "CurrentThing",
|
|
247
|
+
register(container) {
|
|
248
|
+
container.register(CurrentThingPresenter);
|
|
249
|
+
},
|
|
250
|
+
resolve(container) {
|
|
251
|
+
return {
|
|
252
|
+
presenter: container.resolve(PresenterAbstraction)
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
The Presenter implementation injects the headless feature's abstraction (e.g., a use case):
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
// src/admin/presentation/CurrentThing/CurrentThingPresenter.ts
|
|
262
|
+
import { GetThingUseCase } from "~/features/GetThing/abstractions.js";
|
|
263
|
+
|
|
264
|
+
class CurrentThingPresenterImpl implements CurrentThingPresenter.Interface {
|
|
265
|
+
vm: CurrentThingPresenter.ViewModel = { loading: false, entity: undefined, error: undefined };
|
|
266
|
+
|
|
267
|
+
constructor(private getThingUseCase: GetThingUseCase.Interface) {}
|
|
268
|
+
|
|
269
|
+
async init() {
|
|
270
|
+
this.vm.loading = true;
|
|
271
|
+
const result = await this.getThingUseCase.execute();
|
|
272
|
+
// ... update vm
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export default CurrentThingPresenter.createImplementation({
|
|
277
|
+
implementation: CurrentThingPresenterImpl,
|
|
278
|
+
dependencies: [GetThingUseCase]
|
|
279
|
+
});
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### One-off Pattern: Presenter with its own repository/gateway
|
|
283
|
+
|
|
284
|
+
On rare occasions, when a presentation feature does not contain reusable business logic (no headless feature to depend on), the presentation feature can contain its own repository and gateway alongside the presenter:
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
// src/admin/presentation/Dashboard/feature.ts — one-off, no headless feature
|
|
288
|
+
import { createFeature } from "webiny/admin";
|
|
289
|
+
import { DashboardPresenter as PresenterAbstraction } from "./abstractions.js";
|
|
290
|
+
import { DashboardPresenter } from "./DashboardPresenter.js";
|
|
291
|
+
import { DashboardRepository } from "./DashboardRepository.js";
|
|
292
|
+
import { DashboardGateway } from "./DashboardGateway.js";
|
|
293
|
+
|
|
294
|
+
export const DashboardFeature = createFeature({
|
|
295
|
+
name: "Dashboard",
|
|
296
|
+
register(container) {
|
|
297
|
+
container.register(DashboardPresenter);
|
|
298
|
+
container.register(DashboardRepository).inSingletonScope();
|
|
299
|
+
container.register(DashboardGateway).inSingletonScope();
|
|
300
|
+
},
|
|
301
|
+
resolve(container) {
|
|
302
|
+
return {
|
|
303
|
+
presenter: container.resolve(PresenterAbstraction)
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
> **Prefer the typical pattern.** Only use the one-off pattern when the business logic is truly presentation-specific and will never be reused by other features.
|
|
310
|
+
|
|
311
|
+
## Reading Admin BuildParams
|
|
312
|
+
|
|
313
|
+
There are two ways to read build parameters on the Admin side:
|
|
314
|
+
|
|
315
|
+
### 1. Via `useBuildParams()` hook — in React hooks and components
|
|
316
|
+
|
|
317
|
+
```tsx
|
|
318
|
+
import { useBuildParams } from "webiny/admin/build-params";
|
|
319
|
+
|
|
320
|
+
const MyComponent = () => {
|
|
321
|
+
const buildParams = useBuildParams();
|
|
322
|
+
// Returns T | null — always handle null
|
|
323
|
+
const dashboardUrl = buildParams.get<string>("DASHBOARD_URL");
|
|
324
|
+
|
|
325
|
+
return dashboardUrl ? <a href={dashboardUrl}>Dashboard</a> : null;
|
|
326
|
+
};
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### 2. Via `BuildParams` DI abstraction — in Presenters, Repositories, Gateways
|
|
330
|
+
|
|
331
|
+
When you need build params inside a DI-managed class (Presenter, Repository, etc.), inject `BuildParams` as a dependency:
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
import { BuildParams } from "webiny/admin/build-params";
|
|
335
|
+
|
|
336
|
+
class MyPresenterImpl implements MyPresenter.Interface {
|
|
337
|
+
constructor(private buildParams: BuildParams.Interface) {}
|
|
338
|
+
|
|
339
|
+
get vm() {
|
|
340
|
+
const apiUrl = this.buildParams.get<string>("MY_API_URL");
|
|
341
|
+
// ...
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export default MyPresenter.createImplementation({
|
|
346
|
+
implementation: MyPresenterImpl,
|
|
347
|
+
dependencies: [BuildParams]
|
|
348
|
+
});
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
> **Note:** `buildParams.get<T>()` returns `T | null` — always handle the null case.
|
|
352
|
+
|
|
353
|
+
> BuildParam _declarations_ (`<Admin.BuildParam>`) live in the top-level extension component — see the **webiny-full-stack-architect** skill.
|
|
354
|
+
|
|
355
|
+
## Core APIs
|
|
356
|
+
|
|
357
|
+
### `createAbstraction<T>(name: string)`
|
|
358
|
+
|
|
359
|
+
Creates a typed DI token for admin-side abstractions.
|
|
360
|
+
|
|
361
|
+
| Import | `import { createAbstraction } from "webiny/admin"` |
|
|
362
|
+
| ------- | -------------------------------------------------- |
|
|
363
|
+
| Returns | `Abstraction<T>` |
|
|
364
|
+
|
|
365
|
+
### `createFeature(def)`
|
|
366
|
+
|
|
367
|
+
Creates a feature definition for the admin runtime.
|
|
368
|
+
|
|
369
|
+
| Import | `import { createFeature } from "webiny/admin"` |
|
|
370
|
+
| ------------------------- | ---------------------------------------------------------------- |
|
|
371
|
+
| `def.name` | Unique feature name (convention: `"AppName/FeatureName"`) |
|
|
372
|
+
| `def.register(container)` | Called at startup with the DI `Container` instance |
|
|
373
|
+
| `def.resolve(container)` | **Required.** Resolves abstractions for `useFeature()` consumers |
|
|
374
|
+
|
|
375
|
+
## Key Rules
|
|
376
|
+
|
|
377
|
+
1. **Abstractions first** — any new business logic MUST be encapsulated in `createAbstraction` + `createFeature`.
|
|
378
|
+
2. **Namespace convention** — every abstraction exports `namespace MyAbstraction { export type Interface = ...; }`.
|
|
379
|
+
3. **`resolve` is mandatory** — every `createFeature` (headless or presentation) MUST provide a `resolve` function. This is how `useFeature()` accesses resolved instances from DI.
|
|
380
|
+
4. **Headless vs Presentation** — business logic (use cases, repos, gateways) goes in `features/`; UI layer (presenters, hooks, components) goes in `presentation/`.
|
|
381
|
+
5. **Scoping** — use cases = transient (default), repositories/gateways = singleton (`.inSingletonScope()`).
|
|
382
|
+
6. **`useFeature` is the bridge** — presentation hooks call `useFeature(SomeFeature)` to get resolved exports, then wrap them for React consumption.
|
|
383
|
+
7. **Import extensions** — always use `.js` extensions in import paths (ESM).
|
|
384
|
+
|
|
385
|
+
## Related Skills
|
|
386
|
+
|
|
387
|
+
- **webiny-full-stack-architect** — Top-level component, shared domain layer, BuildParam declarations
|
|
388
|
+
- **webiny-dependency-injection** — The `createImplementation` DI pattern and injectable services
|
|
389
|
+
- **webiny-admin-ui-extensions** — Admin UI customization, decorators, theming, forms, and config
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: webiny-admin-ui-extensions
|
|
3
|
+
context: webiny-extensions
|
|
4
|
+
description: >
|
|
5
|
+
Customizing the Webiny Admin UI -- white-labeling, custom data list columns, page-type forms,
|
|
6
|
+
and Lexical editor plugins. Use this skill when the developer wants to change branding (logo,
|
|
7
|
+
title, theme colors), add custom columns to content entry list views, create custom forms
|
|
8
|
+
for Website Builder page types, or extend the Lexical rich text editor. Covers AdminConfig,
|
|
9
|
+
ContentEntryListConfig, Browser.Table.Column, Bind, useForm, and form validation.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Admin UI Extensions
|
|
13
|
+
|
|
14
|
+
## TL;DR
|
|
15
|
+
|
|
16
|
+
Admin extensions customize the Webiny Admin application. There are three main categories: **white-labeling** (logos, titles, theme colors), **custom data list columns** (adding columns to content entry tables), and **custom page-type forms** (custom form fields for Website Builder page types). All are React components registered via `<Admin.Extension>` in `webiny.config.tsx`.
|
|
17
|
+
|
|
18
|
+
## White-Labeling
|
|
19
|
+
|
|
20
|
+
### Theme Colors
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
// extensions/AdminBranding/AdminTheme.tsx
|
|
24
|
+
import React from "react";
|
|
25
|
+
import { AdminConfig } from "webiny/admin/configs";
|
|
26
|
+
|
|
27
|
+
const { Theme } = AdminConfig;
|
|
28
|
+
|
|
29
|
+
const AdminTheme = () => {
|
|
30
|
+
return (
|
|
31
|
+
<AdminConfig.Public>
|
|
32
|
+
<Theme.Color palette={"primary"} color={"purple"} />
|
|
33
|
+
<Theme.Color palette={"secondary"} color={"green"} />
|
|
34
|
+
</AdminConfig.Public>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export default AdminTheme;
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- `palette` -- `"primary"`, `"secondary"`, `"neutral"`, etc.
|
|
42
|
+
- `color` -- any CSS color value: named colors, hex (`"#6B46C1"`), or RGB.
|
|
43
|
+
|
|
44
|
+
### Logo and Title
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
// extensions/AdminBranding/AdminTitleLogo.tsx
|
|
48
|
+
import React from "react";
|
|
49
|
+
import { AdminConfig } from "webiny/admin/configs";
|
|
50
|
+
import squareLogo from "./logo.png";
|
|
51
|
+
import horizontalLogo from "./logo.png";
|
|
52
|
+
|
|
53
|
+
const { Title, Logo } = AdminConfig;
|
|
54
|
+
|
|
55
|
+
const AdminTitleLogo = () => {
|
|
56
|
+
return (
|
|
57
|
+
<AdminConfig.Public>
|
|
58
|
+
<Title value={"ACME Corp"} />
|
|
59
|
+
<Logo
|
|
60
|
+
squareLogo={<img src={squareLogo} alt={"ACME Corp"} />}
|
|
61
|
+
horizontalLogo={<img src={horizontalLogo} alt={"ACME Corp"} />}
|
|
62
|
+
/>
|
|
63
|
+
</AdminConfig.Public>
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export default AdminTitleLogo;
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Register both:
|
|
71
|
+
|
|
72
|
+
```tsx
|
|
73
|
+
<Admin.Extension src={"/extensions/AdminBranding/AdminTheme.tsx"} />
|
|
74
|
+
<Admin.Extension src={"/extensions/AdminBranding/AdminTitleLogo.tsx"} />
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Available AdminConfig Components
|
|
78
|
+
|
|
79
|
+
| Component | Purpose |
|
|
80
|
+
| ------------------------------------------------ | ------------------------------- |
|
|
81
|
+
| `<Theme.Color palette="..." color="..." />` | Set theme color palette |
|
|
82
|
+
| `<Title value="..." />` | Set the Admin app title |
|
|
83
|
+
| `<Logo squareLogo={...} horizontalLogo={...} />` | Set square and horizontal logos |
|
|
84
|
+
|
|
85
|
+
All must be wrapped in `<AdminConfig.Public>`.
|
|
86
|
+
|
|
87
|
+
## Custom Data List Columns
|
|
88
|
+
|
|
89
|
+
Add custom columns to the content entry list view in the Admin UI. Columns can be restricted to specific content models.
|
|
90
|
+
|
|
91
|
+
### Full Example: Email Columns for Contact Submissions
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
// extensions/contactSubmission/EmailEntryListColumn.tsx
|
|
95
|
+
import React from "react";
|
|
96
|
+
import { ContentEntryListConfig } from "webiny/admin/cms/entry/list";
|
|
97
|
+
|
|
98
|
+
const { Browser } = ContentEntryListConfig;
|
|
99
|
+
|
|
100
|
+
// Custom cell component for the Email Type column
|
|
101
|
+
interface ContactSubmissionTableRow {
|
|
102
|
+
values: {
|
|
103
|
+
emailType: "work" | "personal";
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const EmailTypeCell = () => {
|
|
108
|
+
const { useTableRow, isFolderRow } = ContentEntryListConfig.Browser.Table.Column;
|
|
109
|
+
const { row } = useTableRow<ContactSubmissionTableRow>();
|
|
110
|
+
|
|
111
|
+
if (isFolderRow(row)) {
|
|
112
|
+
return <>{"-"}</>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const emailType = row.data.values.emailType;
|
|
116
|
+
return emailType === "work" ? <>{"Business"}</> : <>{"Personal"}</>;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// Main extension component
|
|
120
|
+
const EmailEntryListColumn = () => {
|
|
121
|
+
return (
|
|
122
|
+
<ContentEntryListConfig>
|
|
123
|
+
{/* Simple column using path (no custom cell needed) */}
|
|
124
|
+
<Browser.Table.Column
|
|
125
|
+
name={"email"}
|
|
126
|
+
after={"name"}
|
|
127
|
+
path={"values.email"}
|
|
128
|
+
header={"Email"}
|
|
129
|
+
modelIds={["contactSubmission"]}
|
|
130
|
+
/>
|
|
131
|
+
{/* Custom cell column */}
|
|
132
|
+
<Browser.Table.Column
|
|
133
|
+
name={"emailType"}
|
|
134
|
+
after={"email"}
|
|
135
|
+
header={"Email Type"}
|
|
136
|
+
modelIds={["contactSubmission"]}
|
|
137
|
+
cell={<EmailTypeCell />}
|
|
138
|
+
/>
|
|
139
|
+
</ContentEntryListConfig>
|
|
140
|
+
);
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export default EmailEntryListColumn;
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Register:
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
<Admin.Extension src={"/extensions/contactSubmission/EmailEntryListColumn.tsx"} />
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Column Props Reference
|
|
153
|
+
|
|
154
|
+
| Prop | Type | Description |
|
|
155
|
+
| ---------- | -------------- | ------------------------------------------------------------------------- |
|
|
156
|
+
| `name` | `string` | Unique column identifier |
|
|
157
|
+
| `header` | `string` | Column header text |
|
|
158
|
+
| `path` | `string` | Dot-path to the data field (e.g., `"values.email"`) -- for simple columns |
|
|
159
|
+
| `cell` | `ReactElement` | Custom React component for complex rendering |
|
|
160
|
+
| `modelIds` | `string[]` | Restrict column to specific content models |
|
|
161
|
+
| `after` | `string` | Position this column after another column by name |
|
|
162
|
+
|
|
163
|
+
### Custom Cell Hooks
|
|
164
|
+
|
|
165
|
+
Inside a custom `cell` component:
|
|
166
|
+
|
|
167
|
+
- `useTableRow<T>()` -- access the full row data, typed with your interface
|
|
168
|
+
- `isFolderRow(row)` -- check if the current row is a folder (return placeholder content)
|
|
169
|
+
|
|
170
|
+
## Custom Page-Type Forms
|
|
171
|
+
|
|
172
|
+
Create custom forms for Website Builder page types using Webiny's form components:
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
// extensions/customPageTypes/RetailPageForm.tsx
|
|
176
|
+
import React from "react";
|
|
177
|
+
import { Grid, Input, Select } from "webiny/admin/ui";
|
|
178
|
+
import { pagePathFromTitle } from "webiny/admin/website-builder";
|
|
179
|
+
import type { FormApi } from "webiny/admin/form";
|
|
180
|
+
import { Bind, UnsetOnUnmount, useForm, validation } from "webiny/admin/form";
|
|
181
|
+
|
|
182
|
+
const generatePath = (form: FormApi) => () => {
|
|
183
|
+
const title = form.getValue("properties.title");
|
|
184
|
+
const language = form.getValue("extensions.language");
|
|
185
|
+
|
|
186
|
+
const titlePath = pagePathFromTitle(title ?? "");
|
|
187
|
+
const parts = [language, titlePath].filter(Boolean);
|
|
188
|
+
|
|
189
|
+
form.setValue("properties.path", `/${parts.join("/")}`);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export const RetailPageForm = () => {
|
|
193
|
+
const form = useForm();
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<>
|
|
197
|
+
<Grid.Column span={12}>
|
|
198
|
+
<UnsetOnUnmount name={"properties.title"}>
|
|
199
|
+
<Bind name={"properties.title"} validators={[validation.create("required")]}>
|
|
200
|
+
<Input label={"Title"} onBlur={generatePath(form)} />
|
|
201
|
+
</Bind>
|
|
202
|
+
</UnsetOnUnmount>
|
|
203
|
+
</Grid.Column>
|
|
204
|
+
<Grid.Column span={12}>
|
|
205
|
+
<UnsetOnUnmount name={"extensions.language"}>
|
|
206
|
+
<Bind
|
|
207
|
+
name={"extensions.language"}
|
|
208
|
+
validators={[validation.create("required")]}
|
|
209
|
+
afterChange={generatePath(form)}
|
|
210
|
+
>
|
|
211
|
+
<Select
|
|
212
|
+
placeholder={"Select a language"}
|
|
213
|
+
label={"Language"}
|
|
214
|
+
options={[
|
|
215
|
+
{ label: "English", value: "en" },
|
|
216
|
+
{ label: "German", value: "de" },
|
|
217
|
+
{ label: "French", value: "fr" }
|
|
218
|
+
]}
|
|
219
|
+
/>
|
|
220
|
+
</Bind>
|
|
221
|
+
</UnsetOnUnmount>
|
|
222
|
+
</Grid.Column>
|
|
223
|
+
<Grid.Column span={12}>
|
|
224
|
+
<UnsetOnUnmount name={"properties.path"}>
|
|
225
|
+
<Bind name={"properties.path"} validators={[validation.create("required")]}>
|
|
226
|
+
<Input label={"Path"} />
|
|
227
|
+
</Bind>
|
|
228
|
+
</UnsetOnUnmount>
|
|
229
|
+
</Grid.Column>
|
|
230
|
+
</>
|
|
231
|
+
);
|
|
232
|
+
};
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Form Components Reference
|
|
236
|
+
|
|
237
|
+
| Component / Hook | Import | Purpose |
|
|
238
|
+
| ---------------- | --------------------- | --------------------------------------------------- |
|
|
239
|
+
| `Bind` | `"webiny/admin/form"` | Bind a form field to a name path |
|
|
240
|
+
| `useForm()` | `"webiny/admin/form"` | Access the form API (`getValue`, `setValue`) |
|
|
241
|
+
| `validation` | `"webiny/admin/form"` | Create validators (`validation.create("required")`) |
|
|
242
|
+
| `UnsetOnUnmount` | `"webiny/admin/form"` | Clear the field value when the component unmounts |
|
|
243
|
+
| `Grid.Column` | `"webiny/admin/ui"` | Layout grid column (`span={12}` for full width) |
|
|
244
|
+
| `Input` | `"webiny/admin/ui"` | Text input field |
|
|
245
|
+
| `Select` | `"webiny/admin/ui"` | Dropdown select with options |
|
|
246
|
+
| `FormApi` | `"webiny/admin/form"` | Type for the form API object |
|
|
247
|
+
|
|
248
|
+
## Lexical Editor Plugins
|
|
249
|
+
|
|
250
|
+
Admin extensions can also add custom plugins to the Lexical rich text editor used in both the Headless CMS and the Website Builder. These are registered as `<Admin.Extension>` and use imports from `"webiny/admin/lexical"`, `"webiny/admin/cms/lexical"`, and `"webiny/admin/website-builder/lexical"`.
|
|
251
|
+
|
|
252
|
+
## Quick Reference
|
|
253
|
+
|
|
254
|
+
```
|
|
255
|
+
White-label import: import { AdminConfig } from "webiny/admin/configs";
|
|
256
|
+
Data list import: import { ContentEntryListConfig } from "webiny/admin/cms/entry/list";
|
|
257
|
+
Form imports: import { Bind, useForm, validation } from "webiny/admin/form";
|
|
258
|
+
UI imports: import { Grid, Input, Select } from "webiny/admin/ui";
|
|
259
|
+
Register: <Admin.Extension src={"/extensions/MyAdminExtension.tsx"} />
|
|
260
|
+
Develop: yarn webiny watch admin
|
|
261
|
+
Deploy: yarn webiny deploy admin
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
## Related Skills
|
|
265
|
+
|
|
266
|
+
- `webiny-project-structure` -- How to register Admin extensions
|
|
267
|
+
- `webiny-full-stack-architect` -- Full-stack extension skeleton and registration
|
|
268
|
+
- `webiny-admin-architect` -- Admin-side architecture patterns (headless + presentation features)
|