@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.8.6",
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.1.0",
49
+ "@postxl/ui-components": "^1.2.0",
50
50
  "@postxl/utils": "^1.3.0"
51
51
  },
52
52
  "devDependencies": {},