@sincpro/web-ui 1.6.0 → 1.7.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/dist/components/attachments/Attachments.d.ts +56 -0
- package/dist/components/attachments/Attachments.js +93 -0
- package/dist/components/attachments/files.d.ts +28 -0
- package/dist/components/attachments/files.js +100 -0
- package/dist/components/attachments/index.d.ts +2 -0
- package/dist/components/attachments/index.js +3 -0
- package/dist/components/flow/StepFlow.d.ts +37 -0
- package/dist/components/flow/StepFlow.js +74 -0
- package/dist/components/flow/index.d.ts +1 -0
- package/dist/components/flow/index.js +2 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +2 -0
- package/dist/components/listing/SortMenu.d.ts +24 -20
- package/dist/components/listing/SortMenu.js +51 -91
- package/dist/components/listing/index.d.ts +2 -1
- package/dist/components/listing/index.js +2 -1
- package/dist/components/listing/modelCells.d.ts +7 -0
- package/dist/components/listing/modelCells.js +56 -0
- package/dist/components/listing/modelColumns.d.ts +9 -0
- package/dist/components/listing/modelColumns.js +59 -14
- package/dist/components/search/index.d.ts +1 -1
- package/dist/components/search/index.js +1 -1
- package/dist/components/search/model.d.ts +1 -0
- package/dist/components/search/model.js +7 -0
- package/dist/components/search/query.js +2 -3
- package/dist/foundations/copy.d.ts +131 -0
- package/dist/foundations/copy.es.js +86 -0
- package/dist/foundations/copy.js +86 -0
- package/dist/primitives/button/Button.js +3 -1
- package/dist/primitives/field/CardNumberInput.d.ts +41 -0
- package/dist/primitives/field/CardNumberInput.js +117 -0
- package/dist/primitives/field/CodeInput.d.ts +18 -0
- package/dist/primitives/field/CodeInput.js +37 -0
- package/dist/primitives/field/ColorInput.d.ts +21 -0
- package/dist/primitives/field/ColorInput.js +42 -0
- package/dist/primitives/field/ContactInputs.d.ts +18 -0
- package/dist/primitives/field/ContactInputs.js +70 -0
- package/dist/primitives/field/DurationInput.d.ts +20 -0
- package/dist/primitives/field/DurationInput.js +54 -0
- package/dist/primitives/field/Field.d.ts +24 -4
- package/dist/primitives/field/Field.js +17 -4
- package/dist/primitives/field/ImageInput.d.ts +51 -0
- package/dist/primitives/field/ImageInput.js +267 -0
- package/dist/primitives/field/PasswordInput.d.ts +29 -0
- package/dist/primitives/field/PasswordInput.js +44 -0
- package/dist/primitives/field/PatternInput.d.ts +23 -0
- package/dist/primitives/field/PatternInput.js +48 -0
- package/dist/primitives/field/RichTextEditor.d.ts +18 -0
- package/dist/primitives/field/RichTextEditor.js +224 -0
- package/dist/primitives/field/RichTextInput.d.ts +30 -0
- package/dist/primitives/field/RichTextInput.js +38 -0
- package/dist/primitives/field/SignatureInput.d.ts +34 -0
- package/dist/primitives/field/SignatureInput.js +215 -0
- package/dist/primitives/field/card.d.ts +33 -0
- package/dist/primitives/field/card.js +173 -0
- package/dist/primitives/field/clipboard.js +3 -2
- package/dist/primitives/field/contact.d.ts +17 -0
- package/dist/primitives/field/contact.js +65 -0
- package/dist/primitives/field/duration.d.ts +5 -0
- package/dist/primitives/field/duration.js +48 -0
- package/dist/primitives/field/index.d.ts +14 -0
- package/dist/primitives/field/index.js +20 -0
- package/dist/primitives/field/mask.d.ts +33 -0
- package/dist/primitives/field/mask.js +56 -0
- package/dist/primitives/field/media.d.ts +99 -0
- package/dist/primitives/field/media.js +136 -0
- package/dist/primitives/field/pattern.d.ts +10 -0
- package/dist/primitives/field/pattern.js +68 -0
- package/dist/primitives/menu/Menu.d.ts +16 -0
- package/dist/primitives/menu/Menu.js +26 -8
- package/dist/primitives/menu/index.d.ts +1 -1
- package/dist/primitives/menu/index.js +1 -1
- package/dist/primitives/table/DataTable.d.ts +5 -3
- package/dist/primitives/table/DataTable.js +2 -4
- package/dist/primitives/table/Table.d.ts +5 -8
- package/dist/primitives/table/Table.js +63 -15
- package/dist/primitives/table/index.d.ts +2 -1
- package/dist/primitives/table/index.js +2 -1
- package/dist/primitives/table/sorting.d.ts +42 -0
- package/dist/primitives/table/sorting.js +110 -0
- package/dist/primitives/value/RichText.d.ts +10 -0
- package/dist/primitives/value/RichText.js +130 -0
- package/dist/primitives/value/Value.d.ts +20 -2
- package/dist/primitives/value/Value.js +26 -7
- package/dist/primitives/value/index.d.ts +3 -1
- package/dist/primitives/value/index.js +4 -1
- package/dist/primitives/value/rounding.d.ts +9 -0
- package/dist/primitives/value/rounding.js +48 -0
- package/dist/tokens.css +103 -0
- package/dist/views/list/List.d.ts +5 -4
- package/dist/views/list/ListView.js +2 -2
- package/dist/views/list/types.d.ts +6 -6
- package/package.json +9 -1
|
@@ -1,105 +1,65 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { useUiCopy } from "../../foundations/copy.js";
|
|
4
|
-
import { Menu } from "../../primitives/menu/Menu.js";
|
|
5
|
-
import {
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
4
|
+
import { Menu, menuTriggerClasses } from "../../primitives/menu/Menu.js";
|
|
5
|
+
import { Popover } from "../../primitives/overlay/Popover.js";
|
|
6
|
+
import { compareBy, flipSort, moveSort, } from "../../primitives/table/sorting.js";
|
|
7
|
+
import { ArrowDownUp, ArrowDownWideNarrow, ArrowUpNarrowWide, ChevronDown, GripVertical, Plus, X, } from "lucide-react";
|
|
8
|
+
import { Button as AriaButton, Dialog, DialogTrigger, GridList, GridListItem, useDragAndDrop, } from "react-aria-components";
|
|
6
9
|
/**
|
|
7
|
-
* Las filas, ordenadas ac
|
|
10
|
+
* Las filas, ordenadas acá, por cada criterio en su orden.
|
|
8
11
|
*
|
|
9
|
-
* Sólo
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
+
* Sólo por los campos que declaran `value`; los demás se saltean —ordenar una
|
|
13
|
+
* página de un conjunto que se pagina afuera produce un orden que es mentira,
|
|
14
|
+
* porque ordena las doce que llegaron y no las ciento ochenta y siete. Sin
|
|
15
|
+
* ningún criterio legible, se devuelven tal cual.
|
|
12
16
|
*
|
|
13
|
-
* Los faltantes van SIEMPRE al final, suba o baje el orden
|
|
14
|
-
* cualquier planilla, y la razón es que un vacío no es «el más chico»: es la
|
|
15
|
-
* ausencia de respuesta, y ponerlo primero al invertir el orden llena la primera
|
|
16
|
-
* pantalla con las filas que menos dicen.
|
|
17
|
+
* Los faltantes van SIEMPRE al final, suba o baje el orden (ver `compareBy`).
|
|
17
18
|
*/
|
|
18
19
|
export function sortItems(items, fields, sort) {
|
|
19
|
-
if (sort ===
|
|
20
|
+
if (sort === undefined || sort.length === 0)
|
|
20
21
|
return items;
|
|
21
|
-
const
|
|
22
|
-
if (
|
|
22
|
+
const read = (key) => fields.find((one) => one.field === key)?.value;
|
|
23
|
+
if (!sort.some((one) => read(one.key) !== undefined))
|
|
23
24
|
return items;
|
|
24
|
-
|
|
25
|
-
const sign = sort.direction === "asc" ? 1 : -1;
|
|
26
|
-
return [...items].sort((a, b) => {
|
|
27
|
-
const left = read(a);
|
|
28
|
-
const right = read(b);
|
|
29
|
-
const leftMissing = left === null || left === undefined || left === "";
|
|
30
|
-
const rightMissing = right === null || right === undefined || right === "";
|
|
31
|
-
if (leftMissing && rightMissing)
|
|
32
|
-
return 0;
|
|
33
|
-
if (leftMissing)
|
|
34
|
-
return 1;
|
|
35
|
-
if (rightMissing)
|
|
36
|
-
return -1;
|
|
37
|
-
if (typeof left === "number" && typeof right === "number")
|
|
38
|
-
return (left - right) * sign;
|
|
39
|
-
return String(left).localeCompare(String(right)) * sign;
|
|
40
|
-
});
|
|
25
|
+
return [...items].sort(compareBy(sort, read));
|
|
41
26
|
}
|
|
42
27
|
export function SortMenu({ fields, sort, onSortChange, surface }) {
|
|
43
28
|
const copy = useUiCopy();
|
|
29
|
+
const fieldOf = (key) => fields.find((one) => one.field === key);
|
|
30
|
+
const labelOf = (key) => fieldOf(key)?.label ?? key;
|
|
31
|
+
const { dragAndDropHooks } = useDragAndDrop({
|
|
32
|
+
getItems: (keys) => [...keys].map((key) => ({ "text/plain": labelOf(String(key)) })),
|
|
33
|
+
onReorder: (event) => onSortChange(moveSort(sort, new Set([...event.keys].map(String)), String(event.target.key), event.target.dropPosition === "before" ? "before" : "after")),
|
|
34
|
+
});
|
|
44
35
|
if (fields.length === 0)
|
|
45
36
|
return null;
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
},
|
|
75
|
-
],
|
|
76
|
-
},
|
|
77
|
-
{ options: [{ id: "__none__", label: copy.sorting.clear }] },
|
|
78
|
-
]),
|
|
79
|
-
];
|
|
80
|
-
return (_jsx(Menu, { label: current === undefined ? copy.sorting.label : current.label, icon: sort === null || sort === undefined
|
|
81
|
-
? ArrowDownUp
|
|
82
|
-
: sort.direction === "asc"
|
|
83
|
-
? ArrowUpNarrowWide
|
|
84
|
-
: ArrowDownWideNarrow, groups: groups, active: sort !== null && sort !== undefined, size: "sm", width: "sm", closeOnSelect: false, ...(surface === undefined ? {} : { surface }), onSelect: (id) => {
|
|
85
|
-
if (id === "__none__")
|
|
86
|
-
return onSortChange(null);
|
|
87
|
-
if (id === "__asc__") {
|
|
88
|
-
return onSortChange(sort === null ? null : { ...sort, direction: "asc" });
|
|
89
|
-
}
|
|
90
|
-
if (id === "__desc__") {
|
|
91
|
-
return onSortChange(sort === null ? null : { ...sort, direction: "desc" });
|
|
92
|
-
}
|
|
93
|
-
/* Volver a elegir el campo que ya está puesto DA VUELTA la dirección, que
|
|
94
|
-
es lo que hace el clic en un encabezado. Sin eso, invertir el orden son
|
|
95
|
-
dos gestos en un menú y uno en la tabla — la misma capacidad costando
|
|
96
|
-
distinto según la presentación. */
|
|
97
|
-
if (sort?.key === id) {
|
|
98
|
-
return onSortChange({
|
|
99
|
-
key: id,
|
|
100
|
-
direction: sort.direction === "asc" ? "desc" : "asc",
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
onSortChange({ key: id, direction: "asc" });
|
|
104
|
-
} }));
|
|
37
|
+
const first = sort[0];
|
|
38
|
+
const label = first === undefined
|
|
39
|
+
? copy.sorting.label
|
|
40
|
+
: sort.length === 1
|
|
41
|
+
? labelOf(first.key)
|
|
42
|
+
: fillCopy(copy.sorting.more, { label: labelOf(first.key), count: sort.length - 1 });
|
|
43
|
+
const TriggerIcon = first === undefined
|
|
44
|
+
? ArrowDownUp
|
|
45
|
+
: first.direction === "asc"
|
|
46
|
+
? ArrowUpNarrowWide
|
|
47
|
+
: ArrowDownWideNarrow;
|
|
48
|
+
const left = fields.filter((one) => !sort.some((s) => s.key === one.field));
|
|
49
|
+
return (_jsxs(DialogTrigger, { children: [_jsxs(AriaButton, { className: ({ isPressed, isFocusVisible }) => menuTriggerClasses({
|
|
50
|
+
...(surface === undefined ? {} : { surface }),
|
|
51
|
+
active: sort.length > 0,
|
|
52
|
+
open: isPressed,
|
|
53
|
+
focusVisible: isFocusVisible,
|
|
54
|
+
}), children: [_jsx(TriggerIcon, { className: "size-(--size-icon-sm)", strokeWidth: 2, "aria-hidden": true }), label, _jsx(ChevronDown, { className: "size-(--size-icon-sm)", strokeWidth: 2, "aria-hidden": true })] }), _jsx(Popover, { placement: "bottom start", className: "w-80", children: _jsxs(Dialog, { "aria-label": copy.sorting.label, className: "flex flex-col gap-1 p-2 outline-0", children: [sort.length === 0 ? (_jsx("p", { className: "px-1 py-1 text-meta text-muted", children: copy.sorting.none })) : (_jsx(GridList, { "aria-label": copy.sorting.label, items: sort.map((one, index) => ({ ...one, id: one.key, index })), dragAndDropHooks: dragAndDropHooks, className: "flex flex-col outline-0", children: (item) => {
|
|
55
|
+
const field = fieldOf(item.key);
|
|
56
|
+
const name = labelOf(item.key);
|
|
57
|
+
const direction = item.direction === "asc"
|
|
58
|
+
? (field?.ascLabel ?? copy.sorting.asc)
|
|
59
|
+
: (field?.descLabel ?? copy.sorting.desc);
|
|
60
|
+
const DirectionIcon = item.direction === "asc" ? ArrowUpNarrowWide : ArrowDownWideNarrow;
|
|
61
|
+
return (_jsxs(GridListItem, { id: item.id, textValue: `${item.index + 1}. ${name}, ${direction}`, className: "flex min-w-0 items-center gap-1.5 rounded-control px-1 py-1 outline-0 transition-colors duration-(--duration-instant) data-dragging:opacity-50 data-drop-target:bg-primary/8 data-focus-visible:outline-2 data-focus-visible:-outline-offset-2 data-focus-visible:outline-primary data-hovered:bg-ink/4", children: [_jsx(AriaButton, { slot: "drag", "aria-label": fillCopy(copy.sorting.move, { field: name }), className: "inline-flex shrink-0 cursor-grab items-center rounded-mark p-0.5 text-muted outline-0 hover:text-ink data-focus-visible:outline-2 data-focus-visible:outline-primary", children: _jsx(GripVertical, { className: "size-(--size-icon-sm)", strokeWidth: 2 }) }), _jsx("span", { className: "w-16 shrink-0 text-micro text-muted", children: item.index === 0 ? copy.sorting.by : copy.sorting.then }), _jsx("span", { className: "min-w-0 flex-1 truncate text-meta text-ink", children: name }), _jsxs(AriaButton, { "aria-label": `${fillCopy(copy.sorting.flip, { field: name })}: ${direction}`, onPress: () => onSortChange(flipSort(sort, item.index)), className: "inline-flex shrink-0 items-center gap-1 rounded-mark px-1 py-0.5 text-micro text-primary outline-0 hover:bg-primary/8 data-focus-visible:outline-2 data-focus-visible:outline-primary", children: [_jsx(DirectionIcon, { className: "size-(--size-icon-sm)", strokeWidth: 2, "aria-hidden": true }), direction] }), _jsx(AriaButton, { "aria-label": fillCopy(copy.sorting.remove, { field: name }), onPress: () => onSortChange(sort.filter((_, i) => i !== item.index)), className: "inline-flex shrink-0 items-center rounded-mark p-0.5 text-muted outline-0 hover:bg-ink/6 hover:text-ink data-focus-visible:outline-2 data-focus-visible:outline-primary", children: _jsx(X, { className: "size-(--size-icon-sm)", strokeWidth: 2, "aria-hidden": true }) })] }));
|
|
62
|
+
} })), _jsxs("div", { className: "flex items-center justify-between gap-2 border-t border-line pt-2", children: [left.length === 0 ? (_jsx("span", {})) : (_jsx(Menu, { label: copy.sorting.add, icon: Plus, variant: "text", chevron: false, width: "sm", groups: [
|
|
63
|
+
{ options: left.map((one) => ({ id: one.field, label: one.label })) },
|
|
64
|
+
], onSelect: (id) => onSortChange([...sort, { key: id, direction: "asc" }]) })), sort.length === 0 ? null : (_jsx(AriaButton, { onPress: () => onSortChange([]), className: "rounded-mark px-1.5 py-0.5 text-micro text-muted outline-0 hover:bg-ink/6 hover:text-ink data-focus-visible:outline-2 data-focus-visible:outline-primary", children: copy.sorting.clear }))] })] }) })] }));
|
|
105
65
|
}
|
|
@@ -2,7 +2,8 @@ export { type ColumnControl, type ColumnSpec, defaultVisibleIds, reconcileVisibl
|
|
|
2
2
|
export { ColumnsMenu, specsFromColumns, } from "./ColumnsMenu.js";
|
|
3
3
|
export { Listing } from "./Listing.js";
|
|
4
4
|
export { type ListIdentity, ListingBar } from "./ListingBar.js";
|
|
5
|
-
export { type
|
|
5
|
+
export { type ModelCellKind } from "./modelCells.js";
|
|
6
|
+
export { type ClaimedField, claimedFields, columnsFromModel, modelCellKind, readModelValue, } from "./modelColumns.js";
|
|
6
7
|
export { type CursorSource, DEFAULT_PAGE_SIZE, PAGE_SIZES, type Pages, type PaginationBridge, type ServerPage, useCursorPagination, usePagination, } from "./pages.js";
|
|
7
8
|
export { Pagination } from "./Pagination.js";
|
|
8
9
|
export { RecordLink } from "./RecordLink.js";
|
|
@@ -4,7 +4,8 @@ export { defaultVisibleIds, reconcileVisible, toggleColumn, useColumnControl, vi
|
|
|
4
4
|
export { ColumnsMenu, specsFromColumns, } from "./ColumnsMenu.js";
|
|
5
5
|
export { Listing } from "./Listing.js";
|
|
6
6
|
export { ListingBar } from "./ListingBar.js";
|
|
7
|
-
export {
|
|
7
|
+
export {} from "./modelCells.js";
|
|
8
|
+
export { claimedFields, columnsFromModel, modelCellKind, readModelValue, } from "./modelColumns.js";
|
|
8
9
|
export { DEFAULT_PAGE_SIZE, PAGE_SIZES, useCursorPagination, usePagination, } from "./pages.js";
|
|
9
10
|
export { Pagination } from "./Pagination.js";
|
|
10
11
|
export { RecordLink } from "./RecordLink.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** How a model field is drawn. `text` is today's behaviour, for any type not known here. */
|
|
2
|
+
export type ModelCellKind = "text" | "number" | "email" | "url" | "phone" | "duration" | "boolean" | "date" | "datetime";
|
|
3
|
+
/** One cell of a model column. `value` is what `readModelValue` read — never `null` here. */
|
|
4
|
+
export declare function ModelCell({ kind, value, }: {
|
|
5
|
+
kind: ModelCellKind;
|
|
6
|
+
value: string | number | boolean;
|
|
7
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useUiCopy } from "../../foundations/copy.js";
|
|
4
|
+
import { emailHref, telHref, urlHref, urlLabel, } from "../../primitives/field/contact.js";
|
|
5
|
+
import { formatDuration } from "../../primitives/field/duration.js";
|
|
6
|
+
import { DateValue } from "../../primitives/value/DateValue.js";
|
|
7
|
+
import { Numeric } from "../../primitives/value/Value.js";
|
|
8
|
+
import { Check } from "lucide-react";
|
|
9
|
+
const LINK = "block truncate rounded-mark text-primary underline-offset-2 hover:underline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary";
|
|
10
|
+
function Plain({ text }) {
|
|
11
|
+
return (_jsx("span", { className: "block truncate text-muted", title: text, children: text }));
|
|
12
|
+
}
|
|
13
|
+
/** A link in a cell, or the plain text when the value does not make one. */
|
|
14
|
+
function Link({ text, href, label, external = false, }) {
|
|
15
|
+
const copy = useUiCopy();
|
|
16
|
+
if (href === null)
|
|
17
|
+
return _jsx(Plain, { text: text });
|
|
18
|
+
return (_jsxs("a", { href: href, title: text, className: LINK,
|
|
19
|
+
/* The row opens its record on a click; writing to its email must not ALSO open it. */
|
|
20
|
+
onClick: (event) => event.stopPropagation(), ...(external ? { target: "_blank", rel: "noopener noreferrer" } : {}), children: [label ?? text, external ? _jsxs("span", { className: "sr-only", children: [" (", copy.field.newTab, ")"] }) : null] }));
|
|
21
|
+
}
|
|
22
|
+
/** A yes/no in words: a check alone is not read by everyone the same way, and «no» needs
|
|
23
|
+
* no icon — an empty mark next to every false row would only add noise to the column. */
|
|
24
|
+
function YesNo({ value }) {
|
|
25
|
+
const copy = useUiCopy();
|
|
26
|
+
if (!value)
|
|
27
|
+
return _jsx("span", { className: "text-muted", children: copy.table.no });
|
|
28
|
+
return (_jsxs("span", { className: "inline-flex items-center gap-1 text-ink", children: [_jsx(Check, { "aria-hidden": true, className: "size-(--size-icon-sm) text-good", strokeWidth: 2.5 }), copy.table.yes] }));
|
|
29
|
+
}
|
|
30
|
+
/** One cell of a model column. `value` is what `readModelValue` read — never `null` here. */
|
|
31
|
+
export function ModelCell({ kind, value, }) {
|
|
32
|
+
const text = String(value);
|
|
33
|
+
switch (kind) {
|
|
34
|
+
case "number":
|
|
35
|
+
/* A figure in the locale's digits and right aligned, so a column of amounts lines up
|
|
36
|
+
— never «1234.5» as text. */
|
|
37
|
+
return typeof value === "number" ? (_jsx(Numeric, { value: value, format: "decimal" })) : (_jsx(Plain, { text: text }));
|
|
38
|
+
case "email":
|
|
39
|
+
return _jsx(Link, { text: text, href: emailHref(text) });
|
|
40
|
+
case "phone":
|
|
41
|
+
return _jsx(Link, { text: text, href: telHref(text) });
|
|
42
|
+
case "url":
|
|
43
|
+
return _jsx(Link, { text: text, href: urlHref(text), label: urlLabel(text), external: true });
|
|
44
|
+
case "duration":
|
|
45
|
+
return typeof value === "number" ? (
|
|
46
|
+
/* Right aligned like `Numeric`: a column of hours lines up by the colon. */
|
|
47
|
+
_jsx("span", { className: "block text-right text-body tabular-nums whitespace-nowrap text-ink", children: formatDuration(value) })) : (_jsx(Plain, { text: text }));
|
|
48
|
+
case "boolean":
|
|
49
|
+
return typeof value === "boolean" ? _jsx(YesNo, { value: value }) : _jsx(Plain, { text: text });
|
|
50
|
+
case "date":
|
|
51
|
+
case "datetime":
|
|
52
|
+
return typeof value === "boolean" ? (_jsx(Plain, { text: text })) : (_jsx(DateValue, { when: value, format: kind === "date" ? "date" : "relative" }));
|
|
53
|
+
default:
|
|
54
|
+
return _jsx(Plain, { text: text });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ModelCellKind } from "./modelCells.js";
|
|
1
2
|
import { type ModelDefinition } from "../search/model.js";
|
|
2
3
|
import type { DataColumn } from "../../primitives/table/DataTable.js";
|
|
3
4
|
/** Qué campo del modelo muestra una columna declarada a mano.
|
|
@@ -28,6 +29,14 @@ export declare function claimedFields(columns: readonly ClaimedField[]): Set<str
|
|
|
28
29
|
* Las fechas se dejan como vienen —ISO— porque en ISO el orden alfabético ES
|
|
29
30
|
* el cronológico, y así la columna ordena bien sin parsear nada. */
|
|
30
31
|
export declare function readModelValue(raw: unknown): string | number | boolean | null;
|
|
32
|
+
/** How a field of the model is drawn, by its `type` and in any case — «Email» and `email` are
|
|
33
|
+
* the same field. A type not known here stays `text`, exactly as before.
|
|
34
|
+
*
|
|
35
|
+
* in "monetary" out "number"
|
|
36
|
+
* in "float_time" out "duration" «8:30», not «8.5»
|
|
37
|
+
* in "jsonb" out "text"
|
|
38
|
+
*/
|
|
39
|
+
export declare function modelCellKind(type: string): ModelCellKind;
|
|
31
40
|
/**
|
|
32
41
|
* Las columnas declaradas, más una por cada campo del modelo que ninguna
|
|
33
42
|
* contestaba.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { ModelCell } from "./modelCells.js";
|
|
3
|
+
import { fieldLabel, isNumericType, } from "../search/model.js";
|
|
4
|
+
import { formatDuration } from "../../primitives/field/duration.js";
|
|
3
5
|
/** Los campos que las columnas declaradas ya contestan.
|
|
4
6
|
*
|
|
5
7
|
* in [{id:"rows", field:"row_count"}, {id:"name"}]
|
|
@@ -38,14 +40,45 @@ export function readModelValue(raw) {
|
|
|
38
40
|
return raw === "" ? null : raw;
|
|
39
41
|
return JSON.stringify(raw);
|
|
40
42
|
}
|
|
43
|
+
/** The names an engine publishes for each kind it knows how to draw — Odoo's (`char` with a
|
|
44
|
+
* widget aside, `float_time`, `boolean`, `date`, `datetime`) and the plain ones a schema
|
|
45
|
+
* uses (`email`, `url`, `phone`, `duration`, `bool`, `timestamp`). */
|
|
46
|
+
const KIND_OF_TYPE = {
|
|
47
|
+
email: "email",
|
|
48
|
+
mail: "email",
|
|
49
|
+
url: "url",
|
|
50
|
+
uri: "url",
|
|
51
|
+
website: "url",
|
|
52
|
+
phone: "phone",
|
|
53
|
+
tel: "phone",
|
|
54
|
+
float_time: "duration",
|
|
55
|
+
duration: "duration",
|
|
56
|
+
boolean: "boolean",
|
|
57
|
+
bool: "boolean",
|
|
58
|
+
date: "date",
|
|
59
|
+
datetime: "datetime",
|
|
60
|
+
timestamp: "datetime",
|
|
61
|
+
};
|
|
62
|
+
/** How a field of the model is drawn, by its `type` and in any case — «Email» and `email` are
|
|
63
|
+
* the same field. A type not known here stays `text`, exactly as before.
|
|
64
|
+
*
|
|
65
|
+
* in "monetary" out "number"
|
|
66
|
+
* in "float_time" out "duration" «8:30», not «8.5»
|
|
67
|
+
* in "jsonb" out "text"
|
|
68
|
+
*/
|
|
69
|
+
export function modelCellKind(type) {
|
|
70
|
+
if (isNumericType(type))
|
|
71
|
+
return "number";
|
|
72
|
+
return KIND_OF_TYPE[type.toLowerCase()] ?? "text";
|
|
73
|
+
}
|
|
41
74
|
/** Cuánto ancho merece un campo, por su tipo. Una huella son 64 caracteres y
|
|
42
75
|
* una cuenta son seis: darles el mismo ancho arruina las dos. */
|
|
43
|
-
function widthFor(
|
|
44
|
-
if (
|
|
76
|
+
function widthFor(kind) {
|
|
77
|
+
if (kind === "number" || kind === "duration")
|
|
45
78
|
return 120;
|
|
46
|
-
if (
|
|
79
|
+
if (kind === "datetime" || kind === "date")
|
|
47
80
|
return 190;
|
|
48
|
-
if (
|
|
81
|
+
if (kind === "boolean")
|
|
49
82
|
return 90;
|
|
50
83
|
return 260;
|
|
51
84
|
}
|
|
@@ -76,27 +109,39 @@ export function columnsFromModel(declared, model, locale = "default") {
|
|
|
76
109
|
for (const [field, definition] of Object.entries(model.fields)) {
|
|
77
110
|
if (claimed.has(field))
|
|
78
111
|
continue;
|
|
79
|
-
const
|
|
112
|
+
const kind = modelCellKind(definition.type);
|
|
113
|
+
/* A duration is a number of hours: it sorts, sums and aligns as one. */
|
|
114
|
+
const numeric = kind === "number" || kind === "duration";
|
|
115
|
+
const read = (row) => readModelValue(row[field]);
|
|
80
116
|
extra.push({
|
|
81
117
|
id: field,
|
|
82
118
|
field,
|
|
83
119
|
header: fieldLabel(model, field, locale),
|
|
84
|
-
value:
|
|
85
|
-
/*
|
|
86
|
-
|
|
87
|
-
por encima y leerla. */
|
|
120
|
+
value: read,
|
|
121
|
+
/* Drawn by what the model says the field IS (`modelCells.tsx`); a plain text keeps
|
|
122
|
+
its whole value in the `title` — a 64 character fingerprint fits no cell. */
|
|
88
123
|
cell: (row) => {
|
|
89
|
-
const value =
|
|
124
|
+
const value = read(row);
|
|
90
125
|
if (value === null)
|
|
91
126
|
return _jsx("span", { className: "text-subtle", children: "\u2014" });
|
|
92
|
-
|
|
93
|
-
return (_jsx("span", { className: "block truncate text-muted", title: text, children: text }));
|
|
127
|
+
return _jsx(ModelCell, { kind: kind, value: value });
|
|
94
128
|
},
|
|
129
|
+
/* Where a node cannot be drawn — the sheet, the footer's total — a duration still
|
|
130
|
+
reads «42:30» and never «42.5». */
|
|
131
|
+
...(kind === "duration"
|
|
132
|
+
? {
|
|
133
|
+
format: formatDuration,
|
|
134
|
+
text: (row) => {
|
|
135
|
+
const value = read(row);
|
|
136
|
+
return typeof value === "number" ? formatDuration(value) : String(value ?? "");
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
: {}),
|
|
95
140
|
...(numeric ? { numeric: true } : {}),
|
|
96
141
|
sortable: definition.sortable,
|
|
97
142
|
optional: true,
|
|
98
143
|
defaultHidden: true,
|
|
99
|
-
width: widthFor(
|
|
144
|
+
width: widthFor(kind),
|
|
100
145
|
});
|
|
101
146
|
}
|
|
102
147
|
return [...declared, ...extra];
|
|
@@ -5,6 +5,6 @@ export { formatJson, JsonEditor } from "./JsonEditor.js";
|
|
|
5
5
|
export { SearchBar, type SearchBarProps, type SearchFacet, } from "./SearchBar.js";
|
|
6
6
|
export { at, criteriaId, type CriteriaJoin, type CriteriaPath, filterCriteria, filtersOf, type GroupCriteria, groupCriteria, groupsOf, hasNamed, levelsOf, nextJoin, removeAt, replaceAt, type SearchCriteria, toggleNamed, whereOf, withLevels, } from "./criteria.js";
|
|
7
7
|
export { CriteriaChips, type CriteriaChipsProps, } from "./CriteriaChips.js";
|
|
8
|
-
export { type FieldDefinition, fieldLabel, filterableNames, isFilterable, type ModelDefinition, type ModelTranslations, } from "./model.js";
|
|
8
|
+
export { type FieldDefinition, fieldLabel, filterableNames, isFilterable, isNumericType, type ModelDefinition, type ModelTranslations, } from "./model.js";
|
|
9
9
|
export { answers, defaultOp, describeCondition, globToLike, opLabel, type QueryDraft, type QueryField, queryFields, type QueryKind, type QuerySuggestion, readTyped, suggest, type TypedValue, valueFor, type ValueShape, withoutValue, } from "./query.js";
|
|
10
10
|
export { type DatePeriod, datePeriodGroup, datePeriodOptions, type DateRange, periodId, readPeriodId, recentMonths, recentYears, resolvePeriods, } from "./datePeriods.js";
|
|
@@ -12,7 +12,7 @@ export { CriteriaChips, } from "./CriteriaChips.js";
|
|
|
12
12
|
/* EL VOCABULARIO DEL MODELO — lo que el servidor publica sobre sus campos. Lo
|
|
13
13
|
hablan el constructor de filtros, el de criterias y la barra de búsqueda, así
|
|
14
14
|
que vive aparte de los tres. */
|
|
15
|
-
export { fieldLabel, filterableNames, isFilterable, } from "./model.js";
|
|
15
|
+
export { fieldLabel, filterableNames, isFilterable, isNumericType, } from "./model.js";
|
|
16
16
|
/* LA GRAMÁTICA DE LA BARRA: qué se puede preguntar sobre un modelo, y con qué.
|
|
17
17
|
Tres pasos —campo, operador, valor— que se pueden escribir en uno. */
|
|
18
18
|
export { answers, defaultOp, describeCondition, globToLike, opLabel, queryFields, readTyped, suggest, valueFor, withoutValue, } from "./query.js";
|
|
@@ -23,6 +23,7 @@ export interface FieldDefinition {
|
|
|
23
23
|
/** Los valores que un campo de enum puede tomar — de ahí sale un `<select>`. */
|
|
24
24
|
choices?: readonly unknown[];
|
|
25
25
|
}
|
|
26
|
+
export declare function isNumericType(type: string): boolean;
|
|
26
27
|
/** Si este campo se puede usar en un `where`, un `order` o un `grouping.by` directamente.
|
|
27
28
|
* `ops.length === 0` alcanza como señal: la publican tanto `embedded` como los tres
|
|
28
29
|
* relacionales, así que no hace falta mirar `kind` aparte. */
|
|
@@ -13,6 +13,13 @@
|
|
|
13
13
|
* para saber cómo se llama un operador. Un vocabulario compartido no puede
|
|
14
14
|
* colgar de uno de los que lo hablan.
|
|
15
15
|
* ==================================================================== */
|
|
16
|
+
/** The types an engine publishes for an AMOUNT — integer, float, decimal, money. A search
|
|
17
|
+
* compares them as numbers and a listing aligns and formats them as numbers: «monetary»
|
|
18
|
+
* read as text made «total <= 1000» impossible and drew «1234.5» flush left. */
|
|
19
|
+
const NUMERIC_TYPES = new Set(["integer", "number", "float", "decimal", "monetary", "money"]);
|
|
20
|
+
export function isNumericType(type) {
|
|
21
|
+
return NUMERIC_TYPES.has(type.toLowerCase());
|
|
22
|
+
}
|
|
16
23
|
/** Si este campo se puede usar en un `where`, un `order` o un `grouping.by` directamente.
|
|
17
24
|
* `ops.length === 0` alcanza como señal: la publican tanto `embedded` como los tres
|
|
18
25
|
* relacionales, así que no hace falta mirar `kind` aparte. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fieldLabel, isFilterable, } from "./model.js";
|
|
1
|
+
import { fieldLabel, isFilterable, isNumericType, } from "./model.js";
|
|
2
2
|
import { currentCopy, fillCopy } from "../../foundations/copy.js";
|
|
3
3
|
/**
|
|
4
4
|
* Las palabras que, después del numeral, abren un GRUPO en vez de nombrar un
|
|
@@ -97,7 +97,6 @@ export function readTyped(text) {
|
|
|
97
97
|
export function globToLike(value) {
|
|
98
98
|
return value.replace(/\*/g, "%");
|
|
99
99
|
}
|
|
100
|
-
const NUMERIC = new Set(["integer", "number", "float", "decimal", "monetary", "money"]);
|
|
101
100
|
/** Qué clase de valor es un campo, leída del tipo que publica el modelo. */
|
|
102
101
|
function kindOf(definition) {
|
|
103
102
|
if (definition.kind === "relation")
|
|
@@ -107,7 +106,7 @@ function kindOf(definition) {
|
|
|
107
106
|
const type = definition.type.toLowerCase();
|
|
108
107
|
/* An amount is a number: «decimal» and «monetary» read as text made «total <= 1000»
|
|
109
108
|
impossible to ask of an invoice. */
|
|
110
|
-
if (
|
|
109
|
+
if (isNumericType(type))
|
|
111
110
|
return "number";
|
|
112
111
|
if (type === "date" || type === "datetime")
|
|
113
112
|
return "date";
|
|
@@ -122,6 +122,22 @@ export type UiCopy = {
|
|
|
122
122
|
none: string;
|
|
123
123
|
/** Next to the label of a field whose value changed and is not saved yet. */
|
|
124
124
|
changed: string;
|
|
125
|
+
/** The eye button of a password: show it… */
|
|
126
|
+
showSecret: string;
|
|
127
|
+
/** …and hide it again. */
|
|
128
|
+
hideSecret: string;
|
|
129
|
+
/** Under a password while Caps Lock is on: the most common reason a right password fails. */
|
|
130
|
+
capsLock: string;
|
|
131
|
+
/** What a stored password says in read mode, for a screen reader: there is one, not which. */
|
|
132
|
+
secretSet: string;
|
|
133
|
+
/** Said after a link that leaves for another tab — a website, WhatsApp. */
|
|
134
|
+
newTab: string;
|
|
135
|
+
/** The unit after a duration: «8:30 h». */
|
|
136
|
+
hours: string;
|
|
137
|
+
/** The name of the unit (or currency) chooser inside a number field. */
|
|
138
|
+
unit: string;
|
|
139
|
+
/** The row of colours a colour field offers. */
|
|
140
|
+
palette: string;
|
|
125
141
|
};
|
|
126
142
|
/** The choice controls: combo boxes, selects, suggestion lists. */
|
|
127
143
|
choice: {
|
|
@@ -343,6 +359,32 @@ export type UiCopy = {
|
|
|
343
359
|
/** A field with nothing in it: said, not a silent success. */
|
|
344
360
|
empty: string;
|
|
345
361
|
};
|
|
362
|
+
/** The rich-text editor: its toolbar, its link form, its list of people to mention. */
|
|
363
|
+
richText: {
|
|
364
|
+
toolbar: string;
|
|
365
|
+
bold: string;
|
|
366
|
+
italic: string;
|
|
367
|
+
underline: string;
|
|
368
|
+
strike: string;
|
|
369
|
+
heading: string;
|
|
370
|
+
subheading: string;
|
|
371
|
+
bulletList: string;
|
|
372
|
+
orderedList: string;
|
|
373
|
+
quote: string;
|
|
374
|
+
link: string;
|
|
375
|
+
linkUrl: string;
|
|
376
|
+
applyLink: string;
|
|
377
|
+
removeLink: string;
|
|
378
|
+
/** Said when the address is not a website, an email or a phone: what to write instead. */
|
|
379
|
+
badLink: string;
|
|
380
|
+
table: string;
|
|
381
|
+
undo: string;
|
|
382
|
+
redo: string;
|
|
383
|
+
clear: string;
|
|
384
|
+
/** The list that opens on «@»: people to mention. */
|
|
385
|
+
mentions: string;
|
|
386
|
+
noMatches: string;
|
|
387
|
+
};
|
|
346
388
|
/** Ordenar, cuando la presentación no tiene encabezados donde hacer clic. */
|
|
347
389
|
sorting: {
|
|
348
390
|
label: string;
|
|
@@ -350,6 +392,24 @@ export type UiCopy = {
|
|
|
350
392
|
asc: string;
|
|
351
393
|
desc: string;
|
|
352
394
|
clear: string;
|
|
395
|
+
/** On a sortable header: how to add it as a tie-breaker instead of replacing the order. */
|
|
396
|
+
addHint: string;
|
|
397
|
+
/** What a tie-breaker header says of itself: «sort 2, descending». */
|
|
398
|
+
place: string;
|
|
399
|
+
/** Adds one more criterion at the end of the order. */
|
|
400
|
+
add: string;
|
|
401
|
+
/** Where the criteria are listed when there are none: the rows go as they came. */
|
|
402
|
+
none: string;
|
|
403
|
+
/** The direction button of a criterion, named by its field. */
|
|
404
|
+
flip: string;
|
|
405
|
+
/** Takes one criterion out, named by its field. */
|
|
406
|
+
remove: string;
|
|
407
|
+
/** The handle a criterion is dragged by, named by its field. */
|
|
408
|
+
move: string;
|
|
409
|
+
/** The trigger with more than one criterion: «Fecha +2». */
|
|
410
|
+
more: string;
|
|
411
|
+
/** The second and later criteria: they only decide between rows the first leaves equal. */
|
|
412
|
+
then: string;
|
|
353
413
|
};
|
|
354
414
|
/** Los recortes de fecha RELATIVOS. Las unidades van en singular y plural
|
|
355
415
|
* porque «1 mes» y «3 meses» no se resuelven con una `s` en todos los
|
|
@@ -470,6 +530,26 @@ export type UiCopy = {
|
|
|
470
530
|
finish: string;
|
|
471
531
|
busy: string;
|
|
472
532
|
};
|
|
533
|
+
/** A multi-step operation done in one sitting (`StepFlow`): where you are, what is left,
|
|
534
|
+
* and its own buttons. */
|
|
535
|
+
stepFlow: {
|
|
536
|
+
/** The accessible name of the strip of steps. */
|
|
537
|
+
steps: string;
|
|
538
|
+
/** Over the step's title: «Step 2 of 3». */
|
|
539
|
+
position: string;
|
|
540
|
+
/** Read by the live region on every move: `{position}` is the line above, `{title}`
|
|
541
|
+
* the step's. */
|
|
542
|
+
announce: string;
|
|
543
|
+
/** The state of a step on the strip, said to a screen reader — never only a colour. */
|
|
544
|
+
done: string;
|
|
545
|
+
pending: string;
|
|
546
|
+
cancel: string;
|
|
547
|
+
back: string;
|
|
548
|
+
next: string;
|
|
549
|
+
finish: string;
|
|
550
|
+
/** Read by the live region while the operation runs. */
|
|
551
|
+
working: string;
|
|
552
|
+
};
|
|
473
553
|
report: {
|
|
474
554
|
open: string;
|
|
475
555
|
close: string;
|
|
@@ -701,6 +781,28 @@ export type UiCopy = {
|
|
|
701
781
|
/** The accessible name of an editable cell. `{column}` is the column's header. */
|
|
702
782
|
editCell: string;
|
|
703
783
|
};
|
|
784
|
+
/** The files of a record (`Attachments`): listing, adding, removing and their states. */
|
|
785
|
+
attachments: {
|
|
786
|
+
/** The accessible name of the list. */
|
|
787
|
+
label: string;
|
|
788
|
+
/** An empty list in read mode. */
|
|
789
|
+
empty: string;
|
|
790
|
+
/** The drop zone's sentence when there is no file yet… */
|
|
791
|
+
emptyAdd: string;
|
|
792
|
+
/** …and when there already are some. */
|
|
793
|
+
add: string;
|
|
794
|
+
download: string;
|
|
795
|
+
remove: string;
|
|
796
|
+
/** The notice after removing. `{name}` is the file. */
|
|
797
|
+
removed: string;
|
|
798
|
+
undo: string;
|
|
799
|
+
/** The name of an upload's progress bar. `{name}` is the file. */
|
|
800
|
+
uploading: string;
|
|
801
|
+
cancelUpload: string;
|
|
802
|
+
/** A failed upload, when the server did not say why. */
|
|
803
|
+
failed: string;
|
|
804
|
+
retry: string;
|
|
805
|
+
};
|
|
704
806
|
/** The theme switch: the three choices an app offers for the palette. */
|
|
705
807
|
theme: {
|
|
706
808
|
/** The name of the control — what a screen reader says for the group. */
|
|
@@ -763,6 +865,35 @@ export type UiCopy = {
|
|
|
763
865
|
/** A value that was empty before the change. */
|
|
764
866
|
empty: string;
|
|
765
867
|
};
|
|
868
|
+
/** The media controls: an image chosen and cropped (`ImageInput`), a signature drawn or
|
|
869
|
+
* typed (`SignatureInput`). */
|
|
870
|
+
media: {
|
|
871
|
+
/** What the empty image zone says. */
|
|
872
|
+
dropImage: string;
|
|
873
|
+
chooseImage: string;
|
|
874
|
+
changeImage: string;
|
|
875
|
+
removeImage: string;
|
|
876
|
+
/** A file that is not an image: what to choose instead. */
|
|
877
|
+
notImage: string;
|
|
878
|
+
/** The name of the crop area, and how it is moved from the keyboard. */
|
|
879
|
+
crop: string;
|
|
880
|
+
cropHelp: string;
|
|
881
|
+
zoomIn: string;
|
|
882
|
+
zoomOut: string;
|
|
883
|
+
applyCrop: string;
|
|
884
|
+
cancel: string;
|
|
885
|
+
/** The name of the drawing pad, and what to do there. */
|
|
886
|
+
signaturePad: string;
|
|
887
|
+
signatureHelp: string;
|
|
888
|
+
clear: string;
|
|
889
|
+
/** The way in for whoever cannot draw: type the name, and it is written as the signature. */
|
|
890
|
+
typeName: string;
|
|
891
|
+
drawInstead: string;
|
|
892
|
+
/** The label of the box the name is typed in. */
|
|
893
|
+
typedName: string;
|
|
894
|
+
/** The image of a signature, when the form does not say whose it is. */
|
|
895
|
+
signature: string;
|
|
896
|
+
};
|
|
766
897
|
};
|
|
767
898
|
export declare const ENGLISH_COPY: UiCopy;
|
|
768
899
|
/** What changes of web-ui's words, at any depth: `{ search: { joins: { any: "o" } } }`. */
|