@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,589 @@
1
+ ---
2
+ name: details-form-station
3
+ input:
4
+ - EntityFeatures
5
+ - DiscoveredPaths
6
+ - Reference: field-component-mapping
7
+ - Reference: field-validation-schema
8
+ output:
9
+ - path: '{workflowsPath}/src/Stations/{EntityPlural}/{Entity}Details/{Entity}DetailsForm.tsx'
10
+ description:
11
+ 'Main form component with query, submit logic, fields, info panel'
12
+ ---
13
+
14
+ # Generate DetailsForm Component
15
+
16
+ Core details form with data fetching, dynamic mutation building, field
17
+ rendering, and info panel.
18
+
19
+ ## Discovery
20
+
21
+ **Discover dependencies before generating:**
22
+
23
+ 1. **getEnumLabel** - Search for `getEnumLabel` in `src/Util` or similar
24
+ - Used for displaying enum fields in info panel
25
+ - If not found, use inline implementation (see Fallback section)
26
+
27
+ ## Template Structure
28
+
29
+ The file has these sections:
30
+
31
+ 1. Imports
32
+ 2. Validation schema
33
+ 3. Main component function
34
+ 4. Panel sub-component (info panel)
35
+ 5. Form sub-component (fields)
36
+ 6. Helper function (createUpdateDto)
37
+
38
+ ## Section 1: Imports
39
+
40
+ ```typescript
41
+ // If ExtensionsContext discovered:
42
+ import { ID } from '@axinom/mosaic-managed-workflow-integration';
43
+ // End if
44
+ import {
45
+ CheckboxField,
46
+ createUpdateGQLFragmentGenerator,
47
+ CustomTagsField,
48
+ DateTimeTextField,
49
+ Details,
50
+ DetailsProps,
51
+ formatDateTime,
52
+ generateArrayMutations,
53
+ getFormDiff,
54
+ InfoPanel,
55
+ Paragraph,
56
+ RadioField,
57
+ Section,
58
+ SelectField,
59
+ SingleLineTextField,
60
+ TagsField,
61
+ TextAreaField,
62
+ } from '@axinom/mosaic-ui';
63
+ import { Field, useFormikContext } from 'formik';
64
+ import gql from 'graphql-tag';
65
+ import { ObjectSchemaDefinition } from 'ObjectSchemaDefinition';
66
+ import React, { useCallback, useContext, useMemo } from 'react';
67
+ import * as Yup from 'yup';
68
+ import { client } from '../../../apolloClient';
69
+ // If ExtensionsContext discovered:
70
+ import { ExtensionsContext } from '{discoveredPaths.extensionsContextPath}';
71
+ // End if
72
+ import {
73
+ {Entity},
74
+ {Entity}Document,
75
+ // For each ManyToMany association:
76
+ {ManyToMany.relatedType},
77
+ // End for
78
+ Mutation,
79
+ // TagLike create/delete mutation arg types:
80
+ // For each TagLike association:
81
+ MutationCreate{PascalConnectionField}Args,
82
+ MutationDelete{PascalConnectionField}Args,
83
+ // End for
84
+ // ManyToMany create/delete mutation arg types:
85
+ // For each ManyToMany association:
86
+ MutationCreate{PascalJunctionType}Args,
87
+ MutationDelete{PascalJunctionType}Args,
88
+ // End for
89
+ // Search query types for TagLike associations:
90
+ // For each TagLike association:
91
+ Search{Entity}{Label}Document,
92
+ Search{Entity}{Label}Query,
93
+ Search{Entity}{Label}QueryVariables,
94
+ // End for
95
+ Update{Entity}Input,
96
+ use{Entity}Query,
97
+ } from '../../../generated/graphql';
98
+ // If getEnumLabel discovered:
99
+ import { getEnumLabel } from '{discoveredPath}/StringEnumMapper/StringEnumMapper';
100
+ // End if
101
+ import { use{Entity}DetailsActions } from './{Entity}Details.actions';
102
+ import classes from './{Entity}Details.module.scss'; // Create empty file if needed
103
+ import { {Entity}DetailsFormData } from './{Entity}Details.types';
104
+ ```
105
+
106
+ ## Section 2: Validation Schema
107
+
108
+ Apply validation rules from `field-validation-schema`:
109
+
110
+ ```typescript
111
+ const {entityCamel}DetailSchema = Yup.object<
112
+ ObjectSchemaDefinition<{Entity}DetailsFormData>
113
+ >({
114
+ // Generate validation for each scalar field based on `field-validation-schema`
115
+
116
+ // Only generate validations for editable fields
117
+ });
118
+ ```
119
+
120
+ ## Section 3: Main Component Function
121
+
122
+ ```typescript
123
+ interface {Entity}DetailsFormProps {
124
+ {entityCamel}Id: number;
125
+ }
126
+
127
+ export const {Entity}DetailsForm: React.FC<{Entity}DetailsFormProps> = ({
128
+ {entityCamel}Id,
129
+ }) => {
130
+ // 3.1: Query hook
131
+ const { loading, data, error } = use{Entity}Query({
132
+ client,
133
+ variables: { id: {entityCamel}Id },
134
+ fetchPolicy: 'network-only',
135
+ });
136
+
137
+ // 3.2: Data transformation (associations to arrays)
138
+ const {
139
+ // For each ManyToMany: all{Label}
140
+ // For each TagLike: use semantic plural name (see types.md for naming rules)
141
+ // ManyToMany global data:
142
+ // For each ManyToMany association:
143
+ all{Label},
144
+ // End for
145
+ // TagLike arrays (semantic plural: tags, cast, productionCountries):
146
+ // For each TagLike association:
147
+ {semanticPluralName}, // derived from connection field name
148
+ // End for
149
+ // ManyToMany arrays:
150
+ // For each ManyToMany association:
151
+ {label.toLowerCase()}, // e.g., genres
152
+ // End for
153
+ } = useMemo(
154
+ () => ({
155
+ // ManyToMany global data (options for dropdowns):
156
+ // For each ManyToMany association:
157
+ all{Label}: data?.{relatedQueryField}?.nodes.reduce<{
158
+ [key: string]: Partial<{RelatedType}>;
159
+ }>((result, current) => {
160
+ result[current.{displayField}] = current;
161
+ return result;
162
+ }, {}) ?? {},
163
+ // End for
164
+
165
+ // TagLike association arrays:
166
+ // For each TagLike association:
167
+ {semanticPluralName}: data?.{entityCamel}?.{connectionField}.nodes.map(
168
+ (node) => node.{displayField}
169
+ ),
170
+ // End for
171
+
172
+ // ManyToMany association arrays:
173
+ // For each ManyToMany association:
174
+ {label.toLowerCase()}: data?.{entityCamel}?.{connectionField}.nodes.map(
175
+ (node) => node.{relatedField}?.{displayField} ?? ''
176
+ ),
177
+ // End for
178
+ }),
179
+ [data]
180
+ );
181
+
182
+ // 3.3: Actions hook
183
+ const { actions } = use{Entity}DetailsActions({entityCamel}Id);
184
+
185
+ // 3.4: Submit handler (see Section 3.4 below)
186
+ const onSubmit = useCallback(
187
+ async (
188
+ formData: {Entity}DetailsFormData,
189
+ initialData: DetailsProps<{Entity}DetailsFormData>['initialData']
190
+ ): Promise<void> => {
191
+ // Submit logic here...
192
+ },
193
+ [
194
+ // Dependencies: all{Label} for each ManyToMany
195
+ // For each ManyToMany:
196
+ all{Label},
197
+ // End for
198
+ {entityCamel}Id
199
+ ]
200
+ );
201
+
202
+ // 3.5: Render Details component
203
+ return (
204
+ <Details<{Entity}DetailsFormData>
205
+ defaultTitle="{Entity}"
206
+ titleProperty="{titleField}"
207
+ subtitle="Properties"
208
+ alwaysShowActionsPanel={true}
209
+ actions={actions}
210
+ validationSchema={{entityCamel}DetailSchema}
211
+ initialData={{
212
+ data: {
213
+ ...data?.{entityCamel},
214
+ // Association arrays:
215
+ // For each TagLike:
216
+ {semanticPluralName},
217
+ // End for
218
+ // For each ManyToMany:
219
+ {label.toLowerCase()},
220
+ // End for
221
+ },
222
+ loading,
223
+ entityNotFound: data?.{entityCamel} === null,
224
+ error: error?.message,
225
+ }}
226
+ saveData={onSubmit}
227
+ infoPanel={<Panel />}
228
+ >
229
+ <Form
230
+ // For each ManyToMany association:
231
+ {label.toLowerCase()}Options={Object.keys(all{Label})}
232
+ // End for
233
+ />
234
+ </Details>
235
+ );
236
+ };
237
+ ```
238
+
239
+ ## Section 3.4: Submit Handler (Dynamic Mutations)
240
+
241
+ ```typescript
242
+ const onSubmit = useCallback(
243
+ async (
244
+ formData: {Entity}DetailsFormData,
245
+ initialData: DetailsProps<{Entity}DetailsFormData>['initialData']
246
+ ): Promise<void> => {
247
+ const generateUpdateGQLFragment =
248
+ createUpdateGQLFragmentGenerator<Mutation>();
249
+
250
+ // TagLike association mutations:
251
+ // For each TagLike association:
252
+ const {semanticPluralName}AssignmentMutations = generateArrayMutations({
253
+ current: formData.{semanticPluralName},
254
+ original: initialData.data?.{semanticPluralName},
255
+ generateCreateMutation: ({displayField}) =>
256
+ generateUpdateGQLFragment<MutationCreate{PascalConnectionField}Args>(
257
+ 'create{PascalConnectionField}',
258
+ { input: { {camelConnectionField}: { {displayField}, {entityCamel}Id } } }
259
+ ),
260
+ generateDeleteMutation: ({displayField}) =>
261
+ generateUpdateGQLFragment<MutationDelete{PascalConnectionField}Args>(
262
+ 'delete{PascalConnectionField}',
263
+ { input: { {entityCamel}Id, {displayField} } }
264
+ ),
265
+ prefix: '{camelConnectionField}',
266
+ });
267
+ // End for
268
+
269
+ // ManyToMany association mutations:
270
+ // For each ManyToMany association:
271
+ const {label.toLowerCase()}AssignmentMutations = generateArrayMutations({
272
+ current: formData.{label.toLowerCase()},
273
+ original: initialData.data?.{label.toLowerCase()},
274
+ generateCreateMutation: ({displayField}) => {
275
+ const {relatedCamel}Id = all{Label}[{displayField}].id;
276
+
277
+ if ({relatedCamel}Id) {
278
+ return generateUpdateGQLFragment<MutationCreate{PascalJunctionType}Args>(
279
+ 'create{PascalJunctionType}',
280
+ {
281
+ input: {
282
+ {camelJunctionType}: {
283
+ {entityCamel}Id,
284
+ {relatedCamel}Id,
285
+ },
286
+ },
287
+ }
288
+ );
289
+ } else {
290
+ return '';
291
+ }
292
+ },
293
+ generateDeleteMutation: ({displayField}) => {
294
+ const {relatedCamel}Id = all{Label}[{displayField}].id;
295
+ if ({relatedCamel}Id) {
296
+ return generateUpdateGQLFragment<MutationDelete{PascalJunctionType}Args>(
297
+ 'delete{PascalJunctionType}',
298
+ {
299
+ input: { {entityCamel}Id, {relatedCamel}Id },
300
+ }
301
+ );
302
+ } else {
303
+ return '';
304
+ }
305
+ },
306
+ prefix: '{camelJunctionType}',
307
+ });
308
+ // End for
309
+
310
+ // Create patch (diff of scalar fields only)
311
+ const patch = createUpdateDto(formData, initialData.data);
312
+
313
+ // Build dynamic mutation document
314
+ const GqlMutationDocument = gql`mutation Update{Entity}($input: Update{Entity}Input!) {
315
+ update{Entity}(input: $input) {
316
+ clientMutationId
317
+ {entityCamel} {
318
+ id
319
+ {titleField}
320
+ }
321
+ }
322
+ // For each TagLike:
323
+ ${{{semanticPluralName}AssignmentMutations}}
324
+ // End for
325
+ // For each ManyToMany:
326
+ ${{{label.toLowerCase()}AssignmentMutations}}
327
+ // End for
328
+ }`;
329
+
330
+ await client.mutate<unknown, { input: Update{Entity}Input }>({
331
+ mutation: GqlMutationDocument,
332
+ variables: { input: { id: {entityCamel}Id, patch } },
333
+ refetchQueries: [{Entity}Document],
334
+ awaitRefetchQueries: true,
335
+ });
336
+ },
337
+ [
338
+ // For each ManyToMany:
339
+ all{Label},
340
+ // End for
341
+ {entityCamel}Id
342
+ ]
343
+ );
344
+ ```
345
+
346
+ ## Section 4: Panel Sub-Component (Info Panel)
347
+
348
+ ```typescript
349
+ const Panel: React.FC = () => {
350
+ // If ExtensionsContext discovered:
351
+ const { ImageCover } = useContext(ExtensionsContext);
352
+ // End if
353
+ const { values } = useFormikContext<{Entity}>();
354
+
355
+ return useMemo(() => {
356
+ // If features.capabilities.images exists and ExtensionsContext discovered:
357
+ let coverImageId: ID;
358
+ let coverImageCount = 0;
359
+ let teaserImageCount = 0;
360
+
361
+ values.{features.capabilities.images.connectionField}?.nodes.forEach(
362
+ ({ imageId, imageType }) => {
363
+ switch (imageType) {
364
+ // For each image type value:
365
+ case {ImageTypeEnum}.{VALUE}:
366
+ {value.toLowerCase()}ImageCount++;
367
+ // If COVER (first value):
368
+ coverImageId = imageId;
369
+ // End if
370
+ break;
371
+ // End for
372
+ default:
373
+ break;
374
+ }
375
+ }
376
+ );
377
+ // End if
378
+
379
+ return (
380
+ <InfoPanel>
381
+ // If ExtensionsContext and images capability:
382
+ <Section>
383
+ <ImageCover id={coverImageId} />
384
+ </Section>
385
+ // End if
386
+
387
+ <Section title="Additional Information">
388
+ <Paragraph title="ID">{values.id}</Paragraph>
389
+ // For each enum field in features.fields.scalars where type === 'Enum':
390
+ // Only display enums (already filtered by isMutationControlled during extraction)
391
+ {values.{enumField} && (
392
+ <Paragraph title="{Label}">
393
+ {getEnumLabel(values.{enumField})}
394
+ </Paragraph>
395
+ )}
396
+ // End for
397
+ <Paragraph title="Created">
398
+ {formatDateTime(values.createdDate)} by {values.createdUser}
399
+ </Paragraph>
400
+ <Paragraph title="Last Modified">
401
+ {formatDateTime(values.updatedDate)} by {values.updatedUser}
402
+ </Paragraph>
403
+ </Section>
404
+
405
+ // If features.capabilities exists (videos or images):
406
+ <Section title="Assigned Items">
407
+ // If features.capabilities.videos exists:
408
+ <Paragraph title="Videos">
409
+ <div className={classes.datalist}>
410
+ // If mainVideoField exists:
411
+ <div>Main Video</div>
412
+ <div className={classes.rightAlignment}>
413
+ {values.{mainVideoField} ? 1 : 0}/1
414
+ </div>
415
+ // End if
416
+ // If trailersConnection exists:
417
+ <div>Trailers</div>
418
+ <div className={classes.rightAlignment}>
419
+ {values.{trailersConnection}?.totalCount}/many
420
+ </div>
421
+ // End if
422
+ </div>
423
+ </Paragraph>
424
+ // End if
425
+
426
+ // If features.capabilities.images exists:
427
+ <Paragraph title="Images">
428
+ <div className={classes.datalist}>
429
+ // For each image type value:
430
+ <div>{Value}</div>
431
+ <div className={classes.rightAlignment}>
432
+ {{value.toLowerCase()}ImageCount} / 1
433
+ </div>
434
+ // End for
435
+ </div>
436
+ </Paragraph>
437
+ // End if
438
+ </Section>
439
+ // End if
440
+ </InfoPanel>
441
+ );
442
+ }, [
443
+ // If ExtensionsContext:
444
+ ImageCover,
445
+ // End if
446
+ values.createdDate,
447
+ values.createdUser,
448
+ values.id,
449
+ // If videos capability:
450
+ // If mainVideoField:
451
+ values.{mainVideoField},
452
+ // End if
453
+ // If trailersConnection:
454
+ values.{trailersConnection}?.totalCount,
455
+ // End if
456
+ // End if
457
+ // If images capability:
458
+ values.{connectionField}?.nodes,
459
+ // End if
460
+ values.updatedDate,
461
+ values.updatedUser,
462
+ ]);
463
+ };
464
+ ```
465
+
466
+ ## Section 5: Form Sub-Component (Fields)
467
+
468
+ ```typescript
469
+ const Form: React.FC<{
470
+ // For each ManyToMany:
471
+ {label.toLowerCase()}Options?: string[];
472
+ // End for
473
+ }> = ({
474
+ // For each ManyToMany:
475
+ {label.toLowerCase()}Options,
476
+ // End for
477
+ }) => {
478
+ // TagLike suggestion resolvers:
479
+ // For each TagLike association:
480
+ const {semanticPluralName}Resolver = async (
481
+ value: string
482
+ ): Promise<(string | null)[]> => {
483
+ const { data } = await client.query<
484
+ Search{Entity}{Label}Query,
485
+ Search{Entity}{Label}QueryVariables
486
+ >({
487
+ query: Search{Entity}{Label}Document,
488
+ variables: { searchKey: value, limit: 10 },
489
+ });
490
+ return data.get{PascalConnectionField}Values?.nodes ?? [];
491
+ };
492
+ // End for
493
+
494
+ return (
495
+ <>
496
+ // For each scalar field in features.fields.scalars:
497
+ // Apply component mapping from `field-component-mapping` based on scalar.type:
498
+ // - String (short) → SingleLineTextField
499
+ // - String (long) → TextAreaField (name contains: description, notes, comment, body, content)
500
+ // - Int/Float → SingleLineTextField with type="number"
501
+ // - Boolean → CheckboxField or RadioField (AI decides)
502
+ // - Date → DateTimeTextField with modifyTime={false}
503
+ // - Datetime → DateTimeTextField
504
+ // - Enum → SelectField with enum options
505
+ // End for
506
+
507
+ // TagLike associations (autocomplete):
508
+ // For each TagLike association:
509
+ <Field
510
+ name="{semanticPluralName}"
511
+ label="{tagLike.label}"
512
+ liveSuggestionsResolver={{semanticPluralName}Resolver}
513
+ as={CustomTagsField}
514
+ />
515
+ // End for
516
+
517
+ // ManyToMany associations (predefined options):
518
+ // For each ManyToMany association:
519
+ <Field
520
+ name="{label.toLowerCase()}"
521
+ label="{manyToMany.label}"
522
+ tagsOptions={{label.toLowerCase()}Options}
523
+ as={TagsField}
524
+ />
525
+ // End for
526
+ </>
527
+ );
528
+ };
529
+ ```
530
+
531
+ ## Section 6: Helper Function
532
+
533
+ ```typescript
534
+ function createUpdateDto(
535
+ currentValues: {Entity}DetailsFormData,
536
+ initialValues?: {Entity}DetailsFormData | null
537
+ ): Partial<{Entity}DetailsFormData> {
538
+ const {
539
+ // Remove association arrays from diff:
540
+ // For each TagLike:
541
+ {semanticPluralName},
542
+ // End for
543
+ // For each ManyToMany:
544
+ {label.toLowerCase()},
545
+ // End for
546
+ ...rest
547
+ } = getFormDiff(currentValues, initialValues);
548
+
549
+ return rest;
550
+ }
551
+ ```
552
+
553
+ ## Fallback: getEnumLabel
554
+
555
+ If `getEnumLabel` not discovered in project, add inline implementation:
556
+
557
+ ```typescript
558
+ /**
559
+ * Converts SCREAMING_SNAKE_CASE enum values to Title Case
560
+ * @example "PUBLISH_STATUS" → "Publish Status"
561
+ */
562
+ const getEnumLabel = (value: string | null | undefined): string => {
563
+ if (!value) return '';
564
+ return value
565
+ .split('_')
566
+ .map((word) => word.charAt(0) + word.slice(1).toLowerCase())
567
+ .join(' ');
568
+ };
569
+ ```
570
+
571
+ ## CSS Module (minimal)
572
+
573
+ Create `{Entity}Details.module.scss`:
574
+
575
+ ```scss
576
+ .datalist {
577
+ display: grid;
578
+ grid-template-columns: 1fr auto;
579
+ gap: 0.5rem;
580
+ }
581
+
582
+ .rightAlignment {
583
+ text-align: right;
584
+ }
585
+
586
+ .externalId {
587
+ grid-column: 1 / -1;
588
+ }
589
+ ```
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: details-wrapper
3
+ input:
4
+ - EntityFeatures
5
+ output:
6
+ - path: '{workflowsPath}/src/Stations/{EntityPlural}/{Entity}Details/{Entity}Details.tsx'
7
+ description: 'Wrapper component that extracts URL params'
8
+ ---
9
+
10
+ # Generate Router Component
11
+
12
+ Simple wrapper that extracts URL parameter and delegates to DetailsForm.
13
+
14
+ ## Template
15
+
16
+ ```typescript
17
+ import React from 'react';
18
+ import { useParams } from 'react-router-dom';
19
+ import { {Entity}DetailsForm } from './{Entity}DetailsForm';
20
+
21
+ interface UrlParams {
22
+ {entityCamel}Id: string;
23
+ }
24
+
25
+ export const {Entity}Details: React.FC = () => {
26
+ const { {entityCamel}Id } = useParams<UrlParams>();
27
+
28
+ return <{Entity}DetailsForm {entityCamel}Id={Number({entityCamel}Id)} />;
29
+ };
30
+ ```
31
+
32
+ ## Notes
33
+
34
+ - URL parameter name follows pattern: `{entityCamel}Id`
35
+ - Always converts string param to `Number` (handles UUID if needed in future)
36
+ - Delegates all logic to `{Entity}DetailsForm` component
37
+
38
+ ## Example
39
+
40
+ ```typescript
41
+ import React from 'react';
42
+ import { useParams } from 'react-router-dom';
43
+ import { MovieDetailsForm } from './MovieDetailsForm';
44
+
45
+ interface UrlParams {
46
+ movieId: string;
47
+ }
48
+
49
+ export const MovieDetails: React.FC = () => {
50
+ const { movieId } = useParams<UrlParams>();
51
+
52
+ return <MovieDetailsForm movieId={Number(movieId)} />;
53
+ };
54
+ ```
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: form-data-types
3
+ input:
4
+ - EntityFeatures
5
+ - Generated GraphQL types
6
+ output:
7
+ - path: "{workflowsPath}/src/Stations/{EntityPlural}/{Entity}Details/{Entity}Details.types.ts"
8
+ description: "Form data type definition"
9
+ ---
10
+
11
+ # Generate types.ts
12
+
13
+ Form data type extends GraphQL update patch with transformed association arrays.
14
+
15
+ ## Template
16
+
17
+ ```typescript
18
+ import { Nullable } from '@axinom/mosaic-ui';
19
+ import { MutationUpdate{Entity}Args } from '../../../generated/graphql';
20
+
21
+ export type {Entity}DetailsFormData = Nullable<
22
+ MutationUpdate{Entity}Args['input']['patch']
23
+ > & {
24
+ // For each TagLike association, use natural/semantic plural:
25
+ {semanticPluralName}?: string[];
26
+
27
+ // For each ManyToMany association, use lowercase plural of label:
28
+ {manyToMany.label.toLowerCase()}?: string[];
29
+ };
30
+ ```
31
+
32
+ ## Association Field Naming
33
+
34
+ **TagLike associations:**
35
+
36
+ Use natural/semantic field names based on the association's meaning:
37
+ - `moviesTags` (displayField: "name") → `tags` (naturally plural)
38
+ - `moviesCasts` (displayField: "name") → `cast` (collective noun, singular)
39
+ - `moviesProductionCountries` (displayField: "name") → `productionCountries` (naturally plural)
40
+
41
+ Pattern: Derive from connection field name, removing entity prefix and using natural plural form.
42
+
43
+ **ManyToMany associations:**
44
+
45
+ - Use lowercase plural form of `label`
46
+ - Examples: `genres`, `categories`, `types`
47
+
48
+ ## Example
49
+
50
+ ```typescript
51
+ import { Nullable } from '@axinom/mosaic-ui';
52
+ import { MutationUpdateMovieArgs } from '../../../generated/graphql';
53
+
54
+ export type MovieDetailsFormData = Nullable<
55
+ MutationUpdateMovieArgs['input']['patch']
56
+ > & {
57
+ tags?: string[]; // from moviesTags
58
+ genres?: string[]; // from moviesMovieGenres
59
+ cast?: string[]; // from moviesCasts (collective noun)
60
+ productionCountries?: string[]; // from moviesProductionCountries
61
+ };
62
+ ```