@asteby/metacore-runtime-react 17.0.4 → 18.0.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
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @asteby/metacore-runtime-react
|
|
2
2
|
|
|
3
|
+
## 18.0.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ce9dd72: `DynamicSelectField` (the searchable FK / option picker) now renders each
|
|
8
|
+
option's leading visual: a photo thumbnail (FK relations with an image), else a
|
|
9
|
+
declared icon, else a colored dot for enum/status options that carry a `color`.
|
|
10
|
+
Previously only image thumbnails showed, so enum selects (state, origin, …) read
|
|
11
|
+
as plain text. Plain options with no image/color/icon stay plain.
|
|
12
|
+
- Updated dependencies [8439e9e]
|
|
13
|
+
- @asteby/metacore-ui@2.5.0
|
|
14
|
+
|
|
3
15
|
## 17.0.4
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-select-field.d.ts","sourceRoot":"","sources":["../src/dynamic-select-field.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dynamic-select-field.d.ts","sourceRoot":"","sources":["../src/dynamic-select-field.tsx"],"names":[],"mappings":"AAyCA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AA+F7C,MAAM,WAAW,uBAAuB;IACpC,KAAK,EAAE,cAAc,CAAA;IACrB,KAAK,EAAE,GAAG,CAAA;IACV,QAAQ,EAAE,CAAC,CAAC,EAAE,GAAG,KAAK,IAAI,CAAA;CAC7B;AAED,wBAAgB,kBAAkB,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,uBAAuB,+BA0KrF;AAED,eAAe,kBAAkB,CAAA"}
|
|
@@ -25,6 +25,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
25
25
|
import { useEffect, useState } from 'react';
|
|
26
26
|
import { Button, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, Popover, PopoverContent, PopoverTrigger, } from '@asteby/metacore-ui/primitives';
|
|
27
27
|
import { Check, ChevronsUpDown, ImageIcon, Loader2, Plus } from 'lucide-react';
|
|
28
|
+
import { resolveColorCss } from '@asteby/metacore-ui/lib';
|
|
29
|
+
import { DynamicIcon } from './dynamic-icon';
|
|
28
30
|
import { useOptionsResolver } from './use-options-resolver';
|
|
29
31
|
import { getFieldRef } from './dynamic-form-schema';
|
|
30
32
|
/**
|
|
@@ -46,6 +48,29 @@ function OptionThumb({ image, size = 20 }) {
|
|
|
46
48
|
e.currentTarget.style.visibility = 'hidden';
|
|
47
49
|
} }));
|
|
48
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Leading visual for an option: a photo thumbnail (FK relations with an image),
|
|
53
|
+
* else a declared icon, else a color dot (enum/status options with a color).
|
|
54
|
+
* Returns null when the option carries none, so plain text options stay plain.
|
|
55
|
+
*/
|
|
56
|
+
function OptionLead({ option, size = 20, }) {
|
|
57
|
+
if (!option)
|
|
58
|
+
return null;
|
|
59
|
+
if (option.image)
|
|
60
|
+
return _jsx(OptionThumb, { image: option.image, size: size });
|
|
61
|
+
if (option.icon) {
|
|
62
|
+
return (_jsx("span", { className: "flex shrink-0 items-center justify-center", style: { width: size, height: size, color: option.color ? resolveColorCss(option.color) : undefined }, "aria-hidden": true, children: _jsx(DynamicIcon, { name: option.icon, className: "size-4" }) }));
|
|
63
|
+
}
|
|
64
|
+
if (option.color) {
|
|
65
|
+
return (_jsx("span", { className: "shrink-0 rounded-full", style: { width: Math.round(size * 0.5), height: Math.round(size * 0.5), background: resolveColorCss(option.color) }, "aria-hidden": true }));
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
/** True when any option (or the selected one) carries a renderable visual. */
|
|
70
|
+
function optionsHaveVisual(options, selected) {
|
|
71
|
+
const has = (o) => !!(o && (o.image || o.color || o.icon));
|
|
72
|
+
return has(selected) || options.some(has);
|
|
73
|
+
}
|
|
49
74
|
function useDebounced(value, ms) {
|
|
50
75
|
const [debounced, setDebounced] = useState(value);
|
|
51
76
|
useEffect(() => {
|
|
@@ -87,7 +112,7 @@ export function DynamicSelectField({ field, value, onChange }) {
|
|
|
87
112
|
// Only switch the picker into "with thumbnails" mode when the data actually
|
|
88
113
|
// carries images — a relation whose options have no `image` keeps the plain
|
|
89
114
|
// text list it had before (no empty placeholder column).
|
|
90
|
-
const
|
|
115
|
+
const hasVisual = optionsHaveVisual(options, selectedOption);
|
|
91
116
|
const handlePick = (opt) => {
|
|
92
117
|
setPicked(opt);
|
|
93
118
|
onChange(String(opt.id));
|
|
@@ -119,12 +144,12 @@ export function DynamicSelectField({ field, value, onChange }) {
|
|
|
119
144
|
// to the cell. Without min-w-0 the combobox+button row sizes to its content
|
|
120
145
|
// (the long empty-state placeholder) and overflows the column, pushing the
|
|
121
146
|
// "+" off-screen — it only "fit" once a short value was selected.
|
|
122
|
-
return (_jsxs("div", { className: "flex w-full min-w-0 items-center gap-1.5", children: [_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", role: "combobox", "aria-expanded": open, id: field.key, className: "min-w-0 flex-1 justify-between font-normal", "data-empty": !value, children: [_jsxs("span", { className: "flex min-w-0 flex-1 items-center gap-2 text-left", children: [
|
|
147
|
+
return (_jsxs("div", { className: "flex w-full min-w-0 items-center gap-1.5", children: [_jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", role: "combobox", "aria-expanded": open, id: field.key, className: "min-w-0 flex-1 justify-between font-normal", "data-empty": !value, children: [_jsxs("span", { className: "flex min-w-0 flex-1 items-center gap-2 text-left", children: [hasVisual && value ? (_jsx(OptionLead, { option: selectedOption, size: 20 })) : null, _jsx("span", { className: 'min-w-0 flex-1 truncate ' + (selectedLabel ? '' : 'text-muted-foreground'), children: selectedLabel || field.placeholder || 'Buscar…' })] }), _jsx(ChevronsUpDown, { className: "ml-2 size-4 shrink-0 opacity-50" })] }) }), _jsx(PopoverContent, { className: "p-0", align: "start",
|
|
123
148
|
// Match the trigger width without an arbitrary Tailwind class
|
|
124
149
|
// (those don't always survive a consuming app's Tailwind scan).
|
|
125
150
|
style: { width: 'var(--radix-popover-trigger-width)' }, children: _jsxs(Command, { shouldFilter: false, children: [_jsx(CommandInput, { placeholder: field.placeholder || 'Buscar…', value: search, onValueChange: setSearch }), _jsxs(CommandList, { children: [loading && (_jsxs("div", { className: "text-muted-foreground flex items-center justify-center gap-2 py-6 text-sm", children: [_jsx(Loader2, { className: "size-4 animate-spin" }), "Buscando\u2026"] })), !loading && options.length === 0 && (_jsx(CommandEmpty, { children: debounced ? 'Sin resultados' : 'Escribí para buscar…' })), !loading && options.length > 0 && (_jsx(CommandGroup, { className: "max-h-64 overflow-auto", children: options.map((opt) => {
|
|
126
151
|
const isSel = String(opt.id) === String(value);
|
|
127
|
-
return (_jsxs(CommandItem, { value: String(opt.id), onSelect: () => handlePick(opt), children: [_jsx(Check, { className: 'mr-2 size-4 shrink-0 ' + (isSel ? 'opacity-100' : 'opacity-0') }),
|
|
152
|
+
return (_jsxs(CommandItem, { value: String(opt.id), onSelect: () => handlePick(opt), children: [_jsx(Check, { className: 'mr-2 size-4 shrink-0 ' + (isSel ? 'opacity-100' : 'opacity-0') }), hasVisual && (_jsx(OptionLead, { option: opt, size: 24 })), _jsxs("div", { className: "ml-2 flex min-w-0 flex-col", children: [_jsx("span", { className: "truncate", children: opt.label }), opt.description && (_jsx("span", { className: "text-muted-foreground truncate text-xs", children: opt.description }))] })] }, String(opt.id)));
|
|
128
153
|
}) }))] })] }) })] }), fieldRef && (_jsx(Button, { type: "button", variant: "outline", size: "icon", className: "size-9 shrink-0", onClick: openCreate, title: `Crear ${field.label ?? fieldRef}`, "aria-label": `Crear ${field.label ?? fieldRef}`, children: _jsx(Plus, { className: "size-4" }) }))] }));
|
|
129
154
|
}
|
|
130
155
|
export default DynamicSelectField;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@asteby/metacore-runtime-react",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "18.0.0",
|
|
4
4
|
"description": "React runtime for metacore hosts — renders addon contributions dynamically",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"date-fns": ">=3",
|
|
35
35
|
"react-day-picker": ">=8",
|
|
36
36
|
"@asteby/metacore-sdk": "^3.2.0",
|
|
37
|
-
"@asteby/metacore-ui": "^2.
|
|
37
|
+
"@asteby/metacore-ui": "^2.5.0"
|
|
38
38
|
},
|
|
39
39
|
"peerDependenciesMeta": {
|
|
40
40
|
"@tanstack/react-router": {
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"vitest": "^4.0.0",
|
|
63
63
|
"zustand": "^5.0.0",
|
|
64
64
|
"@asteby/metacore-sdk": "3.2.0",
|
|
65
|
-
"@asteby/metacore-ui": "2.
|
|
65
|
+
"@asteby/metacore-ui": "2.5.0"
|
|
66
66
|
},
|
|
67
67
|
"scripts": {
|
|
68
68
|
"build": "tsc -p tsconfig.json",
|
|
@@ -35,6 +35,8 @@ import {
|
|
|
35
35
|
PopoverTrigger,
|
|
36
36
|
} from '@asteby/metacore-ui/primitives'
|
|
37
37
|
import { Check, ChevronsUpDown, ImageIcon, Loader2, Plus } from 'lucide-react'
|
|
38
|
+
import { resolveColorCss } from '@asteby/metacore-ui/lib'
|
|
39
|
+
import { DynamicIcon } from './dynamic-icon'
|
|
38
40
|
import { useOptionsResolver, type ResolvedOption } from './use-options-resolver'
|
|
39
41
|
import { getFieldRef } from './dynamic-form-schema'
|
|
40
42
|
import type { ActionFieldDef } from './types'
|
|
@@ -76,6 +78,53 @@ function OptionThumb({ image, size = 20 }: { image?: string | null; size?: numbe
|
|
|
76
78
|
)
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Leading visual for an option: a photo thumbnail (FK relations with an image),
|
|
83
|
+
* else a declared icon, else a color dot (enum/status options with a color).
|
|
84
|
+
* Returns null when the option carries none, so plain text options stay plain.
|
|
85
|
+
*/
|
|
86
|
+
function OptionLead({
|
|
87
|
+
option,
|
|
88
|
+
size = 20,
|
|
89
|
+
}: {
|
|
90
|
+
option?: Pick<ResolvedOption, 'image' | 'color' | 'icon'> | null
|
|
91
|
+
size?: number
|
|
92
|
+
}) {
|
|
93
|
+
if (!option) return null
|
|
94
|
+
if (option.image) return <OptionThumb image={option.image} size={size} />
|
|
95
|
+
if (option.icon) {
|
|
96
|
+
return (
|
|
97
|
+
<span
|
|
98
|
+
className="flex shrink-0 items-center justify-center"
|
|
99
|
+
style={{ width: size, height: size, color: option.color ? resolveColorCss(option.color) : undefined }}
|
|
100
|
+
aria-hidden
|
|
101
|
+
>
|
|
102
|
+
<DynamicIcon name={option.icon} className="size-4" />
|
|
103
|
+
</span>
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
if (option.color) {
|
|
107
|
+
return (
|
|
108
|
+
<span
|
|
109
|
+
className="shrink-0 rounded-full"
|
|
110
|
+
style={{ width: Math.round(size * 0.5), height: Math.round(size * 0.5), background: resolveColorCss(option.color) }}
|
|
111
|
+
aria-hidden
|
|
112
|
+
/>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** True when any option (or the selected one) carries a renderable visual. */
|
|
119
|
+
function optionsHaveVisual(
|
|
120
|
+
options: ReadonlyArray<Pick<ResolvedOption, 'image' | 'color' | 'icon'>>,
|
|
121
|
+
selected?: Pick<ResolvedOption, 'image' | 'color' | 'icon'> | null,
|
|
122
|
+
): boolean {
|
|
123
|
+
const has = (o?: Pick<ResolvedOption, 'image' | 'color' | 'icon'> | null) =>
|
|
124
|
+
!!(o && (o.image || o.color || o.icon))
|
|
125
|
+
return has(selected) || options.some(has)
|
|
126
|
+
}
|
|
127
|
+
|
|
79
128
|
function useDebounced<T>(value: T, ms: number): T {
|
|
80
129
|
const [debounced, setDebounced] = useState(value)
|
|
81
130
|
useEffect(() => {
|
|
@@ -130,8 +179,7 @@ export function DynamicSelectField({ field, value, onChange }: DynamicSelectFiel
|
|
|
130
179
|
// Only switch the picker into "with thumbnails" mode when the data actually
|
|
131
180
|
// carries images — a relation whose options have no `image` keeps the plain
|
|
132
181
|
// text list it had before (no empty placeholder column).
|
|
133
|
-
const
|
|
134
|
-
!!selectedOption?.image || options.some((o) => !!o.image)
|
|
182
|
+
const hasVisual = optionsHaveVisual(options, selectedOption)
|
|
135
183
|
|
|
136
184
|
const handlePick = (opt: ResolvedOption) => {
|
|
137
185
|
setPicked(opt)
|
|
@@ -181,8 +229,8 @@ export function DynamicSelectField({ field, value, onChange }: DynamicSelectFiel
|
|
|
181
229
|
data-empty={!value}
|
|
182
230
|
>
|
|
183
231
|
<span className="flex min-w-0 flex-1 items-center gap-2 text-left">
|
|
184
|
-
{
|
|
185
|
-
<
|
|
232
|
+
{hasVisual && value ? (
|
|
233
|
+
<OptionLead option={selectedOption} size={20} />
|
|
186
234
|
) : null}
|
|
187
235
|
<span className={'min-w-0 flex-1 truncate ' + (selectedLabel ? '' : 'text-muted-foreground')}>
|
|
188
236
|
{selectedLabel || field.placeholder || 'Buscar…'}
|
|
@@ -227,8 +275,8 @@ export function DynamicSelectField({ field, value, onChange }: DynamicSelectFiel
|
|
|
227
275
|
onSelect={() => handlePick(opt)}
|
|
228
276
|
>
|
|
229
277
|
<Check className={'mr-2 size-4 shrink-0 ' + (isSel ? 'opacity-100' : 'opacity-0')} />
|
|
230
|
-
{
|
|
231
|
-
<
|
|
278
|
+
{hasVisual && (
|
|
279
|
+
<OptionLead option={opt} size={24} />
|
|
232
280
|
)}
|
|
233
281
|
<div className="ml-2 flex min-w-0 flex-col">
|
|
234
282
|
<span className="truncate">{opt.label}</span>
|