@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,220 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: explorer-filters
|
|
3
|
+
input:
|
|
4
|
+
- EntityFeatures
|
|
5
|
+
- selectedFilters
|
|
6
|
+
output:
|
|
7
|
+
- path: '{workflowsPath}/src/Stations/{EntityPlural}/{EntityPlural}Explorer/common/{EntityPlural}.filters.ts'
|
|
8
|
+
description: 'Filter options and transform functions'
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Generate filters.ts
|
|
12
|
+
|
|
13
|
+
**Discovery:** FilterUtils
|
|
14
|
+
|
|
15
|
+
- Pattern: `createTextFilter|createOptionsFilter|createSearchableFilter`
|
|
16
|
+
- Locations: `Util/FilterUtils/`, `utils/`
|
|
17
|
+
|
|
18
|
+
**Discovery:** getEnumLabel (if enum filters exist)
|
|
19
|
+
|
|
20
|
+
- Pattern: `getEnumLabel`
|
|
21
|
+
- Locations: `Util/StringEnumMapper/`, `utils/`
|
|
22
|
+
|
|
23
|
+
## Template
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { createDateRangeFilterValidators, filterToPostGraphileFilter, FilterType, FilterValues, transformRange } from '@axinom/mosaic-ui';
|
|
27
|
+
import { client } from '{discoveredPath}/apolloClient';
|
|
28
|
+
import { {Entity}Filter, /* filter option query types */ } from '../../../../generated/graphql';
|
|
29
|
+
import { createTextFilter, createSearchableFilter, createOptionsFilter, createDateRangeFilters, createNumericFilter } from '{discoveredPath}/FilterUtils';
|
|
30
|
+
// If enum filters:
|
|
31
|
+
import { getEnumLabel } from '{discoveredPath}/StringEnumMapper/StringEnumMapper';
|
|
32
|
+
import { {Entity}Data } from './{EntityPlural}.types';
|
|
33
|
+
|
|
34
|
+
// If associations in filters:
|
|
35
|
+
function use{Entity}FilterData(): {
|
|
36
|
+
{associationCamelPlural}: /* type */;
|
|
37
|
+
} {
|
|
38
|
+
const {associationCamelPlural} = use{Entity}{association.label}FilterOptionsQuery({
|
|
39
|
+
client,
|
|
40
|
+
variables: { orderBy: [{association.orderByType}.{DisplayField}Asc] },
|
|
41
|
+
fetchPolicy: 'network-only',
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
{associationCamelPlural}: {associationCamelPlural}.data?.{associationCamelPlural}?.nodes ?? [],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function use{EntityPlural}Filters(): {
|
|
50
|
+
readonly filterOptions: FilterType<{Entity}Data>[];
|
|
51
|
+
readonly transformFilters: (filters: FilterValues<{Entity}Data>) => {Entity}Filter | undefined;
|
|
52
|
+
} {
|
|
53
|
+
const [createFromDateFilterValidator, createToDateFilterValidator] =
|
|
54
|
+
createDateRangeFilterValidators<{Entity}Data>();
|
|
55
|
+
|
|
56
|
+
// If associations:
|
|
57
|
+
const { {associationCamelPlural} } = use{Entity}FilterData();
|
|
58
|
+
|
|
59
|
+
const filterOptions: FilterType<{Entity}Data>[] = [
|
|
60
|
+
createTextFilter('Title', '{titleField}'),
|
|
61
|
+
// Searchable association:
|
|
62
|
+
createSearchableFilter(
|
|
63
|
+
'{AssociationLabel}',
|
|
64
|
+
'{entityCamel}{AssociationPlural}',
|
|
65
|
+
{associationCamelPlural},
|
|
66
|
+
(item) => item?.{displayField} ?? '',
|
|
67
|
+
'Search {AssociationLabel}',
|
|
68
|
+
),
|
|
69
|
+
// Date range:
|
|
70
|
+
...createDateRangeFilters(
|
|
71
|
+
'{dateField}',
|
|
72
|
+
'{Label} (From)',
|
|
73
|
+
'{Label} (To)',
|
|
74
|
+
createFromDateFilterValidator,
|
|
75
|
+
createToDateFilterValidator,
|
|
76
|
+
),
|
|
77
|
+
createNumericFilter('ID', 'id'),
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const transformFilters = (filters: FilterValues<{Entity}Data>): {Entity}Filter | undefined => {
|
|
81
|
+
return filterToPostGraphileFilter<{Entity}Filter>(filters, {
|
|
82
|
+
{titleField}: 'includesInsensitive',
|
|
83
|
+
{entityCamel}{AssociationPlural}: ['some', '{displayField}', 'includesInsensitive'],
|
|
84
|
+
{dateField}: transformRange,
|
|
85
|
+
id: 'equalTo',
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return { filterOptions, transformFilters };
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Fallbacks
|
|
94
|
+
|
|
95
|
+
If utilities not found, use inline implementations:
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
// If FilterUtils not found:
|
|
99
|
+
// TODO: [MISSING_UTILITY] FilterUtils not found - using inline implementations
|
|
100
|
+
import {
|
|
101
|
+
Data,
|
|
102
|
+
FilterType,
|
|
103
|
+
FilterTypes,
|
|
104
|
+
FilterValidatorFunction,
|
|
105
|
+
} from '@axinom/mosaic-ui';
|
|
106
|
+
|
|
107
|
+
function createTextFilter<T extends Data>(
|
|
108
|
+
label: string,
|
|
109
|
+
property: keyof T,
|
|
110
|
+
): FilterType<T> {
|
|
111
|
+
return { label, property, type: FilterTypes.FreeText };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function createNumericFilter<T extends Data>(
|
|
115
|
+
label: string,
|
|
116
|
+
property: keyof T,
|
|
117
|
+
): FilterType<T> {
|
|
118
|
+
return { label, property, type: FilterTypes.Numeric };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function createOptionsFilter<T extends Data>(
|
|
122
|
+
label: string,
|
|
123
|
+
property: keyof T,
|
|
124
|
+
options: { label: string; value: string | number | boolean }[],
|
|
125
|
+
): FilterType<T> {
|
|
126
|
+
return { label, property, type: FilterTypes.Options, options };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function createSearchableFilter<T extends Data, K>(
|
|
130
|
+
label: string,
|
|
131
|
+
property: keyof T,
|
|
132
|
+
items: K[] | undefined,
|
|
133
|
+
labelSelector: (item: K) => string,
|
|
134
|
+
placeholder: string,
|
|
135
|
+
maxItems = 10,
|
|
136
|
+
): FilterType<T> {
|
|
137
|
+
return {
|
|
138
|
+
label,
|
|
139
|
+
property,
|
|
140
|
+
type: FilterTypes.SearcheableOptions,
|
|
141
|
+
optionsProvider: createSearchableOptionsProvider(
|
|
142
|
+
items,
|
|
143
|
+
labelSelector,
|
|
144
|
+
labelSelector,
|
|
145
|
+
maxItems,
|
|
146
|
+
),
|
|
147
|
+
searchInputPlaceholder: placeholder,
|
|
148
|
+
maxItems,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function createDateRangeFilters<T extends Data>(
|
|
153
|
+
property: keyof T,
|
|
154
|
+
fromLabel: string,
|
|
155
|
+
toLabel: string,
|
|
156
|
+
createFromValidator: (key: keyof T) => FilterValidatorFunction<T>,
|
|
157
|
+
createToValidator: (key: keyof T) => FilterValidatorFunction<T>,
|
|
158
|
+
): FilterType<T>[] {
|
|
159
|
+
return [
|
|
160
|
+
{
|
|
161
|
+
label: fromLabel,
|
|
162
|
+
property,
|
|
163
|
+
type: FilterTypes.Date,
|
|
164
|
+
onValidate: createFromValidator(property),
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
label: toLabel,
|
|
168
|
+
property,
|
|
169
|
+
type: FilterTypes.Date,
|
|
170
|
+
onValidate: createToValidator(property),
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function createSearchableOptionsProvider<T>(
|
|
176
|
+
items: T[] | undefined,
|
|
177
|
+
labelSelector: (item: T) => string,
|
|
178
|
+
valueSelector: (item: T) => string = labelSelector,
|
|
179
|
+
maxItems = 10,
|
|
180
|
+
): (searchText: string) => { label: string; value: string }[] {
|
|
181
|
+
return (searchText: string) => {
|
|
182
|
+
const searchLower = searchText.trim().toLowerCase();
|
|
183
|
+
const uniqueItems = removeDuplicatesBy(items || [], valueSelector);
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
uniqueItems
|
|
187
|
+
.filter((item) =>
|
|
188
|
+
labelSelector(item).toLowerCase().includes(searchLower),
|
|
189
|
+
)
|
|
190
|
+
.slice(0, maxItems)
|
|
191
|
+
.map((item) => ({
|
|
192
|
+
label: labelSelector(item),
|
|
193
|
+
value: valueSelector(item),
|
|
194
|
+
})) || []
|
|
195
|
+
);
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function removeDuplicatesBy<T, K extends string | number>(
|
|
200
|
+
array: T[],
|
|
201
|
+
keySelector: (item: T) => K | null | undefined,
|
|
202
|
+
): T[] {
|
|
203
|
+
const seen = new Set<K>();
|
|
204
|
+
return array.filter((item) => {
|
|
205
|
+
const key = keySelector(item);
|
|
206
|
+
if (key != null && !seen.has(key)) {
|
|
207
|
+
seen.add(key);
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function getEnumLabel(value: string): string {
|
|
215
|
+
return value
|
|
216
|
+
.split('_')
|
|
217
|
+
.map((w) => w.charAt(0) + w.slice(1).toLowerCase())
|
|
218
|
+
.join(' ');
|
|
219
|
+
}
|
|
220
|
+
```
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: explorer-inline-actions
|
|
3
|
+
input:
|
|
4
|
+
- EntityFeatures
|
|
5
|
+
output:
|
|
6
|
+
- path: '{workflowsPath}/src/Stations/{EntityPlural}/{EntityPlural}Explorer/common/{EntityPlural}.inlineActions.ts'
|
|
7
|
+
description: 'Inline action menu generator'
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Generate inlineActions.ts
|
|
11
|
+
|
|
12
|
+
**Always generate** with TODOs for missing features.
|
|
13
|
+
|
|
14
|
+
## Template
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { ActionData, IconName } from '@axinom/mosaic-ui';
|
|
18
|
+
import { {Entity}Data } from './{EntityPlural}.types';
|
|
19
|
+
|
|
20
|
+
export function use{EntityPlural}InlineActions(): {
|
|
21
|
+
readonly generateInlineMenuActions: (data: {Entity}Data) => ActionData[];
|
|
22
|
+
} {
|
|
23
|
+
const generateInlineMenuActions = ({ id }: {Entity}Data): ActionData[] => {
|
|
24
|
+
return [
|
|
25
|
+
// TODO: Add Delete inline action once the Details station with mutations exists
|
|
26
|
+
// Example structure:
|
|
27
|
+
// {
|
|
28
|
+
// label: 'Delete',
|
|
29
|
+
// onActionSelected: async () => {
|
|
30
|
+
// await deleteEntityMutation({ variables: { input: { id } } });
|
|
31
|
+
// },
|
|
32
|
+
// icon: IconName.Delete,
|
|
33
|
+
// confirmationMode: 'Simple',
|
|
34
|
+
// },
|
|
35
|
+
// TODO: Add Open Details inline action once Details station exists
|
|
36
|
+
// {
|
|
37
|
+
// label: 'Open Details',
|
|
38
|
+
// path: `/{kebabCasePlural}/${id}`,
|
|
39
|
+
// },
|
|
40
|
+
];
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return { generateInlineMenuActions };
|
|
44
|
+
}
|
|
45
|
+
```
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: explorer-types
|
|
3
|
+
input:
|
|
4
|
+
- EntityFeatures
|
|
5
|
+
- Generated GraphQL types
|
|
6
|
+
output:
|
|
7
|
+
- path: '{workflowsPath}/src/Stations/{EntityPlural}/{EntityPlural}Explorer/common/{EntityPlural}.types.ts'
|
|
8
|
+
description: 'TypeScript type definitions'
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Generate types.ts
|
|
12
|
+
|
|
13
|
+
## Template
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { SelectionExplorerProps } from '@axinom/mosaic-ui';
|
|
17
|
+
import { {EntityPlural}Query } from '../../../../generated/graphql';
|
|
18
|
+
|
|
19
|
+
export type {Entity}Data = NonNullable<{EntityPlural}Query['filtered']>['nodes'][number];
|
|
20
|
+
|
|
21
|
+
interface Props {
|
|
22
|
+
excludeItems?: {Entity}Data['id'][];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface {Entity}SelectionExplorerProps
|
|
26
|
+
extends Omit<SelectionExplorerProps<{Entity}Data>, 'columns' | 'dataProvider' | 'filterOptions'>,
|
|
27
|
+
Props {}
|
|
28
|
+
```
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: explorer-graphql-operations
|
|
3
|
+
input:
|
|
4
|
+
- EntityFeatures
|
|
5
|
+
- selectedColumns
|
|
6
|
+
- selectedFilters
|
|
7
|
+
output:
|
|
8
|
+
- path: '{workflowsPath}/src/Stations/{EntityPlural}/{EntityPlural}Explorer/{EntityPlural}.graphql'
|
|
9
|
+
description: 'GraphQL fragment, query, and filter queries'
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Generate GraphQL File
|
|
13
|
+
|
|
14
|
+
Single file with all operations using exact names from EntityFeatures.
|
|
15
|
+
|
|
16
|
+
## Template
|
|
17
|
+
|
|
18
|
+
```graphql
|
|
19
|
+
# Fragment - all fields based on the `EntityFeatures` needed for `selectedColumns` & `selectedFilters`
|
|
20
|
+
fragment {features.name}ExplorerProperties on {features.schema.entity} {
|
|
21
|
+
id
|
|
22
|
+
{features.titleField}
|
|
23
|
+
|
|
24
|
+
# Scalar fields from `selectedColumns` and `selectedFilters`
|
|
25
|
+
{scalarField1}
|
|
26
|
+
{scalarField2}
|
|
27
|
+
|
|
28
|
+
# Image capability (if features.capabilities.images)
|
|
29
|
+
{features.capabilities.images.connectionField}(condition: { imageType: COVER }) {
|
|
30
|
+
nodes {
|
|
31
|
+
imageId
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# TagLike associations from selectedColumns/selectedFilters
|
|
36
|
+
# Use: association.connectionField, association.displayField
|
|
37
|
+
{association.connectionField} {
|
|
38
|
+
nodes {
|
|
39
|
+
{association.displayField}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# ManyToMany associations from selectedColumns/selectedFilters
|
|
44
|
+
# Use: association.connectionField, association.relatedField, association.displayField
|
|
45
|
+
{association.connectionField} {
|
|
46
|
+
nodes {
|
|
47
|
+
{association.relatedField} {
|
|
48
|
+
{association.displayField}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Audit fields
|
|
54
|
+
createdDate
|
|
55
|
+
updatedDate
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Main query (always)
|
|
59
|
+
query {features.namePlural}(
|
|
60
|
+
$filter: {features.schema.filter},
|
|
61
|
+
$orderBy: [{features.schema.orderBy}!],
|
|
62
|
+
$after: Cursor
|
|
63
|
+
) {
|
|
64
|
+
filtered: {features.queries.names.list.field}(
|
|
65
|
+
filter: $filter
|
|
66
|
+
orderBy: $orderBy
|
|
67
|
+
first: 30
|
|
68
|
+
after: $after
|
|
69
|
+
) {
|
|
70
|
+
totalCount
|
|
71
|
+
pageInfo {
|
|
72
|
+
hasNextPage
|
|
73
|
+
endCursor
|
|
74
|
+
}
|
|
75
|
+
nodes {
|
|
76
|
+
...{features.name}ExplorerProperties
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
nonFiltered: {features.queries.names.list.field} {
|
|
80
|
+
totalCount
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# Subscription (if features.queries.subscription === true)
|
|
85
|
+
subscription {features.namePlural}Mutated {
|
|
86
|
+
{features.subscriptions.mutated.field} {
|
|
87
|
+
id
|
|
88
|
+
eventKey
|
|
89
|
+
{features.camelCase} {
|
|
90
|
+
...{features.name}ExplorerProperties
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# Filter option queries for each association in selectedFilters
|
|
96
|
+
|
|
97
|
+
# TagLike associations
|
|
98
|
+
# For each tagLike association in selectedFilters:
|
|
99
|
+
# DO NOT use the helper queries like get{Entity}{Association}Values which return flat string arrays
|
|
100
|
+
query {features.name}{association.label}FilterOptions(
|
|
101
|
+
$orderBy: [{association.orderByType}!]
|
|
102
|
+
) {
|
|
103
|
+
{association.connectionField}(orderBy: $orderBy) {
|
|
104
|
+
nodes {
|
|
105
|
+
{association.displayField}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# ManyToMany associations
|
|
111
|
+
# For each manyToMany association in selectedFilters:
|
|
112
|
+
query {features.name}{association.label}FilterOptions(
|
|
113
|
+
$orderBy: [{association.relatedOrderByType}!]
|
|
114
|
+
) {
|
|
115
|
+
{association.relatedQueryField}(orderBy: $orderBy) {
|
|
116
|
+
nodes {
|
|
117
|
+
{association.displayField}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# Bulk action mutations (if features.mutations.bulkActions === true)
|
|
123
|
+
# Generate for each mutation in features.mutations.names.bulkActions
|
|
124
|
+
# Example for deleteMovies:
|
|
125
|
+
mutation BulkDeleteMovies($filter: MovieFilter) {
|
|
126
|
+
deleteMovies(filter: $filter) {
|
|
127
|
+
affectedIds
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Field Selection Rules
|
|
133
|
+
|
|
134
|
+
Include in fragment:
|
|
135
|
+
|
|
136
|
+
- All fields from `selectedColumns`
|
|
137
|
+
- All fields from `selectedFilters`
|
|
138
|
+
- Always: `id`, `titleField`, audit fields (`createdDate`, `updatedDate`)
|
|
139
|
+
|
|
140
|
+
## Example (Movie entity)
|
|
141
|
+
|
|
142
|
+
```graphql
|
|
143
|
+
fragment MovieExplorerProperties on Movie {
|
|
144
|
+
id
|
|
145
|
+
title
|
|
146
|
+
originalTitle
|
|
147
|
+
synopsis
|
|
148
|
+
released
|
|
149
|
+
studio
|
|
150
|
+
# Image capability
|
|
151
|
+
moviesImages(condition: { imageType: COVER }) {
|
|
152
|
+
nodes {
|
|
153
|
+
imageId
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
# TagLike association
|
|
157
|
+
moviesTags {
|
|
158
|
+
nodes {
|
|
159
|
+
name
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
# ManyToMany association
|
|
163
|
+
moviesMovieGenres {
|
|
164
|
+
nodes {
|
|
165
|
+
movieGenres {
|
|
166
|
+
title
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
createdDate
|
|
171
|
+
updatedDate
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
query Movies($filter: MovieFilter, $orderBy: [MoviesOrderBy!], $after: Cursor) {
|
|
175
|
+
filtered: movies(
|
|
176
|
+
filter: $filter
|
|
177
|
+
orderBy: $orderBy
|
|
178
|
+
first: 30
|
|
179
|
+
after: $after
|
|
180
|
+
) {
|
|
181
|
+
totalCount
|
|
182
|
+
pageInfo {
|
|
183
|
+
hasNextPage
|
|
184
|
+
endCursor
|
|
185
|
+
}
|
|
186
|
+
nodes {
|
|
187
|
+
...MovieExplorerProperties
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
nonFiltered: movies {
|
|
191
|
+
totalCount
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
subscription MoviesMutated {
|
|
196
|
+
movieMutated {
|
|
197
|
+
id
|
|
198
|
+
eventKey
|
|
199
|
+
movie {
|
|
200
|
+
...MovieExplorerProperties
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
# TagLike filter query
|
|
206
|
+
query MovieTagsFilterOptions($orderBy: [MoviesTagsOrderBy!]) {
|
|
207
|
+
moviesTags(orderBy: $orderBy) {
|
|
208
|
+
nodes {
|
|
209
|
+
name
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
# ManyToMany filter query
|
|
215
|
+
query MovieGenresFilterOptions($orderBy: [MovieGenresOrderBy!]) {
|
|
216
|
+
movieGenres(orderBy: $orderBy) {
|
|
217
|
+
nodes {
|
|
218
|
+
title
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
mutation BulkDeleteMovies($filter: MovieFilter) {
|
|
224
|
+
deleteMovies(filter: $filter) {
|
|
225
|
+
affectedIds
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## Notes
|
|
231
|
+
|
|
232
|
+
- VS Code may briefly show "Syntax Error: Unexpected <EOF>" when creating the
|
|
233
|
+
GraphQL file. This is a false positive caused by a race condition between file
|
|
234
|
+
creation and the `Apollo` extension's validation. It auto-resolves within
|
|
235
|
+
seconds and is safe to ignore and proceed.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: navigation-explorer-station
|
|
3
|
+
input:
|
|
4
|
+
- EntityFeatures
|
|
5
|
+
- All common files (columns, filters, dataProvider, etc.)
|
|
6
|
+
output:
|
|
7
|
+
- path: '{workflowsPath}/src/Stations/{EntityPlural}/{EntityPlural}Explorer/{EntityPlural}.tsx'
|
|
8
|
+
description: 'Main NavigationExplorer station component'
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Generate Main Station
|
|
12
|
+
|
|
13
|
+
Direct NavigationExplorer component with TODOs for missing features.
|
|
14
|
+
|
|
15
|
+
## Template
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { NavigationExplorer } from '@axinom/mosaic-ui';
|
|
19
|
+
// If features.mutations.bulkEdit === true:
|
|
20
|
+
import { generateBulkEditMutation } from '@axinom/mosaic-ui';
|
|
21
|
+
import { gql } from 'graphql-tag';
|
|
22
|
+
import { client } from '{discoveredPath}/apolloClient';
|
|
23
|
+
import { {EntityPlural}BulkEdit } from './BulkEdit/{EntityPlural}BulkEdit';
|
|
24
|
+
import { {EntityPlural}BulkEditConfig } from './BulkEdit/{EntityPlural}BulkEditConfig';
|
|
25
|
+
// End if
|
|
26
|
+
|
|
27
|
+
import React from 'react';
|
|
28
|
+
// If features.mutations.bulkActions === true:
|
|
29
|
+
import { use{EntityPlural}BulkActions } from './common/{EntityPlural}.bulkActions';
|
|
30
|
+
// End if
|
|
31
|
+
import { {entityCamel}Columns } from './common/{EntityPlural}.columns';
|
|
32
|
+
import { create{EntityPlural}DataProvider } from './common/{EntityPlural}.dataProvider';
|
|
33
|
+
import { use{EntityPlural}Filters } from './common/{EntityPlural}.filters';
|
|
34
|
+
import { use{EntityPlural}InlineActions } from './common/{EntityPlural}.inlineActions';
|
|
35
|
+
import { {Entity}Data } from './common/{EntityPlural}.types';
|
|
36
|
+
|
|
37
|
+
export const {EntityPlural}: React.FC = () => {
|
|
38
|
+
const { transformFilters, filterOptions } = use{EntityPlural}Filters();
|
|
39
|
+
// If features.mutations.bulkActions === true:
|
|
40
|
+
const { bulkActions } = use{EntityPlural}BulkActions();
|
|
41
|
+
// End if
|
|
42
|
+
const { generateInlineMenuActions } = use{EntityPlural}InlineActions();
|
|
43
|
+
|
|
44
|
+
const dataProvider = create{EntityPlural}DataProvider(transformFilters);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<NavigationExplorer<{Entity}Data>
|
|
48
|
+
title="{EntityPlural}"
|
|
49
|
+
stationKey="{EntityPlural}Explorer"
|
|
50
|
+
// TODO: Update once Details station exists
|
|
51
|
+
calculateNavigateUrl={(item) => `/{kebabCasePlural}/${item.id}`}
|
|
52
|
+
// If features.mutations.create === true:
|
|
53
|
+
// TODO: Update once Create station exists
|
|
54
|
+
onCreateAction="/{kebabCasePlural}/create"
|
|
55
|
+
// End if
|
|
56
|
+
columns={{entityCamel}Columns}
|
|
57
|
+
dataProvider={dataProvider}
|
|
58
|
+
filterOptions={filterOptions}
|
|
59
|
+
defaultSortOrder={{ column: 'updatedDate', direction: 'desc' }}
|
|
60
|
+
inlineMenuActions={generateInlineMenuActions}
|
|
61
|
+
// If features.mutations.bulkActions === true:
|
|
62
|
+
bulkActions={bulkActions}
|
|
63
|
+
// End if
|
|
64
|
+
quickEditRegistrations={[
|
|
65
|
+
// TODO: Add quick edit registrations once Details/Image/Video stations exist
|
|
66
|
+
// Example structure:
|
|
67
|
+
// { component: <{Entity}DetailsQuickEdit />, label: '{Entity} Details' },
|
|
68
|
+
// { component: <{Entity}ImageManagementQuickEdit />, label: 'Manage Images' },
|
|
69
|
+
// { component: <{Entity}VideoManagementQuickEdit />, label: 'Manage Videos' },
|
|
70
|
+
]}
|
|
71
|
+
// If features.mutations.bulkEdit === true:
|
|
72
|
+
bulkEditRegistration={{
|
|
73
|
+
component: <{EntityPlural}BulkEdit />,
|
|
74
|
+
saveData: async (data, items) => {
|
|
75
|
+
let filter = undefined as Record<string, unknown> | undefined;
|
|
76
|
+
|
|
77
|
+
if (items.mode === 'SINGLE_ITEMS' && items.items && items.items.length > 0) {
|
|
78
|
+
filter = { id: { in: items.items.map((item) => item.id) } };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (items.mode === 'SELECT_ALL') {
|
|
82
|
+
filter = transformFilters(items.filters);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const mutation = generateBulkEditMutation({EntityPlural}BulkEditConfig, data, filter);
|
|
86
|
+
|
|
87
|
+
await client.mutate({ mutation: gql`${mutation}` });
|
|
88
|
+
},
|
|
89
|
+
}}
|
|
90
|
+
// End if
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## TODOs in Generated Code
|
|
97
|
+
|
|
98
|
+
1. **calculateNavigateUrl** - Route works but needs verification once Details
|
|
99
|
+
station exists
|
|
100
|
+
2. **onCreateAction** - Only generated if `features.mutations.create === true`,
|
|
101
|
+
needs verification once Create station exists
|
|
102
|
+
3. **inlineActions.ts** - Empty array with commented examples (delete/Open
|
|
103
|
+
Details actions added by build-details skill)
|
|
104
|
+
4. **quickEditRegistrations** - Empty array with commented examples (added when
|
|
105
|
+
Details/Image/Video stations exist)
|
|
106
|
+
|
|
107
|
+
## Example (Product - minimal)
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
import { NavigationExplorer } from '@axinom/mosaic-ui';
|
|
111
|
+
import React from 'react';
|
|
112
|
+
import { productsColumns } from './common/Products.columns';
|
|
113
|
+
import { createProductsDataProvider } from './common/Products.dataProvider';
|
|
114
|
+
import { useProductsFilters } from './common/Products.filters';
|
|
115
|
+
import { useProductsInlineActions } from './common/Products.inlineActions';
|
|
116
|
+
import { ProductData } from './common/Products.types';
|
|
117
|
+
|
|
118
|
+
export const Products: React.FC = () => {
|
|
119
|
+
const { transformFilters, filterOptions } = useProductsFilters();
|
|
120
|
+
const { generateInlineMenuActions } = useProductsInlineActions();
|
|
121
|
+
const dataProvider = createProductsDataProvider(transformFilters);
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<NavigationExplorer<ProductData>
|
|
125
|
+
title="Products"
|
|
126
|
+
stationKey="ProductsExplorer"
|
|
127
|
+
// TODO: Update once Details station exists
|
|
128
|
+
calculateNavigateUrl={(item) => `/products/${item.id}`}
|
|
129
|
+
// TODO: Update once Create station exists
|
|
130
|
+
onCreateAction="/products/create"
|
|
131
|
+
columns={productsColumns}
|
|
132
|
+
dataProvider={dataProvider}
|
|
133
|
+
filterOptions={filterOptions}
|
|
134
|
+
defaultSortOrder={{ column: 'updatedDate', direction: 'desc' }}
|
|
135
|
+
inlineMenuActions={generateInlineMenuActions}
|
|
136
|
+
quickEditRegistrations={
|
|
137
|
+
[
|
|
138
|
+
// TODO: Add quick edit registrations once Details/Image/Video stations exist
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
/>
|
|
142
|
+
);
|
|
143
|
+
};
|
|
144
|
+
```
|