@asteby/metacore-runtime-react 49.4.0 → 49.6.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.
- package/CHANGELOG.md +14 -0
- package/dist/api-context.d.ts +9 -1
- package/dist/api-context.d.ts.map +1 -1
- package/dist/api-context.js +6 -4
- package/dist/business/customer-picker.d.ts +8 -2
- package/dist/business/customer-picker.d.ts.map +1 -1
- package/dist/business/customer-picker.js +30 -27
- package/dist/business/index.d.ts +1 -0
- package/dist/business/index.d.ts.map +1 -1
- package/dist/business/index.js +1 -0
- package/dist/business/line-product-cell.d.ts +8 -10
- package/dist/business/line-product-cell.d.ts.map +1 -1
- package/dist/business/line-product-cell.js +13 -90
- package/dist/business/product-options.d.ts +41 -0
- package/dist/business/product-options.d.ts.map +1 -0
- package/dist/business/product-options.js +58 -0
- package/dist/business/product-picker.d.ts +5 -1
- package/dist/business/product-picker.d.ts.map +1 -1
- package/dist/business/product-picker.js +26 -35
- package/dist/business/relate-documents.d.ts +1 -1
- package/dist/business/relate-documents.d.ts.map +1 -1
- package/dist/business/relate-documents.js +6 -8
- package/dist/business/vehicle-picker.d.ts +8 -2
- package/dist/business/vehicle-picker.d.ts.map +1 -1
- package/dist/business/vehicle-picker.js +28 -28
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +28 -43
- package/dist/dynamic-multi-select-field.d.ts.map +1 -1
- package/dist/dynamic-multi-select-field.js +17 -5
- package/dist/dynamic-select-field.d.ts +7 -23
- package/dist/dynamic-select-field.d.ts.map +1 -1
- package/dist/dynamic-select-field.js +67 -161
- package/dist/dynamic-table.d.ts +31 -1
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +37 -4
- package/dist/entity-select.d.ts +5 -2
- package/dist/entity-select.d.ts.map +1 -1
- package/dist/entity-select.js +23 -116
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/query-batch.d.ts.map +1 -1
- package/dist/query-batch.js +17 -1
- package/dist/record-picker-actions.d.ts +40 -2
- package/dist/record-picker-actions.d.ts.map +1 -1
- package/dist/record-picker-actions.js +44 -4
- package/dist/record-picker-dialog.d.ts +23 -0
- package/dist/record-picker-dialog.d.ts.map +1 -0
- package/dist/record-picker-dialog.js +37 -0
- package/dist/record-picker-option.d.ts +25 -0
- package/dist/record-picker-option.d.ts.map +1 -0
- package/dist/record-picker-option.js +55 -0
- package/dist/record-picker.d.ts +149 -0
- package/dist/record-picker.d.ts.map +1 -0
- package/dist/record-picker.js +372 -0
- package/package.json +1 -1
- package/src/__tests__/api-batch-opt-out.test.tsx +83 -0
- package/src/__tests__/business-pickers.test.tsx +6 -3
- package/src/__tests__/document-editor-inline-selects.test.tsx +2 -1
- package/src/__tests__/document-form-dialog.test.tsx +4 -3
- package/src/__tests__/document-lines-grid.test.tsx +2 -1
- package/src/__tests__/dynamic-table-bulk-actions.test.tsx +140 -0
- package/src/__tests__/dynamic-table-load-error.test.tsx +97 -0
- package/src/__tests__/record-picker.test.tsx +469 -0
- package/src/api-context.tsx +19 -4
- package/src/business/customer-picker.tsx +87 -98
- package/src/business/index.ts +1 -0
- package/src/business/line-product-cell.tsx +55 -180
- package/src/business/product-options.tsx +119 -0
- package/src/business/product-picker.tsx +63 -126
- package/src/business/relate-documents.tsx +36 -45
- package/src/business/vehicle-picker.tsx +76 -99
- package/src/dialogs/dynamic-record.tsx +44 -97
- package/src/dynamic-multi-select-field.tsx +39 -13
- package/src/dynamic-select-field.tsx +143 -368
- package/src/dynamic-table.tsx +91 -4
- package/src/entity-select.tsx +53 -202
- package/src/index.ts +27 -2
- package/src/query-batch.ts +18 -1
- package/src/record-picker-actions.tsx +101 -8
- package/src/record-picker-dialog.tsx +80 -0
- package/src/record-picker-option.tsx +96 -0
- package/src/record-picker.tsx +971 -0
- package/dist/business/use-async-search.d.ts +0 -10
- package/dist/business/use-async-search.d.ts.map +0 -1
- package/dist/business/use-async-search.js +0 -38
- package/src/business/use-async-search.ts +0 -43
|
@@ -39,12 +39,6 @@ import {
|
|
|
39
39
|
Popover,
|
|
40
40
|
PopoverContent,
|
|
41
41
|
PopoverTrigger,
|
|
42
|
-
Command,
|
|
43
|
-
CommandEmpty,
|
|
44
|
-
CommandGroup,
|
|
45
|
-
CommandInput,
|
|
46
|
-
CommandItem,
|
|
47
|
-
CommandList,
|
|
48
42
|
Calendar,
|
|
49
43
|
} from '@asteby/metacore-ui/primitives'
|
|
50
44
|
import { cn } from '@asteby/metacore-ui/lib'
|
|
@@ -52,12 +46,13 @@ import { toast } from 'sonner'
|
|
|
52
46
|
import { format, parseISO } from 'date-fns'
|
|
53
47
|
import { parseCalendarDate } from '../calendar-date'
|
|
54
48
|
import { es } from 'date-fns/locale'
|
|
55
|
-
import { ExternalLink, Loader2, CalendarIcon,
|
|
49
|
+
import { ExternalLink, Loader2, CalendarIcon, Upload, X as XIcon, ScanLine } from 'lucide-react'
|
|
56
50
|
import { BarcodeScanner } from '../barcode-scanner'
|
|
57
51
|
import { useApi } from '../api-context'
|
|
58
52
|
import { useBranchCreateGate } from '../branch-create-gate'
|
|
59
53
|
import { toastServerError, extractFieldErrors, localizeFieldIssue, localizeFieldErrorMap } from '../server-error'
|
|
60
54
|
import { DynamicSelectField, OptionLead, OptionThumb } from '../dynamic-select-field'
|
|
55
|
+
import { RecordPicker } from '../record-picker'
|
|
61
56
|
import { DynamicMultiSelectField } from '../dynamic-multi-select-field'
|
|
62
57
|
import { DynamicRelations } from '../dynamic-relations'
|
|
63
58
|
import { AuditInfo, readAuditMeta } from '../audit-info'
|
|
@@ -2380,108 +2375,60 @@ const searchCache = new Map<string, any[]>()
|
|
|
2380
2375
|
|
|
2381
2376
|
function SearchField({ field, value, onChange }: { field: FieldDef; value: any; onChange: (val: any) => void }) {
|
|
2382
2377
|
const api = useApi()
|
|
2383
|
-
const [open, setOpen] = useState(false)
|
|
2384
|
-
const [query, setQuery] = useState('')
|
|
2385
|
-
const [results, setResults] = useState<any[]>([])
|
|
2386
|
-
const [loading, setLoading] = useState(false)
|
|
2387
2378
|
const [selectedLabel, setSelectedLabel] = useState('')
|
|
2379
|
+
const ep = field.searchEndpoint!
|
|
2388
2380
|
|
|
2389
2381
|
useEffect(() => {
|
|
2390
|
-
if (!value || !
|
|
2391
|
-
const cached = searchCache.get(
|
|
2382
|
+
if (!value || !ep) return
|
|
2383
|
+
const cached = searchCache.get(ep)
|
|
2392
2384
|
if (cached) {
|
|
2393
2385
|
const match = cached.find((item: any) => item.value === value || item.id === value)
|
|
2394
2386
|
if (match) { setSelectedLabel(match.label || match.name || ''); return }
|
|
2395
2387
|
}
|
|
2396
|
-
api.get(
|
|
2388
|
+
api.get(ep, { params: { search: '', limit: 50 } }).then(res => {
|
|
2397
2389
|
const items = extractArray(res)
|
|
2398
|
-
searchCache.set(
|
|
2390
|
+
searchCache.set(ep, items)
|
|
2399
2391
|
const match = items.find((item: any) => item.value === value || item.id === value)
|
|
2400
2392
|
if (match) setSelectedLabel(match.label || match.name || '')
|
|
2401
2393
|
}).catch(() => {})
|
|
2402
|
-
}, [value,
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2394
|
+
}, [value, ep, api])
|
|
2395
|
+
|
|
2396
|
+
// Legacy `type: search` + searchEndpoint — same <RecordPicker> as every
|
|
2397
|
+
// relation field; the empty query is served from the per-endpoint cache.
|
|
2398
|
+
const search = useCallback(async (q: string) => {
|
|
2399
|
+
if (!q) {
|
|
2400
|
+
const cached = searchCache.get(ep)
|
|
2401
|
+
if (cached) return cached
|
|
2409
2402
|
}
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
}, query ? 250 : 0)
|
|
2419
|
-
return () => clearTimeout(timer)
|
|
2420
|
-
}, [query, open, field.searchEndpoint, api])
|
|
2421
|
-
|
|
2403
|
+
const items = extractArray(await api.get(ep, { params: { search: q, limit: 20 } }))
|
|
2404
|
+
if (!q) searchCache.set(ep, items)
|
|
2405
|
+
return items
|
|
2406
|
+
}, [api, ep])
|
|
2407
|
+
|
|
2408
|
+
const itemValue = (item: any) => item.value ?? item.id
|
|
2409
|
+
const itemLabel = (item: any): string => item.label ?? item.name ?? ''
|
|
2410
|
+
const what = field.label?.toLowerCase() || ''
|
|
2422
2411
|
return (
|
|
2423
|
-
<
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
</
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
<CommandList className="max-h-[200px]">
|
|
2445
|
-
{loading ? (
|
|
2446
|
-
<div className="py-6 text-center text-sm">
|
|
2447
|
-
<Loader2 className="h-4 w-4 animate-spin mx-auto mb-1 text-muted-foreground" />
|
|
2448
|
-
<span className="text-muted-foreground text-xs">Buscando...</span>
|
|
2449
|
-
</div>
|
|
2450
|
-
) : results.length === 0 ? (
|
|
2451
|
-
<CommandEmpty>Sin resultados.</CommandEmpty>
|
|
2452
|
-
) : (
|
|
2453
|
-
<CommandGroup>
|
|
2454
|
-
{results.map((item: any) => {
|
|
2455
|
-
const itemValue = item.value ?? item.id
|
|
2456
|
-
const itemLabel = item.label ?? item.name ?? ''
|
|
2457
|
-
const isSelected = value === itemValue
|
|
2458
|
-
return (
|
|
2459
|
-
<CommandItem
|
|
2460
|
-
key={itemValue}
|
|
2461
|
-
value={String(itemValue)}
|
|
2462
|
-
onSelect={() => {
|
|
2463
|
-
onChange(itemValue)
|
|
2464
|
-
setSelectedLabel(itemLabel)
|
|
2465
|
-
setOpen(false)
|
|
2466
|
-
setQuery('')
|
|
2467
|
-
}}
|
|
2468
|
-
>
|
|
2469
|
-
{isSelected && <Check className="mr-2 h-3.5 w-3.5 shrink-0 text-primary" />}
|
|
2470
|
-
<OptionThumb image={item.image} name={itemLabel} size={20} />
|
|
2471
|
-
<div className="flex flex-col min-w-0 ml-2">
|
|
2472
|
-
<span className="truncate">{itemLabel}</span>
|
|
2473
|
-
{item.description && (
|
|
2474
|
-
<span className="text-[11px] text-muted-foreground truncate">{item.description}</span>
|
|
2475
|
-
)}
|
|
2476
|
-
</div>
|
|
2477
|
-
</CommandItem>
|
|
2478
|
-
)
|
|
2479
|
-
})}
|
|
2480
|
-
</CommandGroup>
|
|
2481
|
-
)}
|
|
2482
|
-
</CommandList>
|
|
2483
|
-
</Command>
|
|
2484
|
-
</PopoverContent>
|
|
2485
|
-
</Popover>
|
|
2412
|
+
<RecordPicker<any>
|
|
2413
|
+
search={search}
|
|
2414
|
+
minChars={0}
|
|
2415
|
+
getKey={(item) => String(itemValue(item))}
|
|
2416
|
+
getLabel={itemLabel}
|
|
2417
|
+
getDescription={(item) => item.description}
|
|
2418
|
+
renderLead={(item) => <OptionThumb image={item.image} name={itemLabel(item)} size={20} />}
|
|
2419
|
+
value={value}
|
|
2420
|
+
selected={value ? { value, label: selectedLabel } : null}
|
|
2421
|
+
renderValue={() => (
|
|
2422
|
+
<span className={cn('min-w-0 flex-1 truncate', !selectedLabel && 'text-muted-foreground')}>
|
|
2423
|
+
{selectedLabel || `Seleccionar ${what}...`}
|
|
2424
|
+
</span>
|
|
2425
|
+
)}
|
|
2426
|
+
onSelect={(item) => {
|
|
2427
|
+
onChange(itemValue(item))
|
|
2428
|
+
setSelectedLabel(itemLabel(item))
|
|
2429
|
+
}}
|
|
2430
|
+
searchPlaceholder={`Buscar ${what}...`}
|
|
2431
|
+
emptyText="Sin resultados."
|
|
2432
|
+
/>
|
|
2486
2433
|
)
|
|
2487
2434
|
}
|
|
@@ -12,15 +12,16 @@
|
|
|
12
12
|
//
|
|
13
13
|
// Options are resolved once (a single page, not per-keystroke) through the
|
|
14
14
|
// same canonical `/api/options/<ref>?field=id` endpoint DynamicSelectField
|
|
15
|
-
// uses — the
|
|
15
|
+
// uses — the shared <RecordPicker multiple> then filters that page client-side as the
|
|
16
16
|
// user types. That's the right tradeoff for the FK sets this targets
|
|
17
17
|
// (segments, tags, categories — tens, not thousands of rows); a field with a
|
|
18
18
|
// genuinely large option set should keep using a single dynamic_select per
|
|
19
19
|
// value instead.
|
|
20
|
-
import { useMemo } from 'react'
|
|
21
|
-
import { MultiSelect } from '@asteby/metacore-ui/primitives'
|
|
20
|
+
import { useMemo, useState } from 'react'
|
|
22
21
|
import { getOptionFilter } from './option-filter'
|
|
23
|
-
import {
|
|
22
|
+
import { RecordPicker } from './record-picker'
|
|
23
|
+
import { OptionLead } from './record-picker-option'
|
|
24
|
+
import { useOptionsResolver, type ResolvedOption } from './use-options-resolver'
|
|
24
25
|
import { getFieldRef } from './dynamic-form-schema'
|
|
25
26
|
import type { ActionFieldDef } from './types'
|
|
26
27
|
|
|
@@ -33,6 +34,7 @@ export interface DynamicMultiSelectFieldProps {
|
|
|
33
34
|
|
|
34
35
|
export function DynamicMultiSelectField({ field, value, onChange }: DynamicMultiSelectFieldProps) {
|
|
35
36
|
const ref = getFieldRef(field)
|
|
37
|
+
const [query, setQuery] = useState('')
|
|
36
38
|
const { options, loading } = useOptionsResolver({
|
|
37
39
|
modelKey: '',
|
|
38
40
|
fieldKey: 'id',
|
|
@@ -43,19 +45,43 @@ export function DynamicMultiSelectField({ field, value, onChange }: DynamicMulti
|
|
|
43
45
|
})
|
|
44
46
|
|
|
45
47
|
const selected = useMemo(() => (Array.isArray(value) ? value.map(String) : []), [value])
|
|
46
|
-
const
|
|
47
|
-
() =>
|
|
48
|
-
|
|
48
|
+
const selectedItems = useMemo(
|
|
49
|
+
() =>
|
|
50
|
+
selected
|
|
51
|
+
.map((id) => options.find((o) => String(o.id) === id))
|
|
52
|
+
.filter((o): o is ResolvedOption => !!o),
|
|
53
|
+
[selected, options],
|
|
49
54
|
)
|
|
55
|
+
const shown = useMemo(() => {
|
|
56
|
+
const q = query.trim().toLowerCase()
|
|
57
|
+
return q ? options.filter((o) => String(o.label ?? '').toLowerCase().includes(q)) : options
|
|
58
|
+
}, [options, query])
|
|
59
|
+
|
|
60
|
+
const toggle = (o: ResolvedOption) => {
|
|
61
|
+
const id = String(o.id)
|
|
62
|
+
onChange(selected.includes(id) ? selected.filter((x) => x !== id) : [...selected, id])
|
|
63
|
+
}
|
|
50
64
|
|
|
51
65
|
return (
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
66
|
+
<RecordPicker<ResolvedOption>
|
|
67
|
+
multiple
|
|
68
|
+
slot="dynamic-multi-select"
|
|
69
|
+
id={field.key}
|
|
70
|
+
items={shown}
|
|
71
|
+
loading={loading}
|
|
72
|
+
getKey={(o) => String(o.id)}
|
|
73
|
+
getLabel={(o) => o.label}
|
|
74
|
+
renderLead={(o) => <OptionLead option={o} size={20} />}
|
|
75
|
+
getDescription={(o) => o.description}
|
|
76
|
+
value={selected}
|
|
77
|
+
selected={selectedItems}
|
|
78
|
+
onSelect={toggle}
|
|
79
|
+
onRemove={toggle}
|
|
80
|
+
query={query}
|
|
81
|
+
onQueryChange={setQuery}
|
|
82
|
+
placeholder={loading ? 'Cargando…' : field.placeholder || 'Seleccionar...'}
|
|
57
83
|
searchPlaceholder="Buscar..."
|
|
58
|
-
|
|
84
|
+
emptyText="Sin resultados."
|
|
59
85
|
/>
|
|
60
86
|
)
|
|
61
87
|
}
|