@asteby/metacore-runtime-react 23.0.0 → 23.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.
- package/CHANGELOG.md +29 -0
- package/dist/dynamic-kanban.d.ts +9 -16
- package/dist/dynamic-kanban.d.ts.map +1 -1
- package/dist/dynamic-kanban.js +21 -75
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +60 -7
- package/dist/filter-chips.d.ts +60 -0
- package/dist/filter-chips.d.ts.map +1 -0
- package/dist/filter-chips.js +93 -0
- package/package.json +3 -3
- package/src/__tests__/dynamic-kanban.test.tsx +40 -0
- package/src/__tests__/dynamic-table-filters.test.tsx +102 -0
- package/src/__tests__/filter-chips.test.tsx +119 -0
- package/src/dynamic-kanban.tsx +42 -141
- package/src/dynamic-table.tsx +69 -6
- package/src/filter-chips.tsx +175 -0
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// Shared filter presentation: the removable active-filter chip row + the pure
|
|
2
|
+
// helpers that summarize a filter's value and resolve its color. Used by BOTH
|
|
3
|
+
// DynamicKanban and DynamicTable so the two surfaces render identical chips
|
|
4
|
+
// (one component, no drift).
|
|
5
|
+
import { useTranslation } from 'react-i18next'
|
|
6
|
+
import { X } from 'lucide-react'
|
|
7
|
+
import { Badge, Button } from '@asteby/metacore-ui/primitives'
|
|
8
|
+
import { resolveColorCss } from '@asteby/metacore-ui/lib'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Human-readable summary of a field's selected filter values for a chip / row.
|
|
12
|
+
* Resolves option labels, unwraps the wire operators
|
|
13
|
+
* (`IN:`/`ILIKE:`/`RANGE:`/`GTE:`/`LTE:`/date `from_to`), and caps the list at
|
|
14
|
+
* `maxShown` values with a `+n` overflow. Pure — exported for unit tests.
|
|
15
|
+
*/
|
|
16
|
+
export function summarizeFilterValues(
|
|
17
|
+
values: string[] | undefined,
|
|
18
|
+
options: { label: string; value: string }[] | undefined,
|
|
19
|
+
maxShown = 2,
|
|
20
|
+
): string {
|
|
21
|
+
if (!values || values.length === 0) return ''
|
|
22
|
+
const opts = options ?? []
|
|
23
|
+
const labelFor = (v: string) => opts.find((o) => o.value === v)?.label ?? v
|
|
24
|
+
const first = values[0]
|
|
25
|
+
if (values.length === 1) {
|
|
26
|
+
if (first.startsWith('ILIKE:')) return `"${first.slice(6)}"`
|
|
27
|
+
if (first.startsWith('IN:')) {
|
|
28
|
+
return summarizeList(first.slice(3).split(','), labelFor, maxShown)
|
|
29
|
+
}
|
|
30
|
+
if (first.startsWith('RANGE:')) {
|
|
31
|
+
const [min, max] = first.slice(6).split(',')
|
|
32
|
+
return `${min || '…'} – ${max || '…'}`
|
|
33
|
+
}
|
|
34
|
+
if (/^\d{4}-\d{2}-\d{2}_/.test(first)) return first.replace('_', ' – ')
|
|
35
|
+
}
|
|
36
|
+
if (first.startsWith('GTE:') || first.startsWith('LTE:')) {
|
|
37
|
+
const min = values.find((v) => v.startsWith('GTE:'))?.slice(4) ?? ''
|
|
38
|
+
const max = values.find((v) => v.startsWith('LTE:'))?.slice(4) ?? ''
|
|
39
|
+
return `${min || '…'} – ${max || '…'}`
|
|
40
|
+
}
|
|
41
|
+
return summarizeList(values, labelFor, maxShown)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function summarizeList(
|
|
45
|
+
items: string[],
|
|
46
|
+
labelFor: (v: string) => string,
|
|
47
|
+
maxShown: number,
|
|
48
|
+
): string {
|
|
49
|
+
const labels = items.map(labelFor)
|
|
50
|
+
if (labels.length <= maxShown) return labels.join(', ')
|
|
51
|
+
return `${labels.slice(0, maxShown).join(', ')} +${labels.length - maxShown}`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The color of a filter's first selected value (for the chip's dot) — e.g. a
|
|
56
|
+
* stage's palette color. Returns a resolved CSS color, or undefined for
|
|
57
|
+
* operator/range/free-text values that carry no option color.
|
|
58
|
+
*/
|
|
59
|
+
export function chipValueColor(config: {
|
|
60
|
+
selectedValues: string[]
|
|
61
|
+
options: { value: string; color?: string }[]
|
|
62
|
+
}): string | undefined {
|
|
63
|
+
const sel = config.selectedValues
|
|
64
|
+
if (!sel || sel.length === 0) return undefined
|
|
65
|
+
const first = sel[0]
|
|
66
|
+
let value = first
|
|
67
|
+
if (first.startsWith('IN:')) value = first.slice(3).split(',')[0]
|
|
68
|
+
else if (/^(ILIKE|RANGE|GTE|LTE):/.test(first)) return undefined
|
|
69
|
+
else if (/^\d{4}-\d{2}-\d{2}_/.test(first)) return undefined
|
|
70
|
+
const opt = config.options.find((o) => o.value === value)
|
|
71
|
+
return opt?.color ? resolveColorCss(opt.color) : undefined
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Translates option labels through the app translator (manifest i18n keys →
|
|
76
|
+
* localized text). A raw value with no matching key falls through to itself via
|
|
77
|
+
* `defaultValue`. Pure — exported for unit tests.
|
|
78
|
+
*/
|
|
79
|
+
export function translateOptionLabels<T extends { label: string }>(
|
|
80
|
+
options: T[],
|
|
81
|
+
translate: (key: string) => string,
|
|
82
|
+
): T[] {
|
|
83
|
+
return options.map((o) => ({ ...o, label: translate(o.label) }))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface FilterChipField {
|
|
87
|
+
key: string
|
|
88
|
+
label: string
|
|
89
|
+
config: {
|
|
90
|
+
selectedValues: string[]
|
|
91
|
+
options: { label: string; value: string; color?: string }[]
|
|
92
|
+
filterKey: string
|
|
93
|
+
onFilterChange: (filterKey: string, values: string[]) => void
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface FilterChipsRowProps {
|
|
98
|
+
/** Fields with an active selection (one chip each). */
|
|
99
|
+
fields: FilterChipField[]
|
|
100
|
+
/** Clears every filter (the trailing "Limpiar todo"). */
|
|
101
|
+
onClearAll: () => void
|
|
102
|
+
className?: string
|
|
103
|
+
'data-testid'?: string
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The removable active-filter chip row shown under a board/table toolbar. Each
|
|
108
|
+
* chip: an optional value-color dot, "Campo: valor(es)" (capped + `+n`), and an
|
|
109
|
+
* X that clears that field; a trailing "Limpiar todo". Renders nothing when no
|
|
110
|
+
* field is active.
|
|
111
|
+
*/
|
|
112
|
+
export function FilterChipsRow({
|
|
113
|
+
fields,
|
|
114
|
+
onClearAll,
|
|
115
|
+
className,
|
|
116
|
+
'data-testid': testId,
|
|
117
|
+
}: FilterChipsRowProps) {
|
|
118
|
+
const { t } = useTranslation()
|
|
119
|
+
if (fields.length === 0) return null
|
|
120
|
+
return (
|
|
121
|
+
<div
|
|
122
|
+
className={`flex flex-wrap items-center gap-1.5${className ? ` ${className}` : ''}`}
|
|
123
|
+
data-testid={testId}
|
|
124
|
+
>
|
|
125
|
+
{fields.map((field) => {
|
|
126
|
+
const summary = summarizeFilterValues(
|
|
127
|
+
field.config.selectedValues,
|
|
128
|
+
field.config.options,
|
|
129
|
+
)
|
|
130
|
+
const dot = chipValueColor(field.config)
|
|
131
|
+
return (
|
|
132
|
+
<Badge
|
|
133
|
+
key={field.key}
|
|
134
|
+
variant="secondary"
|
|
135
|
+
className="h-6 gap-1.5 rounded-md pl-2 pr-1 text-xs font-normal"
|
|
136
|
+
>
|
|
137
|
+
{dot && (
|
|
138
|
+
<span
|
|
139
|
+
className="size-2 shrink-0 rounded-full"
|
|
140
|
+
style={{ backgroundColor: dot }}
|
|
141
|
+
/>
|
|
142
|
+
)}
|
|
143
|
+
<span className="font-medium">{field.label}:</span>
|
|
144
|
+
<span className="max-w-[180px] truncate text-muted-foreground">
|
|
145
|
+
{summary}
|
|
146
|
+
</span>
|
|
147
|
+
<button
|
|
148
|
+
type="button"
|
|
149
|
+
onClick={() =>
|
|
150
|
+
field.config.onFilterChange(
|
|
151
|
+
field.config.filterKey,
|
|
152
|
+
[],
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
className="ml-0.5 rounded-sm p-0.5 transition-colors hover:bg-muted-foreground/20"
|
|
156
|
+
aria-label={t('filters.removeFilter', {
|
|
157
|
+
defaultValue: 'Quitar filtro',
|
|
158
|
+
})}
|
|
159
|
+
>
|
|
160
|
+
<X className="h-3 w-3" />
|
|
161
|
+
</button>
|
|
162
|
+
</Badge>
|
|
163
|
+
)
|
|
164
|
+
})}
|
|
165
|
+
<Button
|
|
166
|
+
variant="ghost"
|
|
167
|
+
size="sm"
|
|
168
|
+
className="h-6 gap-1 px-2 text-xs text-muted-foreground"
|
|
169
|
+
onClick={onClearAll}
|
|
170
|
+
>
|
|
171
|
+
{t('filters.clearAll', { defaultValue: 'Limpiar todo' })}
|
|
172
|
+
</Button>
|
|
173
|
+
</div>
|
|
174
|
+
)
|
|
175
|
+
}
|