@izumisy-tailor/tailor-data-viewer 0.1.49 → 0.1.51
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/docs/custom-renderers.md +36 -22
- package/docs/labels.md +29 -5
- package/package.json +1 -1
- package/src/component/built-in-renderers.test.tsx +178 -0
- package/src/component/built-in-renderers.tsx +60 -25
- package/src/component/data-table.tsx +1 -1
- package/src/component/hooks/use-labels.ts +16 -57
- package/src/component/index.ts +2 -1
- package/src/component/label-resolver.test.ts +87 -1
- package/src/component/label-resolver.ts +53 -0
- package/src/component/ui/table.tsx +11 -2
package/docs/custom-renderers.md
CHANGED
|
@@ -128,7 +128,11 @@ const DataViewer = createDataViewer({
|
|
|
128
128
|
|
|
129
129
|
// Table-specific renderers
|
|
130
130
|
"task:status": StatusBadge({
|
|
131
|
-
|
|
131
|
+
valuesMap: {
|
|
132
|
+
todo: { color: "gray" },
|
|
133
|
+
"in-progress": { color: "blue" },
|
|
134
|
+
done: { color: "green" },
|
|
135
|
+
},
|
|
132
136
|
}),
|
|
133
137
|
},
|
|
134
138
|
byFieldType: {
|
|
@@ -181,7 +185,11 @@ export const DataViewer = createDataViewer({
|
|
|
181
185
|
"*:*Email": EmailLink,
|
|
182
186
|
"*:id": TruncatedId,
|
|
183
187
|
"orders:status": StatusBadge({
|
|
184
|
-
|
|
188
|
+
valuesMap: {
|
|
189
|
+
pending: { color: "yellow" },
|
|
190
|
+
approved: { color: "green" },
|
|
191
|
+
rejected: { color: "red" },
|
|
192
|
+
},
|
|
185
193
|
}),
|
|
186
194
|
},
|
|
187
195
|
byFieldType: {
|
|
@@ -207,17 +215,17 @@ const { EmailLink, StatusBadge, BooleanIcon, TruncatedId } = builtInRenderers;
|
|
|
207
215
|
byFieldName: {
|
|
208
216
|
// Table-specific renderers
|
|
209
217
|
"orders:status": StatusBadge({
|
|
210
|
-
|
|
211
|
-
"pending": "yellow",
|
|
212
|
-
"approved": "green",
|
|
213
|
-
"rejected": "red",
|
|
218
|
+
valuesMap: {
|
|
219
|
+
"pending": { color: "yellow" },
|
|
220
|
+
"approved": { color: "green" },
|
|
221
|
+
"rejected": { color: "red" },
|
|
214
222
|
},
|
|
215
223
|
}),
|
|
216
224
|
"invoices:status": StatusBadge({
|
|
217
|
-
|
|
218
|
-
"draft": "gray",
|
|
219
|
-
"sent": "blue",
|
|
220
|
-
"paid": "green",
|
|
225
|
+
valuesMap: {
|
|
226
|
+
"draft": { color: "gray" },
|
|
227
|
+
"sent": { color: "blue" },
|
|
228
|
+
"paid": { color: "green" },
|
|
221
229
|
},
|
|
222
230
|
}),
|
|
223
231
|
|
|
@@ -296,22 +304,28 @@ byFieldType: {
|
|
|
296
304
|
|
|
297
305
|
### StatusBadge
|
|
298
306
|
|
|
299
|
-
Factory function that creates a colored badge renderer
|
|
307
|
+
Factory function that creates a colored badge renderer with custom display labels.
|
|
300
308
|
|
|
301
309
|
```tsx
|
|
302
310
|
StatusBadge({
|
|
303
|
-
|
|
304
|
-
"pending": "yellow",
|
|
305
|
-
"approved": "green",
|
|
306
|
-
"rejected": "red",
|
|
307
|
-
"pending*": "yellow",
|
|
308
|
-
"*approved": "green",
|
|
309
|
-
"*error*": "red",
|
|
311
|
+
valuesMap: {
|
|
312
|
+
"pending": { color: "yellow", label: "保留中" },
|
|
313
|
+
"approved": { color: "green", label: "承認済み" },
|
|
314
|
+
"rejected": { color: "red", label: "却下" },
|
|
315
|
+
"pending*": { color: "yellow" }, // Prefix match: pending, pending_review
|
|
316
|
+
"*approved": { color: "green" }, // Suffix match: manager_approved
|
|
317
|
+
"*error*": { color: "red" }, // Contains match: validation_error
|
|
310
318
|
},
|
|
311
|
-
defaultColor: "gray",
|
|
319
|
+
defaultColor: "gray", // Default for unmatched values
|
|
312
320
|
})
|
|
313
321
|
```
|
|
314
322
|
|
|
323
|
+
**Options:**
|
|
324
|
+
- `valuesMap?: Record<string, StatusBadgeValue>` — Mapping from value to color and label (supports wildcard patterns)
|
|
325
|
+
- `color?: BadgeColor` — Badge color (preset name or custom colors)
|
|
326
|
+
- `label?: string` — Display label (if not specified, the raw value is displayed)
|
|
327
|
+
- `defaultColor?: BadgeColor` — Default color for unmatched values (default: "gray")
|
|
328
|
+
|
|
315
329
|
**Preset Colors:**
|
|
316
330
|
- `gray`: #F3F4F6 bg, #374151 text
|
|
317
331
|
- `red`: #FEE2E2 bg, #991B1B text
|
|
@@ -322,13 +336,13 @@ StatusBadge({
|
|
|
322
336
|
**Custom Colors:**
|
|
323
337
|
```tsx
|
|
324
338
|
StatusBadge({
|
|
325
|
-
|
|
326
|
-
"custom_status": { bg: "#E0F2FE", text: "#0369A1" },
|
|
339
|
+
valuesMap: {
|
|
340
|
+
"custom_status": { color: { bg: "#E0F2FE", text: "#0369A1" } },
|
|
327
341
|
},
|
|
328
342
|
})
|
|
329
343
|
```
|
|
330
344
|
|
|
331
|
-
**Pattern Priority:**
|
|
345
|
+
**Pattern Priority (for valuesMap keys):**
|
|
332
346
|
1. Exact match (`"approved"`)
|
|
333
347
|
2. Prefix match (`"approved*"`)
|
|
334
348
|
3. Suffix match (`"*approved"`)
|
package/docs/labels.md
CHANGED
|
@@ -20,6 +20,8 @@ type Labels = Record<string, string>;
|
|
|
20
20
|
|------------|-------------|---------|
|
|
21
21
|
| `tableName:fieldName` | Field label for specific table | `"orders:status"` → "Order Status" |
|
|
22
22
|
| `*:fieldName` | Field label across all tables (wildcard) | `"*:createdAt"` → "Created" |
|
|
23
|
+
| `tableName:fieldName=value` | Value label for specific field | `"task:status=pending"` → "保留中" |
|
|
24
|
+
| `*:fieldName=value` | Value label across all tables (wildcard) | `"*:status=active"` → "有効" |
|
|
23
25
|
| `tableName` | Table name label | `"orders"` → "Orders" |
|
|
24
26
|
| `$:uiKey` | Built-in UI text override | `"$:refresh"` → "Refresh" |
|
|
25
27
|
|
|
@@ -37,6 +39,11 @@ For nested relation fields (`getLabel("task:assignee.name")`):
|
|
|
37
39
|
4. `labels["*:name"]` — target field wildcard
|
|
38
40
|
5. `"name"` — fallback to target field name
|
|
39
41
|
|
|
42
|
+
For value labels (`getValueLabel("task", "status", "pending")`):
|
|
43
|
+
1. `labels["task:status=pending"]` — exact match (highest)
|
|
44
|
+
2. `labels["*:status=pending"]` — wildcard match
|
|
45
|
+
3. `"pending"` — fallback to string representation
|
|
46
|
+
|
|
40
47
|
For UI labels (`getLabel("$:refresh")`):
|
|
41
48
|
1. `labels["$:refresh"]` — custom override (highest)
|
|
42
49
|
2. `DEFAULT_UI_LABELS["$:refresh"]` — built-in default
|
|
@@ -148,34 +155,51 @@ Access labels within components using the `useLabels` hook:
|
|
|
148
155
|
import { useLabels } from "@izumisy-tailor/tailor-data-viewer/component";
|
|
149
156
|
|
|
150
157
|
function MyComponent() {
|
|
151
|
-
const { getLabel } = useLabels();
|
|
158
|
+
const { getLabel, getValueLabel } = useLabels();
|
|
152
159
|
|
|
153
160
|
return (
|
|
154
161
|
<div>
|
|
155
162
|
<h2>{getLabel("orders")}</h2> {/* Table label */}
|
|
156
163
|
<span>{getLabel("orders:status")}</span> {/* Field label */}
|
|
157
164
|
<button>{getLabel("$:refresh")}</button> {/* UI label */}
|
|
165
|
+
<span>{getValueLabel("task", "status", "pending")}</span> {/* Value label */}
|
|
158
166
|
</div>
|
|
159
167
|
);
|
|
160
168
|
}
|
|
161
169
|
```
|
|
162
170
|
|
|
163
|
-
## Using resolveLabel
|
|
171
|
+
## Using resolveLabel and resolveValueLabel Functions
|
|
164
172
|
|
|
165
173
|
For use outside of React components or context:
|
|
166
174
|
|
|
167
175
|
```tsx
|
|
168
|
-
import { resolveLabel } from "@izumisy-tailor/tailor-data-viewer/component";
|
|
176
|
+
import { resolveLabel, resolveValueLabel } from "@izumisy-tailor/tailor-data-viewer/component";
|
|
169
177
|
|
|
170
|
-
const labels = {
|
|
178
|
+
const labels = {
|
|
179
|
+
"orders:status": "Order Status",
|
|
180
|
+
"task:status=pending": "保留中",
|
|
181
|
+
"*:status=active": "有効",
|
|
182
|
+
};
|
|
171
183
|
|
|
172
|
-
// Resolve a label
|
|
184
|
+
// Resolve a field label
|
|
173
185
|
const label = resolveLabel(labels, "orders:status");
|
|
174
186
|
// → "Order Status"
|
|
175
187
|
|
|
176
188
|
// Fallback to field name
|
|
177
189
|
const fallback = resolveLabel(labels, "orders:unknown");
|
|
178
190
|
// → "unknown"
|
|
191
|
+
|
|
192
|
+
// Resolve a value label
|
|
193
|
+
const valueLabel = resolveValueLabel(labels, "task", "status", "pending");
|
|
194
|
+
// → "保留中"
|
|
195
|
+
|
|
196
|
+
// Wildcard value label
|
|
197
|
+
const wildcardValueLabel = resolveValueLabel(labels, "order", "status", "active");
|
|
198
|
+
// → "有効"
|
|
199
|
+
|
|
200
|
+
// Value parameter is optional (returns empty string when omitted)
|
|
201
|
+
const emptyLabel = resolveValueLabel(labels, "task", "status");
|
|
202
|
+
// → ""
|
|
179
203
|
```
|
|
180
204
|
|
|
181
205
|
## Built-in UI Labels (DEFAULT_UI_LABELS)
|
package/package.json
CHANGED
|
@@ -230,3 +230,181 @@ describe("FileImageThumbnail", () => {
|
|
|
230
230
|
});
|
|
231
231
|
});
|
|
232
232
|
});
|
|
233
|
+
|
|
234
|
+
describe("StatusBadge", () => {
|
|
235
|
+
const { StatusBadge } = builtInRenderers;
|
|
236
|
+
|
|
237
|
+
it("renders '-' for null value", () => {
|
|
238
|
+
const renderer = StatusBadge();
|
|
239
|
+
const props = createMockProps({ value: null });
|
|
240
|
+
render(<>{renderer(props)}</>);
|
|
241
|
+
expect(screen.getByText("-")).toBeInTheDocument();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("renders '-' for empty string value", () => {
|
|
245
|
+
const renderer = StatusBadge();
|
|
246
|
+
const props = createMockProps({ value: "" });
|
|
247
|
+
render(<>{renderer(props)}</>);
|
|
248
|
+
expect(screen.getByText("-")).toBeInTheDocument();
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it("renders value as badge", () => {
|
|
252
|
+
const renderer = StatusBadge();
|
|
253
|
+
const props = createMockProps({ value: "active" });
|
|
254
|
+
render(<>{renderer(props)}</>);
|
|
255
|
+
expect(screen.getByText("active")).toBeInTheDocument();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it("applies color from valuesMap", () => {
|
|
259
|
+
const renderer = StatusBadge({
|
|
260
|
+
valuesMap: { active: { color: "green" } },
|
|
261
|
+
});
|
|
262
|
+
const props = createMockProps({ value: "active" });
|
|
263
|
+
render(<>{renderer(props)}</>);
|
|
264
|
+
|
|
265
|
+
const badge = screen.getByText("active");
|
|
266
|
+
expect(badge).toHaveStyle({ backgroundColor: "#D1FAE5", color: "#065F46" });
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("applies custom color from valuesMap", () => {
|
|
270
|
+
const renderer = StatusBadge({
|
|
271
|
+
valuesMap: { active: { color: { bg: "#ff0000", text: "#ffffff" } } },
|
|
272
|
+
});
|
|
273
|
+
const props = createMockProps({ value: "active" });
|
|
274
|
+
render(<>{renderer(props)}</>);
|
|
275
|
+
|
|
276
|
+
const badge = screen.getByText("active");
|
|
277
|
+
expect(badge).toHaveStyle({ backgroundColor: "#ff0000", color: "#ffffff" });
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("applies defaultColor when value not in valuesMap", () => {
|
|
281
|
+
const renderer = StatusBadge({
|
|
282
|
+
valuesMap: { active: { color: "green" } },
|
|
283
|
+
defaultColor: "red",
|
|
284
|
+
});
|
|
285
|
+
const props = createMockProps({ value: "unknown" });
|
|
286
|
+
render(<>{renderer(props)}</>);
|
|
287
|
+
|
|
288
|
+
const badge = screen.getByText("unknown");
|
|
289
|
+
expect(badge).toHaveStyle({ backgroundColor: "#FEE2E2", color: "#991B1B" });
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe("valuesMap with label", () => {
|
|
293
|
+
it("displays label from valuesMap instead of raw value", () => {
|
|
294
|
+
const renderer = StatusBadge({
|
|
295
|
+
valuesMap: {
|
|
296
|
+
pending: { label: "保留中" },
|
|
297
|
+
active: { label: "有効" },
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
const props = createMockProps({ value: "pending" });
|
|
301
|
+
render(<>{renderer(props)}</>);
|
|
302
|
+
expect(screen.getByText("保留中")).toBeInTheDocument();
|
|
303
|
+
expect(screen.queryByText("pending")).not.toBeInTheDocument();
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it("displays raw value when valuesMap does not contain the value", () => {
|
|
307
|
+
const renderer = StatusBadge({
|
|
308
|
+
valuesMap: {
|
|
309
|
+
pending: { label: "保留中" },
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
const props = createMockProps({ value: "unknown" });
|
|
313
|
+
render(<>{renderer(props)}</>);
|
|
314
|
+
expect(screen.getByText("unknown")).toBeInTheDocument();
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("combines label and color in valuesMap", () => {
|
|
318
|
+
const renderer = StatusBadge({
|
|
319
|
+
valuesMap: {
|
|
320
|
+
active: { color: "green", label: "有効" },
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
const props = createMockProps({ value: "active" });
|
|
324
|
+
render(<>{renderer(props)}</>);
|
|
325
|
+
|
|
326
|
+
const badge = screen.getByText("有効");
|
|
327
|
+
expect(badge).toBeInTheDocument();
|
|
328
|
+
expect(badge).toHaveStyle({
|
|
329
|
+
backgroundColor: "#D1FAE5",
|
|
330
|
+
color: "#065F46",
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("uses defaultColor when only label is specified", () => {
|
|
335
|
+
const renderer = StatusBadge({
|
|
336
|
+
valuesMap: {
|
|
337
|
+
pending: { label: "保留中" },
|
|
338
|
+
},
|
|
339
|
+
defaultColor: "yellow",
|
|
340
|
+
});
|
|
341
|
+
const props = createMockProps({ value: "pending" });
|
|
342
|
+
render(<>{renderer(props)}</>);
|
|
343
|
+
|
|
344
|
+
const badge = screen.getByText("保留中");
|
|
345
|
+
expect(badge).toHaveStyle({
|
|
346
|
+
backgroundColor: "#FEF3C7",
|
|
347
|
+
color: "#92400E",
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
describe("wildcard valuesMap patterns", () => {
|
|
353
|
+
it("matches prefix pattern", () => {
|
|
354
|
+
const renderer = StatusBadge({
|
|
355
|
+
valuesMap: { "active*": { color: "green" } },
|
|
356
|
+
});
|
|
357
|
+
const props = createMockProps({ value: "active_state" });
|
|
358
|
+
render(<>{renderer(props)}</>);
|
|
359
|
+
|
|
360
|
+
const badge = screen.getByText("active_state");
|
|
361
|
+
expect(badge).toHaveStyle({
|
|
362
|
+
backgroundColor: "#D1FAE5",
|
|
363
|
+
color: "#065F46",
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("matches suffix pattern", () => {
|
|
368
|
+
const renderer = StatusBadge({
|
|
369
|
+
valuesMap: { "*_active": { color: "green" } },
|
|
370
|
+
});
|
|
371
|
+
const props = createMockProps({ value: "is_active" });
|
|
372
|
+
render(<>{renderer(props)}</>);
|
|
373
|
+
|
|
374
|
+
const badge = screen.getByText("is_active");
|
|
375
|
+
expect(badge).toHaveStyle({
|
|
376
|
+
backgroundColor: "#D1FAE5",
|
|
377
|
+
color: "#065F46",
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("matches contains pattern", () => {
|
|
382
|
+
const renderer = StatusBadge({
|
|
383
|
+
valuesMap: { "*active*": { color: "green" } },
|
|
384
|
+
});
|
|
385
|
+
const props = createMockProps({ value: "is_active_now" });
|
|
386
|
+
render(<>{renderer(props)}</>);
|
|
387
|
+
|
|
388
|
+
const badge = screen.getByText("is_active_now");
|
|
389
|
+
expect(badge).toHaveStyle({
|
|
390
|
+
backgroundColor: "#D1FAE5",
|
|
391
|
+
color: "#065F46",
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("applies label from wildcard match", () => {
|
|
396
|
+
const renderer = StatusBadge({
|
|
397
|
+
valuesMap: { "error*": { color: "red", label: "エラー" } },
|
|
398
|
+
});
|
|
399
|
+
const props = createMockProps({ value: "error_500" });
|
|
400
|
+
render(<>{renderer(props)}</>);
|
|
401
|
+
|
|
402
|
+
// wildcard match でもlabelが設定されていれば表示される
|
|
403
|
+
const badge = screen.getByText("エラー");
|
|
404
|
+
expect(badge).toHaveStyle({
|
|
405
|
+
backgroundColor: "#FEE2E2",
|
|
406
|
+
color: "#991B1B",
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
});
|
|
@@ -28,12 +28,22 @@ export interface CustomColor {
|
|
|
28
28
|
*/
|
|
29
29
|
export type BadgeColor = PresetColor | CustomColor;
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Value configuration for StatusBadge
|
|
33
|
+
*/
|
|
34
|
+
export interface StatusBadgeValue {
|
|
35
|
+
/** Badge color (preset name or custom colors) */
|
|
36
|
+
color?: BadgeColor;
|
|
37
|
+
/** Display label (if not specified, the raw value is displayed) */
|
|
38
|
+
label?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
/**
|
|
32
42
|
* Options for StatusBadge renderer
|
|
33
43
|
*/
|
|
34
44
|
export interface StatusBadgeOptions {
|
|
35
45
|
/**
|
|
36
|
-
* Mapping from value to color
|
|
46
|
+
* Mapping from value to color and label configuration
|
|
37
47
|
*
|
|
38
48
|
* Key format (wildcard support):
|
|
39
49
|
* - "pending" → exact match
|
|
@@ -42,10 +52,12 @@ export interface StatusBadgeOptions {
|
|
|
42
52
|
* - "*pending*" → contains match
|
|
43
53
|
*
|
|
44
54
|
* Value format:
|
|
45
|
-
* - "green"
|
|
46
|
-
* - {
|
|
55
|
+
* - { color: "green", label: "有効" } → both color and label
|
|
56
|
+
* - { color: "green" } → color only
|
|
57
|
+
* - { label: "保留中" } → label only
|
|
58
|
+
* - { color: { bg: "#xxx", text: "#xxx" } } → custom color
|
|
47
59
|
*/
|
|
48
|
-
|
|
60
|
+
valuesMap?: Record<string, StatusBadgeValue>;
|
|
49
61
|
defaultColor?: BadgeColor;
|
|
50
62
|
}
|
|
51
63
|
|
|
@@ -115,38 +127,62 @@ function matchPattern(value: string, pattern: string): boolean {
|
|
|
115
127
|
}
|
|
116
128
|
}
|
|
117
129
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
130
|
+
/**
|
|
131
|
+
* Find matching key from valuesMap with wildcard support
|
|
132
|
+
* Priority: exact → prefix → suffix → contains
|
|
133
|
+
*/
|
|
134
|
+
function findMatchingKey(
|
|
135
|
+
valuesMap: Record<string, StatusBadgeValue>,
|
|
136
|
+
strValue: string,
|
|
137
|
+
): string | undefined {
|
|
126
138
|
// 1. Exact match
|
|
127
|
-
if (
|
|
128
|
-
return
|
|
139
|
+
if (valuesMap[strValue]) {
|
|
140
|
+
return strValue;
|
|
129
141
|
}
|
|
130
142
|
|
|
131
143
|
// 2. Prefix match (pattern*)
|
|
132
|
-
const prefixKey = Object.keys(
|
|
144
|
+
const prefixKey = Object.keys(valuesMap).find(
|
|
133
145
|
(k) => k.endsWith("*") && !k.startsWith("*") && matchPattern(strValue, k),
|
|
134
146
|
);
|
|
135
|
-
if (prefixKey) return
|
|
147
|
+
if (prefixKey) return prefixKey;
|
|
136
148
|
|
|
137
149
|
// 3. Suffix match (*pattern)
|
|
138
|
-
const suffixKey = Object.keys(
|
|
150
|
+
const suffixKey = Object.keys(valuesMap).find(
|
|
139
151
|
(k) => k.startsWith("*") && !k.endsWith("*") && matchPattern(strValue, k),
|
|
140
152
|
);
|
|
141
|
-
if (suffixKey) return
|
|
153
|
+
if (suffixKey) return suffixKey;
|
|
142
154
|
|
|
143
155
|
// 4. Contains match (*pattern*)
|
|
144
|
-
const containsKey = Object.keys(
|
|
156
|
+
const containsKey = Object.keys(valuesMap).find(
|
|
145
157
|
(k) => k.startsWith("*") && k.endsWith("*") && matchPattern(strValue, k),
|
|
146
158
|
);
|
|
147
|
-
if (containsKey) return
|
|
159
|
+
if (containsKey) return containsKey;
|
|
148
160
|
|
|
149
|
-
return
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function resolveStatusConfig(
|
|
165
|
+
value: unknown,
|
|
166
|
+
options: StatusBadgeOptions,
|
|
167
|
+
): { color: CustomColor; label: string } {
|
|
168
|
+
const { valuesMap = {}, defaultColor = "gray" } = options;
|
|
169
|
+
const strValue = String(value ?? "");
|
|
170
|
+
|
|
171
|
+
const matchedKey = findMatchingKey(valuesMap, strValue);
|
|
172
|
+
if (matchedKey) {
|
|
173
|
+
const config = valuesMap[matchedKey];
|
|
174
|
+
return {
|
|
175
|
+
color: config.color
|
|
176
|
+
? resolveColor(config.color)
|
|
177
|
+
: resolveColor(defaultColor),
|
|
178
|
+
label: config.label ?? strValue,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
color: resolveColor(defaultColor),
|
|
184
|
+
label: strValue,
|
|
185
|
+
};
|
|
150
186
|
}
|
|
151
187
|
|
|
152
188
|
// =============================================================================
|
|
@@ -266,7 +302,7 @@ const ArrayBadges: CellRenderer = ({ value }) => {
|
|
|
266
302
|
|
|
267
303
|
/**
|
|
268
304
|
* Factory function to create a StatusBadge renderer with custom options
|
|
269
|
-
* Renders values as colored badges based on
|
|
305
|
+
* Renders values as colored badges based on valuesMap configuration
|
|
270
306
|
*/
|
|
271
307
|
function StatusBadge(options: StatusBadgeOptions = {}): CellRenderer {
|
|
272
308
|
return ({ value }) => {
|
|
@@ -274,15 +310,14 @@ function StatusBadge(options: StatusBadgeOptions = {}): CellRenderer {
|
|
|
274
310
|
return <span className="text-muted-foreground">-</span>;
|
|
275
311
|
}
|
|
276
312
|
|
|
277
|
-
const
|
|
278
|
-
const color = resolveStatusColor(strValue, options);
|
|
313
|
+
const { color, label } = resolveStatusConfig(value, options);
|
|
279
314
|
|
|
280
315
|
return (
|
|
281
316
|
<span
|
|
282
317
|
className="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium"
|
|
283
318
|
style={{ backgroundColor: color.bg, color: color.text }}
|
|
284
319
|
>
|
|
285
|
-
{
|
|
320
|
+
{label}
|
|
286
321
|
</span>
|
|
287
322
|
);
|
|
288
323
|
};
|
|
@@ -1,69 +1,18 @@
|
|
|
1
1
|
import { useCallback } from "react";
|
|
2
2
|
import { useDataViewer } from "../contexts";
|
|
3
|
-
import
|
|
4
|
-
import { DEFAULT_UI_LABELS } from "../ui-labels";
|
|
3
|
+
import { resolveLabel, resolveValueLabel } from "../label-resolver";
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
*
|
|
9
|
-
* @param labels Labels configuration
|
|
10
|
-
* @param key Label key (e.g., "orders:status", "*:createdAt", "orders", "$:refresh")
|
|
11
|
-
* @returns Resolved label with wildcard fallback for "table:field" format
|
|
12
|
-
*
|
|
13
|
-
* Resolution order for UI labels (`$:*` format):
|
|
14
|
-
* 1. labels["$:key"]
|
|
15
|
-
* 2. DEFAULT_UI_LABELS["$:key"]
|
|
16
|
-
*
|
|
17
|
-
* Resolution order for "tableName:fieldName" format:
|
|
18
|
-
* 1. labels["tableName:fieldName"]
|
|
19
|
-
* 2. labels["*:fieldName"] (wildcard fallback)
|
|
20
|
-
* 3. "fieldName" (default)
|
|
21
|
-
*
|
|
22
|
-
* Resolution order for other formats (e.g., "tableName", "fieldName"):
|
|
23
|
-
* 1. labels[key]
|
|
24
|
-
* 2. key itself (default, no wildcard fallback)
|
|
25
|
-
*
|
|
26
|
-
* @example
|
|
27
|
-
* ```tsx
|
|
28
|
-
* resolveLabel(labels, "$:refresh") // labels["$:refresh"] ?? DEFAULT_UI_LABELS["$:refresh"]
|
|
29
|
-
* resolveLabel(labels, "orders:status") // labels["orders:status"] ?? labels["*:status"] ?? "status"
|
|
30
|
-
* resolveLabel(labels, "orders") // labels["orders"] ?? "orders"
|
|
31
|
-
* ```
|
|
32
|
-
*/
|
|
33
|
-
export function resolveLabel(labels: Labels | undefined, key: string): string {
|
|
34
|
-
// If key exists in labels, return it
|
|
35
|
-
if (labels?.[key]) {
|
|
36
|
-
return labels[key];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Handle UI labels ($:* format) - fallback to DEFAULT_UI_LABELS
|
|
40
|
-
if (key.startsWith("$:")) {
|
|
41
|
-
return DEFAULT_UI_LABELS[key] ?? key;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const colonIndex = key.indexOf(":");
|
|
45
|
-
if (colonIndex >= 0) {
|
|
46
|
-
const fieldName = key.slice(colonIndex + 1);
|
|
47
|
-
// For "table:field" format, try wildcard fallback
|
|
48
|
-
const wildcardKey = `*:${fieldName}`;
|
|
49
|
-
if (labels?.[wildcardKey]) {
|
|
50
|
-
return labels[wildcardKey];
|
|
51
|
-
}
|
|
52
|
-
return fieldName;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// For keys without colon, return key itself (no wildcard fallback)
|
|
56
|
-
return key;
|
|
57
|
-
}
|
|
5
|
+
// Re-export for backward compatibility
|
|
6
|
+
export { resolveLabel, resolveValueLabel };
|
|
58
7
|
|
|
59
8
|
/**
|
|
60
9
|
* Hook to get labels from DataViewer context
|
|
61
10
|
*
|
|
62
|
-
* @returns Object with getLabel
|
|
11
|
+
* @returns Object with getLabel and getValueLabel functions
|
|
63
12
|
*
|
|
64
13
|
* @example
|
|
65
14
|
* ```tsx
|
|
66
|
-
* const { getLabel } = useLabels();
|
|
15
|
+
* const { getLabel, getValueLabel } = useLabels();
|
|
67
16
|
*
|
|
68
17
|
* // Field label
|
|
69
18
|
* <span>{getLabel(`${tableName}:${field.name}`)}</span>
|
|
@@ -73,6 +22,9 @@ export function resolveLabel(labels: Labels | undefined, key: string): string {
|
|
|
73
22
|
*
|
|
74
23
|
* // Cross-table field label
|
|
75
24
|
* <span>{getLabel(`*:${field.name}`)}</span>
|
|
25
|
+
*
|
|
26
|
+
* // Value label (e.g., enum display name)
|
|
27
|
+
* <span>{getValueLabel(tableName, fieldName, value)}</span>
|
|
76
28
|
* ```
|
|
77
29
|
*/
|
|
78
30
|
export function useLabels() {
|
|
@@ -85,5 +37,12 @@ export function useLabels() {
|
|
|
85
37
|
[labels],
|
|
86
38
|
);
|
|
87
39
|
|
|
88
|
-
|
|
40
|
+
const getValueLabel = useCallback(
|
|
41
|
+
(tableName: string, fieldName: string, value?: unknown): string => {
|
|
42
|
+
return resolveValueLabel(labels, tableName, fieldName, value);
|
|
43
|
+
},
|
|
44
|
+
[labels],
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return { getLabel, getValueLabel, labels };
|
|
89
48
|
}
|
package/src/component/index.ts
CHANGED
|
@@ -55,6 +55,7 @@ export type {
|
|
|
55
55
|
PresetColor,
|
|
56
56
|
CustomColor,
|
|
57
57
|
BadgeColor,
|
|
58
|
+
StatusBadgeValue,
|
|
58
59
|
StatusBadgeOptions,
|
|
59
60
|
FileImageThumbnailOptions,
|
|
60
61
|
BuiltInRenderers,
|
|
@@ -103,7 +104,7 @@ export type {
|
|
|
103
104
|
export { isFileValue, OPERATOR_LABELS, OPERATORS_BY_FIELD_TYPE } from "./types";
|
|
104
105
|
|
|
105
106
|
// Label utilities
|
|
106
|
-
export { resolveLabel } from "./label-resolver";
|
|
107
|
+
export { resolveLabel, resolveValueLabel } from "./label-resolver";
|
|
107
108
|
export { useLabels } from "./hooks/use-labels";
|
|
108
109
|
export { DEFAULT_UI_LABELS } from "./ui-labels";
|
|
109
110
|
export type { UILabelKey } from "./ui-labels";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { resolveLabel } from "./label-resolver";
|
|
2
|
+
import { resolveLabel, resolveValueLabel } from "./label-resolver";
|
|
3
3
|
import type { Labels } from "./types";
|
|
4
4
|
|
|
5
5
|
describe("resolveLabel", () => {
|
|
@@ -141,3 +141,89 @@ describe("resolveLabel", () => {
|
|
|
141
141
|
});
|
|
142
142
|
});
|
|
143
143
|
});
|
|
144
|
+
|
|
145
|
+
describe("resolveValueLabel", () => {
|
|
146
|
+
it("returns value label when exact match exists", () => {
|
|
147
|
+
const labels: Labels = {
|
|
148
|
+
"task:status=pending": "保留中",
|
|
149
|
+
"task:status=active": "有効",
|
|
150
|
+
};
|
|
151
|
+
expect(resolveValueLabel(labels, "task", "status", "pending")).toBe(
|
|
152
|
+
"保留中",
|
|
153
|
+
);
|
|
154
|
+
expect(resolveValueLabel(labels, "task", "status", "active")).toBe("有効");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("falls back to wildcard match", () => {
|
|
158
|
+
const labels: Labels = {
|
|
159
|
+
"*:status=completed": "完了",
|
|
160
|
+
};
|
|
161
|
+
expect(resolveValueLabel(labels, "task", "status", "completed")).toBe(
|
|
162
|
+
"完了",
|
|
163
|
+
);
|
|
164
|
+
expect(resolveValueLabel(labels, "order", "status", "completed")).toBe(
|
|
165
|
+
"完了",
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("falls back to string representation when no label found", () => {
|
|
170
|
+
const labels: Labels = {};
|
|
171
|
+
expect(resolveValueLabel(labels, "task", "status", "unknown")).toBe(
|
|
172
|
+
"unknown",
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("handles undefined labels", () => {
|
|
177
|
+
expect(resolveValueLabel(undefined, "task", "status", "pending")).toBe(
|
|
178
|
+
"pending",
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("handles null and undefined values", () => {
|
|
183
|
+
const labels: Labels = {};
|
|
184
|
+
expect(resolveValueLabel(labels, "task", "status", null)).toBe("");
|
|
185
|
+
expect(resolveValueLabel(labels, "task", "status", undefined)).toBe("");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("handles omitted value parameter", () => {
|
|
189
|
+
const labels: Labels = {};
|
|
190
|
+
expect(resolveValueLabel(labels, "task", "status")).toBe("");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("prefers exact match over wildcard", () => {
|
|
194
|
+
const labels: Labels = {
|
|
195
|
+
"task:status=pending": "タスク保留中",
|
|
196
|
+
"*:status=pending": "保留中(共通)",
|
|
197
|
+
};
|
|
198
|
+
expect(resolveValueLabel(labels, "task", "status", "pending")).toBe(
|
|
199
|
+
"タスク保留中",
|
|
200
|
+
);
|
|
201
|
+
expect(resolveValueLabel(labels, "order", "status", "pending")).toBe(
|
|
202
|
+
"保留中(共通)",
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("handles numeric values", () => {
|
|
207
|
+
const labels: Labels = {
|
|
208
|
+
"task:priority=1": "低",
|
|
209
|
+
"task:priority=2": "中",
|
|
210
|
+
"task:priority=3": "高",
|
|
211
|
+
};
|
|
212
|
+
expect(resolveValueLabel(labels, "task", "priority", 1)).toBe("低");
|
|
213
|
+
expect(resolveValueLabel(labels, "task", "priority", 2)).toBe("中");
|
|
214
|
+
expect(resolveValueLabel(labels, "task", "priority", 3)).toBe("高");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("handles boolean values", () => {
|
|
218
|
+
const labels: Labels = {
|
|
219
|
+
"task:isActive=true": "アクティブ",
|
|
220
|
+
"task:isActive=false": "非アクティブ",
|
|
221
|
+
};
|
|
222
|
+
expect(resolveValueLabel(labels, "task", "isActive", true)).toBe(
|
|
223
|
+
"アクティブ",
|
|
224
|
+
);
|
|
225
|
+
expect(resolveValueLabel(labels, "task", "isActive", false)).toBe(
|
|
226
|
+
"非アクティブ",
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
@@ -86,3 +86,56 @@ export function resolveLabel(labels: Labels | undefined, key: string): string {
|
|
|
86
86
|
// For keys without colon, return key itself (no wildcard fallback)
|
|
87
87
|
return key;
|
|
88
88
|
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resolve a value label (e.g., enum value display name)
|
|
92
|
+
*
|
|
93
|
+
* @param labels Labels configuration
|
|
94
|
+
* @param tableName Table name
|
|
95
|
+
* @param fieldName Field name
|
|
96
|
+
* @param value The actual value to resolve a label for (optional)
|
|
97
|
+
* @returns Resolved label or the original value as string
|
|
98
|
+
*
|
|
99
|
+
* Resolution order:
|
|
100
|
+
* 1. labels["tableName:fieldName=value"] - exact match
|
|
101
|
+
* 2. labels["*:fieldName=value"] - wildcard match
|
|
102
|
+
* 3. String(value) - fallback to string representation
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```tsx
|
|
106
|
+
* const labels = {
|
|
107
|
+
* "task:status=pending": "保留中",
|
|
108
|
+
* "task:status=active": "有効",
|
|
109
|
+
* "*:status=completed": "完了",
|
|
110
|
+
* };
|
|
111
|
+
*
|
|
112
|
+
* resolveValueLabel(labels, "task", "status", "pending") // → "保留中"
|
|
113
|
+
* resolveValueLabel(labels, "task", "status", "active") // → "有効"
|
|
114
|
+
* resolveValueLabel(labels, "order", "status", "completed") // → "完了" (wildcard)
|
|
115
|
+
* resolveValueLabel(labels, "task", "status", "unknown") // → "unknown"
|
|
116
|
+
* resolveValueLabel(labels, "task", "status") // → ""
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
export function resolveValueLabel(
|
|
120
|
+
labels: Labels | undefined,
|
|
121
|
+
tableName: string,
|
|
122
|
+
fieldName: string,
|
|
123
|
+
value?: unknown,
|
|
124
|
+
): string {
|
|
125
|
+
const strValue = String(value ?? "");
|
|
126
|
+
|
|
127
|
+
// 1. Exact match: tableName:fieldName=value
|
|
128
|
+
const exactKey = `${tableName}:${fieldName}=${strValue}`;
|
|
129
|
+
if (labels?.[exactKey]) {
|
|
130
|
+
return labels[exactKey];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 2. Wildcard match: *:fieldName=value
|
|
134
|
+
const wildcardKey = `*:${fieldName}=${strValue}`;
|
|
135
|
+
if (labels?.[wildcardKey]) {
|
|
136
|
+
return labels[wildcardKey];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 3. Fallback to string representation
|
|
140
|
+
return strValue;
|
|
141
|
+
}
|
|
@@ -4,7 +4,12 @@ import * as React from "react";
|
|
|
4
4
|
|
|
5
5
|
import { cn } from "../lib/utils";
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
interface TableProps extends React.ComponentProps<"table"> {
|
|
8
|
+
/** Table layout algorithm. Defaults to "fixed" to respect explicit column widths. */
|
|
9
|
+
tableLayout?: "fixed" | "auto";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function Table({ className, tableLayout = "fixed", ...props }: TableProps) {
|
|
8
13
|
return (
|
|
9
14
|
<div
|
|
10
15
|
data-slot="table-container"
|
|
@@ -12,7 +17,11 @@ function Table({ className, ...props }: React.ComponentProps<"table">) {
|
|
|
12
17
|
>
|
|
13
18
|
<table
|
|
14
19
|
data-slot="table"
|
|
15
|
-
className={cn(
|
|
20
|
+
className={cn(
|
|
21
|
+
"w-full caption-bottom text-sm",
|
|
22
|
+
tableLayout === "fixed" ? "table-fixed" : "table-auto",
|
|
23
|
+
className,
|
|
24
|
+
)}
|
|
16
25
|
{...props}
|
|
17
26
|
/>
|
|
18
27
|
</div>
|