@postxl/generators 1.8.6 → 1.9.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.
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query'
|
|
2
|
+
import { DateFilter, FilterConfig, FilterFieldName, FilterState, FilterValue, NumberFilter, StringFilter } from '@types'
|
|
3
|
+
|
|
4
|
+
import { useMemo } from 'react'
|
|
5
|
+
|
|
6
|
+
import { Slicer } from '@postxl/ui-components'
|
|
7
|
+
|
|
8
|
+
export const AdminSlicer = <TField extends FilterFieldName, TFilters extends FilterState>({
|
|
9
|
+
field,
|
|
10
|
+
filters,
|
|
11
|
+
onChange,
|
|
12
|
+
config,
|
|
13
|
+
getFilterOptions,
|
|
14
|
+
title,
|
|
15
|
+
optionsHeight,
|
|
16
|
+
className,
|
|
17
|
+
}: {
|
|
18
|
+
field: TField
|
|
19
|
+
filters: TFilters
|
|
20
|
+
onChange: (val: FilterValue) => void
|
|
21
|
+
config: FilterConfig<TField, TFilters>
|
|
22
|
+
getFilterOptions: (args: { field: TField; filters: TFilters }) => any
|
|
23
|
+
title?: string
|
|
24
|
+
optionsHeight?: number
|
|
25
|
+
className?: string
|
|
26
|
+
}) => {
|
|
27
|
+
const fieldConfig = config[field]
|
|
28
|
+
const { filterKey, valueType: fieldType } = fieldConfig
|
|
29
|
+
|
|
30
|
+
// Exclude the current field's values from the query to prevent self-filtering
|
|
31
|
+
// (options list shouldn't filter itself when selecting items).
|
|
32
|
+
// However, keep other filter properties like min/max, contains, etc. so the backend
|
|
33
|
+
// can gray out options that don't match those constraints.
|
|
34
|
+
const queryFilters = useMemo(() => {
|
|
35
|
+
const key = filterKey
|
|
36
|
+
if (!key) {
|
|
37
|
+
return filters
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const currentFilter = filters[key]
|
|
41
|
+
|
|
42
|
+
// For number filters, keep min/max but exclude values
|
|
43
|
+
if (fieldType === 'number') {
|
|
44
|
+
const val = currentFilter as NumberFilter | undefined
|
|
45
|
+
if (val && (val.min !== undefined || val.max !== undefined)) {
|
|
46
|
+
return {
|
|
47
|
+
...filters,
|
|
48
|
+
[key]: { min: val.min, max: val.max },
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// For date filters, keep start/end but exclude values
|
|
54
|
+
if (fieldType === 'date') {
|
|
55
|
+
const val = currentFilter as DateFilter | undefined
|
|
56
|
+
if (val && (val.start !== undefined || val.end !== undefined)) {
|
|
57
|
+
return {
|
|
58
|
+
...filters,
|
|
59
|
+
[key]: { start: val.start, end: val.end },
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// For string filters, keep text filters but exclude values
|
|
65
|
+
if (fieldType === 'string') {
|
|
66
|
+
const val = currentFilter as StringFilter | undefined
|
|
67
|
+
if (val && (val.contains || val.startsWith || val.endsWith || val.exclude)) {
|
|
68
|
+
return {
|
|
69
|
+
...filters,
|
|
70
|
+
[key]: {
|
|
71
|
+
contains: val.contains,
|
|
72
|
+
startsWith: val.startsWith,
|
|
73
|
+
endsWith: val.endsWith,
|
|
74
|
+
exclude: val.exclude,
|
|
75
|
+
},
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Exclude the entire field from the query
|
|
81
|
+
const { [key]: _ignored, ...rest } = filters
|
|
82
|
+
return rest as TFilters
|
|
83
|
+
}, [filterKey, filters, fieldType])
|
|
84
|
+
|
|
85
|
+
// Always fetch options (unlike TableFilter which only fetches when open)
|
|
86
|
+
const { data: optionsData, isLoading } = useQuery({
|
|
87
|
+
...getFilterOptions({ field, filters: queryFilters }),
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const options = useMemo(() => {
|
|
91
|
+
if (!optionsData || !Array.isArray(optionsData)) {
|
|
92
|
+
return []
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return optionsData
|
|
96
|
+
.filter((opt): opt is { value: string; label: string; hasMatches: boolean } => opt != null && 'value' in opt)
|
|
97
|
+
.map((option) => {
|
|
98
|
+
let displayLabel = option.label
|
|
99
|
+
if (option.value !== '') {
|
|
100
|
+
if (fieldType === 'date') {
|
|
101
|
+
displayLabel = new Date(option.label).toLocaleDateString()
|
|
102
|
+
} else if (fieldType === 'number') {
|
|
103
|
+
displayLabel = Number(option.label).toLocaleString()
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return { ...option, label: displayLabel }
|
|
107
|
+
})
|
|
108
|
+
}, [optionsData, fieldType])
|
|
109
|
+
|
|
110
|
+
// Check if this field uses StringFilter
|
|
111
|
+
const isStringFilter = fieldType === 'string'
|
|
112
|
+
|
|
113
|
+
// Get currently selected values from the filter state
|
|
114
|
+
const selectedValues = useMemo(() => {
|
|
115
|
+
const key = fieldConfig.filterKey
|
|
116
|
+
const val = filters[key]
|
|
117
|
+
if (!val) {
|
|
118
|
+
return new Set<string>()
|
|
119
|
+
}
|
|
120
|
+
if (fieldType === 'number') {
|
|
121
|
+
return new Set(((val as NumberFilter).values || []).map(String))
|
|
122
|
+
}
|
|
123
|
+
if (fieldType === 'date') {
|
|
124
|
+
return new Set((val as DateFilter).values || [])
|
|
125
|
+
}
|
|
126
|
+
if (fieldType === 'boolean') {
|
|
127
|
+
return new Set((val as boolean[]).map(String))
|
|
128
|
+
}
|
|
129
|
+
if (isStringFilter) {
|
|
130
|
+
return new Set(((val as StringFilter).values || []).map(String))
|
|
131
|
+
}
|
|
132
|
+
return new Set(val as string[])
|
|
133
|
+
}, [fieldConfig.filterKey, fieldType, filters, isStringFilter])
|
|
134
|
+
|
|
135
|
+
// Handle value changes - preserves other filter properties (min/max, contains, etc.)
|
|
136
|
+
const handleValueChange = (newValues: string[]) => {
|
|
137
|
+
const key = fieldConfig.filterKey
|
|
138
|
+
const currentFilter = filters[key]
|
|
139
|
+
|
|
140
|
+
if (fieldType === 'number') {
|
|
141
|
+
const current = currentFilter as NumberFilter | undefined
|
|
142
|
+
const values = newValues.length > 0 ? newValues.map(Number) : undefined
|
|
143
|
+
if (!values && current?.min === undefined && current?.max === undefined) {
|
|
144
|
+
onChange(undefined)
|
|
145
|
+
} else {
|
|
146
|
+
onChange({ min: current?.min, max: current?.max, values })
|
|
147
|
+
}
|
|
148
|
+
} else if (fieldType === 'date') {
|
|
149
|
+
const current = currentFilter as DateFilter | undefined
|
|
150
|
+
const values = newValues.length > 0 ? newValues : undefined
|
|
151
|
+
if (!values && current?.start === undefined && current?.end === undefined) {
|
|
152
|
+
onChange(undefined)
|
|
153
|
+
} else {
|
|
154
|
+
onChange({ start: current?.start, end: current?.end, values })
|
|
155
|
+
}
|
|
156
|
+
} else if (isStringFilter) {
|
|
157
|
+
const current = currentFilter as StringFilter | undefined
|
|
158
|
+
const values = newValues.length > 0 ? newValues : undefined
|
|
159
|
+
if (!values && !current?.contains && !current?.startsWith && !current?.endsWith && !current?.exclude) {
|
|
160
|
+
onChange(undefined)
|
|
161
|
+
} else {
|
|
162
|
+
onChange({
|
|
163
|
+
contains: current?.contains,
|
|
164
|
+
startsWith: current?.startsWith,
|
|
165
|
+
endsWith: current?.endsWith,
|
|
166
|
+
exclude: current?.exclude,
|
|
167
|
+
values,
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
} else if (fieldType === 'boolean') {
|
|
171
|
+
onChange(newValues.map((v) => v === 'true'))
|
|
172
|
+
} else {
|
|
173
|
+
onChange(newValues.length > 0 ? newValues : undefined)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return (
|
|
178
|
+
<Slicer
|
|
179
|
+
filterValues={options}
|
|
180
|
+
selectedValues={selectedValues}
|
|
181
|
+
onChange={handleValueChange}
|
|
182
|
+
title={title || String(field)}
|
|
183
|
+
isLoading={isLoading}
|
|
184
|
+
optionsHeight={optionsHeight}
|
|
185
|
+
className={className}
|
|
186
|
+
/>
|
|
187
|
+
)
|
|
188
|
+
}
|
|
@@ -651,6 +651,7 @@ const FilterItem = ({
|
|
|
651
651
|
}) => {
|
|
652
652
|
let displayLabel = option.label
|
|
653
653
|
|
|
654
|
+
// needs to happen in frontend (and can't be directly provided by backend) because of localization
|
|
654
655
|
if (option.value !== '') {
|
|
655
656
|
if (fieldType === 'date') {
|
|
656
657
|
displayLabel = new Date(option.label).toLocaleDateString()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@postxl/generators",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Code generators for PXL - generates backend, frontend, Prisma schemas, and more",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.js",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"exceljs": "^4.4.0",
|
|
47
47
|
"@postxl/generator": "^1.3.2",
|
|
48
48
|
"@postxl/schema": "^1.2.0",
|
|
49
|
-
"@postxl/ui-components": "^1.
|
|
49
|
+
"@postxl/ui-components": "^1.2.0",
|
|
50
50
|
"@postxl/utils": "^1.3.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {},
|