@postxl/generators 1.0.13 → 1.1.0

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 (41) hide show
  1. package/dist/backend-router-trpc/generators/model-routes.generator.js +24 -3
  2. package/dist/backend-router-trpc/generators/model-routes.generator.js.map +1 -1
  3. package/dist/backend-router-trpc/router-trpc.generator.d.ts +2 -0
  4. package/dist/backend-router-trpc/router-trpc.generator.js +2 -0
  5. package/dist/backend-router-trpc/router-trpc.generator.js.map +1 -1
  6. package/dist/backend-view/model-view-service.generator.js +276 -0
  7. package/dist/backend-view/model-view-service.generator.js.map +1 -1
  8. package/dist/backend-view/template/filter.utils.test.ts +387 -0
  9. package/dist/backend-view/template/filter.utils.ts +218 -0
  10. package/dist/backend-view/view.generator.js +5 -1
  11. package/dist/backend-view/view.generator.js.map +1 -1
  12. package/dist/frontend-admin/generators/model-admin-page.generator.js +38 -8
  13. package/dist/frontend-admin/generators/model-admin-page.generator.js.map +1 -1
  14. package/dist/frontend-core/frontend.generator.js +1 -0
  15. package/dist/frontend-core/frontend.generator.js.map +1 -1
  16. package/dist/frontend-core/template/src/components/admin/table-filter-header-icon.tsx +30 -0
  17. package/dist/frontend-core/template/src/components/admin/table-filter.tsx +537 -0
  18. package/dist/frontend-core/template/src/components/ui/checkbox/checkbox.stories.tsx +2 -1
  19. package/dist/frontend-core/template/src/components/ui/checkbox/checkbox.tsx +10 -1
  20. package/dist/frontend-core/template/src/components/ui/data-grid/cell-variants/gantt-cell.tsx +5 -3
  21. package/dist/frontend-core/template/src/components/ui/data-grid/data-grid-column-header.tsx +32 -2
  22. package/dist/frontend-core/template/src/components/ui/data-grid/data-grid-context-menu.tsx +12 -3
  23. package/dist/frontend-core/template/src/components/ui/data-grid/data-grid-types.ts +10 -2
  24. package/dist/frontend-core/template/src/components/ui/data-grid/data-grid.stories.tsx +780 -0
  25. package/dist/frontend-core/template/src/components/ui/data-grid/hooks/use-data-grid.tsx +5 -1
  26. package/dist/frontend-core/template/src/styles/styles.css +14 -2
  27. package/dist/frontend-tables/generators/model-table.generator.js +69 -46
  28. package/dist/frontend-tables/generators/model-table.generator.js.map +1 -1
  29. package/dist/frontend-trpc-client/generators/model-hook.generator.js +33 -5
  30. package/dist/frontend-trpc-client/generators/model-hook.generator.js.map +1 -1
  31. package/dist/frontend-trpc-client/trpc-client.generator.d.ts +8 -0
  32. package/dist/frontend-trpc-client/trpc-client.generator.js +2 -0
  33. package/dist/frontend-trpc-client/trpc-client.generator.js.map +1 -1
  34. package/dist/types/generators/model-type.generator.d.ts +2 -0
  35. package/dist/types/generators/model-type.generator.js +241 -0
  36. package/dist/types/generators/model-type.generator.js.map +1 -1
  37. package/dist/types/template/filter.types.ts +70 -0
  38. package/dist/types/types.generator.d.ts +25 -0
  39. package/dist/types/types.generator.js +17 -0
  40. package/dist/types/types.generator.js.map +1 -1
  41. package/package.json +2 -2
@@ -0,0 +1,387 @@
1
+ import { describe, expect, it } from '@jest/globals'
2
+
3
+ import {
4
+ applyRangeFilters,
5
+ formatAndSortOptions,
6
+ getMatches,
7
+ getOptions,
8
+ matchDateFilter,
9
+ matchFilter,
10
+ matchNumberFilter,
11
+ } from './filter.utils'
12
+
13
+ describe('matchFilter', () => {
14
+ it('should return true when no filter values are provided', () => {
15
+ expect(matchFilter('test', undefined)).toBe(true)
16
+ expect(matchFilter('test', [])).toBe(true)
17
+ })
18
+
19
+ it('should match string values', () => {
20
+ expect(matchFilter('a', ['a', 'b'])).toBe(true)
21
+ expect(matchFilter('a', ['b', 'c'])).toBe(false)
22
+ })
23
+
24
+ it('should match number values', () => {
25
+ expect(matchFilter(1, [1, 2])).toBe(true)
26
+ expect(matchFilter(1, [2, 3])).toBe(false)
27
+ })
28
+
29
+ it('should match empty string and null/undefined', () => {
30
+ expect(matchFilter(null, [''])).toBe(true)
31
+ expect(matchFilter(undefined, [''])).toBe(true)
32
+ expect(matchFilter('', [''])).toBe(true)
33
+ })
34
+
35
+ it('should not match empty value when filter does not include empty string', () => {
36
+ expect(matchFilter(null, ['a'])).toBe(false)
37
+ expect(matchFilter(undefined, ['a'])).toBe(false)
38
+ expect(matchFilter('', ['a'])).toBe(false)
39
+ })
40
+
41
+ it('should convert Date objects to ISO string for comparison', () => {
42
+ const date = new Date('2020-01-01')
43
+ const isoString = date.toISOString()
44
+ expect(matchFilter(date, [isoString])).toBe(true)
45
+ })
46
+
47
+ it('should convert values to strings for comparison', () => {
48
+ expect(matchFilter(123, ['123'])).toBe(true)
49
+ expect(matchFilter(true, ['true'])).toBe(true)
50
+ })
51
+ })
52
+
53
+ describe('matchNumberFilter', () => {
54
+ it('should return true when no filter is provided', () => {
55
+ expect(matchNumberFilter(50, undefined)).toBe(true)
56
+ })
57
+
58
+ it('should apply range filter with min and max', () => {
59
+ const filter = { min: 10, max: 100 }
60
+ expect(matchNumberFilter(10, filter)).toBe(true)
61
+ expect(matchNumberFilter(50, filter)).toBe(true)
62
+ expect(matchNumberFilter(5, filter)).toBe(false)
63
+ expect(matchNumberFilter(100, filter)).toBe(true)
64
+ expect(matchNumberFilter(150, filter)).toBe(false)
65
+ })
66
+
67
+ it('should apply only min filter', () => {
68
+ const filter = { min: 10 }
69
+ expect(matchNumberFilter(50, filter)).toBe(true)
70
+ expect(matchNumberFilter(10, filter)).toBe(true)
71
+ expect(matchNumberFilter(5, filter)).toBe(false)
72
+ expect(matchNumberFilter(150, filter)).toBe(true)
73
+ })
74
+
75
+ it('should apply only max filter', () => {
76
+ const filter = { max: 100 }
77
+ expect(matchNumberFilter(50, filter)).toBe(true)
78
+ expect(matchNumberFilter(100, filter)).toBe(true)
79
+ expect(matchNumberFilter(5, filter)).toBe(true)
80
+ expect(matchNumberFilter(150, filter)).toBe(false)
81
+ })
82
+
83
+ it('should handle null values with range filters', () => {
84
+ expect(matchNumberFilter(null, { min: 0 })).toBe(false)
85
+ expect(matchNumberFilter(null, { max: 100 })).toBe(false)
86
+ expect(matchNumberFilter(null, {})).toBe(true)
87
+ })
88
+
89
+ it('should match values in the values array', () => {
90
+ const filter = { values: [42, 100] }
91
+ expect(matchNumberFilter(42, filter)).toBe(true)
92
+ expect(matchNumberFilter(100, filter)).toBe(true)
93
+ expect(matchNumberFilter(50, filter)).toBe(false)
94
+ })
95
+
96
+ it('should combine range and values filters', () => {
97
+ const filter = { min: 10, max: 100, values: [50, 150] }
98
+ expect(matchNumberFilter(50, filter)).toBe(true)
99
+ expect(matchNumberFilter(150, filter)).toBe(false)
100
+ expect(matchNumberFilter(25, filter)).toBe(false)
101
+ })
102
+ })
103
+
104
+ describe('matchDateFilter', () => {
105
+ it('should return true when no filter is provided', () => {
106
+ const date = new Date('2024-01-15')
107
+ expect(matchDateFilter(date, undefined)).toBe(true)
108
+ })
109
+
110
+ it('should apply range filter with start and end dates', () => {
111
+ const filter = {
112
+ start: new Date('2024-01-01').toISOString(),
113
+ end: new Date('2024-12-31').toISOString(),
114
+ }
115
+ expect(matchDateFilter(new Date('2024-06-15'), filter)).toBe(true)
116
+ expect(matchDateFilter(new Date('2023-01-15'), filter)).toBe(false)
117
+ expect(matchDateFilter(new Date('2025-12-15'), filter)).toBe(false)
118
+ })
119
+
120
+ it('should handle null dates with range filters', () => {
121
+ expect(matchDateFilter(null, { start: '2024-01-01' })).toBe(false)
122
+ expect(matchDateFilter(null, { end: '2024-12-31' })).toBe(false)
123
+ expect(matchDateFilter(null, {})).toBe(true)
124
+ })
125
+
126
+ it('should match values in the values array', () => {
127
+ const date1 = new Date('2024-01-15')
128
+ const date2 = new Date('2024-06-15')
129
+ const date1Iso = date1.toISOString()
130
+ const date2Iso = date2.toISOString()
131
+
132
+ const filter = { values: [date1Iso, date2Iso] }
133
+ expect(matchDateFilter(date1, filter)).toBe(true)
134
+ expect(matchDateFilter(date2, filter)).toBe(true)
135
+ expect(matchDateFilter(new Date('2024-03-15'), filter)).toBe(false)
136
+ })
137
+ })
138
+
139
+ describe('getMatches', () => {
140
+ it('should collect string values from items', () => {
141
+ const items = [{ name: 'apple' }, { name: 'banana' }, { name: 'apple' }]
142
+ const matches = getMatches(items, (item) => item.name)
143
+ expect(matches).toEqual(new Set(['apple', 'banana']))
144
+ })
145
+
146
+ it('should collect number values from items', () => {
147
+ const items = [{ age: 25 }, { age: 30 }, { age: 25 }]
148
+ const matches = getMatches(items, (item) => item.age)
149
+ expect(matches).toEqual(new Set(['25', '30']))
150
+ })
151
+
152
+ it('should handle empty strings, null, and undefined as empty value', () => {
153
+ const items = [{ status: 'active' }, { status: null }, { status: undefined }]
154
+ const matches = getMatches(items, (item) => item.status)
155
+ expect(matches).toEqual(new Set(['active', '']))
156
+ })
157
+
158
+ it('should convert Date objects to ISO strings', () => {
159
+ const date1 = new Date('2024-01-15')
160
+ const date2 = new Date('2024-06-15')
161
+ const items = [{ date: date1 }, { date: date2 }, { date: date1 }]
162
+ const matches = getMatches(items, (item) => item.date)
163
+ expect(matches).toEqual(new Set([date1.toISOString(), date2.toISOString()]))
164
+ })
165
+
166
+ it('should handle empty array', () => {
167
+ const matches = getMatches([], (item: any) => item.field)
168
+ expect(matches).toEqual(new Set())
169
+ })
170
+ })
171
+
172
+ describe('getOptions', () => {
173
+ it('should generate enum options', () => {
174
+ const items = [{ status: 'active' }, { status: 'inactive' }]
175
+ const config = {
176
+ kind: 'enum' as const,
177
+ valueType: 'string' as const,
178
+ filterKey: 'status' as any,
179
+ enumName: 'Status',
180
+ enumValues: ['active', 'inactive', 'pending'],
181
+ }
182
+ const options = getOptions(items, (item) => item.status, config)
183
+ expect(options).toEqual([
184
+ { value: 'active', label: 'active' },
185
+ { value: 'inactive', label: 'inactive' },
186
+ { value: 'pending', label: 'pending' },
187
+ ])
188
+ })
189
+
190
+ it('should generate boolean options', () => {
191
+ const items = [{ isActive: true }, { isActive: false }]
192
+ const config = {
193
+ kind: 'scalar' as const,
194
+ valueType: 'boolean' as const,
195
+ filterKey: 'isActive' as any,
196
+ }
197
+ const options = getOptions(items, (item) => item.isActive, config)
198
+ expect(options).toEqual([
199
+ { value: 'true', label: 'Yes' },
200
+ { value: 'false', label: 'No' },
201
+ ])
202
+ })
203
+
204
+ it('should generate string/scalar options', () => {
205
+ const items = [{ name: 'apple' }, { name: 'banana' }, { name: 'apple' }, { name: null }]
206
+ const config = {
207
+ kind: 'scalar' as const,
208
+ valueType: 'string' as const,
209
+ filterKey: 'name' as any,
210
+ }
211
+ const options = getOptions(items, (item) => item.name, config)
212
+ expect(options.length).toBe(3)
213
+ expect(options).toContainEqual({ value: 'apple', label: 'apple' })
214
+ expect(options).toContainEqual({ value: 'banana', label: 'banana' })
215
+ expect(options).toContainEqual({ value: '', label: 'Empty' })
216
+ })
217
+
218
+ it('should generate relation options from referencedItems', () => {
219
+ const items = [{ userId: '1' }, { userId: '2' }, { userId: '1' }]
220
+ const referencedItems = new Map([
221
+ ['1', { id: '1', name: 'User One' }],
222
+ ['2', { id: '2', name: 'User Two' }],
223
+ ])
224
+ const config = {
225
+ kind: 'relation' as const,
226
+ valueType: 'string' as const,
227
+ filterKey: 'userId' as any,
228
+ referencedModelViewServiceVariableName: 'users',
229
+ referencedModelLabelField: 'name',
230
+ referencedModelIdField: 'id',
231
+ }
232
+ const options = getOptions(items, (item) => item.userId, config, referencedItems)
233
+ expect(options).toEqual([
234
+ { value: '1', label: 'User One' },
235
+ { value: '2', label: 'User Two' },
236
+ ])
237
+ })
238
+
239
+ it('should include empty option for relation with null values', () => {
240
+ const items = [{ userId: '1' }, { userId: null }]
241
+ const referencedItems = new Map([['1', { id: '1', name: 'User One' }]])
242
+ const config = {
243
+ kind: 'relation' as const,
244
+ valueType: 'string' as const,
245
+ filterKey: 'userId' as any,
246
+ referencedModelViewServiceVariableName: 'users',
247
+ referencedModelLabelField: 'name',
248
+ referencedModelIdField: 'id',
249
+ }
250
+ const options = getOptions(items, (item) => item.userId, config, referencedItems)
251
+ expect(options).toContainEqual({ value: '', label: 'Empty' })
252
+ })
253
+ })
254
+
255
+ describe('applyRangeFilters', () => {
256
+ it('should apply number range filters', () => {
257
+ const items = [{ age: 20 }, { age: 50 }, { age: 80 }]
258
+ const config = {
259
+ kind: 'scalar' as const,
260
+ valueType: 'number' as const,
261
+ filterKey: 'age' as any,
262
+ }
263
+ const filters = { age: { min: 30, max: 60 } }
264
+ const result = applyRangeFilters(items, (item) => item.age, config, filters)
265
+ expect(result).toEqual([{ age: 50 }])
266
+ })
267
+
268
+ it('should apply date range filters', () => {
269
+ const date1 = new Date('2024-01-15')
270
+ const date2 = new Date('2024-06-15')
271
+ const date3 = new Date('2024-12-15')
272
+ const items = [{ date: date1 }, { date: date2 }, { date: date3 }]
273
+ const config = {
274
+ kind: 'scalar' as const,
275
+ valueType: 'date' as const,
276
+ filterKey: 'date' as any,
277
+ }
278
+ const filters = {
279
+ date: {
280
+ start: new Date('2024-03-01').toISOString(),
281
+ end: new Date('2024-09-01').toISOString(),
282
+ },
283
+ }
284
+ const result = applyRangeFilters(items, (item) => item.date, config, filters)
285
+ expect(result).toEqual([{ date: date2 }])
286
+ })
287
+
288
+ it('should return all items when no range filter is provided', () => {
289
+ const items = [{ age: 20 }, { age: 50 }]
290
+ const config = {
291
+ kind: 'scalar' as const,
292
+ valueType: 'number' as const,
293
+ filterKey: 'age' as any,
294
+ }
295
+ const filters = { age: undefined }
296
+ const result = applyRangeFilters(items, (item) => item.age, config, filters)
297
+ expect(result).toEqual(items)
298
+ })
299
+
300
+ it('should not apply range filters for non-number/date fields', () => {
301
+ const items = [{ name: 'apple' }, { name: 'banana' }]
302
+ const config = {
303
+ kind: 'scalar' as const,
304
+ valueType: 'string' as const,
305
+ filterKey: 'name' as any,
306
+ }
307
+ const filters = { name: { min: 0, max: 10 } as any }
308
+ const result = applyRangeFilters(items, (item) => item.name, config, filters)
309
+ expect(result).toEqual(items)
310
+ })
311
+ })
312
+
313
+ describe('formatAndSortOptions', () => {
314
+ it('should add hasMatches flag to options', () => {
315
+ const options = [
316
+ { value: 'apple', label: 'Apple' },
317
+ { value: 'banana', label: 'Banana' },
318
+ ]
319
+ const matches = new Set(['apple'])
320
+ const result = formatAndSortOptions(options, matches)
321
+ expect(result).toContainEqual({ value: 'apple', label: 'Apple', hasMatches: true })
322
+ expect(result).toContainEqual({ value: 'banana', label: 'Banana', hasMatches: false })
323
+ })
324
+
325
+ it('should sort options with matches first', () => {
326
+ const options = [
327
+ { value: 'a', label: 'A' },
328
+ { value: 'b', label: 'B' },
329
+ { value: 'c', label: 'C' },
330
+ ]
331
+ const matches = new Set(['b', 'c'])
332
+ const result = formatAndSortOptions(options, matches)
333
+ expect(result[0]?.hasMatches).toBe(true)
334
+ expect(result[1]?.hasMatches).toBe(true)
335
+ expect(result[2]?.hasMatches).toBe(false)
336
+ })
337
+
338
+ it('should place empty option first among matched items', () => {
339
+ const options = [
340
+ { value: 'apple', label: 'Apple' },
341
+ { value: '', label: 'Empty' },
342
+ ]
343
+ const matches = new Set(['', 'apple'])
344
+ const result = formatAndSortOptions(options, matches)
345
+ expect(result[0]?.value).toBe('')
346
+ })
347
+
348
+ it('should place empty option first among non-matched items', () => {
349
+ const options = [
350
+ { value: 'apple', label: 'Apple' },
351
+ { value: '', label: 'Empty' },
352
+ ]
353
+ const matches = new Set(['apple'])
354
+ const result = formatAndSortOptions(options, matches)
355
+ expect(result[1]?.value).toBe('')
356
+ })
357
+
358
+ it('should sort options alphabetically by label when match status is equal', () => {
359
+ const options = [
360
+ { value: 'c', label: 'Charlie' },
361
+ { value: 'a', label: 'Apple' },
362
+ { value: 'b', label: 'Banana' },
363
+ ]
364
+ const matches = new Set<string>([])
365
+ const result = formatAndSortOptions(options, matches)
366
+ expect(result[0]?.label).toBe('Apple')
367
+ expect(result[1]?.label).toBe('Banana')
368
+ expect(result[2]?.label).toBe('Charlie')
369
+ })
370
+
371
+ it('should handle empty options array', () => {
372
+ const options: { value: string; label: string }[] = []
373
+ const matches = new Set<string>()
374
+ const result = formatAndSortOptions(options, matches)
375
+ expect(result).toEqual([])
376
+ })
377
+
378
+ it('should handle empty matches set', () => {
379
+ const options = [
380
+ { value: 'apple', label: 'Apple' },
381
+ { value: 'banana', label: 'Banana' },
382
+ ]
383
+ const matches = new Set<string>()
384
+ const result = formatAndSortOptions(options, matches)
385
+ expect(result.every((opt) => opt.hasMatches === false)).toBe(true)
386
+ })
387
+ })
@@ -0,0 +1,218 @@
1
+ import { DateFilter, FilterConfigItem, FilterState, NumberFilter, RelationFilterConfig } from '@types'
2
+
3
+ export function matchFilter(itemValue: any, filterValues: any[] | undefined): boolean {
4
+ if (!filterValues || filterValues.length === 0) {
5
+ return true
6
+ }
7
+ if (itemValue === null || itemValue === undefined || itemValue === '') {
8
+ return filterValues.includes('')
9
+ }
10
+ const itemString = itemValue instanceof Date ? itemValue.toISOString() : String(itemValue)
11
+ return filterValues.some((f) => String(f) === itemString)
12
+ }
13
+
14
+ export function matchNumberFilter(itemValue: number | null, filter: NumberFilter | undefined): boolean {
15
+ if (!filter) {
16
+ return true
17
+ }
18
+ const { values, min, max } = filter
19
+
20
+ // Check range
21
+ if (itemValue == null) {
22
+ // If entry is empty, it fails range check if min or max is set
23
+ if ((min !== undefined && min !== null) || (max !== undefined && max !== null)) {
24
+ return false
25
+ }
26
+ } else {
27
+ if (min !== undefined && min !== null && itemValue < min) {
28
+ return false
29
+ }
30
+ if (max !== undefined && max !== null && itemValue > max) {
31
+ return false
32
+ }
33
+ }
34
+
35
+ return matchFilter(itemValue, values)
36
+ }
37
+
38
+ export function matchDateFilter(itemValue: Date | null, filter: DateFilter | undefined): boolean {
39
+ if (!filter) {
40
+ return true
41
+ }
42
+ const { values, start, end } = filter
43
+ const itemString = itemValue?.toISOString() ?? null
44
+
45
+ // Check range
46
+ if (itemString) {
47
+ if (start && itemString < start) {
48
+ return false
49
+ }
50
+ if (end && itemString > end) {
51
+ return false
52
+ }
53
+ } else if (start || end) {
54
+ // If entry is empty, it fails range check if start or end is set
55
+ return false
56
+ }
57
+
58
+ return matchFilter(itemValue, values)
59
+ }
60
+
61
+ /**
62
+ * Applies range filters for number and date fields.
63
+ */
64
+ export function applyRangeFilters<T, TFilters extends FilterState>(
65
+ items: T[],
66
+ accessor: (item: T) => any,
67
+ config: FilterConfigItem<TFilters>,
68
+ filters: TFilters,
69
+ ): T[] {
70
+ let filteredItems = items
71
+ if (config.valueType === 'number') {
72
+ const filter = filters[config.filterKey] as NumberFilter | undefined
73
+ if (filter) {
74
+ const { min, max } = filter
75
+ if (min !== undefined || max !== undefined) {
76
+ // Apply range filter manually. We only check the range part here, ignoring 'values'
77
+ filteredItems = filteredItems.filter((item) => matchNumberFilter(accessor(item), { min, max }))
78
+ }
79
+ }
80
+ } else if (config.valueType === 'date') {
81
+ const filter = filters[config.filterKey] as DateFilter | undefined
82
+ if (filter) {
83
+ const { start, end } = filter
84
+ if (start !== undefined || end !== undefined) {
85
+ // Apply range filter manually. We only check the range part here, ignoring 'values'
86
+ filteredItems = filteredItems.filter((item) => matchDateFilter(accessor(item), { start, end }))
87
+ }
88
+ }
89
+ }
90
+ return filteredItems
91
+ }
92
+
93
+ /**
94
+ * Collects matches from the filtered items.
95
+ */
96
+ export function getMatches<T>(filteredItems: T[], accessor: (item: T) => any): Set<string> {
97
+ const matches = new Set<string>()
98
+ for (const item of filteredItems) {
99
+ const val = accessor(item)
100
+ if (val === null || val === undefined || val === '') {
101
+ matches.add('')
102
+ } else if (val instanceof Date) {
103
+ matches.add(val.toISOString())
104
+ } else {
105
+ matches.add(String(val))
106
+ }
107
+ }
108
+ return matches
109
+ }
110
+
111
+ /**
112
+ * Generates all possible options based on field type.
113
+ */
114
+ export function getOptions<T>(
115
+ allItems: T[],
116
+ accessor: (item: T) => any,
117
+ config: FilterConfigItem<any>,
118
+ referencedItems?: Map<string, any>,
119
+ ): { value: string; label: string }[] {
120
+ if (config.kind === 'relation') {
121
+ return referencedItems ? getRelationOptions(allItems, accessor, config, referencedItems) : []
122
+ }
123
+
124
+ if (config.kind === 'enum') {
125
+ return config.enumValues.map((s: string) => ({ value: s, label: s }))
126
+ }
127
+
128
+ if (config.kind === 'discriminatedUnion') {
129
+ return config.duMemberTypes.map((m: { type: string; label: string }) => ({ value: m.type, label: m.label }))
130
+ }
131
+
132
+ if (config.valueType === 'boolean') {
133
+ return [
134
+ { value: 'true', label: 'Yes' },
135
+ { value: 'false', label: 'No' },
136
+ ]
137
+ }
138
+
139
+ return getScalarOptions(allItems, accessor)
140
+ }
141
+
142
+ /**
143
+ * Generates relation options from referenced items.
144
+ */
145
+ function getRelationOptions<T>(
146
+ allItems: T[],
147
+ accessor: (item: T) => any,
148
+ config: RelationFilterConfig<any>,
149
+ referencedItems: Map<string, any>,
150
+ ): { value: string; label: string }[] {
151
+ const options: { value: string; label: string }[] = []
152
+ const usedIds = new Set<any>()
153
+
154
+ for (const item of allItems) {
155
+ usedIds.add(accessor(item))
156
+ }
157
+
158
+ for (const id of usedIds) {
159
+ if (id === null || id === undefined) {
160
+ options.push({ value: '', label: 'Empty' })
161
+ continue
162
+ }
163
+ const ref = referencedItems.get(id)
164
+ if (ref) {
165
+ const label = String(ref[config.referencedModelLabelField] ?? ref[config.referencedModelIdField])
166
+ options.push({ value: String(ref[config.referencedModelIdField]), label })
167
+ }
168
+ }
169
+
170
+ return options
171
+ }
172
+
173
+ /**
174
+ * Generates scalar options from item values.
175
+ */
176
+ function getScalarOptions<T>(allItems: T[], accessor: (item: T) => any): { value: string; label: string }[] {
177
+ const values = new Set<string>()
178
+ for (const item of allItems) {
179
+ const raw = accessor(item)
180
+ const normalized = raw instanceof Date ? raw.toISOString() : String(raw ?? '')
181
+ values.add(normalized)
182
+ }
183
+
184
+ const options: { value: string; label: string }[] = []
185
+ for (const v of values) {
186
+ options.push({ value: v, label: v === '' ? 'Empty' : v })
187
+ }
188
+ return options
189
+ }
190
+
191
+ /**
192
+ * Formats and sorts the filter options.
193
+ */
194
+ export function formatAndSortOptions(
195
+ options: { value: string; label: string }[],
196
+ matches: Set<string>,
197
+ ): { value: string; label: string; hasMatches: boolean }[] {
198
+ return options
199
+ .map((opt) => ({
200
+ ...opt,
201
+ hasMatches: matches.has(opt.value),
202
+ }))
203
+ .sort((a, b) => {
204
+ if (a.hasMatches && !b.hasMatches) {
205
+ return -1
206
+ }
207
+ if (!a.hasMatches && b.hasMatches) {
208
+ return 1
209
+ }
210
+ if (a.value === '' && b.value !== '') {
211
+ return -1
212
+ }
213
+ if (a.value !== '' && b.value === '') {
214
+ return 1
215
+ }
216
+ return a.label.localeCompare(b.label)
217
+ })
218
+ }
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.generator = exports.generatorId = void 0;
37
+ const node_path_1 = require("node:path");
37
38
  const Generator = __importStar(require("@postxl/generator"));
38
39
  const backend_core_1 = require("../backend-core");
39
40
  const backend_repositories_1 = require("../backend-repositories");
@@ -73,13 +74,16 @@ exports.generator = {
73
74
  models,
74
75
  };
75
76
  },
76
- generate: (context) => {
77
+ generate: async (context) => {
77
78
  const vfsSrc = new Generator.VirtualFileSystem();
78
79
  for (const model of context.models.values()) {
79
80
  vfsSrc.write(Generator.toLocalFile(model.view.service), (0, model_view_service_generator_1.generateModelViewService)({ model, context }));
80
81
  }
81
82
  vfsSrc.write(Generator.toLocalFile(context.view.service), (0, view_service_generator_1.generateViewService)(context));
82
83
  vfsSrc.write(Generator.toLocalFile(context.view.module), (0, view_module_generator_1.generateViewModule)(context));
84
+ await vfsSrc.loadFolder({
85
+ diskPath: (0, node_path_1.resolve)(__dirname, './template'),
86
+ });
83
87
  const vfs = new Generator.VirtualFileSystem();
84
88
  vfs.insertFromVfs({ targetPath: 'src', vfs: vfsSrc });
85
89
  vfs.write('/tsconfig.lib.json', Generator.generateTsConfig('view'));
@@ -1 +1 @@
1
- {"version":3,"file":"view.generator.js","sourceRoot":"","sources":["../../src/backend-view/view.generator.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"view.generator.js","sourceRoot":"","sources":["../../src/backend-view/view.generator.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,yCAAmC;AAEnC,6DAA8C;AAE9C,kDAA+E;AAC/E,kEAA0F;AAC1F,kCAAmD;AACnD,oCAAsD;AAEtD,iFAAyE;AACzE,mEAA4D;AAC5D,qEAA8D;AAgCjD,QAAA,WAAW,GAAG,SAAS,CAAC,sBAAsB,CAAC,cAAc,CAAC,CAAA;AAE9D,QAAA,SAAS,GAAiC;IACrD,EAAE,EAAE,mBAAW;IACf,QAAQ,EAAE,CAAC,iCAAkB,EAAE,sBAAe,EAAE,wBAAgB,EAAE,qDAA8B,CAAC;IAEjG,QAAQ,EAAE,CAAsC,OAAgB,EAAiB,EAAE;QACjF,MAAM,MAAM,GAA8B;YACxC,IAAI,EAAE,SAAS,CAAC,WAAW,CAAC,YAAY,CAAC;YACzC,QAAQ,EAAE,SAAS,CAAC,uBAAuB,CAAC,mBAAmB,CAAC;SACjE,CAAA;QAED,MAAM,UAAU,GAAiB;YAC/B,IAAI,EAAE,SAAS,CAAC,mBAAmB,CAAC,MAAM,CAAC;YAC3C,WAAW,EAAE,MAAM;SACpB,CAAA;QACD,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;QAExC,MAAM,MAAM,GAAgC,IAAI,GAAG,EAAE,CAAA;QACrD,KAAK,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YAChD,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,aAAa,CAAC,KAAK,CAAC,CAAC,CAAA;QAC7C,CAAC;QAED,MAAM,IAAI,GAAgB;YACxB,MAAM;YACN,OAAO,EAAE;gBACP,IAAI,EAAE,SAAS,CAAC,WAAW,CAAC,aAAa,CAAC;gBAC1C,QAAQ,EAAE,SAAS,CAAC,uBAAuB,CAAC,oBAAoB,CAAC;aAClE;SACF,CAAA;QACD,OAAO;YACL,GAAG,OAAO;YACV,IAAI;YACJ,MAAM;SACc,CAAA;IACxB,CAAC;IAED,QAAQ,EAAE,KAAK,EAAiC,OAAgB,EAAoB,EAAE;QACpF,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,iBAAiB,EAAE,CAAA;QAEhD,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC;YAC5C,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,WAAW,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,IAAA,uDAAwB,EAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC,CAAA;QACvG,CAAC;QACD,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,IAAA,4CAAmB,EAAC,OAAO,CAAC,CAAC,CAAA;QACvF,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,IAAA,0CAAkB,EAAC,OAAO,CAAC,CAAC,CAAA;QAErF,MAAM,MAAM,CAAC,UAAU,CAAC;YACtB,QAAQ,EAAE,IAAA,mBAAO,EAAC,SAAS,EAAE,YAAY,CAAC;SAC3C,CAAC,CAAA;QAEF,MAAM,GAAG,GAAG,IAAI,SAAS,CAAC,iBAAiB,EAAE,CAAA;QAC7C,GAAG,CAAC,aAAa,CAAC,EAAE,UAAU,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAA;QACrD,GAAG,CAAC,KAAK,CAAC,oBAAoB,EAAE,SAAS,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAA;QAEnE,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,UAAU,EAAE,mBAAmB,EAAE,GAAG,EAAE,CAAC,CAAA;QAEnE,OAAO,OAAO,CAAA;IAChB,CAAC;CACF,CAAA;AAED,SAAS,aAAa,CAAgD,KAAmB;IACvF,MAAM,IAAI,GAAqB;QAC7B,OAAO,EAAE;YACP,IAAI,EAAE,SAAS,CAAC,WAAW,CAAC,GAAG,KAAK,CAAC,WAAW,CAAC,UAAU,aAAa,CAAC;YACzE,QAAQ,EAAE,SAAS,CAAC,uBAAuB,CAAC,SAAS,KAAK,CAAC,WAAW,CAAC,SAAS,eAAe,CAAC;YAChG,YAAY,EAAE,SAAS,CAAC,cAAc,CAAC,GAAG,KAAK,CAAC,WAAW,CAAC,eAAe,EAAE,CAAC;SAC/E;KACF,CAAA;IAED,OAAO,EAAE,GAAG,KAAK,EAAE,IAAI,EAAE,CAAA;AAC3B,CAAC"}