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