nexus-shared 3.0.0 → 3.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 +446 -262
- package/LICENSE +21 -21
- package/README.md +1084 -1000
- package/dist/Client.Index.d.ts +3 -0
- package/dist/Client.Index.js +3 -0
- package/dist/Components/Chats/ChatParts.d.ts +2 -2
- package/dist/Components/Chats/ChatParts.js +3 -7
- package/dist/Components/Chats/ChatThread.js +3 -3
- package/dist/Components/Documents/Button.d.ts +6 -1
- package/dist/Components/Documents/Button.js +2 -2
- package/dist/Components/Documents/Menu.js +21 -9
- package/dist/Components/Documents/SplitButton.d.ts +3 -1
- package/dist/Components/Documents/SplitButton.js +7 -5
- package/dist/Components/Documents/TabButtons.d.ts +5 -2
- package/dist/Components/Documents/TabButtons.js +2 -2
- package/dist/Components/Forms/ApiForm.d.ts +1 -1
- package/dist/Components/Forms/ApiForm.js +1 -1
- package/dist/Components/Forms/Crud.js +177 -58
- package/dist/Components/Forms/CrudParts.d.ts +5 -1
- package/dist/Components/Forms/CrudParts.js +37 -5
- package/dist/Components/Forms/ExcelImport.d.ts +23 -7
- package/dist/Components/Forms/ExcelImport.js +189 -60
- package/dist/Components/Forms/Form.js +5 -13
- package/dist/Components/Forms/SubmitForm.d.ts +11 -2
- package/dist/Components/Forms/SubmitForm.js +23 -12
- package/dist/Components/Inputs/DropdownParts.d.ts +1 -1
- package/dist/Components/Inputs/DropdownParts.js +4 -6
- package/dist/Components/Inputs/NumberBox.js +24 -2
- package/dist/Components/Inputs/RowsInput.js +2 -2
- package/dist/Components/Inputs/Slider.js +4 -1
- package/dist/Components/Inputs/TabularForm.js +42 -10
- package/dist/Components/Inputs/TimePanel.js +4 -3
- package/dist/Components/Layouts/Popup.js +1 -4
- package/dist/Components/Layouts/ThemePicker.d.ts +20 -3
- package/dist/Components/Layouts/ThemePicker.js +77 -6
- package/dist/Components/Layouts/ThemeScript.d.ts +4 -2
- package/dist/Components/Layouts/ThemeScript.js +4 -2
- package/dist/Components/Layouts/ThemeSwitcher.d.ts +6 -3
- package/dist/Components/Layouts/ThemeSwitcher.js +9 -8
- package/dist/Components/Viewers/Avatar.d.ts +21 -0
- package/dist/Components/Viewers/Avatar.js +21 -0
- package/dist/Components/Viewers/DataTable.js +192 -44
- package/dist/Components/Viewers/DataTableColumns.d.ts +7 -3
- package/dist/Components/Viewers/DataTableColumns.js +25 -68
- package/dist/Components/Viewers/DataTableFilters.d.ts +21 -0
- package/dist/Components/Viewers/DataTableFilters.js +68 -0
- package/dist/Components/Viewers/DataTableParts.d.ts +2 -2
- package/dist/Components/Viewers/DataTableParts.js +3 -3
- package/dist/Helpers/AnimationHelpers.d.ts +11 -0
- package/dist/Helpers/AnimationHelpers.js +20 -2
- package/dist/Helpers/CrudBackend.d.ts +7 -4
- package/dist/Helpers/CrudBackend.js +4 -2
- package/dist/Helpers/ExcelBackend.d.ts +3 -3
- package/dist/Helpers/ExcelBackend.js +8 -2
- package/dist/Helpers/ExcelHelpers.d.ts +36 -8
- package/dist/Helpers/ExcelHelpers.js +55 -13
- package/dist/Helpers/FormStore.d.ts +9 -5
- package/dist/Helpers/FormStore.js +25 -5
- package/dist/Helpers/MessageBuilder.js +1 -0
- package/dist/Helpers/NumberHelpers.d.ts +7 -1
- package/dist/Helpers/NumberHelpers.js +41 -3
- package/dist/Helpers/PopoverHelpers.d.ts +17 -5
- package/dist/Helpers/PopoverHelpers.js +52 -17
- package/dist/Helpers/PopoverHooks.d.ts +24 -0
- package/dist/Helpers/PopoverHooks.js +62 -0
- package/dist/Helpers/RowsStore.d.ts +7 -2
- package/dist/Helpers/RowsStore.js +15 -3
- package/dist/Helpers/TableExport.js +12 -12
- package/dist/Helpers/TableFilters.d.ts +25 -0
- package/dist/Helpers/TableFilters.js +162 -0
- package/dist/Helpers/TableHelpers.d.ts +6 -3
- package/dist/Helpers/TableHelpers.js +8 -4
- package/dist/Helpers/TableStore.d.ts +11 -3
- package/dist/Helpers/TableStore.js +16 -5
- package/dist/Helpers/ThemeHelpers.d.ts +21 -4
- package/dist/Helpers/ThemeHelpers.js +46 -9
- package/dist/Helpers/UserPreferences.d.ts +14 -0
- package/dist/Helpers/UserPreferences.js +99 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +20 -1
- package/dist/Interfaces/ExcelInterfaces.d.ts +12 -0
- package/dist/Interfaces/FormInterfaces.d.ts +33 -5
- package/dist/Interfaces/PreferenceInterfaces.d.ts +43 -0
- package/dist/Interfaces/PreferenceInterfaces.js +28 -0
- package/dist/Interfaces/TableInterfaces.d.ts +59 -12
- package/dist/Interfaces/ThemeInterfaces.d.ts +25 -0
- package/dist/Interfaces/ThemeInterfaces.js +15 -0
- package/dist/Services/ExcelApi.d.ts +3 -1
- package/dist/Services/ExcelApi.js +1 -1
- package/dist/Services/ThemeService.d.ts +18 -2
- package/dist/Services/ThemeService.js +80 -16
- package/dist/Shared.Index.d.ts +2 -0
- package/dist/Shared.Index.js +2 -0
- package/package.json +85 -85
- package/src/Styles/Nexus.Avatar.css +79 -0
- package/src/Styles/Nexus.Base.css +117 -70
- package/src/Styles/Nexus.Button.css +180 -153
- package/src/Styles/Nexus.Calendar.css +273 -275
- package/src/Styles/Nexus.Chat.css +1443 -1489
- package/src/Styles/Nexus.Choice.css +343 -345
- package/src/Styles/Nexus.Crud.css +168 -161
- package/src/Styles/Nexus.DateTime.css +81 -81
- package/src/Styles/Nexus.Dropdown.css +725 -774
- package/src/Styles/Nexus.Editor.css +295 -297
- package/src/Styles/Nexus.Excel.css +389 -370
- package/src/Styles/Nexus.Form.css +279 -321
- package/src/Styles/Nexus.Index.css +37 -36
- package/src/Styles/Nexus.Input.css +602 -637
- package/src/Styles/Nexus.Markdown.css +237 -265
- package/src/Styles/Nexus.Menu.css +237 -259
- package/src/Styles/Nexus.Popup.css +724 -731
- package/src/Styles/Nexus.RichText.css +2 -32
- package/src/Styles/Nexus.Rows.css +637 -596
- package/src/Styles/Nexus.Slider.css +276 -278
- package/src/Styles/Nexus.Tab.Buttons.css +432 -419
- package/src/Styles/Nexus.Table.css +1386 -1296
- package/src/Styles/Nexus.Theme.Picker.css +286 -232
- package/src/Styles/Nexus.Themes.css +194 -194
- package/src/Styles/Nexus.Time.css +144 -147
- package/src/Styles/Nexus.Toast.css +263 -265
- package/src/Styles/Nexus.Tokens.css +89 -72
- package/src/Styles/Nexus.Tree.css +431 -435
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { iconFilterOff, InlineIcon } from "nexus-icons";
|
|
4
|
+
import { useEffect, useId, useRef } from "react";
|
|
5
|
+
import { usePopoverPanel } from "../../Helpers/PopoverHooks.js";
|
|
6
|
+
import { activeFilters } from "../../Helpers/TableFilters.js";
|
|
7
|
+
import { Button } from "../Documents/Button.js";
|
|
8
|
+
import { Form, useForm } from "../Forms/Form.js";
|
|
9
|
+
import { formatCount } from "./DataTableParts.js";
|
|
10
|
+
/*
|
|
11
|
+
* The Filters panel: the table's filter inputs as one small form under the options button (⋮), applied as they change.
|
|
12
|
+
* A choice applies at once; a value that can come a key or a step at a time (text, a number, a slider, a date) waits
|
|
13
|
+
* the table's search delay, or Enter, or the panel closing. Esc closes the panel, an open list or calendar first, and
|
|
14
|
+
* never clears a filter on its way out.
|
|
15
|
+
*/
|
|
16
|
+
/** Inputs typed into or dragged: their value waits until the person stops. A date box reads a date from part of one. */
|
|
17
|
+
const TYPED = new Set(["text", "email", "search", "tel", "url", "textarea", "number", "slider", "range-slider", "date", "date-range", "time", "date-time"]);
|
|
18
|
+
/** The Filters panel, opened through its `open()` handle: a `Form` of the filter inputs, the count, and Clear all. */
|
|
19
|
+
export function TableFiltersPanel({ inputs, value, delay, matches, total, onChange, anchor, ref }) {
|
|
20
|
+
const titleId = useId();
|
|
21
|
+
const form = useForm();
|
|
22
|
+
// The form is drawn on the first open and kept: what was typed, and the labels a server list loaded, stay.
|
|
23
|
+
const { panelRef, shown, onToggle } = usePopoverPanel(anchor, ref, () => form.focus());
|
|
24
|
+
const pending = useRef(undefined);
|
|
25
|
+
const count = value ? Object.keys(value).length : 0;
|
|
26
|
+
const apply = () => {
|
|
27
|
+
clearTimeout(pending.current);
|
|
28
|
+
pending.current = undefined;
|
|
29
|
+
onChange(form.getValue());
|
|
30
|
+
};
|
|
31
|
+
const wait = () => {
|
|
32
|
+
clearTimeout(pending.current);
|
|
33
|
+
pending.current = setTimeout(apply, delay);
|
|
34
|
+
};
|
|
35
|
+
const flush = () => pending.current !== undefined && apply();
|
|
36
|
+
// Filters changed from outside reach the form, unless a typed value is still on its way.
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (form.started && pending.current === undefined && JSON.stringify(activeFilters(inputs, form.getValue())) !== JSON.stringify(value))
|
|
39
|
+
form.load(value ?? {});
|
|
40
|
+
}, [form, inputs, value]);
|
|
41
|
+
useEffect(() => () => clearTimeout(pending.current), []);
|
|
42
|
+
return (_jsxs("div", { ref: panelRef, popover: "auto", role: "dialog", tabIndex: -1, "aria-labelledby": titleId, className: "nx-popover nx-table-panel nx-table-filters", onToggle: onToggle, onBeforeToggle: event => {
|
|
43
|
+
// Before the click that closes it lands: a clear that click makes then comes after what was typed.
|
|
44
|
+
if (event.target === event.currentTarget && event.newState === "closed")
|
|
45
|
+
flush();
|
|
46
|
+
}, onKeyDownCapture: event => {
|
|
47
|
+
// Kept from the inputs, whose Esc clears a value first: with no list or calendar open, Esc closes the panel.
|
|
48
|
+
if (event.key === "Escape" && !event.currentTarget.querySelector(":popover-open"))
|
|
49
|
+
event.stopPropagation();
|
|
50
|
+
}, onKeyDown: event => {
|
|
51
|
+
if (event.key === "Enter")
|
|
52
|
+
flush();
|
|
53
|
+
}, onInput: () => {
|
|
54
|
+
// A key that changes nothing the box reads ("2025-01-01 -") is typing too: a waiting value waits on.
|
|
55
|
+
if (pending.current !== undefined)
|
|
56
|
+
wait();
|
|
57
|
+
}, children: [_jsxs("div", { className: "nx-table-panel__head", children: [_jsx("span", { className: "nx-table-panel__title", id: titleId, children: "Filters" }), _jsx("span", { className: "nx-table-panel__count", children: count ? `${count} on` : "None" })] }), _jsx("div", { className: "nx-popover__scroll", children: shown && (_jsx(Form, { form: form, inputs: inputs, size: "sm", defaultValue: value, className: "nx-table-filters__form", onValueChange: (_, name) => {
|
|
58
|
+
if (name === null)
|
|
59
|
+
return;
|
|
60
|
+
if (delay > 0 && TYPED.has(inputs.find(params => params.name === name)?.type ?? ""))
|
|
61
|
+
wait();
|
|
62
|
+
else
|
|
63
|
+
apply();
|
|
64
|
+
} })) }), _jsxs("div", { className: "nx-table-panel__foot", children: [_jsxs("span", { className: "nx-table-filters__rows", "aria-live": "polite", children: [formatCount(matches), " of ", formatCount(total), " rows"] }), _jsx(Button, { size: "sm", variant: "ghost", icon: _jsx(InlineIcon, { icon: iconFilterOff }), disabled: !count, onClick: () => {
|
|
65
|
+
form.load({});
|
|
66
|
+
apply();
|
|
67
|
+
}, children: "Clear all" })] })] }));
|
|
68
|
+
}
|
|
@@ -127,8 +127,8 @@ export declare function TablePageSize({ pageSize, pageSizes, compact, onChange }
|
|
|
127
127
|
onChange: (size: number) => void;
|
|
128
128
|
}): import("react").JSX.Element | null;
|
|
129
129
|
/**
|
|
130
|
-
* Under the table: which rows show and how many the search left ("Showing 41–50 of 234 · filtered
|
|
131
|
-
* of how many, and the pager.
|
|
130
|
+
* Under the table: which rows show and how many the search and the filters left ("Showing 41–50 of 234 · filtered
|
|
131
|
+
* from 1,000"), the page of how many, and the pager.
|
|
132
132
|
*/
|
|
133
133
|
export declare function TableFooter<TRow extends TableRow>({ state, disabled, onPage }: {
|
|
134
134
|
state: TableState<TRow>;
|
|
@@ -143,8 +143,8 @@ export function TablePageSize({ pageSize, pageSizes, compact = false, onChange }
|
|
|
143
143
|
return (_jsx(Menu, { items: items, kind: "radio", size: "sm", align: compact ? "start" : "end", "aria-label": "Rows per page", onSelect: value => onChange(Number(value)), children: _jsx(Button, { variant: "ghost", size: "sm", iconEnd: _jsx(InlineIcon, { icon: iconChevronDown }), className: cx("nx-table__page-size", compact && "nx-table__page-size--field"), "aria-label": `Rows per page: ${pageSize}`, title: "Rows per page", children: compact ? formatCount(pageSize) : `${formatCount(pageSize)} / page` }) }));
|
|
144
144
|
}
|
|
145
145
|
/**
|
|
146
|
-
* Under the table: which rows show and how many the search left ("Showing 41–50 of 234 · filtered
|
|
147
|
-
* of how many, and the pager.
|
|
146
|
+
* Under the table: which rows show and how many the search and the filters left ("Showing 41–50 of 234 · filtered
|
|
147
|
+
* from 1,000"), the page of how many, and the pager.
|
|
148
148
|
*/
|
|
149
149
|
export function TableFooter({ state, disabled, onPage }) {
|
|
150
150
|
const { status, rows, offset, matches, total, query } = state;
|
|
@@ -153,5 +153,5 @@ export function TableFooter({ state, disabled, onPage }) {
|
|
|
153
153
|
const first = rows.length ? offset + 1 : 0;
|
|
154
154
|
const last = offset + rows.length;
|
|
155
155
|
const pages = pageCount(matches, query.pageSize);
|
|
156
|
-
return (_jsxs("div", { className: "nx-table__footer", children: [_jsxs("p", { className: "nx-table__range", "aria-live": "polite", children: [rows.length ? (_jsxs(_Fragment, { children: ["Showing ", _jsx("b", { children: `${formatCount(first)}–${formatCount(last)}` }), " of ", _jsx("b", { children: formatCount(matches) })] })) : (_jsx(_Fragment, { children: "No rows on this page" })), query.search && matches !== total && _jsxs("span", { className: "nx-table__range-all", children: [" \u00B7 filtered from ", formatCount(total)] })] }), _jsxs("div", { className: "nx-table__paging", children: [_jsxs("span", { className: "nx-table__page-info", children: ["Page ", formatCount(query.page), " of ", formatCount(pages)] }), _jsx(TablePager, { page: query.page, matches: matches, pageSize: query.pageSize, disabled: disabled, onPage: onPage })] })] }));
|
|
156
|
+
return (_jsxs("div", { className: "nx-table__footer", children: [_jsxs("p", { className: "nx-table__range", "aria-live": "polite", children: [rows.length ? (_jsxs(_Fragment, { children: ["Showing ", _jsx("b", { children: `${formatCount(first)}–${formatCount(last)}` }), " of ", _jsx("b", { children: formatCount(matches) })] })) : (_jsx(_Fragment, { children: "No rows on this page" })), (query.search || query.filters) && matches !== total && _jsxs("span", { className: "nx-table__range-all", children: [" \u00B7 filtered from ", formatCount(total)] })] }), _jsxs("div", { className: "nx-table__paging", children: [_jsxs("span", { className: "nx-table__page-info", children: ["Page ", formatCount(query.page), " of ", formatCount(pages)] }), _jsx(TablePager, { page: query.page, matches: matches, pageSize: query.pageSize, disabled: disabled, onPage: onPage })] })] }));
|
|
157
157
|
}
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether things may move, as the stylesheets read it: `data-nx-motion` on `<html>`, which `ThemeScript` works out
|
|
3
|
+
* from the visitor's Animations choice and the system setting. Motion started from script (a flight, a smooth scroll)
|
|
4
|
+
* asks this, never `prefers-reduced-motion`, so "on" animates on a machine that reports reduced motion.
|
|
5
|
+
*/
|
|
6
|
+
export declare function motionAllowed(): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Whether a scroll may be smooth: the visitor allows motion, and so does the system, because a browser whose system
|
|
9
|
+
* turned animations off scrolls at once whatever it is asked, and a smooth scroll that is skipped must not be waited on.
|
|
10
|
+
*/
|
|
11
|
+
export declare function smoothScrollAllowed(): boolean;
|
|
1
12
|
/**
|
|
2
13
|
* Calls `done` once the element's CSS animation `name` has finished, or at once when the element runs no animation of
|
|
3
14
|
* that name. Call it after the class that starts the animation is on the element, e.g. in a layout effect.
|
|
@@ -1,8 +1,26 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Exit animations. A closing element gets a class that starts a CSS animation, and the next step (closing a dialog,
|
|
3
|
-
* removing a toast) waits for it. When no animation runs, the step must not wait: with
|
|
4
|
-
* reports when its animations are turned off) or when
|
|
3
|
+
* removing a toast) waits for it. When no animation runs, the step must not wait: with animations off (the visitor's
|
|
4
|
+
* choice, or the system's under "system" - Windows reports reduced motion when its animations are turned off) or when
|
|
5
|
+
* a theme sets `animation: none`.
|
|
5
6
|
*/
|
|
7
|
+
import { MOTION_ATTRIBUTE } from "../Interfaces/ThemeInterfaces.js";
|
|
8
|
+
import { REDUCED_MOTION_QUERY } from "./ThemeHelpers.js";
|
|
9
|
+
/**
|
|
10
|
+
* Whether things may move, as the stylesheets read it: `data-nx-motion` on `<html>`, which `ThemeScript` works out
|
|
11
|
+
* from the visitor's Animations choice and the system setting. Motion started from script (a flight, a smooth scroll)
|
|
12
|
+
* asks this, never `prefers-reduced-motion`, so "on" animates on a machine that reports reduced motion.
|
|
13
|
+
*/
|
|
14
|
+
export function motionAllowed() {
|
|
15
|
+
return typeof document === "undefined" || document.documentElement.getAttribute(MOTION_ATTRIBUTE) !== "reduce";
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Whether a scroll may be smooth: the visitor allows motion, and so does the system, because a browser whose system
|
|
19
|
+
* turned animations off scrolls at once whatever it is asked, and a smooth scroll that is skipped must not be waited on.
|
|
20
|
+
*/
|
|
21
|
+
export function smoothScrollAllowed() {
|
|
22
|
+
return motionAllowed() && !(typeof window !== "undefined" && window.matchMedia?.(REDUCED_MOTION_QUERY).matches);
|
|
23
|
+
}
|
|
6
24
|
/**
|
|
7
25
|
* Calls `done` once the element's CSS animation `name` has finished, or at once when the element runs no animation of
|
|
8
26
|
* that name. Call it after the class that starts the animation is on the element, e.g. in a layout effect.
|
|
@@ -2,8 +2,11 @@ import type { CrudMassActionName, CrudMode } from "../Interfaces/CrudInterfaces.
|
|
|
2
2
|
import type { TableKey, TablePage, TableQuery, TableRow } from "../Interfaces/TableInterfaces.ts";
|
|
3
3
|
type Row = Record<string, unknown>;
|
|
4
4
|
export declare abstract class CrudBackend {
|
|
5
|
-
/**
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* What a server table sends for one page, from the query. `sortKey` is the sorted column's server name. `filters` are
|
|
7
|
+
* the active filters already in the server's names (`filterRequest`); `query.filters` keeps the raw values.
|
|
8
|
+
*/
|
|
9
|
+
abstract pageRequest(query: TableQuery, sortKey: string | undefined, filters?: Readonly<Record<string, unknown>>): unknown;
|
|
7
10
|
/** Reads the page a pagination endpoint answered. */
|
|
8
11
|
abstract readPage<TRow extends TableRow>(result: unknown): TablePage<TRow>;
|
|
9
12
|
/** Reads a whole list: an array, or the rows of a page. */
|
|
@@ -60,12 +63,12 @@ export declare const NEXUS_TRASH_STATUS = 40;
|
|
|
60
63
|
/** The Nexus record status of a deleted record (`ERecordStatus.PermanentDeleted`), the bottom of the 60+ delete band. */
|
|
61
64
|
export declare const NEXUS_DELETED_STATUS = 60;
|
|
62
65
|
/**
|
|
63
|
-
* The Nexus backend: pages as `{ searchKeyword, skipCount, maxCount, sorting, sortType }` answered with
|
|
66
|
+
* The Nexus backend: pages as `{ ...filters, searchKeyword, skipCount, maxCount, sorting, sortType }` answered with
|
|
64
67
|
* `{ results, totalCount, filterCount }`, newest first unless a column is sorted; mass actions send the ids
|
|
65
68
|
* (`{ description, accountIds }` for descriptions); a flagged record has `recordStatus` 25, a pinned one `isPinned`.
|
|
66
69
|
*/
|
|
67
70
|
export declare class NexusCrudBackend extends CrudBackend {
|
|
68
|
-
pageRequest(query: TableQuery, sortKey: string | undefined): unknown;
|
|
71
|
+
pageRequest(query: TableQuery, sortKey: string | undefined, filters?: Readonly<Record<string, unknown>>): unknown;
|
|
69
72
|
readPage<TRow extends TableRow>(result: unknown): TablePage<TRow>;
|
|
70
73
|
massRequest(action: CrudMassActionName, keys: TableKey[], values?: Row): unknown;
|
|
71
74
|
isFlagged(row: TableRow): boolean;
|
|
@@ -86,13 +86,15 @@ export const NEXUS_DELETED_STATUS = 60;
|
|
|
86
86
|
/** Just past the flag band: a flagged record is 25–29, so 30 and up is live but not flagged. */
|
|
87
87
|
const NEXUS_FLAG_BAND_END = 30;
|
|
88
88
|
/**
|
|
89
|
-
* The Nexus backend: pages as `{ searchKeyword, skipCount, maxCount, sorting, sortType }` answered with
|
|
89
|
+
* The Nexus backend: pages as `{ ...filters, searchKeyword, skipCount, maxCount, sorting, sortType }` answered with
|
|
90
90
|
* `{ results, totalCount, filterCount }`, newest first unless a column is sorted; mass actions send the ids
|
|
91
91
|
* (`{ description, accountIds }` for descriptions); a flagged record has `recordStatus` 25, a pinned one `isPinned`.
|
|
92
92
|
*/
|
|
93
93
|
export class NexusCrudBackend extends CrudBackend {
|
|
94
|
-
pageRequest(query, sortKey) {
|
|
94
|
+
pageRequest(query, sortKey, filters) {
|
|
95
95
|
return {
|
|
96
|
+
// Flat and first, so a filter never overwrites a paging field.
|
|
97
|
+
...filters,
|
|
96
98
|
searchKeyword: query.search,
|
|
97
99
|
skipCount: (query.page - 1) * query.pageSize,
|
|
98
100
|
maxCount: query.pageSize,
|
|
@@ -43,8 +43,8 @@ export interface ExcelDownloadHeaders {
|
|
|
43
43
|
export declare abstract class ExcelBackend {
|
|
44
44
|
/** The form field an upload is sent under. */
|
|
45
45
|
abstract get uploadField(): string;
|
|
46
|
-
/** What an import sends: the rows the verify answered with, as the person left them. */
|
|
47
|
-
abstract importRequest(rows: readonly ExcelSheetRow[]): unknown;
|
|
46
|
+
/** What an import sends: the rows the verify answered with, as the person left them, and the status they start in. */
|
|
47
|
+
abstract importRequest(rows: readonly ExcelSheetRow[], recordStatus?: number): unknown;
|
|
48
48
|
/** What an export sends: the search, the sort and the filters the person has on screen. */
|
|
49
49
|
abstract exportRequest(query: ExcelExportQuery): unknown;
|
|
50
50
|
/** Reads what the settings endpoint answered. */
|
|
@@ -67,7 +67,7 @@ export declare abstract class ExcelBackend {
|
|
|
67
67
|
*/
|
|
68
68
|
export declare class NexusExcelBackend extends ExcelBackend {
|
|
69
69
|
get uploadField(): string;
|
|
70
|
-
importRequest(rows: readonly ExcelSheetRow[]): unknown;
|
|
70
|
+
importRequest(rows: readonly ExcelSheetRow[], recordStatus?: number): unknown;
|
|
71
71
|
/**
|
|
72
72
|
* The same request a Nexus listing takes, minus `skipCount` and `maxCount`: an export writes every row the search
|
|
73
73
|
* leaves, up to the backend's own limit, not the page the person happens to be on. With nothing sorted it follows
|
|
@@ -53,8 +53,8 @@ export class NexusExcelBackend extends ExcelBackend {
|
|
|
53
53
|
get uploadField() {
|
|
54
54
|
return "file";
|
|
55
55
|
}
|
|
56
|
-
importRequest(rows) {
|
|
57
|
-
return { rows };
|
|
56
|
+
importRequest(rows, recordStatus) {
|
|
57
|
+
return recordStatus === undefined ? { rows } : { rows, recordStatus };
|
|
58
58
|
}
|
|
59
59
|
/**
|
|
60
60
|
* The same request a Nexus listing takes, minus `skipCount` and `maxCount`: an export writes every row the search
|
|
@@ -79,6 +79,10 @@ export class NexusExcelBackend extends ExcelBackend {
|
|
|
79
79
|
maxImportRows: count(answer.maxImportRows, DEFAULT_MAX_IMPORT_ROWS),
|
|
80
80
|
columns: readColumns(answer.columns),
|
|
81
81
|
inputs: Array.isArray(answer.inputs) ? answer.inputs : [],
|
|
82
|
+
recordStatuses: Array.isArray(answer.recordStatuses)
|
|
83
|
+
? answer.recordStatuses.filter(isRecord).flatMap(status => (typeof status.value === "number" && typeof status.label === "string" ? [{ value: status.value, label: status.label }] : []))
|
|
84
|
+
: [],
|
|
85
|
+
defaultRecordStatus: typeof answer.defaultRecordStatus === "number" ? answer.defaultRecordStatus : undefined,
|
|
82
86
|
};
|
|
83
87
|
}
|
|
84
88
|
readVerify(result) {
|
|
@@ -233,6 +237,8 @@ export function readExcelResponse(body, status) {
|
|
|
233
237
|
messageCode: text(first.messageCode) ?? undefined,
|
|
234
238
|
errorCode: first.errorCode === undefined ? undefined : count(first.errorCode),
|
|
235
239
|
...(Object.keys(errors).length ? { errors } : {}),
|
|
240
|
+
// An import that wrote nothing still says, row by row, what it refused and why (XLS_X044): keep that report.
|
|
241
|
+
...(isRecord(body.result) ? { result: body.result } : {}),
|
|
236
242
|
};
|
|
237
243
|
}
|
|
238
244
|
}
|
|
@@ -8,11 +8,14 @@ export declare const EXCEL_ROW_NUMBER = "__excelRow";
|
|
|
8
8
|
export declare const EXCEL_ROW_KEYS = "__excelKeys";
|
|
9
9
|
/** The field of an editor row that holds what the verify answered for it, so an edit can be told from an untouched row. */
|
|
10
10
|
export declare const EXCEL_ROW_SENT = "__excelSent";
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* One row of a review editor: a value per column, plus the three fields the editor carries for itself. A row added in
|
|
13
|
+
* the browser - by Add, Insert or Duplicate, or every row of a bulk insert - has none of the three, or only some.
|
|
14
|
+
*/
|
|
12
15
|
export interface ExcelEditorRow {
|
|
13
|
-
[EXCEL_ROW_NUMBER]
|
|
14
|
-
[EXCEL_ROW_KEYS]
|
|
15
|
-
[EXCEL_ROW_SENT]
|
|
16
|
+
[EXCEL_ROW_NUMBER]?: number;
|
|
17
|
+
[EXCEL_ROW_KEYS]?: Record<string, string | null>;
|
|
18
|
+
[EXCEL_ROW_SENT]?: Record<string, string | null>;
|
|
16
19
|
[field: string]: unknown;
|
|
17
20
|
}
|
|
18
21
|
declare const DEFAULT_TEXT: {
|
|
@@ -25,7 +28,15 @@ declare const DEFAULT_TEXT: {
|
|
|
25
28
|
readonly verifyFailed: "The file could not be read.";
|
|
26
29
|
readonly newRows: "New rows";
|
|
27
30
|
readonly duplicates: "Already there";
|
|
28
|
-
readonly duplicateHint: "
|
|
31
|
+
readonly duplicateHint: "Already records: these are skipped, and nothing here is updated.";
|
|
32
|
+
readonly failedHead: "Not written";
|
|
33
|
+
readonly skippedHead: "Skipped: already there";
|
|
34
|
+
readonly importAgain: "To import another file, start over.";
|
|
35
|
+
readonly addAgain: "To add more, start over.";
|
|
36
|
+
readonly bulkUnavailable: "The sheet's columns could not be read, so there is nothing to type the rows into.";
|
|
37
|
+
readonly discardTitle: "Discard what you typed?";
|
|
38
|
+
readonly discardLabel: "Discard";
|
|
39
|
+
readonly keepEditing: "Keep editing";
|
|
29
40
|
readonly toFix: "What needs fixing";
|
|
30
41
|
readonly whyDuplicate: "Why these are already there";
|
|
31
42
|
readonly referenceLocked: "Picked in the spreadsheet: this list belongs to the server.";
|
|
@@ -66,18 +77,35 @@ export declare function isExcelColumnEditable(column: ExcelColumnInfo): boolean;
|
|
|
66
77
|
* A reference cell shows the **words** the person picked, because its key is not something anybody reads or types.
|
|
67
78
|
*/
|
|
68
79
|
export declare function excelEditorRows(rows: readonly ExcelReviewRow[], columns: readonly ExcelColumnInfo[]): ExcelEditorRow[];
|
|
80
|
+
/** The sheet row a row came from, or 0 for a row added in the browser. */
|
|
81
|
+
export declare function excelRowNumber(row: ExcelEditorRow): number;
|
|
69
82
|
/**
|
|
70
83
|
* The editors' rows as rows to send: every value as text, every reference as the key it came with, and the sheet's own
|
|
71
84
|
* row number kept so a problem can still name where it is.
|
|
72
85
|
*
|
|
73
86
|
* A cell the person did not touch goes back **exactly** as it came, which is what makes the round trip faithful: the
|
|
74
87
|
* backend reads its own text again, not a number an input rewrote on the way through.
|
|
88
|
+
*
|
|
89
|
+
* A row added in the browser has no sheet row, so it is numbered after the last one the sheet had: no two rows share a
|
|
90
|
+
* number, and a problem still names the row it is about. `after` is that last row - the file's, rows already there and
|
|
91
|
+
* rows taken out of the review included, which are not among the rows sent. A bulk insert, with no sheet at all,
|
|
92
|
+
* numbers its rows 1, 2, 3.
|
|
75
93
|
*/
|
|
76
|
-
export declare function excelSheetRows(rows: readonly ExcelEditorRow[], columns: readonly ExcelColumnInfo[]): ExcelSheetRow[];
|
|
94
|
+
export declare function excelSheetRows(rows: readonly ExcelEditorRow[], columns: readonly ExcelColumnInfo[], after?: number): ExcelSheetRow[];
|
|
77
95
|
/** Whether the person changed anything in a row since the verify answered it. */
|
|
78
96
|
export declare function isExcelRowEdited(row: ExcelEditorRow, columns: readonly ExcelColumnInfo[]): boolean;
|
|
79
|
-
/**
|
|
80
|
-
export declare function
|
|
97
|
+
/** Whether a row has anything in a cell the person types in. A row added and left empty has not, and is not sent. */
|
|
98
|
+
export declare function hasExcelValues(row: ExcelEditorRow, columns: readonly ExcelColumnInfo[]): boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Whether a row holds something only this screen has, which closing it would lose: a sheet's row the person edited, or
|
|
101
|
+
* a row added here with anything in it.
|
|
102
|
+
*/
|
|
103
|
+
export declare function isExcelRowTyped(row: ExcelEditorRow, columns: readonly ExcelColumnInfo[]): boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Where a problem is, as the person reads it: the cell, a middle dot, and the column's heading ("B7 - City name"). Rows
|
|
106
|
+
* typed in the browser have no sheet, so `byRow` names the row instead ("Row 3 - City name").
|
|
107
|
+
*/
|
|
108
|
+
export declare function excelProblemPlace(problem: ExcelCellProblem, byRow?: boolean): string;
|
|
81
109
|
/** Every problem of these rows, in the order the rows read. */
|
|
82
110
|
export declare function excelProblems(rows: readonly ExcelReviewRow[]): ExcelCellProblem[];
|
|
83
111
|
/** The rows of one kind. */
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { ExcelDataType, ExcelRowKind } from "../Interfaces/ExcelInterfaces.js";
|
|
2
|
+
import { toIsoDate } from "./DateHelpers.js";
|
|
3
|
+
import { toIsoTime } from "./TimeHelpers.js";
|
|
2
4
|
/*
|
|
3
5
|
* Turning a sheet's columns into inputs, a verify's rows into editable rows and back, and handing the person a file.
|
|
4
6
|
* Nothing here calls anything: the calls are in Services/ExcelApi.ts.
|
|
@@ -28,7 +30,15 @@ const DEFAULT_TEXT = {
|
|
|
28
30
|
verifyFailed: "The file could not be read.",
|
|
29
31
|
newRows: "New rows",
|
|
30
32
|
duplicates: "Already there",
|
|
31
|
-
duplicateHint: "
|
|
33
|
+
duplicateHint: "Already records: these are skipped, and nothing here is updated.",
|
|
34
|
+
failedHead: "Not written",
|
|
35
|
+
skippedHead: "Skipped: already there",
|
|
36
|
+
importAgain: "To import another file, start over.",
|
|
37
|
+
addAgain: "To add more, start over.",
|
|
38
|
+
bulkUnavailable: "The sheet's columns could not be read, so there is nothing to type the rows into.",
|
|
39
|
+
discardTitle: "Discard what you typed?",
|
|
40
|
+
discardLabel: "Discard",
|
|
41
|
+
keepEditing: "Keep editing",
|
|
32
42
|
toFix: "What needs fixing",
|
|
33
43
|
whyDuplicate: "Why these are already there",
|
|
34
44
|
referenceLocked: "Picked in the spreadsheet: this list belongs to the server.",
|
|
@@ -159,12 +169,15 @@ export function excelEditorRows(rows, columns) {
|
|
|
159
169
|
/** Which types are drawn by a number box, whose empty value is `null`. */
|
|
160
170
|
const NUMBER_TYPES = new Set([ExcelDataType.WholeNumber, ExcelDataType.Decimal, ExcelDataType.Amount]);
|
|
161
171
|
/**
|
|
162
|
-
* A cell's text as the input that draws it wants it: a number as a number, a choice unset rather than empty,
|
|
163
|
-
* everything else as it reads.
|
|
172
|
+
* A cell's text as the input that draws it wants it: a number as a number, a choice unset rather than empty, a date
|
|
173
|
+
* and a time as the inputs hold them, and everything else as it reads.
|
|
164
174
|
*
|
|
165
175
|
* The three empties are not interchangeable. A number box is empty at `null`; a **dropdown** given `""` treats it as a
|
|
166
176
|
* value and reports it as one its list no longer offers ("Not found"), so an empty choice has to be left unset; a text
|
|
167
177
|
* or date box is empty at `""`.
|
|
178
|
+
*
|
|
179
|
+
* A date and a time are read the way their inputs read them: the sheet's "09:30:00" is the time box's "09:30". Read
|
|
180
|
+
* any other way, a cell nobody touched would compare as an edit - and the panel would ask before closing over it.
|
|
168
181
|
*/
|
|
169
182
|
function toInputValue(column, value) {
|
|
170
183
|
if (value === null || value === "")
|
|
@@ -184,6 +197,10 @@ function toInputValue(column, value) {
|
|
|
184
197
|
return "No";
|
|
185
198
|
return value;
|
|
186
199
|
}
|
|
200
|
+
case ExcelDataType.Date:
|
|
201
|
+
return toIsoDate(value) ?? value;
|
|
202
|
+
case ExcelDataType.Time:
|
|
203
|
+
return toIsoTime(value) ?? value;
|
|
187
204
|
default:
|
|
188
205
|
return value;
|
|
189
206
|
}
|
|
@@ -210,17 +227,28 @@ function isSameCell(column, current, sent) {
|
|
|
210
227
|
return true;
|
|
211
228
|
return toInputValue(column, asSheet) === toInputValue(column, sent ?? null);
|
|
212
229
|
}
|
|
230
|
+
/** The sheet row a row came from, or 0 for a row added in the browser. */
|
|
231
|
+
export function excelRowNumber(row) {
|
|
232
|
+
const number = Number(row[EXCEL_ROW_NUMBER]);
|
|
233
|
+
return Number.isFinite(number) && number > 0 ? number : 0;
|
|
234
|
+
}
|
|
213
235
|
/**
|
|
214
236
|
* The editors' rows as rows to send: every value as text, every reference as the key it came with, and the sheet's own
|
|
215
237
|
* row number kept so a problem can still name where it is.
|
|
216
238
|
*
|
|
217
239
|
* A cell the person did not touch goes back **exactly** as it came, which is what makes the round trip faithful: the
|
|
218
240
|
* backend reads its own text again, not a number an input rewrote on the way through.
|
|
241
|
+
*
|
|
242
|
+
* A row added in the browser has no sheet row, so it is numbered after the last one the sheet had: no two rows share a
|
|
243
|
+
* number, and a problem still names the row it is about. `after` is that last row - the file's, rows already there and
|
|
244
|
+
* rows taken out of the review included, which are not among the rows sent. A bulk insert, with no sheet at all,
|
|
245
|
+
* numbers its rows 1, 2, 3.
|
|
219
246
|
*/
|
|
220
|
-
export function excelSheetRows(rows, columns) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const
|
|
247
|
+
export function excelSheetRows(rows, columns, after = 0) {
|
|
248
|
+
let next = rows.reduce((last, row) => Math.max(last, excelRowNumber(row)), Math.max(0, after));
|
|
249
|
+
return rows.map(row => {
|
|
250
|
+
const keys = row[EXCEL_ROW_KEYS] ?? {};
|
|
251
|
+
const sent = row[EXCEL_ROW_SENT] ?? {};
|
|
224
252
|
const values = {};
|
|
225
253
|
const labels = {};
|
|
226
254
|
for (const column of columns) {
|
|
@@ -231,13 +259,12 @@ export function excelSheetRows(rows, columns) {
|
|
|
231
259
|
}
|
|
232
260
|
values[column.field] = isSameCell(column, row[column.field], sent[column.field]) ? (sent[column.field] ?? null) : toSheetValue(row[column.field]);
|
|
233
261
|
}
|
|
234
|
-
|
|
235
|
-
return { number: Number.isFinite(number) && number > 0 ? number : index + 1, values, labels };
|
|
262
|
+
return { number: excelRowNumber(row) || ++next, values, labels };
|
|
236
263
|
});
|
|
237
264
|
}
|
|
238
265
|
/** Whether the person changed anything in a row since the verify answered it. */
|
|
239
266
|
export function isExcelRowEdited(row, columns) {
|
|
240
|
-
const sent =
|
|
267
|
+
const sent = row[EXCEL_ROW_SENT] ?? {};
|
|
241
268
|
for (const column of columns) {
|
|
242
269
|
if (!isExcelColumnEditable(column))
|
|
243
270
|
continue;
|
|
@@ -246,10 +273,25 @@ export function isExcelRowEdited(row, columns) {
|
|
|
246
273
|
}
|
|
247
274
|
return false;
|
|
248
275
|
}
|
|
276
|
+
/** Whether a row has anything in a cell the person types in. A row added and left empty has not, and is not sent. */
|
|
277
|
+
export function hasExcelValues(row, columns) {
|
|
278
|
+
return columns.some(column => isExcelColumnEditable(column) && toSheetValue(row[column.field]) !== null);
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Whether a row holds something only this screen has, which closing it would lose: a sheet's row the person edited, or
|
|
282
|
+
* a row added here with anything in it.
|
|
283
|
+
*/
|
|
284
|
+
export function isExcelRowTyped(row, columns) {
|
|
285
|
+
return excelRowNumber(row) > 0 ? isExcelRowEdited(row, columns) : hasExcelValues(row, columns);
|
|
286
|
+
}
|
|
249
287
|
/* What is wrong, in the person's words */
|
|
250
|
-
/**
|
|
251
|
-
|
|
252
|
-
|
|
288
|
+
/**
|
|
289
|
+
* Where a problem is, as the person reads it: the cell, a middle dot, and the column's heading ("B7 - City name"). Rows
|
|
290
|
+
* typed in the browser have no sheet, so `byRow` names the row instead ("Row 3 - City name").
|
|
291
|
+
*/
|
|
292
|
+
export function excelProblemPlace(problem, byRow = false) {
|
|
293
|
+
const place = byRow ? `Row ${problem.row}` : problem.cell;
|
|
294
|
+
return place ? `${place} ${String.fromCharCode(183)} ${problem.header}` : problem.header;
|
|
253
295
|
}
|
|
254
296
|
/** Every problem of these rows, in the order the rows read. */
|
|
255
297
|
export function excelProblems(rows) {
|
|
@@ -25,7 +25,7 @@ export interface FormStatus {
|
|
|
25
25
|
changed: boolean;
|
|
26
26
|
/** A submit is running. */
|
|
27
27
|
submitting: boolean;
|
|
28
|
-
/** The values are loading: the inputs show
|
|
28
|
+
/** The values are loading: the inputs show, disabled, and take nothing typed until the values are in. */
|
|
29
29
|
loading: boolean;
|
|
30
30
|
/** A submit ran and found problems: every message shows. */
|
|
31
31
|
showAll: boolean;
|
|
@@ -85,6 +85,8 @@ export declare class FormStore<TValues extends FormValues = FormValues> {
|
|
|
85
85
|
private draftSeq;
|
|
86
86
|
/** The user cancelled: no draft is written until they change a value again. */
|
|
87
87
|
private draftDropped;
|
|
88
|
+
/** An input reported a value while the values loaded and was refused: it is drawn again from the store when loading ends. */
|
|
89
|
+
private refusedWhileLoading;
|
|
88
90
|
/** A load during a render tells nobody; `announce` does, once the form has mounted. */
|
|
89
91
|
private quiet;
|
|
90
92
|
private unannounced;
|
|
@@ -137,13 +139,15 @@ export declare class FormStore<TValues extends FormValues = FormValues> {
|
|
|
137
139
|
* Loads values, such as a record from a server. They become the values the form compares with, and every input shows
|
|
138
140
|
* them. With `keepChanged`, the values the user changed stay (a refresh that merges): only the others change, and the
|
|
139
141
|
* names returned are the changed values whose loaded ones also changed, so the page can say so. `draft: false` leaves
|
|
140
|
-
* a saved draft for a later load (the form
|
|
141
|
-
* until `announce()`, for a load during a render.
|
|
142
|
+
* a saved draft for a later load (the form shows disabled while the real values load). `quiet` tells no listener
|
|
143
|
+
* until `announce()`, for a load during a render. `loading` opens the form loading, so whatever reads its status in
|
|
144
|
+
* the same render — a `FormButtons` after it — draws its loading state from the first frame, on the server too.
|
|
142
145
|
*/
|
|
143
|
-
load(values?: Partial<TValues>, { keepChanged, draft, quiet }?: {
|
|
146
|
+
load(values?: Partial<TValues>, { keepChanged, draft, quiet, loading }?: {
|
|
144
147
|
keepChanged?: boolean;
|
|
145
148
|
draft?: boolean;
|
|
146
149
|
quiet?: boolean;
|
|
150
|
+
loading?: boolean;
|
|
147
151
|
}): string[];
|
|
148
152
|
/** Tells listeners about a quiet load. */
|
|
149
153
|
announce(): void;
|
|
@@ -197,7 +201,7 @@ export declare class FormStore<TValues extends FormValues = FormValues> {
|
|
|
197
201
|
commit(saved: TValues, extra?: Partial<TValues>): void;
|
|
198
202
|
/** @internal A save after which the form went back to its opening values (`resetOnSuccess`). */
|
|
199
203
|
markSaved(): void;
|
|
200
|
-
/** @internal */
|
|
204
|
+
/** @internal While the values load, nothing typed is taken (`input`), and a submit waits (`SubmitForm`). */
|
|
201
205
|
setLoading(loading: boolean): void;
|
|
202
206
|
/** @internal */
|
|
203
207
|
setFormId(formId: string | null): void;
|
|
@@ -70,6 +70,8 @@ export class FormStore {
|
|
|
70
70
|
draftSeq = 0;
|
|
71
71
|
/** The user cancelled: no draft is written until they change a value again. */
|
|
72
72
|
draftDropped = false;
|
|
73
|
+
/** An input reported a value while the values loaded and was refused: it is drawn again from the store when loading ends. */
|
|
74
|
+
refusedWhileLoading = false;
|
|
73
75
|
/** A load during a render tells nobody; `announce` does, once the form has mounted. */
|
|
74
76
|
quiet = false;
|
|
75
77
|
unannounced = false;
|
|
@@ -216,14 +218,18 @@ export class FormStore {
|
|
|
216
218
|
* Loads values, such as a record from a server. They become the values the form compares with, and every input shows
|
|
217
219
|
* them. With `keepChanged`, the values the user changed stay (a refresh that merges): only the others change, and the
|
|
218
220
|
* names returned are the changed values whose loaded ones also changed, so the page can say so. `draft: false` leaves
|
|
219
|
-
* a saved draft for a later load (the form
|
|
220
|
-
* until `announce()`, for a load during a render.
|
|
221
|
+
* a saved draft for a later load (the form shows disabled while the real values load). `quiet` tells no listener
|
|
222
|
+
* until `announce()`, for a load during a render. `loading` opens the form loading, so whatever reads its status in
|
|
223
|
+
* the same render — a `FormButtons` after it — draws its loading state from the first frame, on the server too.
|
|
221
224
|
*/
|
|
222
|
-
load(values = {}, { keepChanged = false, draft = true, quiet = false } = {}) {
|
|
225
|
+
load(values = {}, { keepChanged = false, draft = true, quiet = false, loading } = {}) {
|
|
223
226
|
this.quiet = quiet;
|
|
224
227
|
this.unannounced ||= quiet;
|
|
225
228
|
try {
|
|
226
|
-
|
|
229
|
+
const changed = this.loadValues(values, keepChanged, draft);
|
|
230
|
+
if (loading !== undefined)
|
|
231
|
+
this.patchStatus({ loading });
|
|
232
|
+
return changed;
|
|
227
233
|
}
|
|
228
234
|
finally {
|
|
229
235
|
this.quiet = false;
|
|
@@ -264,8 +270,10 @@ export class FormStore {
|
|
|
264
270
|
this.nextRevision(params.name);
|
|
265
271
|
}
|
|
266
272
|
else {
|
|
273
|
+
// Every input mounts again with its loaded value, one that was typed in while loading included.
|
|
267
274
|
this.generation++;
|
|
268
275
|
this.touched.clear();
|
|
276
|
+
this.refusedWhileLoading = false;
|
|
269
277
|
}
|
|
270
278
|
this.serverErrors.clear();
|
|
271
279
|
this.serverMessage = null;
|
|
@@ -321,6 +329,12 @@ export class FormStore {
|
|
|
321
329
|
input(name, value, stamp) {
|
|
322
330
|
if (stamp !== this.stamp(name))
|
|
323
331
|
return;
|
|
332
|
+
// Nothing is taken while the values load, not even from an input whose `disabled` was taken off by hand: the
|
|
333
|
+
// loaded values would land on top of it, and a change to a record that is not there yet means nothing.
|
|
334
|
+
if (this.status.loading) {
|
|
335
|
+
this.refusedWhileLoading = true;
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
324
338
|
this.change([[name, value]], "input");
|
|
325
339
|
}
|
|
326
340
|
/** Marks an input as left by the user: the form's rule messages on it show from now on. */
|
|
@@ -481,8 +495,14 @@ export class FormStore {
|
|
|
481
495
|
markSaved() {
|
|
482
496
|
this.patchStatus({ submitting: false, savedAt: Date.now() });
|
|
483
497
|
}
|
|
484
|
-
/** @internal */
|
|
498
|
+
/** @internal While the values load, nothing typed is taken (`input`), and a submit waits (`SubmitForm`). */
|
|
485
499
|
setLoading(loading) {
|
|
500
|
+
// Something was typed and refused, and no load has drawn the inputs again since: they show the store's values.
|
|
501
|
+
if (!loading && this.refusedWhileLoading) {
|
|
502
|
+
this.refusedWhileLoading = false;
|
|
503
|
+
this.generation++;
|
|
504
|
+
this.bumpAll();
|
|
505
|
+
}
|
|
486
506
|
this.patchStatus({ loading });
|
|
487
507
|
}
|
|
488
508
|
/** @internal */
|
|
@@ -43,6 +43,7 @@ const DEFAULT_TEXTS = {
|
|
|
43
43
|
confirmDetails: {
|
|
44
44
|
delete: "This cannot be undone.",
|
|
45
45
|
trash: "It can be restored from the trash.",
|
|
46
|
+
recover: "It goes back to the active list.",
|
|
46
47
|
},
|
|
47
48
|
reasons: {
|
|
48
49
|
network: "The server cannot be reached. Check your internet connection and try again.",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { NumberFormatOptions } from "../Interfaces/InputInterfaces.ts";
|
|
2
|
+
import { ENumberScales, type ThousandsSeparator } from "../Interfaces/PreferenceInterfaces.ts";
|
|
2
3
|
/** Default locale for number text. Fixed rather than the browser's, so server-rendered text matches the browser's. */
|
|
3
4
|
export declare const DEFAULT_NUMBER_LOCALE = "en-US";
|
|
4
5
|
/** Most digits a number field takes. A JavaScript number holds 15 significant digits exactly; beyond that, digits change. */
|
|
@@ -15,7 +16,12 @@ export interface NumberTextRules {
|
|
|
15
16
|
/** Allows a leading minus sign. */
|
|
16
17
|
negative: boolean;
|
|
17
18
|
}
|
|
18
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* The locale for a number scale and a thousands separator: `en-US` (1,234,567.5) and `en-IN` (12,34,567.5) as they are,
|
|
21
|
+
* else a key such as "en-US|.|," (1.234.567,5) that `formatNumber`, `parseNumber` and `getNumberSymbols` read.
|
|
22
|
+
*/
|
|
23
|
+
export declare function numberLocale(scale: ENumberScales, thousands: ThousandsSeparator): string;
|
|
24
|
+
/** The decimal and group separators of a locale (or of a key from `numberLocale`). */
|
|
19
25
|
export declare function getNumberSymbols(locale?: string): NumberSymbols;
|
|
20
26
|
/**
|
|
21
27
|
* Formats a number for display: `formatNumber(1250, { decimals: 2, fixedDecimals: true, grouping: true })` gives "1,250.00".
|