@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,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
|
+
```
|