@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,256 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: graphql-operations
|
|
3
|
+
input:
|
|
4
|
+
- EntityFeatures
|
|
5
|
+
output:
|
|
6
|
+
- path: '{workflowsPath}/src/Stations/{EntityPlural}/{Entity}Details/{Entity}Details.graphql'
|
|
7
|
+
description:
|
|
8
|
+
'GraphQL query, mutations, and search queries for details station'
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Generate GraphQL File
|
|
12
|
+
|
|
13
|
+
Single file with entity query, update/delete mutations, search queries for
|
|
14
|
+
autocomplete.
|
|
15
|
+
|
|
16
|
+
## Template
|
|
17
|
+
|
|
18
|
+
```graphql
|
|
19
|
+
# Main query - fetch complete entity data
|
|
20
|
+
query {features.name}($id: {features.idType}!) {
|
|
21
|
+
{features.camelCase}(id: $id) {
|
|
22
|
+
# ID field
|
|
23
|
+
id
|
|
24
|
+
|
|
25
|
+
# Scalar fields from features.fields.scalars[]
|
|
26
|
+
# For each scalar in features.fields.scalars:
|
|
27
|
+
{scalar.name}
|
|
28
|
+
|
|
29
|
+
# TagLike associations from features.fields.associations.tagLike[]
|
|
30
|
+
# For each tagLike in features.fields.associations.tagLike:
|
|
31
|
+
{tagLike.connectionField} {
|
|
32
|
+
nodes {
|
|
33
|
+
{tagLike.displayField}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# ManyToMany associations from features.fields.associations.manyToMany[]
|
|
38
|
+
# For each manyToMany in features.fields.associations.manyToMany:
|
|
39
|
+
{manyToMany.connectionField} {
|
|
40
|
+
nodes {
|
|
41
|
+
{manyToMany.relatedField} {
|
|
42
|
+
{manyToMany.displayField}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# If features.capabilities.images exists:
|
|
48
|
+
{features.capabilities.images.connectionField} {
|
|
49
|
+
nodes {
|
|
50
|
+
imageType
|
|
51
|
+
imageId
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# If features.capabilities.videos.mainVideoField exists:
|
|
56
|
+
{features.capabilities.videos.mainVideoField}
|
|
57
|
+
|
|
58
|
+
# If features.capabilities.videos.trailersConnection exists:
|
|
59
|
+
{features.capabilities.videos.trailersConnection} {
|
|
60
|
+
totalCount
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Audit fields
|
|
64
|
+
createdDate
|
|
65
|
+
createdUser
|
|
66
|
+
updatedDate
|
|
67
|
+
updatedUser
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# If manyToMany associations exist, fetch global lookup data:
|
|
71
|
+
# For each manyToMany in features.fields.associations.manyToMany:
|
|
72
|
+
{manyToMany.relatedQueryField} {
|
|
73
|
+
nodes {
|
|
74
|
+
{manyToMany.displayField}
|
|
75
|
+
id
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Update mutation
|
|
81
|
+
mutation Update{features.name}($input: Update{features.name}Input!) {
|
|
82
|
+
update{features.name}(input: $input) {
|
|
83
|
+
clientMutationId
|
|
84
|
+
{features.camelCase} {
|
|
85
|
+
id
|
|
86
|
+
{features.titleField}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Delete mutation (if features.mutations.delete === true)
|
|
92
|
+
mutation Delete{features.name}($input: Delete{features.name}Input!) {
|
|
93
|
+
delete{features.name}(input: $input) {
|
|
94
|
+
clientMutationId
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# Breadcrumb query - minimal fields for breadcrumb display
|
|
99
|
+
query {features.name}Title($id: {features.idType}!) {
|
|
100
|
+
{features.camelCase}(id: $id) {
|
|
101
|
+
id
|
|
102
|
+
{features.titleField}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# Search queries for autocomplete (TagLike associations only)
|
|
107
|
+
# For each tagLike in features.fields.associations.tagLike:
|
|
108
|
+
# Use pattern: get{PascalConnectionField}Values helper query
|
|
109
|
+
query Search{features.name}{tagLike.label}($searchKey: String!, $limit: Int!) {
|
|
110
|
+
get{PascalCase(tagLike.connectionField)}Values(
|
|
111
|
+
filter: { startsWithInsensitive: $searchKey }
|
|
112
|
+
first: $limit
|
|
113
|
+
) {
|
|
114
|
+
nodes
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Field Selection Rules
|
|
120
|
+
|
|
121
|
+
Include in main query:
|
|
122
|
+
|
|
123
|
+
- All `features.fields.scalars[]` fields (user-editable scalars)
|
|
124
|
+
- All `features.fields.associations.tagLike[]` connections
|
|
125
|
+
- All `features.fields.associations.manyToMany[]` connections
|
|
126
|
+
- Image/video capability fields if present
|
|
127
|
+
- Audit fields (`createdDate`, `createdUser`, `updatedDate`, `updatedUser`)
|
|
128
|
+
- Always: `id`, `titleField`, `nameField` (unique identifier fields)
|
|
129
|
+
|
|
130
|
+
**Note:** `features.fields.scalars` already excludes mutation-controlled fields
|
|
131
|
+
(marked with `scalar.isMutationControlled === true` during extraction)
|
|
132
|
+
|
|
133
|
+
## Example
|
|
134
|
+
|
|
135
|
+
```graphql
|
|
136
|
+
query Movie($id: Int!) {
|
|
137
|
+
movie(id: $id) {
|
|
138
|
+
id
|
|
139
|
+
title
|
|
140
|
+
originalTitle
|
|
141
|
+
synopsis
|
|
142
|
+
description
|
|
143
|
+
externalId
|
|
144
|
+
released
|
|
145
|
+
studio
|
|
146
|
+
|
|
147
|
+
moviesTags {
|
|
148
|
+
nodes {
|
|
149
|
+
name
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
moviesMovieGenres {
|
|
154
|
+
nodes {
|
|
155
|
+
movieGenres {
|
|
156
|
+
title
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
moviesCasts {
|
|
162
|
+
nodes {
|
|
163
|
+
name
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
moviesProductionCountries {
|
|
168
|
+
nodes {
|
|
169
|
+
name
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
moviesImages {
|
|
174
|
+
nodes {
|
|
175
|
+
imageType
|
|
176
|
+
imageId
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
mainVideoId
|
|
181
|
+
|
|
182
|
+
moviesTrailers {
|
|
183
|
+
totalCount
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
createdDate
|
|
187
|
+
createdUser
|
|
188
|
+
updatedDate
|
|
189
|
+
updatedUser
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
movieGenres {
|
|
193
|
+
nodes {
|
|
194
|
+
title
|
|
195
|
+
id
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
mutation UpdateMovie($input: UpdateMovieInput!) {
|
|
201
|
+
updateMovie(input: $input) {
|
|
202
|
+
clientMutationId
|
|
203
|
+
movie {
|
|
204
|
+
id
|
|
205
|
+
title
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
mutation DeleteMovie($input: DeleteMovieInput!) {
|
|
211
|
+
deleteMovie(input: $input) {
|
|
212
|
+
clientMutationId
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
query MovieTitle($id: Int!) {
|
|
217
|
+
movie(id: $id) {
|
|
218
|
+
id
|
|
219
|
+
title
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
query SearchMovieTags($searchKey: String!, $limit: Int!) {
|
|
224
|
+
getMoviesTagsValues(
|
|
225
|
+
filter: { startsWithInsensitive: $searchKey }
|
|
226
|
+
first: $limit
|
|
227
|
+
) {
|
|
228
|
+
nodes
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
query SearchMovieCasts($searchKey: String!, $limit: Int!) {
|
|
233
|
+
getMoviesCastsValues(
|
|
234
|
+
filter: { startsWithInsensitive: $searchKey }
|
|
235
|
+
first: $limit
|
|
236
|
+
) {
|
|
237
|
+
nodes
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
query SearchMovieProductionCountries($searchKey: String!, $limit: Int!) {
|
|
242
|
+
getMoviesProductionCountriesValues(
|
|
243
|
+
filter: { startsWithInsensitive: $searchKey }
|
|
244
|
+
first: $limit
|
|
245
|
+
) {
|
|
246
|
+
nodes
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Notes
|
|
252
|
+
|
|
253
|
+
- VS Code may briefly show "Syntax Error: Unexpected <EOF>" when creating the
|
|
254
|
+
GraphQL file. This is a false positive caused by a race condition between file
|
|
255
|
+
creation and the `Apollo` extension's validation. It auto-resolves within
|
|
256
|
+
seconds and is safe to ignore and proceed.
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: image-management-station
|
|
3
|
+
input:
|
|
4
|
+
- EntityFeatures (with capabilities.images)
|
|
5
|
+
- DiscoveredPaths
|
|
6
|
+
output:
|
|
7
|
+
- path: '{workflowsPath}/src/Stations/{EntityPlural}/{Entity}ImageManagement/'
|
|
8
|
+
description: 'Complete image management station (4 files)'
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Generate Image Management Station
|
|
12
|
+
|
|
13
|
+
Creates station for managing entity images (COVER, TEASER, etc.).
|
|
14
|
+
|
|
15
|
+
`Conditional` - only generate if `EntityFeatures.capabilities.images` exists.
|
|
16
|
+
|
|
17
|
+
## Prerequisites
|
|
18
|
+
|
|
19
|
+
**Verify capability exists:**
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
if (!EntityFeatures.capabilities.images) {
|
|
23
|
+
skip this step
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Files to Generate
|
|
28
|
+
|
|
29
|
+
1. `{Entity}ImageManagement.tsx` - Router wrapper
|
|
30
|
+
2. `{Entity}ImageManagementForm.tsx` - Form component
|
|
31
|
+
3. `{Entity}ImageManagement.graphql` - Query
|
|
32
|
+
4. `{Entity}ImageManagementQuickEdit.tsx` - Quick edit wrapper
|
|
33
|
+
|
|
34
|
+
## File 1: Router Wrapper
|
|
35
|
+
|
|
36
|
+
**Path:** `{Entity}ImageManagement/{Entity}ImageManagement.tsx`
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import React from 'react';
|
|
40
|
+
import { useParams } from 'react-router-dom';
|
|
41
|
+
import { {Entity}ImageManagementForm } from './{Entity}ImageManagementForm';
|
|
42
|
+
|
|
43
|
+
interface UrlParams {
|
|
44
|
+
{entityCamel}Id: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const {Entity}ImageManagement: React.FC = () => {
|
|
48
|
+
const { {entityCamel}Id } = useParams<UrlParams>();
|
|
49
|
+
|
|
50
|
+
return <{Entity}ImageManagementForm {entityCamel}Id={Number({entityCamel}Id)} />;
|
|
51
|
+
};
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## File 2: GraphQL Query
|
|
55
|
+
|
|
56
|
+
**Path:** `{Entity}ImageManagement/{Entity}ImageManagement.graphql`
|
|
57
|
+
|
|
58
|
+
```graphql
|
|
59
|
+
query {Entity}Images($id: {features.idType}!) {
|
|
60
|
+
{features.camelCase}(id: $id) {
|
|
61
|
+
{features.capabilities.images.connectionField} {
|
|
62
|
+
nodes {
|
|
63
|
+
imageId
|
|
64
|
+
imageType
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## File 3: Form Component
|
|
72
|
+
|
|
73
|
+
**Path:** `{Entity}ImageManagement/{Entity}ImageManagementForm.tsx`
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import {
|
|
77
|
+
createUpdateGQLFragmentGenerator,
|
|
78
|
+
Details,
|
|
79
|
+
DetailsProps,
|
|
80
|
+
} from '@axinom/mosaic-ui';
|
|
81
|
+
import { Field } from 'formik';
|
|
82
|
+
import gql from 'graphql-tag';
|
|
83
|
+
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
|
84
|
+
import { client } from '../../../apolloClient';
|
|
85
|
+
import { ExtensionsContext } from '{discoveredPaths.extensionsContextPath}';
|
|
86
|
+
import {
|
|
87
|
+
{features.capabilities.images.imageTypeEnum},
|
|
88
|
+
{features.capabilities.images.nodeType},
|
|
89
|
+
Mutation,
|
|
90
|
+
MutationCreate{features.capabilities.images.nodeType}Args,
|
|
91
|
+
MutationDelete{features.capabilities.images.nodeType}By{Entity}IdAndImageTypeArgs,
|
|
92
|
+
MutationUpdate{features.capabilities.images.nodeType}By{Entity}IdAndImageTypeArgs,
|
|
93
|
+
use{Entity}ImagesQuery,
|
|
94
|
+
} from '../../../generated/graphql';
|
|
95
|
+
|
|
96
|
+
interface {Entity}ImageManagementFormProps {
|
|
97
|
+
{entityCamel}Id: number;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
type ImageNodes = Pick<{features.capabilities.images.nodeType}, 'imageId' | 'imageType'> & {
|
|
101
|
+
__typename: '{features.capabilities.images.nodeType}';
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
type FormData = Record<{features.capabilities.images.imageTypeEnum}, string[]>;
|
|
105
|
+
|
|
106
|
+
const Form: React.FC<{ imageSelectField: unknown }> = ({
|
|
107
|
+
imageSelectField,
|
|
108
|
+
}) => {
|
|
109
|
+
return (
|
|
110
|
+
<>
|
|
111
|
+
{Object.keys({features.capabilities.images.imageTypeEnum}).map((type) => {
|
|
112
|
+
const field = {features.capabilities.images.imageTypeEnum}[type];
|
|
113
|
+
return (
|
|
114
|
+
<Field
|
|
115
|
+
key={field}
|
|
116
|
+
name={field}
|
|
117
|
+
label={type}
|
|
118
|
+
as={imageSelectField}
|
|
119
|
+
maxItems={1}
|
|
120
|
+
title="Select Image"
|
|
121
|
+
imageScope="{features.capabilities.images.scope}"
|
|
122
|
+
/>
|
|
123
|
+
);
|
|
124
|
+
})}
|
|
125
|
+
</>
|
|
126
|
+
);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export const {Entity}ImageManagementForm: React.FC<
|
|
130
|
+
{Entity}ImageManagementFormProps
|
|
131
|
+
> = ({ {entityCamel}Id }) => {
|
|
132
|
+
const { loading, data, error } = use{Entity}ImagesQuery({
|
|
133
|
+
client,
|
|
134
|
+
variables: { id: {entityCamel}Id },
|
|
135
|
+
fetchPolicy: 'network-only',
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const { initialImages } = useImageTypes(
|
|
139
|
+
data?.{entityCamel}?.{features.capabilities.images.connectionField}.nodes as ImageNodes[],
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const onSubmit = useCallback(
|
|
143
|
+
async (
|
|
144
|
+
formData: FormData,
|
|
145
|
+
initialData: DetailsProps<FormData>['initialData'],
|
|
146
|
+
): Promise<void> => {
|
|
147
|
+
const generateUpdateGQLFragment =
|
|
148
|
+
createUpdateGQLFragmentGenerator<Mutation>();
|
|
149
|
+
|
|
150
|
+
const mutations: string[] = [];
|
|
151
|
+
|
|
152
|
+
const generateCreateMutation = (
|
|
153
|
+
imageId: string,
|
|
154
|
+
imageType: {features.capabilities.images.imageTypeEnum},
|
|
155
|
+
): string =>
|
|
156
|
+
generateUpdateGQLFragment<MutationCreate{features.capabilities.images.nodeType}Args>(
|
|
157
|
+
'create{features.capabilities.images.nodeType}',
|
|
158
|
+
{
|
|
159
|
+
input: {
|
|
160
|
+
{camelNodeType}: {
|
|
161
|
+
{entityCamel}Id,
|
|
162
|
+
imageId,
|
|
163
|
+
imageType: { type: 'enum', value: imageType },
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const generateDeleteMutation = (imageType: {features.capabilities.images.imageTypeEnum}): string =>
|
|
170
|
+
generateUpdateGQLFragment<MutationDelete{features.capabilities.images.nodeType}By{Entity}IdAndImageTypeArgs>(
|
|
171
|
+
'delete{features.capabilities.images.nodeType}By{Entity}IdAndImageType',
|
|
172
|
+
{
|
|
173
|
+
input: { {entityCamel}Id, imageType: { type: 'enum', value: imageType } },
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const generateUpdateMutation = (
|
|
178
|
+
imageId: string,
|
|
179
|
+
imageType: {features.capabilities.images.imageTypeEnum},
|
|
180
|
+
): string =>
|
|
181
|
+
generateUpdateGQLFragment<MutationUpdate{features.capabilities.images.nodeType}By{Entity}IdAndImageTypeArgs>(
|
|
182
|
+
'update{features.capabilities.images.nodeType}By{Entity}IdAndImageType',
|
|
183
|
+
{
|
|
184
|
+
input: {
|
|
185
|
+
patch: { imageId },
|
|
186
|
+
{entityCamel}Id,
|
|
187
|
+
imageType: { type: 'enum', value: imageType },
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
Object.entries(formData ?? {}).forEach(([imageType, imageId], idx) => {
|
|
193
|
+
const [imgId] = imageId;
|
|
194
|
+
const [initialValue] = initialData?.data?.[imageType];
|
|
195
|
+
const [currentValue] = formData[imageType];
|
|
196
|
+
|
|
197
|
+
switch (true) {
|
|
198
|
+
case initialValue === undefined && currentValue !== undefined:
|
|
199
|
+
mutations.push(
|
|
200
|
+
`assign${idx}:${generateCreateMutation(
|
|
201
|
+
imgId,
|
|
202
|
+
imageType as {features.capabilities.images.imageTypeEnum},
|
|
203
|
+
)}`,
|
|
204
|
+
);
|
|
205
|
+
break;
|
|
206
|
+
case initialValue !== undefined && currentValue === undefined:
|
|
207
|
+
mutations.push(
|
|
208
|
+
`assign${idx}:${generateDeleteMutation(
|
|
209
|
+
imageType as {features.capabilities.images.imageTypeEnum},
|
|
210
|
+
)}`,
|
|
211
|
+
);
|
|
212
|
+
break;
|
|
213
|
+
case initialValue !== currentValue:
|
|
214
|
+
mutations.push(
|
|
215
|
+
`assign${idx}:${generateUpdateMutation(
|
|
216
|
+
currentValue,
|
|
217
|
+
imageType as {features.capabilities.images.imageTypeEnum},
|
|
218
|
+
)}`,
|
|
219
|
+
);
|
|
220
|
+
break;
|
|
221
|
+
default:
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const GqlDoc = gql`mutation ImageAssignments {
|
|
227
|
+
${mutations}
|
|
228
|
+
}`;
|
|
229
|
+
|
|
230
|
+
await client.mutate({ mutation: GqlDoc });
|
|
231
|
+
},
|
|
232
|
+
[{entityCamel}Id],
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const { ImageSelectField } = useContext(ExtensionsContext);
|
|
236
|
+
|
|
237
|
+
return (
|
|
238
|
+
<Details<FormData>
|
|
239
|
+
defaultTitle="Image Management"
|
|
240
|
+
initialData={{
|
|
241
|
+
data: initialImages ?? {},
|
|
242
|
+
loading,
|
|
243
|
+
entityNotFound: data?.{entityCamel} === null,
|
|
244
|
+
error: error?.message,
|
|
245
|
+
}}
|
|
246
|
+
saveData={onSubmit}
|
|
247
|
+
>
|
|
248
|
+
<Form imageSelectField={ImageSelectField} />
|
|
249
|
+
</Details>
|
|
250
|
+
);
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Creates the initial image type values
|
|
255
|
+
* @param nodes - data nodes
|
|
256
|
+
*/
|
|
257
|
+
const useImageTypes = (
|
|
258
|
+
nodes: ImageNodes[] = [],
|
|
259
|
+
): {
|
|
260
|
+
readonly initialImages: FormData;
|
|
261
|
+
} => {
|
|
262
|
+
const [initialImages, setInitialImages] = useState<FormData>(getImageTypes());
|
|
263
|
+
|
|
264
|
+
// Set all currently assigned images on the server
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
if (nodes.length > 0) {
|
|
267
|
+
let temp = {} as FormData;
|
|
268
|
+
|
|
269
|
+
for (const { imageType, imageId } of nodes) {
|
|
270
|
+
temp = { ...temp, [imageType]: [imageId] };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
setInitialImages((prevState) => {
|
|
274
|
+
return { ...prevState, ...temp };
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}, [nodes]);
|
|
278
|
+
|
|
279
|
+
return { initialImages } as const;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Returns an image type with empty array value using the `{ImageTypeEnum}` enum
|
|
284
|
+
*/
|
|
285
|
+
const getImageTypes = (): FormData => {
|
|
286
|
+
let types = {} as FormData;
|
|
287
|
+
|
|
288
|
+
Object.keys({features.capabilities.images.imageTypeEnum}).map((type) => {
|
|
289
|
+
const field = {features.capabilities.images.imageTypeEnum}[type];
|
|
290
|
+
types = { ...types, [field]: [] };
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
return types;
|
|
294
|
+
};
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## File 4: Quick Edit Wrapper
|
|
298
|
+
|
|
299
|
+
**Path:** `{Entity}ImageManagement/{Entity}ImageManagementQuickEdit.tsx`
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
import { QuickEditContext, QuickEditContextType } from '@axinom/mosaic-ui';
|
|
303
|
+
import React, { useContext } from 'react';
|
|
304
|
+
import { {Entity}Data } from '../{EntityPlural}Explorer/common/{EntityPlural}.types';
|
|
305
|
+
import { {Entity}ImageManagementForm } from './{Entity}ImageManagementForm';
|
|
306
|
+
|
|
307
|
+
export const {Entity}ImageManagementQuickEdit: React.FC = () => {
|
|
308
|
+
const { selectedItem } =
|
|
309
|
+
useContext<QuickEditContextType<{Entity}Data>>(QuickEditContext);
|
|
310
|
+
|
|
311
|
+
return <{Entity}ImageManagementForm {entityCamel}Id={selectedItem.id} />;
|
|
312
|
+
};
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Notes
|
|
316
|
+
|
|
317
|
+
- Image scope uses `features.capabilities.images.scope` (e.g., `"movie"`,
|
|
318
|
+
`"episode"`)
|
|
319
|
+
- Form creates one field per image type from `imageTypeValues` enum
|
|
320
|
+
- Uses `ImageSelectField` from `ExtensionsContext` (media management
|
|
321
|
+
integration)
|
|
322
|
+
- Generates create/update/delete mutations dynamically based on form changes
|