@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.
Files changed (72) hide show
  1. package/README.md +121 -0
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +58 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/logger.d.ts +6 -0
  7. package/dist/logger.d.ts.map +1 -0
  8. package/dist/logger.js +15 -0
  9. package/dist/logger.js.map +1 -0
  10. package/dist/tools/graphql/index.d.ts +3 -0
  11. package/dist/tools/graphql/index.d.ts.map +1 -0
  12. package/dist/tools/graphql/index.js +84 -0
  13. package/dist/tools/graphql/index.js.map +1 -0
  14. package/dist/tools/graphql/tools.d.ts +71 -0
  15. package/dist/tools/graphql/tools.d.ts.map +1 -0
  16. package/dist/tools/graphql/tools.js +187 -0
  17. package/dist/tools/graphql/tools.js.map +1 -0
  18. package/dist/tools/graphql/utils.d.ts +20 -0
  19. package/dist/tools/graphql/utils.d.ts.map +1 -0
  20. package/dist/tools/graphql/utils.js +140 -0
  21. package/dist/tools/graphql/utils.js.map +1 -0
  22. package/dist/tools/skills/index.d.ts +3 -0
  23. package/dist/tools/skills/index.d.ts.map +1 -0
  24. package/dist/tools/skills/index.js +62 -0
  25. package/dist/tools/skills/index.js.map +1 -0
  26. package/dist/tools/skills/tools.d.ts +5 -0
  27. package/dist/tools/skills/tools.d.ts.map +1 -0
  28. package/dist/tools/skills/tools.js +67 -0
  29. package/dist/tools/skills/tools.js.map +1 -0
  30. package/dist/tools/skills/types.d.ts +12 -0
  31. package/dist/tools/skills/types.d.ts.map +1 -0
  32. package/dist/tools/skills/types.js +3 -0
  33. package/dist/tools/skills/types.js.map +1 -0
  34. package/dist/tools/skills/utils.d.ts +11 -0
  35. package/dist/tools/skills/utils.d.ts.map +1 -0
  36. package/dist/tools/skills/utils.js +127 -0
  37. package/dist/tools/skills/utils.js.map +1 -0
  38. package/package.json +40 -0
  39. package/skills/_shared/actions/execution-summary.md +53 -0
  40. package/skills/_shared/actions/typescript-codegen.md +32 -0
  41. package/skills/_shared/actions/typescript-validation.md +32 -0
  42. package/skills/_shared/conventions/field-component-mapping.md +155 -0
  43. package/skills/_shared/conventions/field-validation-schema.md +77 -0
  44. package/skills/_shared/discovery/discover-paths.md +52 -0
  45. package/skills/_shared/discovery/extract-entity-features.md +565 -0
  46. package/skills/generate-create-station/SKILL.md +93 -0
  47. package/skills/generate-create-station/refs/finalization/station-registration.md +163 -0
  48. package/skills/generate-create-station/refs/templates/create-form-station.md +206 -0
  49. package/skills/generate-create-station/refs/templates/graphql-operations.md +56 -0
  50. package/skills/generate-details-station/SKILL.md +125 -0
  51. package/skills/generate-details-station/refs/finalization/explorer-integration.md +242 -0
  52. package/skills/generate-details-station/refs/finalization/station-registration.md +139 -0
  53. package/skills/generate-details-station/refs/templates/actions.md +127 -0
  54. package/skills/generate-details-station/refs/templates/breadcrumb.md +67 -0
  55. package/skills/generate-details-station/refs/templates/details-form-station.md +589 -0
  56. package/skills/generate-details-station/refs/templates/details-wrapper.md +54 -0
  57. package/skills/generate-details-station/refs/templates/form-data-types.md +62 -0
  58. package/skills/generate-details-station/refs/templates/graphql-operations.md +256 -0
  59. package/skills/generate-details-station/refs/templates/managed-integrations/image-management-station.md +322 -0
  60. package/skills/generate-details-station/refs/templates/managed-integrations/video-management-station.md +283 -0
  61. package/skills/generate-explorer-station/SKILL.md +121 -0
  62. package/skills/generate-explorer-station/refs/finalization/station-registration.md +145 -0
  63. package/skills/generate-explorer-station/refs/select-columns-filters.md +63 -0
  64. package/skills/generate-explorer-station/refs/templates/bulk-edit.md +369 -0
  65. package/skills/generate-explorer-station/refs/templates/common/bulk-actions.md +64 -0
  66. package/skills/generate-explorer-station/refs/templates/common/columns.md +131 -0
  67. package/skills/generate-explorer-station/refs/templates/common/data-provider.md +83 -0
  68. package/skills/generate-explorer-station/refs/templates/common/filters.md +220 -0
  69. package/skills/generate-explorer-station/refs/templates/common/inline-actions.md +45 -0
  70. package/skills/generate-explorer-station/refs/templates/common/types.md +28 -0
  71. package/skills/generate-explorer-station/refs/templates/graphql-operations.md +235 -0
  72. 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
+ ```