@axinom/mosaic-agent-skills 0.0.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/README.md +121 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +58 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +6 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +15 -0
- package/dist/logger.js.map +1 -0
- package/dist/tools/graphql/index.d.ts +3 -0
- package/dist/tools/graphql/index.d.ts.map +1 -0
- package/dist/tools/graphql/index.js +84 -0
- package/dist/tools/graphql/index.js.map +1 -0
- package/dist/tools/graphql/tools.d.ts +71 -0
- package/dist/tools/graphql/tools.d.ts.map +1 -0
- package/dist/tools/graphql/tools.js +187 -0
- package/dist/tools/graphql/tools.js.map +1 -0
- package/dist/tools/graphql/utils.d.ts +20 -0
- package/dist/tools/graphql/utils.d.ts.map +1 -0
- package/dist/tools/graphql/utils.js +140 -0
- package/dist/tools/graphql/utils.js.map +1 -0
- package/dist/tools/skills/index.d.ts +3 -0
- package/dist/tools/skills/index.d.ts.map +1 -0
- package/dist/tools/skills/index.js +62 -0
- package/dist/tools/skills/index.js.map +1 -0
- package/dist/tools/skills/tools.d.ts +5 -0
- package/dist/tools/skills/tools.d.ts.map +1 -0
- package/dist/tools/skills/tools.js +67 -0
- package/dist/tools/skills/tools.js.map +1 -0
- package/dist/tools/skills/types.d.ts +12 -0
- package/dist/tools/skills/types.d.ts.map +1 -0
- package/dist/tools/skills/types.js +3 -0
- package/dist/tools/skills/types.js.map +1 -0
- package/dist/tools/skills/utils.d.ts +11 -0
- package/dist/tools/skills/utils.d.ts.map +1 -0
- package/dist/tools/skills/utils.js +127 -0
- package/dist/tools/skills/utils.js.map +1 -0
- package/package.json +40 -0
- package/skills/_shared/actions/execution-summary.md +53 -0
- package/skills/_shared/actions/typescript-codegen.md +32 -0
- package/skills/_shared/actions/typescript-validation.md +32 -0
- package/skills/_shared/conventions/field-component-mapping.md +155 -0
- package/skills/_shared/conventions/field-validation-schema.md +77 -0
- package/skills/_shared/discovery/discover-paths.md +52 -0
- package/skills/_shared/discovery/extract-entity-features.md +565 -0
- package/skills/generate-create-station/SKILL.md +93 -0
- package/skills/generate-create-station/refs/finalization/station-registration.md +163 -0
- package/skills/generate-create-station/refs/templates/create-form-station.md +206 -0
- package/skills/generate-create-station/refs/templates/graphql-operations.md +56 -0
- package/skills/generate-details-station/SKILL.md +125 -0
- package/skills/generate-details-station/refs/finalization/explorer-integration.md +242 -0
- package/skills/generate-details-station/refs/finalization/station-registration.md +139 -0
- package/skills/generate-details-station/refs/templates/actions.md +127 -0
- package/skills/generate-details-station/refs/templates/breadcrumb.md +67 -0
- package/skills/generate-details-station/refs/templates/details-form-station.md +589 -0
- package/skills/generate-details-station/refs/templates/details-wrapper.md +54 -0
- package/skills/generate-details-station/refs/templates/form-data-types.md +62 -0
- package/skills/generate-details-station/refs/templates/graphql-operations.md +256 -0
- package/skills/generate-details-station/refs/templates/managed-integrations/image-management-station.md +322 -0
- package/skills/generate-details-station/refs/templates/managed-integrations/video-management-station.md +283 -0
- package/skills/generate-explorer-station/SKILL.md +121 -0
- package/skills/generate-explorer-station/refs/finalization/station-registration.md +145 -0
- package/skills/generate-explorer-station/refs/select-columns-filters.md +63 -0
- package/skills/generate-explorer-station/refs/templates/bulk-edit.md +369 -0
- package/skills/generate-explorer-station/refs/templates/common/bulk-actions.md +64 -0
- package/skills/generate-explorer-station/refs/templates/common/columns.md +131 -0
- package/skills/generate-explorer-station/refs/templates/common/data-provider.md +83 -0
- package/skills/generate-explorer-station/refs/templates/common/filters.md +220 -0
- package/skills/generate-explorer-station/refs/templates/common/inline-actions.md +45 -0
- package/skills/generate-explorer-station/refs/templates/common/types.md +28 -0
- package/skills/generate-explorer-station/refs/templates/graphql-operations.md +235 -0
- package/skills/generate-explorer-station/refs/templates/navigation-explorer-station.md +144 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: explorer-bulk-edit
|
|
3
|
+
input:
|
|
4
|
+
- EntityFeatures
|
|
5
|
+
- DiscoveredPaths
|
|
6
|
+
output:
|
|
7
|
+
- path: '{workflowsPath}/src/Stations/{EntityPlural}/{EntityPlural}Explorer/BulkEdit/{EntityPlural}BulkEditConfig.ts'
|
|
8
|
+
description: 'Bulk edit configuration with field mappings'
|
|
9
|
+
- path: '{workflowsPath}/src/Stations/{EntityPlural}/{EntityPlural}Explorer/BulkEdit/{EntityPlural}BulkEdit.tsx'
|
|
10
|
+
description: 'Bulk edit component'
|
|
11
|
+
- path: '{workflowsPath}/src/Stations/{EntityPlural}/{EntityPlural}Explorer/BulkEdit/index.ts'
|
|
12
|
+
description: 'Barrel export for bulk edit'
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# Generate BulkEdit Files
|
|
16
|
+
|
|
17
|
+
**Generate only if:** `features.mutations.bulkEdit === true`
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 1. {EntityPlural}BulkEditConfig.ts
|
|
22
|
+
|
|
23
|
+
**Discovery:** labelMapper, typeMapper utilities
|
|
24
|
+
|
|
25
|
+
- Pattern: `labelMapper|typeMapper`
|
|
26
|
+
- Locations: `Util/BulkEdit/`, `utils/`
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { BulkEdit{EntityPlural}AsyncFormFieldsConfig } from '../../../../generated/graphql';
|
|
30
|
+
import { labelMapper, typeMapper } from '{discoveredPath}/BulkEdit';
|
|
31
|
+
|
|
32
|
+
export const {EntityPlural}BulkEditConfig = (() => {
|
|
33
|
+
const fields: Partial<typeof BulkEdit{EntityPlural}AsyncFormFieldsConfig.fields> = {
|
|
34
|
+
...BulkEdit{EntityPlural}AsyncFormFieldsConfig.fields,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
labelMapper(fields, {
|
|
38
|
+
// Map field names to human-readable labels
|
|
39
|
+
{entityCamel}{AssociationPlural}Add: '{AssociationLabel} (Add)',
|
|
40
|
+
{entityCamel}{AssociationPlural}Remove: '{AssociationLabel} (Remove)',
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
typeMapper(fields, {
|
|
44
|
+
// Map fields to custom component types (if needed)
|
|
45
|
+
// Example: {associationField}Add: 'CustomSelection',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Delete unwanted fields
|
|
49
|
+
delete fields['unwantedField'];
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
...BulkEdit{EntityPlural}AsyncFormFieldsConfig,
|
|
53
|
+
fields,
|
|
54
|
+
};
|
|
55
|
+
})();
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 2. {EntityPlural}BulkEdit.tsx
|
|
61
|
+
|
|
62
|
+
**Discovery:** BulkEdit utilities
|
|
63
|
+
|
|
64
|
+
- Pattern: `getBulkEditImageSelectField|getVideoSelectField`
|
|
65
|
+
- Locations: `Util/BulkEdit/`, `utils/`
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
import { BulkEditFormFieldsConfigConverter, defaultComponentMap } from '@axinom/mosaic-ui';
|
|
69
|
+
import React from 'react';
|
|
70
|
+
// If features.capabilities.images !== undefined || features.capabilities.videos !== undefined:
|
|
71
|
+
import { {Entity}ImageType } from '../../../../generated/graphql';
|
|
72
|
+
import { getBulkEditImageSelectField, getVideoSelectField } from '{discoveredPath}/BulkEdit';
|
|
73
|
+
// End if
|
|
74
|
+
import { {EntityPlural}BulkEditConfig } from './{EntityPlural}BulkEditConfig';
|
|
75
|
+
|
|
76
|
+
export const {EntityPlural}BulkEdit: React.FC = () => {
|
|
77
|
+
const componentMap = {
|
|
78
|
+
...defaultComponentMap,
|
|
79
|
+
// If features.capabilities.images?.imageTypeValues.includes('COVER'):
|
|
80
|
+
CoverImageSelection: getBulkEditImageSelectField({Entity}ImageType.Cover, '{entityCamel}', 1),
|
|
81
|
+
// If features.capabilities.videos?.trailersConnection !== undefined:
|
|
82
|
+
VideoSelection: getVideoSelectField('TRAILER'),
|
|
83
|
+
// Custom field components as needed
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const fields = {EntityPlural}BulkEditConfig.fields;
|
|
87
|
+
|
|
88
|
+
return BulkEditFormFieldsConfigConverter(
|
|
89
|
+
Object.entries(fields)
|
|
90
|
+
.sort(([, a], [, b]) => (a.label || '').localeCompare(b.label || ''))
|
|
91
|
+
.reduce((acc, [key, value]) => {
|
|
92
|
+
acc[key] = value;
|
|
93
|
+
return acc;
|
|
94
|
+
}, {}),
|
|
95
|
+
componentMap,
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 3. index.ts
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
export * from './{EntityPlural}BulkEdit';
|
|
106
|
+
export * from './{EntityPlural}BulkEditConfig';
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Fallback
|
|
112
|
+
|
|
113
|
+
If BulkEdit utilities (`labelMapper`, `typeMapper`,
|
|
114
|
+
`getBulkEditImageSelectField`, `getVideoSelectField`, `MainVideoSelectionField`)
|
|
115
|
+
are not found in the project, create them in the `~/workflows/src/Util/BulkEdit`
|
|
116
|
+
folder and reuse.
|
|
117
|
+
|
|
118
|
+
If the `{discoveredPaths.extensionsContextPath}` is empty, it means the project
|
|
119
|
+
does not support rendering external components (i.e. image/video rendering), and
|
|
120
|
+
we can skip such component usage in BulkEdit and add a `TODO` for later. Other
|
|
121
|
+
BulkEdit fields will work without custom image/videos components.
|
|
122
|
+
|
|
123
|
+
### Create BulkEdit/helpers.ts
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
import { BulkEditFieldConfigMap } from '@axinom/mosaic-ui';
|
|
127
|
+
|
|
128
|
+
export const labelMapper = <T extends BulkEditFieldConfigMap>(
|
|
129
|
+
fields: Partial<T>,
|
|
130
|
+
labelMap: {
|
|
131
|
+
[key in keyof T]?: string;
|
|
132
|
+
},
|
|
133
|
+
): void => {
|
|
134
|
+
for (const key in fields) {
|
|
135
|
+
const labelValue = labelMap[key];
|
|
136
|
+
const fieldValue = fields[key];
|
|
137
|
+
if (labelValue && fieldValue) {
|
|
138
|
+
fieldValue.label = labelValue;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export const typeMapper = <T extends BulkEditFieldConfigMap>(
|
|
144
|
+
fields: Partial<T>,
|
|
145
|
+
typeMap: {
|
|
146
|
+
[key in keyof T]?: string;
|
|
147
|
+
},
|
|
148
|
+
): void => {
|
|
149
|
+
for (const key in fields) {
|
|
150
|
+
const typeValue = typeMap[key];
|
|
151
|
+
const fieldValue = fields[key];
|
|
152
|
+
if (typeValue && fieldValue) {
|
|
153
|
+
fieldValue.type = typeValue;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Create BulkEdit/ImageSelectField.tsx (if images capability exists)
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
import { ImageSelectFieldProps } from '@axinom/mosaic-managed-workflow-integration';
|
|
163
|
+
import { BulkEditEnumType } from '@axinom/mosaic-ui';
|
|
164
|
+
import React, { useContext } from 'react';
|
|
165
|
+
import { ExtensionsContext } from '{discoveredPaths.extensionsContextPath}';
|
|
166
|
+
|
|
167
|
+
export const getBulkEditImageSelectField: (
|
|
168
|
+
type: string,
|
|
169
|
+
scope: string,
|
|
170
|
+
maxItems?: number,
|
|
171
|
+
) => React.FC<ImageSelectFieldProps> = (type, scope, maxItems) => {
|
|
172
|
+
const Component: React.FC<ImageSelectFieldProps> = (props) => {
|
|
173
|
+
const { ImageSelectField } = useContext(ExtensionsContext);
|
|
174
|
+
return (
|
|
175
|
+
<ImageSelectField
|
|
176
|
+
{...props}
|
|
177
|
+
value={props.value ? props.value.map((item) => item.imageId) : []}
|
|
178
|
+
onChange={(event) => {
|
|
179
|
+
const value = (
|
|
180
|
+
event as { currentTarget: { value: string[] } }
|
|
181
|
+
).currentTarget.value.map((id) => ({
|
|
182
|
+
imageId: id,
|
|
183
|
+
imageType: new BulkEditEnumType(type),
|
|
184
|
+
}));
|
|
185
|
+
|
|
186
|
+
props.onChange({
|
|
187
|
+
currentTarget: {
|
|
188
|
+
name: props.name,
|
|
189
|
+
value,
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
}}
|
|
193
|
+
imageType={`${scope}_${type.toLowerCase()}`}
|
|
194
|
+
maxItems={maxItems}
|
|
195
|
+
/>
|
|
196
|
+
);
|
|
197
|
+
};
|
|
198
|
+
Component.displayName = `ImageSelectField_${type}`;
|
|
199
|
+
return Component;
|
|
200
|
+
};
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Create BulkEdit/VideoSelectField.tsx (if videos capability exists)
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
import { VideoSelectFieldProps } from '@axinom/mosaic-video-workflow-integration';
|
|
207
|
+
import React, { useContext } from 'react';
|
|
208
|
+
import { ExtensionsContext } from '{discoveredPaths.extensionsContextPath}';
|
|
209
|
+
|
|
210
|
+
export const getVideoSelectField: (
|
|
211
|
+
type: string,
|
|
212
|
+
maxItems?: number,
|
|
213
|
+
) => React.FC<VideoSelectFieldProps> = (type, maxItems) => {
|
|
214
|
+
const Component: React.FC<VideoSelectFieldProps> = (props) => {
|
|
215
|
+
const { VideoSelectField } = useContext(ExtensionsContext);
|
|
216
|
+
const { value, onChange, name } = props;
|
|
217
|
+
|
|
218
|
+
return (
|
|
219
|
+
<VideoSelectField
|
|
220
|
+
{...props}
|
|
221
|
+
defaultFilterTag={type}
|
|
222
|
+
value={value?.map((trailer) => trailer.videoId) ?? []}
|
|
223
|
+
onChange={(event) => {
|
|
224
|
+
const value = (
|
|
225
|
+
event as { currentTarget: { value: string[] } }
|
|
226
|
+
).currentTarget.value.map((id) => ({ videoId: id }));
|
|
227
|
+
onChange &&
|
|
228
|
+
onChange({
|
|
229
|
+
...(event as React.ChangeEvent<HTMLInputElement>),
|
|
230
|
+
currentTarget: {
|
|
231
|
+
...(event as React.ChangeEvent<HTMLInputElement>).currentTarget,
|
|
232
|
+
name: name,
|
|
233
|
+
value: value,
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
}}
|
|
237
|
+
maxItems={maxItems}
|
|
238
|
+
/>
|
|
239
|
+
);
|
|
240
|
+
};
|
|
241
|
+
Component.displayName = `VideoSelectField_${type}`;
|
|
242
|
+
return Component;
|
|
243
|
+
};
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Create BulkEdit/MainVideoSelectField.tsx (if mainVideoField exists)
|
|
247
|
+
|
|
248
|
+
```tsx
|
|
249
|
+
import { SingleLineTextProps } from '@axinom/mosaic-ui';
|
|
250
|
+
import React, { useContext } from 'react';
|
|
251
|
+
import { ExtensionsContext } from '{discoveredPaths.extensionsContextPath}';
|
|
252
|
+
|
|
253
|
+
export const MainVideoSelectionField: React.FC<SingleLineTextProps> = (
|
|
254
|
+
props,
|
|
255
|
+
) => {
|
|
256
|
+
const { VideoSelectField } = useContext(ExtensionsContext);
|
|
257
|
+
const { value, onChange, name, label = '' } = props;
|
|
258
|
+
|
|
259
|
+
return (
|
|
260
|
+
<VideoSelectField
|
|
261
|
+
{...props}
|
|
262
|
+
defaultFilterTag="MAIN"
|
|
263
|
+
label={label}
|
|
264
|
+
value={value ? [value] : []}
|
|
265
|
+
onChange={(event) => {
|
|
266
|
+
const value = (event as React.ChangeEvent<HTMLInputElement>)
|
|
267
|
+
.currentTarget.value[0];
|
|
268
|
+
onChange &&
|
|
269
|
+
onChange({
|
|
270
|
+
...(event as React.ChangeEvent<HTMLInputElement>),
|
|
271
|
+
currentTarget: {
|
|
272
|
+
...(event as React.ChangeEvent<HTMLInputElement>).currentTarget,
|
|
273
|
+
name: name,
|
|
274
|
+
value: value,
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
}}
|
|
278
|
+
maxItems={1}
|
|
279
|
+
/>
|
|
280
|
+
);
|
|
281
|
+
};
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Update {EntityPlural}BulkEdit.tsx to import from local directory
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
import { BulkEditFormFieldsConfigConverter, defaultComponentMap } from '@axinom/mosaic-ui';
|
|
288
|
+
import React from 'react';
|
|
289
|
+
// Import from local BulkEdit directory
|
|
290
|
+
import { getBulkEditImageSelectField } from './helpers/ImageSelectField';
|
|
291
|
+
import { getVideoSelectField } from './helpers/VideoSelectField';
|
|
292
|
+
import { MainVideoSelectionField } from './helpers/MainVideoSelectField';
|
|
293
|
+
// If features.capabilities.images !== undefined || features.capabilities.videos !== undefined:
|
|
294
|
+
import { {Entity}ImageType } from '../../../../generated/graphql';
|
|
295
|
+
// End if
|
|
296
|
+
import { {EntityPlural}BulkEditConfig } from './{EntityPlural}BulkEditConfig';
|
|
297
|
+
|
|
298
|
+
export const {EntityPlural}BulkEdit: React.FC = () => {
|
|
299
|
+
const componentMap = {
|
|
300
|
+
...defaultComponentMap,
|
|
301
|
+
// If features.capabilities.images?.imageTypeValues.includes('COVER'):
|
|
302
|
+
CoverImageSelection: getBulkEditImageSelectField({Entity}ImageType.Cover, '{entityCamel}', 1),
|
|
303
|
+
// If features.capabilities.videos?.mainVideoField:
|
|
304
|
+
MainVideoSelection: MainVideoSelectionField,
|
|
305
|
+
// If features.capabilities.videos?.trailersConnection !== undefined:
|
|
306
|
+
VideoSelection: getVideoSelectField('TRAILER'),
|
|
307
|
+
// Custom field components as needed
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const fields = {EntityPlural}BulkEditConfig.fields;
|
|
311
|
+
|
|
312
|
+
return BulkEditFormFieldsConfigConverter(
|
|
313
|
+
Object.entries(fields)
|
|
314
|
+
.sort(([, a], [, b]) => (a.label || '').localeCompare(b.label || ''))
|
|
315
|
+
.reduce((acc, [key, value]) => {
|
|
316
|
+
acc[key] = value;
|
|
317
|
+
return acc;
|
|
318
|
+
}, {}),
|
|
319
|
+
componentMap,
|
|
320
|
+
);
|
|
321
|
+
};
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
## Example (Product - simple)
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
// ProductsBulkEditConfig.ts
|
|
328
|
+
import { BulkEditProductsAsyncFormFieldsConfig } from '../../../../generated/graphql';
|
|
329
|
+
import { labelMapper, typeMapper } from '../../../../Util/BulkEdit';
|
|
330
|
+
|
|
331
|
+
export const ProductsBulkEditConfig = (() => {
|
|
332
|
+
const fields: Partial<typeof BulkEditProductsAsyncFormFieldsConfig.fields> = {
|
|
333
|
+
...BulkEditProductsAsyncFormFieldsConfig.fields,
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
labelMapper(fields, {
|
|
337
|
+
productsProductCategoriesAdd: 'Categories (Add)',
|
|
338
|
+
productsProductCategoriesRemove: 'Categories (Remove)',
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
return {
|
|
342
|
+
...BulkEditProductsAsyncFormFieldsConfig,
|
|
343
|
+
fields,
|
|
344
|
+
};
|
|
345
|
+
})();
|
|
346
|
+
|
|
347
|
+
// ProductsBulkEdit.tsx
|
|
348
|
+
import {
|
|
349
|
+
BulkEditFormFieldsConfigConverter,
|
|
350
|
+
defaultComponentMap,
|
|
351
|
+
} from '@axinom/mosaic-ui';
|
|
352
|
+
import React from 'react';
|
|
353
|
+
import { ProductsBulkEditConfig } from './ProductsBulkEditConfig';
|
|
354
|
+
|
|
355
|
+
export const ProductsBulkEdit: React.FC = () => {
|
|
356
|
+
const componentMap = { ...defaultComponentMap };
|
|
357
|
+
const fields = ProductsBulkEditConfig.fields;
|
|
358
|
+
|
|
359
|
+
return BulkEditFormFieldsConfigConverter(
|
|
360
|
+
Object.entries(fields)
|
|
361
|
+
.sort(([, a], [, b]) => (a.label || '').localeCompare(b.label || ''))
|
|
362
|
+
.reduce((acc, [key, value]) => {
|
|
363
|
+
acc[key] = value;
|
|
364
|
+
return acc;
|
|
365
|
+
}, {}),
|
|
366
|
+
componentMap,
|
|
367
|
+
);
|
|
368
|
+
};
|
|
369
|
+
```
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: explorer-bulk-actions
|
|
3
|
+
input:
|
|
4
|
+
- EntityFeatures
|
|
5
|
+
output:
|
|
6
|
+
- path: '{workflowsPath}/src/Stations/{EntityPlural}/{EntityPlural}Explorer/common/{EntityPlural}.bulkActions.ts'
|
|
7
|
+
description: 'Bulk action hooks and actions'
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Generate bulkActions.ts
|
|
11
|
+
|
|
12
|
+
**Generate only if:** `features.mutations.bulkActions === true`
|
|
13
|
+
|
|
14
|
+
Generate mutation hooks and actions for each mutation in
|
|
15
|
+
`features.mutations.names.bulkActions`.
|
|
16
|
+
|
|
17
|
+
## Template
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { ExplorerBulkAction, IconName, ItemSelection, PageHeaderActionType } from '@axinom/mosaic-ui';
|
|
21
|
+
import { client } from '{discoveredPath}/apolloClient';
|
|
22
|
+
import {
|
|
23
|
+
// Import hooks for each bulk action mutation
|
|
24
|
+
// Use{PascalCaseMutationName}Mutation for each mutation in features.mutations.names.bulkActions
|
|
25
|
+
} from '../../../../generated/graphql';
|
|
26
|
+
import { use{EntityPlural}Filters } from './{EntityPlural}.filters';
|
|
27
|
+
import { {Entity}Data } from './{EntityPlural}.types';
|
|
28
|
+
|
|
29
|
+
export function use{EntityPlural}BulkActions(): {
|
|
30
|
+
readonly bulkActions: ExplorerBulkAction<{Entity}Data>[];
|
|
31
|
+
} {
|
|
32
|
+
const { transformFilters } = use{EntityPlural}Filters();
|
|
33
|
+
|
|
34
|
+
// For each mutation in features.mutations.names.bulkActions, create hook and action
|
|
35
|
+
// Example for "deleteMovies":
|
|
36
|
+
const [bulkDelete{EntityPlural}] = useBulkDelete{EntityPlural}Mutation({
|
|
37
|
+
client,
|
|
38
|
+
fetchPolicy: 'no-cache',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const deleteBulkAction: ExplorerBulkAction<{Entity}Data> = {
|
|
42
|
+
label: 'Delete',
|
|
43
|
+
onClick: async (arg?: ItemSelection<{Entity}Data>) => {
|
|
44
|
+
switch (arg?.mode) {
|
|
45
|
+
case 'SELECT_ALL':
|
|
46
|
+
await bulkDelete{EntityPlural}({ variables: { filter: transformFilters(arg.filters) } });
|
|
47
|
+
break;
|
|
48
|
+
case 'SINGLE_ITEMS':
|
|
49
|
+
await bulkDelete{EntityPlural}({
|
|
50
|
+
variables: { filter: { id: { in: arg.items?.map((item) => item.id) } } },
|
|
51
|
+
});
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
actionType: PageHeaderActionType.Context,
|
|
56
|
+
confirmationMode: 'Simple',
|
|
57
|
+
icon: IconName.Delete,
|
|
58
|
+
reloadData: true,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Return all bulk actions
|
|
62
|
+
return { bulkActions: [deleteBulkAction /* , other actions */] };
|
|
63
|
+
}
|
|
64
|
+
```
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: explorer-columns
|
|
3
|
+
input:
|
|
4
|
+
- EntityFeatures
|
|
5
|
+
- selectedColumns
|
|
6
|
+
output:
|
|
7
|
+
- path: '{workflowsPath}/src/Stations/{EntityPlural}/{EntityPlural}Explorer/common/{EntityPlural}.columns.ts'
|
|
8
|
+
description: 'Column definitions with renderers'
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Generate columns.ts
|
|
12
|
+
|
|
13
|
+
For each selected column from a scalar field, check
|
|
14
|
+
`features.fields.scalars[].semanticHint`. If present, apply appropriate
|
|
15
|
+
renderer:
|
|
16
|
+
|
|
17
|
+
- `duration` → `renderDuration` (seconds to "2h 30m")
|
|
18
|
+
- `fileSize` → `renderFileSize` (bytes to "1.5 GB")
|
|
19
|
+
- `currency` → `renderCurrency` (with $ and commas)
|
|
20
|
+
- `percentage` → `renderPercentage` (0.45 to "45%")
|
|
21
|
+
|
|
22
|
+
For enum scalar fields (check `features.fields.scalars[].enumType`), use
|
|
23
|
+
getEnumLabel renderer:
|
|
24
|
+
|
|
25
|
+
- Discover `getEnumLabel` in project (typically in `Util/StringEnumMapper/`)
|
|
26
|
+
- Wrap in arrow function with type casting:
|
|
27
|
+
`(value) => getEnumLabel(value as string)`
|
|
28
|
+
|
|
29
|
+
Discover renderers in project first, fallback to inline implementations (see end
|
|
30
|
+
of this section).
|
|
31
|
+
|
|
32
|
+
## Template
|
|
33
|
+
|
|
34
|
+
```typescript
|
|
35
|
+
import { Column, createConnectionRenderer, DateRenderer } from '@axinom/mosaic-ui';
|
|
36
|
+
// If features.capabilities.images !== undefined:
|
|
37
|
+
import { createThumbnailAndStateRenderer } from '@axinom/mosaic-managed-workflow-integration';
|
|
38
|
+
// End if
|
|
39
|
+
// If enum fields in selectedColumns:
|
|
40
|
+
import { getEnumLabel } from '{discoveredPath}/StringEnumMapper/StringEnumMapper';
|
|
41
|
+
// End if
|
|
42
|
+
import { {Entity}Data } from './{EntityPlural}.types';
|
|
43
|
+
|
|
44
|
+
export const {entityCamel}Columns: Column<{Entity}Data>[] = [
|
|
45
|
+
// If features.capabilities.images !== undefined:
|
|
46
|
+
{
|
|
47
|
+
propertyName: '{entityCamel}Images',
|
|
48
|
+
label: 'Thumb',
|
|
49
|
+
render: createThumbnailAndStateRenderer('{entityCamel}Images'),
|
|
50
|
+
size: '80px',
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
// Title field:
|
|
54
|
+
{ label: '{TitleLabel}', propertyName: '{titleField}', size: '2fr' },
|
|
55
|
+
|
|
56
|
+
// For each selected scalar column:
|
|
57
|
+
// - No semanticHint and no enumType: { label, propertyName }
|
|
58
|
+
// - Has semanticHint: { label, propertyName, render: renderer }
|
|
59
|
+
// - Has enumType: { label, propertyName, render: (value) => getEnumLabel(value as string) }
|
|
60
|
+
|
|
61
|
+
// Boolean fields:
|
|
62
|
+
{
|
|
63
|
+
label: 'Visible',
|
|
64
|
+
propertyName: 'isVisible',
|
|
65
|
+
render: (value: unknown) => (value === true ? 'Yes' : 'No'),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Enum fields:
|
|
69
|
+
{ label: 'Status', propertyName: 'status', render: (value) => getEnumLabel(value as string) },
|
|
70
|
+
|
|
71
|
+
// TagLike associations:
|
|
72
|
+
{
|
|
73
|
+
label: '{Label}',
|
|
74
|
+
propertyName: '{entityCamel}{AssociationPlural}',
|
|
75
|
+
sortable: false,
|
|
76
|
+
render: createConnectionRenderer((node: any) => node.{displayField}),
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
// ManyToMany associations:
|
|
80
|
+
{
|
|
81
|
+
label: '{Label}',
|
|
82
|
+
propertyName: '{entityCamel}{AssociationPlural}',
|
|
83
|
+
sortable: false,
|
|
84
|
+
render: createConnectionRenderer((node: any) => node.{relatedCamel}?.{displayField}),
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
// Date fields:
|
|
88
|
+
{ label: 'Updated At', propertyName: 'updatedDate', render: DateRenderer },
|
|
89
|
+
];
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Semantic Renderer Fallbacks
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
const renderDuration = (value: unknown): string => {
|
|
96
|
+
const seconds = value as number | null | undefined;
|
|
97
|
+
if (!seconds) return '';
|
|
98
|
+
const h = Math.floor(seconds / 3600);
|
|
99
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
100
|
+
const s = seconds % 60;
|
|
101
|
+
return [h && `${h}h`, m && `${m}m`, s && `${s}s`].filter(Boolean).join(' ');
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const renderFileSize = (value: unknown): string => {
|
|
105
|
+
const bytes = value as number | null | undefined;
|
|
106
|
+
if (!bytes) return '';
|
|
107
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
108
|
+
let size = bytes;
|
|
109
|
+
let i = 0;
|
|
110
|
+
while (size >= 1024 && i < 4) {
|
|
111
|
+
size /= 1024;
|
|
112
|
+
i++;
|
|
113
|
+
}
|
|
114
|
+
return `${size.toFixed(i ? 2 : 0)} ${units[i]}`;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const renderCurrency = (value: unknown): string => {
|
|
118
|
+
const num = value as number | null | undefined;
|
|
119
|
+
if (num == null) return '';
|
|
120
|
+
return new Intl.NumberFormat('en-US', {
|
|
121
|
+
style: 'currency',
|
|
122
|
+
currency: 'USD',
|
|
123
|
+
}).format(num);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const renderPercentage = (value: unknown): string => {
|
|
127
|
+
const num = value as number | null | undefined;
|
|
128
|
+
if (num == null) return '';
|
|
129
|
+
return `${(num * 100).toFixed(1)}%`;
|
|
130
|
+
};
|
|
131
|
+
```
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: explorer-data-provider
|
|
3
|
+
input:
|
|
4
|
+
- EntityFeatures
|
|
5
|
+
output:
|
|
6
|
+
- path: '{workflowsPath}/src/Stations/{EntityPlural}/{EntityPlural}Explorer/common/{EntityPlural}.dataProvider.ts'
|
|
7
|
+
description: 'Data provider for loading and subscribing to data'
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Generate dataProvider.ts
|
|
11
|
+
|
|
12
|
+
**Discovery:** Apollo client
|
|
13
|
+
|
|
14
|
+
- Pattern: `export.*client.*ApolloClient`
|
|
15
|
+
- Locations: `src/`, `apolloClient.ts`, `client.ts`
|
|
16
|
+
|
|
17
|
+
## Template
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { ExplorerDataProvider, FilterValues, sortToPostGraphileOrderBy } from '@axinom/mosaic-ui';
|
|
21
|
+
import { client } from '{discoveredPath}/apolloClient';
|
|
22
|
+
import {
|
|
23
|
+
{Entity}Filter,
|
|
24
|
+
{EntityPlural}Document,
|
|
25
|
+
{EntityPlural}OrderBy,
|
|
26
|
+
{EntityPlural}Query,
|
|
27
|
+
{EntityPlural}QueryVariables,
|
|
28
|
+
// If features.queries.subscription === true:
|
|
29
|
+
{EntityPlural}MutatedDocument,
|
|
30
|
+
{EntityPlural}MutatedSubscription,
|
|
31
|
+
{Entity}SubscriptionEventKey,
|
|
32
|
+
} from '../../../../generated/graphql';
|
|
33
|
+
import { {Entity}Data } from './{EntityPlural}.types';
|
|
34
|
+
|
|
35
|
+
export const create{EntityPlural}DataProvider = (
|
|
36
|
+
transformFilters: (filters: FilterValues<{Entity}Data>) => {Entity}Filter | undefined,
|
|
37
|
+
): ExplorerDataProvider<{Entity}Data> => ({
|
|
38
|
+
loadData: async ({ pagingInformation, sorting, filters }) => {
|
|
39
|
+
const result = await client.query<{EntityPlural}Query, {EntityPlural}QueryVariables>({
|
|
40
|
+
query: {EntityPlural}Document,
|
|
41
|
+
variables: {
|
|
42
|
+
filter: transformFilters(filters),
|
|
43
|
+
orderBy: sortToPostGraphileOrderBy(sorting, {EntityPlural}OrderBy),
|
|
44
|
+
after: pagingInformation,
|
|
45
|
+
},
|
|
46
|
+
fetchPolicy: 'network-only',
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
data: result.data.filtered?.nodes ?? [],
|
|
51
|
+
totalCount: result.data.nonFiltered?.totalCount as number,
|
|
52
|
+
filteredCount: result.data.filtered?.totalCount as number,
|
|
53
|
+
hasMoreData: result.data.filtered?.pageInfo.hasNextPage || false,
|
|
54
|
+
pagingInformation: result.data.filtered?.pageInfo.endCursor,
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
// If features.queries.subscription === true:
|
|
59
|
+
connect: ({ change, add, remove }) => {
|
|
60
|
+
const subscription = client
|
|
61
|
+
.subscribe<{EntityPlural}MutatedSubscription>({ query: {EntityPlural}MutatedDocument })
|
|
62
|
+
.subscribe((e) => {
|
|
63
|
+
switch (e.data?.{entityCamel}Mutated?.eventKey) {
|
|
64
|
+
case {Entity}SubscriptionEventKey.{Entity}Changed:
|
|
65
|
+
if (e.data.{entityCamel}Mutated.{entityCamel}) {
|
|
66
|
+
change(e.data.{entityCamel}Mutated.id, e.data.{entityCamel}Mutated.{entityCamel});
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
case {Entity}SubscriptionEventKey.{Entity}Deleted:
|
|
70
|
+
remove(e.data.{entityCamel}Mutated.id);
|
|
71
|
+
break;
|
|
72
|
+
case {Entity}SubscriptionEventKey.{Entity}Created:
|
|
73
|
+
if (e.data.{entityCamel}Mutated.{entityCamel}) {
|
|
74
|
+
add(e.data.{entityCamel}Mutated.{entityCamel});
|
|
75
|
+
}
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return () => { subscription.unsubscribe(); };
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
```
|