nexus-shared 1.1.20 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +903 -0
- package/dist/Client.Index.d.ts +44 -0
- package/dist/Client.Index.js +49 -0
- package/dist/Components/Documents/Button.d.ts +29 -0
- package/dist/Components/Documents/Button.js +11 -0
- package/dist/Components/Documents/Menu.d.ts +102 -0
- package/dist/Components/Documents/Menu.js +238 -0
- package/dist/Components/Documents/SplitButton.d.ts +64 -0
- package/dist/Components/Documents/SplitButton.js +127 -0
- package/dist/Components/Documents/TabButtons.d.ts +62 -0
- package/dist/Components/Documents/TabButtons.js +19 -0
- package/dist/Components/Forms/ApiForm.d.ts +21 -0
- package/dist/Components/Forms/ApiForm.js +99 -0
- package/dist/Components/Forms/Crud.d.ts +17 -0
- package/dist/Components/Forms/Crud.js +556 -0
- package/dist/Components/Forms/CrudParts.d.ts +15 -0
- package/dist/Components/Forms/CrudParts.js +36 -0
- package/dist/Components/Forms/Form.d.ts +52 -0
- package/dist/Components/Forms/Form.js +319 -0
- package/dist/Components/Forms/SubmitForm.d.ts +65 -0
- package/dist/Components/Forms/SubmitForm.js +198 -0
- package/dist/Components/Inputs/Calendar.d.ts +69 -0
- package/dist/Components/Inputs/Calendar.js +258 -0
- package/dist/Components/Inputs/CheckBox.d.ts +53 -0
- package/dist/Components/Inputs/CheckBox.js +87 -0
- package/dist/Components/Inputs/CheckBoxGroup.d.ts +20 -0
- package/dist/Components/Inputs/CheckBoxGroup.js +80 -0
- package/dist/Components/Inputs/ChoiceField.d.ts +122 -0
- package/dist/Components/Inputs/ChoiceField.js +169 -0
- package/dist/Components/Inputs/DatePicker.d.ts +54 -0
- package/dist/Components/Inputs/DatePicker.js +246 -0
- package/dist/Components/Inputs/DateRangePicker.d.ts +62 -0
- package/dist/Components/Inputs/DateRangePicker.js +283 -0
- package/dist/Components/Inputs/DateTimePicker.d.ts +75 -0
- package/dist/Components/Inputs/DateTimePicker.js +399 -0
- package/dist/Components/Inputs/Dropdown.d.ts +117 -0
- package/dist/Components/Inputs/Dropdown.js +839 -0
- package/dist/Components/Inputs/DropdownParts.d.ts +84 -0
- package/dist/Components/Inputs/DropdownParts.js +102 -0
- package/dist/Components/Inputs/EditorShell.d.ts +45 -0
- package/dist/Components/Inputs/EditorShell.js +214 -0
- package/dist/Components/Inputs/Field.d.ts +53 -0
- package/dist/Components/Inputs/Field.js +16 -0
- package/dist/Components/Inputs/GroupForm.d.ts +20 -0
- package/dist/Components/Inputs/GroupForm.js +170 -0
- package/dist/Components/Inputs/InputField.d.ts +59 -0
- package/dist/Components/Inputs/InputField.js +169 -0
- package/dist/Components/Inputs/InputRenderer.d.ts +35 -0
- package/dist/Components/Inputs/InputRenderer.js +99 -0
- package/dist/Components/Inputs/MarkdownEditor.d.ts +67 -0
- package/dist/Components/Inputs/MarkdownEditor.js +289 -0
- package/dist/Components/Inputs/NumberBox.d.ts +53 -0
- package/dist/Components/Inputs/NumberBox.js +228 -0
- package/dist/Components/Inputs/RadioGroup.d.ts +20 -0
- package/dist/Components/Inputs/RadioGroup.js +43 -0
- package/dist/Components/Inputs/ReadOnlyNotice.d.ts +36 -0
- package/dist/Components/Inputs/ReadOnlyNotice.js +116 -0
- package/dist/Components/Inputs/RichTextEditor.d.ts +103 -0
- package/dist/Components/Inputs/RichTextEditor.js +1170 -0
- package/dist/Components/Inputs/RichTextParts.d.ts +63 -0
- package/dist/Components/Inputs/RichTextParts.js +157 -0
- package/dist/Components/Inputs/RowsInput.d.ts +104 -0
- package/dist/Components/Inputs/RowsInput.js +259 -0
- package/dist/Components/Inputs/Slider.d.ts +87 -0
- package/dist/Components/Inputs/Slider.js +216 -0
- package/dist/Components/Inputs/TabularForm.d.ts +23 -0
- package/dist/Components/Inputs/TabularForm.js +283 -0
- package/dist/Components/Inputs/TextArea.d.ts +43 -0
- package/dist/Components/Inputs/TextArea.js +31 -0
- package/dist/Components/Inputs/TextBox.d.ts +52 -0
- package/dist/Components/Inputs/TextBox.js +58 -0
- package/dist/Components/Inputs/TextField.d.ts +31 -0
- package/dist/Components/Inputs/TextField.js +35 -0
- package/dist/Components/Inputs/TimePanel.d.ts +34 -0
- package/dist/Components/Inputs/TimePanel.js +288 -0
- package/dist/Components/Inputs/TimePicker.d.ts +54 -0
- package/dist/Components/Inputs/TimePicker.js +239 -0
- package/dist/Components/Inputs/TreeView.d.ts +160 -0
- package/dist/Components/Inputs/TreeView.js +947 -0
- package/dist/Components/Layouts/MessageBox.d.ts +71 -0
- package/dist/Components/Layouts/MessageBox.js +104 -0
- package/dist/Components/Layouts/Popup.d.ts +92 -0
- package/dist/Components/Layouts/Popup.js +376 -0
- package/dist/Components/Layouts/PopupContext.d.ts +2 -0
- package/dist/Components/Layouts/PopupContext.js +4 -0
- package/dist/Components/Layouts/PopupDock.d.ts +22 -0
- package/dist/Components/Layouts/PopupDock.js +121 -0
- package/dist/Components/Layouts/PopupHost.d.ts +56 -0
- package/dist/Components/Layouts/PopupHost.js +163 -0
- package/dist/Components/Layouts/PopupRules.d.ts +30 -0
- package/dist/Components/Layouts/PopupRules.js +63 -0
- package/dist/Components/Layouts/ThemePicker.d.ts +13 -0
- package/dist/Components/Layouts/ThemePicker.js +22 -0
- package/dist/Components/Layouts/ThemeScript.d.ts +14 -0
- package/dist/Components/Layouts/ThemeScript.js +13 -0
- package/dist/Components/Layouts/ThemeSwitcher.d.ts +13 -0
- package/dist/Components/Layouts/ThemeSwitcher.js +29 -0
- package/dist/Components/Layouts/Toaster.d.ts +76 -0
- package/dist/Components/Layouts/Toaster.js +172 -0
- package/dist/Components/Viewers/DataTable.d.ts +25 -0
- package/dist/Components/Viewers/DataTable.js +542 -0
- package/dist/Components/Viewers/DataTableParts.d.ts +134 -0
- package/dist/Components/Viewers/DataTableParts.js +145 -0
- package/dist/Components/Viewers/MarkdownView.d.ts +26 -0
- package/dist/Components/Viewers/MarkdownView.js +79 -0
- package/dist/Components/Viewers/RichTextView.d.ts +18 -0
- package/dist/Components/Viewers/RichTextView.js +17 -0
- package/dist/Helpers/AnimationHelpers.d.ts +6 -0
- package/dist/Helpers/AnimationHelpers.js +35 -0
- package/dist/Helpers/ApiClient.d.ts +74 -0
- package/dist/Helpers/ApiClient.js +197 -0
- package/dist/Helpers/ApiFormHelpers.d.ts +26 -0
- package/dist/Helpers/ApiFormHelpers.js +56 -0
- package/dist/Helpers/ApiModules.d.ts +135 -0
- package/dist/Helpers/ApiModules.js +194 -0
- package/dist/Helpers/ApiResponses.d.ts +41 -0
- package/dist/Helpers/ApiResponses.js +159 -0
- package/dist/Helpers/ApiRoutes.d.ts +150 -0
- package/dist/Helpers/ApiRoutes.js +191 -0
- package/dist/Helpers/ApiToasts.d.ts +21 -0
- package/dist/Helpers/ApiToasts.js +22 -0
- package/dist/Helpers/ClassHelpers.d.ts +3 -0
- package/dist/Helpers/ClassHelpers.js +4 -0
- package/dist/Helpers/CrudBackend.d.ts +56 -0
- package/dist/Helpers/CrudBackend.js +119 -0
- package/dist/Helpers/CrudHelpers.d.ts +76 -0
- package/dist/Helpers/CrudHelpers.js +202 -0
- package/dist/Helpers/DateHelpers.d.ts +88 -0
- package/dist/Helpers/DateHelpers.js +363 -0
- package/dist/Helpers/DatePreferences.d.ts +14 -0
- package/dist/Helpers/DatePreferences.js +94 -0
- package/dist/Helpers/DateTimeHelpers.d.ts +87 -0
- package/dist/Helpers/DateTimeHelpers.js +286 -0
- package/dist/Helpers/DragHelpers.d.ts +129 -0
- package/dist/Helpers/DragHelpers.js +281 -0
- package/dist/Helpers/DropdownHelpers.d.ts +56 -0
- package/dist/Helpers/DropdownHelpers.js +180 -0
- package/dist/Helpers/FormDrafts.d.ts +18 -0
- package/dist/Helpers/FormDrafts.js +76 -0
- package/dist/Helpers/FormStore.d.ts +231 -0
- package/dist/Helpers/FormStore.js +734 -0
- package/dist/Helpers/InputParamsHelpers.d.ts +79 -0
- package/dist/Helpers/InputParamsHelpers.js +388 -0
- package/dist/Helpers/MarkdownEditing.d.ts +27 -0
- package/dist/Helpers/MarkdownEditing.js +250 -0
- package/dist/Helpers/MarkdownHelpers.d.ts +82 -0
- package/dist/Helpers/MarkdownHelpers.js +775 -0
- package/dist/Helpers/MessageBuilder.d.ts +74 -0
- package/dist/Helpers/MessageBuilder.js +254 -0
- package/dist/Helpers/NepaliCalendar.d.ts +25 -0
- package/dist/Helpers/NepaliCalendar.js +107 -0
- package/dist/Helpers/NumberHelpers.d.ts +71 -0
- package/dist/Helpers/NumberHelpers.js +202 -0
- package/dist/Helpers/Permissions.d.ts +62 -0
- package/dist/Helpers/Permissions.js +133 -0
- package/dist/Helpers/PopoverHelpers.d.ts +43 -0
- package/dist/Helpers/PopoverHelpers.js +91 -0
- package/dist/Helpers/RichTextHelpers.d.ts +77 -0
- package/dist/Helpers/RichTextHelpers.js +1072 -0
- package/dist/Helpers/RowsStore.d.ts +160 -0
- package/dist/Helpers/RowsStore.js +584 -0
- package/dist/Helpers/TableColumns.d.ts +40 -0
- package/dist/Helpers/TableColumns.js +254 -0
- package/dist/Helpers/TableExport.d.ts +30 -0
- package/dist/Helpers/TableExport.js +97 -0
- package/dist/Helpers/TableHelpers.d.ts +102 -0
- package/dist/Helpers/TableHelpers.js +281 -0
- package/dist/Helpers/TableStore.d.ts +142 -0
- package/dist/Helpers/TableStore.js +530 -0
- package/dist/Helpers/ThemeHelpers.d.ts +15 -0
- package/dist/Helpers/ThemeHelpers.js +16 -0
- package/dist/Helpers/TimeHelpers.d.ts +73 -0
- package/dist/Helpers/TimeHelpers.js +253 -0
- package/dist/Helpers/TreeStore.d.ts +188 -0
- package/dist/Helpers/TreeStore.js +577 -0
- package/dist/Helpers/ValidationHelpers.d.ts +124 -0
- package/dist/Helpers/ValidationHelpers.js +301 -0
- package/dist/Interfaces/ApiInterfaces.d.ts +149 -0
- package/dist/Interfaces/ApiInterfaces.js +24 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +171 -0
- package/{src/sso-client.tsx → dist/Interfaces/CrudInterfaces.js} +1 -1
- package/dist/Interfaces/DateInterfaces.d.ts +154 -0
- package/dist/Interfaces/DateInterfaces.js +29 -0
- package/dist/Interfaces/FormInterfaces.d.ts +352 -0
- package/dist/Interfaces/FormInterfaces.js +1 -0
- package/dist/Interfaces/InputInterfaces.d.ts +141 -0
- package/dist/Interfaces/InputInterfaces.js +1 -0
- package/dist/Interfaces/MessageInterfaces.d.ts +111 -0
- package/dist/Interfaces/MessageInterfaces.js +5 -0
- package/dist/Interfaces/TableInterfaces.d.ts +291 -0
- package/dist/Interfaces/TableInterfaces.js +1 -0
- package/dist/Interfaces/ThemeInterfaces.d.ts +17 -0
- package/dist/Interfaces/ThemeInterfaces.js +17 -0
- package/dist/Interfaces/TypeInterfaces.d.ts +4 -0
- package/dist/Interfaces/TypeInterfaces.js +1 -0
- package/dist/Server.Index.d.ts +2 -0
- package/dist/Server.Index.js +5 -0
- package/dist/Services/ApiProxy.d.ts +32 -0
- package/dist/Services/ApiProxy.js +111 -0
- package/dist/Services/BrowserApi.d.ts +78 -0
- package/dist/Services/BrowserApi.js +111 -0
- package/dist/Services/ServerApi.d.ts +49 -0
- package/dist/Services/ServerApi.js +75 -0
- package/dist/Services/ThemeService.d.ts +17 -0
- package/dist/Services/ThemeService.js +83 -0
- package/dist/Shared.Index.d.ts +43 -0
- package/dist/Shared.Index.js +47 -0
- package/package.json +82 -33
- package/src/Styles/Nexus.Base.css +70 -0
- package/src/Styles/Nexus.Button.css +151 -0
- package/src/Styles/Nexus.Calendar.css +275 -0
- package/src/Styles/Nexus.Choice.css +345 -0
- package/src/Styles/Nexus.Crud.css +84 -0
- package/src/Styles/Nexus.DateTime.css +81 -0
- package/src/Styles/Nexus.Dropdown.css +774 -0
- package/src/Styles/Nexus.Editor.css +297 -0
- package/src/Styles/Nexus.Form.css +298 -0
- package/src/Styles/Nexus.Index.css +34 -0
- package/src/Styles/Nexus.Input.css +637 -0
- package/src/Styles/Nexus.Markdown.css +265 -0
- package/src/Styles/Nexus.Menu.css +249 -0
- package/src/Styles/Nexus.Popup.css +731 -0
- package/src/Styles/Nexus.RichText.css +463 -0
- package/src/Styles/Nexus.Rows.css +566 -0
- package/src/Styles/Nexus.Slider.css +278 -0
- package/src/Styles/Nexus.Tab.Buttons.css +364 -0
- package/src/Styles/Nexus.Table.css +1014 -0
- package/src/Styles/Nexus.Theme.Picker.css +159 -0
- package/src/Styles/Nexus.Themes.css +194 -0
- package/src/Styles/Nexus.Time.css +147 -0
- package/src/Styles/Nexus.Toast.css +265 -0
- package/src/Styles/Nexus.Tokens.css +72 -0
- package/src/Styles/Nexus.Tree.css +435 -0
- package/src/api-services/authentication-service.tsx +0 -23
- package/src/api-services/preference-service.tsx +0 -5
- package/src/api-services/system-service.tsx +0 -34
- package/src/client.ts +0 -31
- package/src/components/documents/button.tsx +0 -142
- package/src/components/documents/icon-box.tsx +0 -93
- package/src/components/documents/page-title.tsx +0 -7
- package/src/components/documents/tab-button.tsx +0 -172
- package/src/components/documents/tag.tsx +0 -30
- package/src/components/index.js +0 -0
- package/src/components/inputs/checkbox-input.tsx +0 -66
- package/src/components/inputs/input-box.tsx +0 -48
- package/src/components/inputs/input-element.tsx +0 -65
- package/src/components/inputs/input-form.tsx +0 -164
- package/src/components/inputs/input.tsx +0 -181
- package/src/components/inputs/number-input.tsx +0 -108
- package/src/components/inputs/radiobox-input.tsx +0 -53
- package/src/components/inputs/textarea-input.tsx +0 -47
- package/src/components/inputs/textbox-input.tsx +0 -45
- package/src/components/layouts/global-dialogbox.tsx +0 -431
- package/src/components/layouts/global-layout.tsx +0 -59
- package/src/components/layouts/layout-helpers.tsx +0 -20
- package/src/components/layouts/panels/user-panel.tsx +0 -112
- package/src/components/layouts/utility-menu.tsx +0 -49
- package/src/components/panels/theme-panel.tsx +0 -46
- package/src/helpers/bitwise-helpers.tsx +0 -11
- package/src/helpers/browser-helpers.tsx +0 -156
- package/src/helpers/datasource-helpers.tsx +0 -99
- package/src/helpers/element-helpers.tsx +0 -57
- package/src/helpers/input-helpers.tsx +0 -74
- package/src/helpers/string-helpers.tsx +0 -28
- package/src/helpers/utility-helpers.tsx +0 -45
- package/src/helpers/validation-helpers.tsx +0 -302
- package/src/index.ts +0 -26
- package/src/interface.ts +0 -19
- package/src/interfaces/auth-token-interfaces.tsx +0 -6
- package/src/interfaces/browser-interfaces.tsx +0 -36
- package/src/interfaces/button-interfaces.tsx +0 -63
- package/src/interfaces/datasource-interfaces.tsx +0 -22
- package/src/interfaces/datatable-interfaces.tsx +0 -25
- package/src/interfaces/dialogbox-interfaces.tsx +0 -5
- package/src/interfaces/exception-interfaces.tsx +0 -99
- package/src/interfaces/http-interfaces.tsx +0 -129
- package/src/interfaces/icon-interfaces.tsx +0 -129
- package/src/interfaces/input-interfaces.tsx +0 -410
- package/src/interfaces/layout-interfaces.tsx +0 -191
- package/src/interfaces/menu-interfaces.tsx +0 -49
- package/src/interfaces/message-interfaces.tsx +0 -32
- package/src/interfaces/permission-interfaces.tsx +0 -9
- package/src/interfaces/storage-interfaces.tsx +0 -5
- package/src/interfaces/system-interfaces.tsx +0 -22
- package/src/interfaces/theme-interfaces.tsx +0 -123
- package/src/interfaces/type-interfaces.tsx +0 -28
- package/src/interfaces/user-interfaces.tsx +0 -47
- package/src/nexus-client.tsx +0 -21
- package/src/nexus.environments.tsx +0 -66
- package/src/proxy-api/proxy-backend.tsx +0 -60
- package/src/proxy-api/proxy-constants.tsx +0 -28
- package/src/proxy-api/proxy-helpers.tsx +0 -91
- package/src/proxy-api-client.tsx +0 -1
- package/src/proxy-api-server.tsx +0 -2
- package/src/services/http-client-services.tsx +0 -75
- package/src/services/http-services.tsx +0 -158
- package/src/services/loader-service.tsx +0 -185
- package/src/services/localstorage-service.tsx +0 -119
- package/src/services/message-services.tsx +0 -383
- package/src/services/theme-service.tsx +0 -161
- package/src/services/user-services.tsx +0 -10
- package/src/sso-config/auth-token-validation.ts +0 -16
- package/src/sso-config/callback-route.tsx +0 -35
- package/src/sso-config/cookie-encryption.ts +0 -51
- package/src/sso-config/cookie-helpers.tsx +0 -102
- package/src/sso-config/forgot-password-route.tsx +0 -36
- package/src/sso-config/oauth-callback-state.ts +0 -61
- package/src/sso-config/pkce-helpers.ts +0 -9
- package/src/sso-config/provider-complete-route.tsx +0 -18
- package/src/sso-config/redirect-context-actions.tsx +0 -45
- package/src/sso-config/redirect-context-helpers.tsx +0 -159
- package/src/sso-config/redirect-context-persist.tsx +0 -18
- package/src/sso-config/redirect-context-route.tsx +0 -45
- package/src/sso-config/refresh-route.tsx +0 -23
- package/src/sso-config/sign-in-route.tsx +0 -74
- package/src/sso-config/sign-out-route.tsx +0 -57
- package/src/sso-config/sign-up-route.tsx +0 -57
- package/src/sso-config/sso-backend-flow.ts +0 -38
- package/src/sso-config/sso-complete-route.tsx +0 -50
- package/src/sso-config/sso-config.ts +0 -59
- package/src/sso-config/sso-interfaces.tsx +0 -99
- package/src/sso-config/sso-response-helpers.tsx +0 -20
- package/src/sso-server.tsx +0 -21
- package/src/styles/nexus.animation.css +0 -269
- package/src/styles/nexus.core.css +0 -119
- package/src/styles/nexus.dialog.css +0 -144
- package/src/styles/nexus.icon.css +0 -51
- package/src/styles/nexus.input.css +0 -207
- package/src/styles/nexus.loader.css +0 -11
- package/src/styles/nexus.logic.css +0 -47
- package/src/styles/nexus.utility.css +0 -347
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { buildMessage, describeResult } from "./MessageBuilder.js";
|
|
2
|
+
/*
|
|
3
|
+
* Reading a server's answer as success or failure, the same way for every call: Nexus API responses, ASP.NET Core problem
|
|
4
|
+
* details, or any JSON by its status. Pure, so it works in tests, in the browser, and on a server.
|
|
5
|
+
*/
|
|
6
|
+
/** Shown when nothing reached the server (the default wording; `messages.reason({ network: true })` follows `configureMessages`). */
|
|
7
|
+
export const NETWORK_ERROR_MESSAGE = buildMessage("reason", { network: true });
|
|
8
|
+
/** A message for a failed request that came without one of its own, by HTTP status and what the request did. Default action `save`. */
|
|
9
|
+
export function apiStatusMessage(status, action = "save") {
|
|
10
|
+
return buildMessage("reason", { action, status });
|
|
11
|
+
}
|
|
12
|
+
function isRecord(value) {
|
|
13
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
function text(value) {
|
|
16
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
17
|
+
}
|
|
18
|
+
/** Messages by field from `{ errors: { Email: ["…"], "Addresses[0].City": "…" } }`. */
|
|
19
|
+
function readErrorMap(errors) {
|
|
20
|
+
if (!isRecord(errors))
|
|
21
|
+
return undefined;
|
|
22
|
+
const result = {};
|
|
23
|
+
for (const [key, value] of Object.entries(errors)) {
|
|
24
|
+
const message = Array.isArray(value) ? value.map(text).filter(Boolean).join(" ") : text(value);
|
|
25
|
+
if (message)
|
|
26
|
+
result[key] = message;
|
|
27
|
+
}
|
|
28
|
+
return Object.keys(result).length ? result : undefined;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Reads a server's answer as success or failure:
|
|
32
|
+
* - Nexus API responses, `{ isSuccess, message, messageCode, errorCode, inputName, result }`: `inputName` names the
|
|
33
|
+
* inputs the message is about, several joined with "|".
|
|
34
|
+
* - ASP.NET Core problem details and validation errors, `{ title, detail, errors: { Email: ["…"] } }`.
|
|
35
|
+
* - Anything else by its HTTP status; the body is the result.
|
|
36
|
+
*
|
|
37
|
+
* A failure without a message of its own gets one by status, worded for what the call did (`context`, default: a save).
|
|
38
|
+
*/
|
|
39
|
+
export function readApiResponse(body, status, context) {
|
|
40
|
+
const fallback = () => buildMessage("reason", { action: "save", ...context, status });
|
|
41
|
+
const statusOk = status === undefined || (status >= 200 && status < 300);
|
|
42
|
+
if (isRecord(body)) {
|
|
43
|
+
if (typeof body.isSuccess === "boolean") {
|
|
44
|
+
const ok = body.isSuccess && statusOk;
|
|
45
|
+
const message = text(body.message);
|
|
46
|
+
let errors;
|
|
47
|
+
const names = text(body.inputName);
|
|
48
|
+
if (!ok && names && message) {
|
|
49
|
+
errors = {};
|
|
50
|
+
for (const name of names.split("|"))
|
|
51
|
+
if (name.trim())
|
|
52
|
+
errors[name.trim()] = message;
|
|
53
|
+
}
|
|
54
|
+
// The backend's defaults ("SUCCESS", "GENERIC_ERROR", error 0) say nothing, so they are left out.
|
|
55
|
+
const code = text(body.messageCode);
|
|
56
|
+
const messageCode = code && code !== (ok ? "SUCCESS" : "GENERIC_ERROR") ? code : undefined;
|
|
57
|
+
const errorCode = typeof body.errorCode === "number" && body.errorCode !== 0 ? body.errorCode : undefined;
|
|
58
|
+
return {
|
|
59
|
+
ok,
|
|
60
|
+
status,
|
|
61
|
+
result: body.result,
|
|
62
|
+
message: message ?? (ok ? undefined : fallback()),
|
|
63
|
+
errors,
|
|
64
|
+
...(messageCode ? { messageCode } : {}),
|
|
65
|
+
...(errorCode !== undefined ? { errorCode } : {}),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const errors = readErrorMap(body.errors);
|
|
69
|
+
if (errors || !statusOk) {
|
|
70
|
+
const message = text(body.message) ?? (errors ? undefined : (text(body.detail) ?? text(body.title)));
|
|
71
|
+
return { ok: false, status, result: body, message: message ?? (errors ? undefined : fallback()), errors };
|
|
72
|
+
}
|
|
73
|
+
return { ok: true, status, result: body, message: text(body.message) };
|
|
74
|
+
}
|
|
75
|
+
if (!statusOk) {
|
|
76
|
+
// A short text answer is the message; an HTML error page is not.
|
|
77
|
+
const plain = text(body);
|
|
78
|
+
const message = plain && plain.length <= 300 && !plain.startsWith("<") ? plain : fallback();
|
|
79
|
+
return { ok: false, status, message };
|
|
80
|
+
}
|
|
81
|
+
return { ok: true, status, result: body === "" ? undefined : body };
|
|
82
|
+
}
|
|
83
|
+
/** Reads a `fetch` response: its JSON (or text) through `read`. */
|
|
84
|
+
export async function readFetchResponse(response, read = readApiResponse) {
|
|
85
|
+
const type = response.headers.get("content-type") ?? "";
|
|
86
|
+
let body = null;
|
|
87
|
+
try {
|
|
88
|
+
body = type.includes("json") ? await response.json() : await response.text();
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
body = null;
|
|
92
|
+
}
|
|
93
|
+
return read(body, response.status);
|
|
94
|
+
}
|
|
95
|
+
/** Whether an error is a request stopped on purpose (the form closed), which shows nothing. */
|
|
96
|
+
export function isAbortError(error) {
|
|
97
|
+
return error?.name === "AbortError";
|
|
98
|
+
}
|
|
99
|
+
/** A thrown error as a message: its own, or the network message for a failed `fetch`. */
|
|
100
|
+
export function errorMessage(error, fallback = "Could not save the changes.") {
|
|
101
|
+
if (error instanceof TypeError)
|
|
102
|
+
return buildMessage("reason", { network: true });
|
|
103
|
+
return text(error?.message) ?? (typeof error === "string" && error ? error : fallback);
|
|
104
|
+
}
|
|
105
|
+
/** Values as form fields: lists of plain values repeat their name, objects and lists of objects go as JSON text. */
|
|
106
|
+
export function toFormData(values) {
|
|
107
|
+
if (typeof FormData !== "undefined" && values instanceof FormData)
|
|
108
|
+
return values;
|
|
109
|
+
const data = new FormData();
|
|
110
|
+
if (!isRecord(values))
|
|
111
|
+
return data;
|
|
112
|
+
for (const [key, value] of Object.entries(values)) {
|
|
113
|
+
if (value === undefined)
|
|
114
|
+
continue;
|
|
115
|
+
if (value instanceof Blob)
|
|
116
|
+
data.append(key, value);
|
|
117
|
+
else if (Array.isArray(value) && value.every(item => item === null || typeof item !== "object" || item instanceof Blob))
|
|
118
|
+
for (const item of value)
|
|
119
|
+
data.append(key, item instanceof Blob ? item : item === null ? "" : String(item));
|
|
120
|
+
else if (value !== null && typeof value === "object")
|
|
121
|
+
data.append(key, JSON.stringify(value));
|
|
122
|
+
else
|
|
123
|
+
data.append(key, value === null ? "" : String(value));
|
|
124
|
+
}
|
|
125
|
+
return data;
|
|
126
|
+
}
|
|
127
|
+
const RESULT_KEYS = ["result", "message", "errors", "status", "network", "timeout", "aborted", "messageCode", "errorCode"];
|
|
128
|
+
/** Whether a value is already a read answer (`{ ok, … }`), not a server's body. */
|
|
129
|
+
export function isApiResult(value) {
|
|
130
|
+
if (!isRecord(value) || typeof value.ok !== "boolean" || "isSuccess" in value)
|
|
131
|
+
return false;
|
|
132
|
+
const keys = Object.keys(value);
|
|
133
|
+
return keys.length === 1 || keys.some(key => RESULT_KEYS.includes(key));
|
|
134
|
+
}
|
|
135
|
+
/** A failed answer as an error, for code that throws: a server component's error boundary, a form's `loadValue`. */
|
|
136
|
+
export class ApiError extends Error {
|
|
137
|
+
response;
|
|
138
|
+
constructor(response) {
|
|
139
|
+
super(describeResult(response)?.message ?? response.message ?? "The request failed.");
|
|
140
|
+
this.name = response.aborted ? "AbortError" : "ApiError";
|
|
141
|
+
this.response = response;
|
|
142
|
+
}
|
|
143
|
+
get status() {
|
|
144
|
+
return this.response.status;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* What the server sent back, or an `ApiError` thrown for a failure (an `AbortError` for an aborted call, which forms and
|
|
149
|
+
* loaders ignore).
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* const rooms = unwrapApiResult(await serverApi.get<Room[]>(ROOMS.listing));
|
|
153
|
+
* <ApiForm loadValue={signal => api.get(ROOMS.details, { params: { id }, signal }).then(unwrapApiResult)} … />
|
|
154
|
+
*/
|
|
155
|
+
export function unwrapApiResult(response) {
|
|
156
|
+
if (!response.ok)
|
|
157
|
+
throw new ApiError(response);
|
|
158
|
+
return response.result;
|
|
159
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { ApiEndpoint, ApiModuleValue, ApiQuery, HttpMethod } from "../Interfaces/ApiInterfaces.ts";
|
|
2
|
+
import type { CrudMode } from "../Interfaces/CrudInterfaces.ts";
|
|
3
|
+
/**
|
|
4
|
+
* The standard actions of a Nexus controller, as the backend names them. `apiController` builds its endpoints from
|
|
5
|
+
* these; use them for an endpoint of your own too: `apiEndpoint.get(module, "Room/" + API_ACTIONS.activeListing)`.
|
|
6
|
+
*/
|
|
7
|
+
export declare const API_ACTIONS: {
|
|
8
|
+
readonly details: "Detail";
|
|
9
|
+
/** Every active record: the `normal` list. */
|
|
10
|
+
readonly listing: "NormalListing";
|
|
11
|
+
readonly activeListing: "ActiveListing";
|
|
12
|
+
readonly findListing: "FindListing";
|
|
13
|
+
/** One page of the `normal` list. */
|
|
14
|
+
readonly pagination: "NormalPagination";
|
|
15
|
+
/** Every record of each list a CRUD shows. */
|
|
16
|
+
readonly listings: {
|
|
17
|
+
readonly normal: "NormalListing";
|
|
18
|
+
readonly flag: "FlagListing";
|
|
19
|
+
readonly archive: "ArchiveListing";
|
|
20
|
+
readonly trash: "TrashListing";
|
|
21
|
+
readonly deleted: "DeleteListing";
|
|
22
|
+
};
|
|
23
|
+
/** One page of each list a CRUD shows. */
|
|
24
|
+
readonly paginations: {
|
|
25
|
+
readonly normal: "NormalPagination";
|
|
26
|
+
readonly flag: "FlagPagination";
|
|
27
|
+
readonly archive: "ArchivePagination";
|
|
28
|
+
readonly trash: "TrashPagination";
|
|
29
|
+
readonly deleted: "DeletePagination";
|
|
30
|
+
};
|
|
31
|
+
/** The records the user pinned, which a CRUD shows above its list. */
|
|
32
|
+
readonly pinListing: "PinListing";
|
|
33
|
+
readonly create: "Create";
|
|
34
|
+
readonly update: "Update";
|
|
35
|
+
readonly delete: "Delete";
|
|
36
|
+
readonly trash: "Trash";
|
|
37
|
+
readonly trashFromDelete: "TrashFromDelete";
|
|
38
|
+
readonly archive: "Archive";
|
|
39
|
+
readonly recover: "Recover";
|
|
40
|
+
readonly restore: "Restore";
|
|
41
|
+
readonly flag: "Flag";
|
|
42
|
+
readonly pin: "Pin";
|
|
43
|
+
readonly audit: "AuditListing";
|
|
44
|
+
readonly accessActions: "AccessActions";
|
|
45
|
+
readonly massDescription: "MassDescription";
|
|
46
|
+
readonly massArchive: "MassArchive";
|
|
47
|
+
readonly massTrash: "MassTrash";
|
|
48
|
+
readonly massFlag: "MassFlag";
|
|
49
|
+
readonly massPin: "MassPin";
|
|
50
|
+
readonly massDelete: "MassDelete";
|
|
51
|
+
readonly massRecover: "MassRecover";
|
|
52
|
+
readonly massRestore: "MassRestore";
|
|
53
|
+
readonly massTrashFromDelete: "MassTrashFromDelete";
|
|
54
|
+
};
|
|
55
|
+
/** An endpoint's optional parts: `bodyType`, `query`, `action`, `subject`. */
|
|
56
|
+
export type ApiEndpointExtras = Partial<Omit<ApiEndpoint, "module" | "path" | "method">>;
|
|
57
|
+
type EndpointExtras = ApiEndpointExtras;
|
|
58
|
+
/**
|
|
59
|
+
* Builds an endpoint: `apiEndpoint.get(NexusModule.Message, "EmailProvider/ActiveListing")`. The path is under the
|
|
60
|
+
* module's root; `{name}` in it is filled from the call's `params`, then its body.
|
|
61
|
+
*/
|
|
62
|
+
export declare const apiEndpoint: {
|
|
63
|
+
get: <TModule extends ApiModuleValue>(module: TModule, path: string, extras?: EndpointExtras) => ApiEndpoint<TModule>;
|
|
64
|
+
post: <TModule extends ApiModuleValue>(module: TModule, path: string, extras?: EndpointExtras) => ApiEndpoint<TModule>;
|
|
65
|
+
put: <TModule extends ApiModuleValue>(module: TModule, path: string, extras?: EndpointExtras) => ApiEndpoint<TModule>;
|
|
66
|
+
patch: <TModule extends ApiModuleValue>(module: TModule, path: string, extras?: EndpointExtras) => ApiEndpoint<TModule>;
|
|
67
|
+
delete: <TModule extends ApiModuleValue>(module: TModule, path: string, extras?: EndpointExtras) => ApiEndpoint<TModule>;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* The endpoints of a Nexus controller, named as the backend names them. `subject` is what the controller keeps, for
|
|
71
|
+
* messages ("room" → "Room saved.", "3 rooms deleted."); a call can name the record instead (`subject: "Room 204"`).
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* export const ROOMS = apiController(NexusModule.Reservation, "Room", "room");
|
|
75
|
+
* api.get(ROOMS.details, { params: { id: 5 } }); // GET /api/app/reservation/Room/Detail/5
|
|
76
|
+
* api.put(ROOMS.update, values); // PUT /api/app/reservation/Room/Update → "Room saved."
|
|
77
|
+
* api.delete(ROOMS.delete, { params: { id: 5 } }); // DELETE .../Room/Delete/5 → "Room deleted."
|
|
78
|
+
*/
|
|
79
|
+
export declare function apiController<TModule extends ApiModuleValue>(module: TModule, controller: string, subject?: string): {
|
|
80
|
+
module: TModule;
|
|
81
|
+
controller: string;
|
|
82
|
+
/** Every active row: GET `Room/NormalListing`. */
|
|
83
|
+
listing: ApiEndpoint<TModule>;
|
|
84
|
+
/** Active rows for dropdowns: GET `Room/ActiveListing`. */
|
|
85
|
+
activeListing: ApiEndpoint<TModule>;
|
|
86
|
+
/** Rows by id, for saved values a list no longer shows: POST `Room/FindListing` with the ids. */
|
|
87
|
+
findListing: ApiEndpoint<TModule>;
|
|
88
|
+
/** One page of active rows: POST `Room/NormalPagination` with `{ searchKeyword, skipCount, maxCount, sorting, sortType }`. */
|
|
89
|
+
pagination: ApiEndpoint<TModule>;
|
|
90
|
+
/** Every row of each list, for a CRUD that pages in the browser: GET `Room/NormalListing`, `Room/TrashListing`, … */
|
|
91
|
+
lists: Record<CrudMode, ApiEndpoint<TModule>>;
|
|
92
|
+
/** One page of each list, for a CRUD that pages on the server: POST `Room/NormalPagination`, `Room/TrashPagination`, … */
|
|
93
|
+
pages: Record<CrudMode, ApiEndpoint<TModule>>;
|
|
94
|
+
/** The records the user pinned, shown above the list: GET `Room/PinListing`. */
|
|
95
|
+
pins: ApiEndpoint<TModule>;
|
|
96
|
+
/** One record: GET `Room/Detail/{id}`. */
|
|
97
|
+
details: ApiEndpoint<TModule>;
|
|
98
|
+
create: ApiEndpoint<TModule>;
|
|
99
|
+
update: ApiEndpoint<TModule>;
|
|
100
|
+
delete: ApiEndpoint<TModule>;
|
|
101
|
+
trash: ApiEndpoint<TModule>;
|
|
102
|
+
trashFromDelete: ApiEndpoint<TModule>;
|
|
103
|
+
archive: ApiEndpoint<TModule>;
|
|
104
|
+
restore: ApiEndpoint<TModule>;
|
|
105
|
+
recover: ApiEndpoint<TModule>;
|
|
106
|
+
flag: ApiEndpoint<TModule>;
|
|
107
|
+
pin: ApiEndpoint<TModule>;
|
|
108
|
+
/** A record's history: GET `Room/AuditListing/{ownerId}/{skipCount}`. */
|
|
109
|
+
audit: ApiEndpoint<TModule>;
|
|
110
|
+
/** What the user may do here: GET `Room/AccessActions`. */
|
|
111
|
+
accessActions: ApiEndpoint<TModule>;
|
|
112
|
+
/** Actions on many rows at once: POST `Room/MassDelete` and the rest, with the ids. */
|
|
113
|
+
mass: {
|
|
114
|
+
description: ApiEndpoint<TModule>;
|
|
115
|
+
archive: ApiEndpoint<TModule>;
|
|
116
|
+
trash: ApiEndpoint<TModule>;
|
|
117
|
+
trashFromDelete: ApiEndpoint<TModule>;
|
|
118
|
+
flag: ApiEndpoint<TModule>;
|
|
119
|
+
pin: ApiEndpoint<TModule>;
|
|
120
|
+
delete: ApiEndpoint<TModule>;
|
|
121
|
+
recover: ApiEndpoint<TModule>;
|
|
122
|
+
restore: ApiEndpoint<TModule>;
|
|
123
|
+
};
|
|
124
|
+
/** Any other action of the controller: `ROOMS.action("PUT", "Block/{id}", { action: "update" })`. */
|
|
125
|
+
action: (method: HttpMethod, action: string, extras?: EndpointExtras) => ApiEndpoint<TModule>;
|
|
126
|
+
};
|
|
127
|
+
export type ApiControllerEndpoints = ReturnType<typeof apiController>;
|
|
128
|
+
/** Whether a value is an endpoint (not a URL or a request of another shape). */
|
|
129
|
+
export declare function isApiEndpoint(value: unknown): value is ApiEndpoint;
|
|
130
|
+
/** Thrown when a path names a value the call did not give, so a request never goes to ".../Detail/undefined". */
|
|
131
|
+
export declare class ApiPathError extends Error {
|
|
132
|
+
readonly path: string;
|
|
133
|
+
readonly missing: string;
|
|
134
|
+
constructor(path: string, missing: string);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Fills `{name}` in a path from `params`, then from the body's fields (names without case), and encodes each value.
|
|
138
|
+
* Leading and trailing slashes go.
|
|
139
|
+
*/
|
|
140
|
+
export declare function fillApiPath(path: string, params?: Record<string, unknown> | null, body?: unknown): string;
|
|
141
|
+
/** Adds query values to a URL: lists repeat their name, dates go as ISO text, `null` and `undefined` are left out. */
|
|
142
|
+
export declare function appendQuery(url: string, ...queries: (ApiQuery | null | undefined)[]): string;
|
|
143
|
+
/** Joins a root URL and a path with one slash between them. */
|
|
144
|
+
export declare function joinUrl(root: string, path: string): string;
|
|
145
|
+
export interface ApiUrlParts {
|
|
146
|
+
params?: Record<string, unknown> | null;
|
|
147
|
+
query?: ApiQuery;
|
|
148
|
+
body?: unknown;
|
|
149
|
+
}
|
|
150
|
+
export {};
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Endpoints and their URLs. An endpoint is a path under a module's root; which roots exist, and where, is the app's
|
|
3
|
+
* `ApiModules` class (ApiModules.ts). Define endpoints once, with `apiEndpoint` or `apiController` (or the same
|
|
4
|
+
* methods of the app's modules), and call them from anywhere.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* The standard actions of a Nexus controller, as the backend names them. `apiController` builds its endpoints from
|
|
8
|
+
* these; use them for an endpoint of your own too: `apiEndpoint.get(module, "Room/" + API_ACTIONS.activeListing)`.
|
|
9
|
+
*/
|
|
10
|
+
export const API_ACTIONS = {
|
|
11
|
+
details: "Detail",
|
|
12
|
+
/** Every active record: the `normal` list. */
|
|
13
|
+
listing: "NormalListing",
|
|
14
|
+
activeListing: "ActiveListing",
|
|
15
|
+
findListing: "FindListing",
|
|
16
|
+
/** One page of the `normal` list. */
|
|
17
|
+
pagination: "NormalPagination",
|
|
18
|
+
/** Every record of each list a CRUD shows. */
|
|
19
|
+
listings: { normal: "NormalListing", flag: "FlagListing", archive: "ArchiveListing", trash: "TrashListing", deleted: "DeleteListing" },
|
|
20
|
+
/** One page of each list a CRUD shows. */
|
|
21
|
+
paginations: { normal: "NormalPagination", flag: "FlagPagination", archive: "ArchivePagination", trash: "TrashPagination", deleted: "DeletePagination" },
|
|
22
|
+
/** The records the user pinned, which a CRUD shows above its list. */
|
|
23
|
+
pinListing: "PinListing",
|
|
24
|
+
create: "Create",
|
|
25
|
+
update: "Update",
|
|
26
|
+
delete: "Delete",
|
|
27
|
+
trash: "Trash",
|
|
28
|
+
trashFromDelete: "TrashFromDelete",
|
|
29
|
+
archive: "Archive",
|
|
30
|
+
recover: "Recover",
|
|
31
|
+
restore: "Restore",
|
|
32
|
+
flag: "Flag",
|
|
33
|
+
pin: "Pin",
|
|
34
|
+
audit: "AuditListing",
|
|
35
|
+
accessActions: "AccessActions",
|
|
36
|
+
massDescription: "MassDescription",
|
|
37
|
+
massArchive: "MassArchive",
|
|
38
|
+
massTrash: "MassTrash",
|
|
39
|
+
massFlag: "MassFlag",
|
|
40
|
+
massPin: "MassPin",
|
|
41
|
+
massDelete: "MassDelete",
|
|
42
|
+
massRecover: "MassRecover",
|
|
43
|
+
massRestore: "MassRestore",
|
|
44
|
+
massTrashFromDelete: "MassTrashFromDelete",
|
|
45
|
+
};
|
|
46
|
+
const define = (method) => (module, path, extras) => ({ ...extras, module, path, method });
|
|
47
|
+
/**
|
|
48
|
+
* Builds an endpoint: `apiEndpoint.get(NexusModule.Message, "EmailProvider/ActiveListing")`. The path is under the
|
|
49
|
+
* module's root; `{name}` in it is filled from the call's `params`, then its body.
|
|
50
|
+
*/
|
|
51
|
+
export const apiEndpoint = {
|
|
52
|
+
get: define("GET"),
|
|
53
|
+
post: define("POST"),
|
|
54
|
+
put: define("PUT"),
|
|
55
|
+
patch: define("PATCH"),
|
|
56
|
+
delete: define("DELETE"),
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* The endpoints of a Nexus controller, named as the backend names them. `subject` is what the controller keeps, for
|
|
60
|
+
* messages ("room" → "Room saved.", "3 rooms deleted."); a call can name the record instead (`subject: "Room 204"`).
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* export const ROOMS = apiController(NexusModule.Reservation, "Room", "room");
|
|
64
|
+
* api.get(ROOMS.details, { params: { id: 5 } }); // GET /api/app/reservation/Room/Detail/5
|
|
65
|
+
* api.put(ROOMS.update, values); // PUT /api/app/reservation/Room/Update → "Room saved."
|
|
66
|
+
* api.delete(ROOMS.delete, { params: { id: 5 } }); // DELETE .../Room/Delete/5 → "Room deleted."
|
|
67
|
+
*/
|
|
68
|
+
export function apiController(module, controller, subject) {
|
|
69
|
+
const at = (action) => `${controller}/${action}`;
|
|
70
|
+
const endpoint = (method, path, extras) => ({ module, path, method, ...(subject && { subject }), ...extras });
|
|
71
|
+
const A = API_ACTIONS;
|
|
72
|
+
const modes = Object.keys(A.listings);
|
|
73
|
+
const byMode = (method, actions) => Object.fromEntries(modes.map(mode => [mode, endpoint(method, at(actions[mode]), { action: "load" })]));
|
|
74
|
+
return {
|
|
75
|
+
module,
|
|
76
|
+
controller,
|
|
77
|
+
/** Every active row: GET `Room/NormalListing`. */
|
|
78
|
+
listing: endpoint("GET", at(A.listing), { action: "load" }),
|
|
79
|
+
/** Active rows for dropdowns: GET `Room/ActiveListing`. */
|
|
80
|
+
activeListing: endpoint("GET", at(A.activeListing), { action: "load" }),
|
|
81
|
+
/** Rows by id, for saved values a list no longer shows: POST `Room/FindListing` with the ids. */
|
|
82
|
+
findListing: endpoint("POST", at(A.findListing), { action: "load" }),
|
|
83
|
+
/** One page of active rows: POST `Room/NormalPagination` with `{ searchKeyword, skipCount, maxCount, sorting, sortType }`. */
|
|
84
|
+
pagination: endpoint("POST", at(A.pagination), { action: "load" }),
|
|
85
|
+
/** Every row of each list, for a CRUD that pages in the browser: GET `Room/NormalListing`, `Room/TrashListing`, … */
|
|
86
|
+
lists: byMode("GET", A.listings),
|
|
87
|
+
/** One page of each list, for a CRUD that pages on the server: POST `Room/NormalPagination`, `Room/TrashPagination`, … */
|
|
88
|
+
pages: byMode("POST", A.paginations),
|
|
89
|
+
/** The records the user pinned, shown above the list: GET `Room/PinListing`. */
|
|
90
|
+
pins: endpoint("GET", at(A.pinListing), { action: "load" }),
|
|
91
|
+
/** One record: GET `Room/Detail/{id}`. */
|
|
92
|
+
details: endpoint("GET", at(`${A.details}/{id}`), { action: "load" }),
|
|
93
|
+
create: endpoint("POST", at(A.create), { action: "create" }),
|
|
94
|
+
update: endpoint("PUT", at(A.update), { action: "save" }),
|
|
95
|
+
delete: endpoint("DELETE", at(`${A.delete}/{id}`), { action: "delete" }),
|
|
96
|
+
trash: endpoint("DELETE", at(`${A.trash}/{id}`), { action: "trash" }),
|
|
97
|
+
trashFromDelete: endpoint("PUT", at(`${A.trashFromDelete}/{id}`), { action: "trash" }),
|
|
98
|
+
archive: endpoint("PUT", at(`${A.archive}/{id}`), { action: "archive" }),
|
|
99
|
+
restore: endpoint("PUT", at(`${A.restore}/{id}`), { action: "restore" }),
|
|
100
|
+
recover: endpoint("PUT", at(`${A.recover}/{id}`), { action: "recover" }),
|
|
101
|
+
flag: endpoint("PUT", at(`${A.flag}/{id}`), { action: "flag" }),
|
|
102
|
+
pin: endpoint("PUT", at(`${A.pin}/{id}`), { action: "pin" }),
|
|
103
|
+
/** A record's history: GET `Room/AuditListing/{ownerId}/{skipCount}`. */
|
|
104
|
+
audit: endpoint("GET", at(`${A.audit}/{ownerId}/{skipCount}`), { action: "load" }),
|
|
105
|
+
/** What the user may do here: GET `Room/AccessActions`. */
|
|
106
|
+
accessActions: endpoint("GET", at(A.accessActions), { action: "load" }),
|
|
107
|
+
/** Actions on many rows at once: POST `Room/MassDelete` and the rest, with the ids. */
|
|
108
|
+
mass: {
|
|
109
|
+
description: endpoint("POST", at(A.massDescription), { action: "update" }),
|
|
110
|
+
archive: endpoint("POST", at(A.massArchive), { action: "archive" }),
|
|
111
|
+
trash: endpoint("POST", at(A.massTrash), { action: "trash" }),
|
|
112
|
+
trashFromDelete: endpoint("POST", at(A.massTrashFromDelete), { action: "trash" }),
|
|
113
|
+
flag: endpoint("POST", at(A.massFlag), { action: "flag" }),
|
|
114
|
+
pin: endpoint("POST", at(A.massPin), { action: "pin" }),
|
|
115
|
+
delete: endpoint("POST", at(A.massDelete), { action: "delete" }),
|
|
116
|
+
recover: endpoint("POST", at(A.massRecover), { action: "recover" }),
|
|
117
|
+
restore: endpoint("POST", at(A.massRestore), { action: "restore" }),
|
|
118
|
+
},
|
|
119
|
+
/** Any other action of the controller: `ROOMS.action("PUT", "Block/{id}", { action: "update" })`. */
|
|
120
|
+
action: (method, action, extras) => endpoint(method, at(action), extras),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/** Whether a value is an endpoint (not a URL or a request of another shape). */
|
|
124
|
+
export function isApiEndpoint(value) {
|
|
125
|
+
const endpoint = value;
|
|
126
|
+
return typeof endpoint === "object" && endpoint !== null && (typeof endpoint.module === "number" || typeof endpoint.module === "string") && typeof endpoint.path === "string" && typeof endpoint.method === "string";
|
|
127
|
+
}
|
|
128
|
+
function isRecord(value) {
|
|
129
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Blob) && !(typeof FormData !== "undefined" && value instanceof FormData);
|
|
130
|
+
}
|
|
131
|
+
/** Thrown when a path names a value the call did not give, so a request never goes to ".../Detail/undefined". */
|
|
132
|
+
export class ApiPathError extends Error {
|
|
133
|
+
path;
|
|
134
|
+
missing;
|
|
135
|
+
constructor(path, missing) {
|
|
136
|
+
super(`The API path "${path}" needs "${missing}", which the call did not give.`);
|
|
137
|
+
this.name = "ApiPathError";
|
|
138
|
+
this.path = path;
|
|
139
|
+
this.missing = missing;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Fills `{name}` in a path from `params`, then from the body's fields (names without case), and encodes each value.
|
|
144
|
+
* Leading and trailing slashes go.
|
|
145
|
+
*/
|
|
146
|
+
export function fillApiPath(path, params, body) {
|
|
147
|
+
const fields = isRecord(body) ? body : undefined;
|
|
148
|
+
const filled = path.replace(/\{(\w+)\}/g, (_whole, name) => {
|
|
149
|
+
let value = params?.[name];
|
|
150
|
+
if ((value === undefined || value === null) && fields) {
|
|
151
|
+
const lower = name.toLowerCase();
|
|
152
|
+
const key = name in fields ? name : Object.keys(fields).find(candidate => candidate.toLowerCase() === lower);
|
|
153
|
+
if (key !== undefined)
|
|
154
|
+
value = fields[key];
|
|
155
|
+
}
|
|
156
|
+
if (value === undefined || value === null || value === "")
|
|
157
|
+
throw new ApiPathError(path, name);
|
|
158
|
+
return encodeURIComponent(String(value));
|
|
159
|
+
});
|
|
160
|
+
return filled.replace(/^\/+|\/+$/g, "");
|
|
161
|
+
}
|
|
162
|
+
function queryText(value) {
|
|
163
|
+
return value instanceof Date ? value.toISOString() : String(value);
|
|
164
|
+
}
|
|
165
|
+
/** Adds query values to a URL: lists repeat their name, dates go as ISO text, `null` and `undefined` are left out. */
|
|
166
|
+
export function appendQuery(url, ...queries) {
|
|
167
|
+
const search = new URLSearchParams();
|
|
168
|
+
for (const query of queries) {
|
|
169
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
170
|
+
if (value === undefined || value === null)
|
|
171
|
+
continue;
|
|
172
|
+
if (Array.isArray(value))
|
|
173
|
+
for (const item of value)
|
|
174
|
+
search.append(key, queryText(item));
|
|
175
|
+
else
|
|
176
|
+
search.append(key, queryText(value));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const text = search.toString();
|
|
180
|
+
if (!text)
|
|
181
|
+
return url;
|
|
182
|
+
const hash = url.indexOf("#");
|
|
183
|
+
const [base, fragment] = hash === -1 ? [url, ""] : [url.slice(0, hash), url.slice(hash)];
|
|
184
|
+
return `${base}${base.includes("?") ? "&" : "?"}${text}${fragment}`;
|
|
185
|
+
}
|
|
186
|
+
/** Joins a root URL and a path with one slash between them. */
|
|
187
|
+
export function joinUrl(root, path) {
|
|
188
|
+
if (!path)
|
|
189
|
+
return root.replace(/\/+$/, "");
|
|
190
|
+
return `${root.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
|
191
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ApiResult, HttpMethod } from "../Interfaces/ApiInterfaces.ts";
|
|
2
|
+
import type { BuiltMessage, MessageContext } from "../Interfaces/MessageInterfaces.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Toasts for one call. `true` shows both, `false` none; or each on its own, with `true`, `false`, or a title of its own:
|
|
5
|
+
* `{ success: "Booking confirmed.", error: false, loading: true }`. `loading` shows a spinner toast while the call runs,
|
|
6
|
+
* which then turns into the success or error toast (or closes).
|
|
7
|
+
*/
|
|
8
|
+
export type ApiToastOption = boolean | {
|
|
9
|
+
success?: boolean | string;
|
|
10
|
+
error?: boolean | string;
|
|
11
|
+
loading?: boolean | string;
|
|
12
|
+
};
|
|
13
|
+
/** The app's defaults: an error toast for every failed call, a success toast for POST, PUT, PATCH, and DELETE. */
|
|
14
|
+
export interface ApiToastDefaults {
|
|
15
|
+
success?: boolean;
|
|
16
|
+
error?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** The toast for a call's answer, or `null` for none. */
|
|
19
|
+
export declare function planApiToast(result: ApiResult, method: HttpMethod, context: MessageContext, option?: ApiToastOption, defaults?: ApiToastDefaults): BuiltMessage | null;
|
|
20
|
+
/** The loading toast's text for a call, or `null` for none. */
|
|
21
|
+
export declare function planLoadingToast(context: MessageContext, option?: ApiToastOption): string | null;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { buildMessage, describeResult } from "./MessageBuilder.js";
|
|
2
|
+
/** The toast for a call's answer, or `null` for none. */
|
|
3
|
+
export function planApiToast(result, method, context, option, defaults = {}) {
|
|
4
|
+
if (result.aborted)
|
|
5
|
+
return null;
|
|
6
|
+
const choice = typeof option === "object" ? (result.ok ? option.success : option.error) : option;
|
|
7
|
+
const wanted = choice ?? (result.ok ? (defaults.success ?? true) && method !== "GET" : (defaults.error ?? true));
|
|
8
|
+
if (!wanted)
|
|
9
|
+
return null;
|
|
10
|
+
const built = describeResult(result, context);
|
|
11
|
+
if (!built || typeof choice !== "string")
|
|
12
|
+
return built;
|
|
13
|
+
// A title of the call's own; a failure keeps the reason under it.
|
|
14
|
+
return result.ok ? { tone: "success", title: choice } : { ...built, title: choice };
|
|
15
|
+
}
|
|
16
|
+
/** The loading toast's text for a call, or `null` for none. */
|
|
17
|
+
export function planLoadingToast(context, option) {
|
|
18
|
+
const loading = typeof option === "object" ? option.loading : undefined;
|
|
19
|
+
if (!loading)
|
|
20
|
+
return null;
|
|
21
|
+
return typeof loading === "string" ? loading : buildMessage("loading", context);
|
|
22
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { CrudMassActionName } from "../Interfaces/CrudInterfaces.ts";
|
|
2
|
+
import type { TableKey, TablePage, TableQuery, TableRow } from "../Interfaces/TableInterfaces.ts";
|
|
3
|
+
type Row = Record<string, unknown>;
|
|
4
|
+
export declare abstract class CrudBackend {
|
|
5
|
+
/** What a server table sends for one page, from the query. `sortKey` is the sorted column's server name. */
|
|
6
|
+
abstract pageRequest(query: TableQuery, sortKey: string | undefined): unknown;
|
|
7
|
+
/** Reads the page a pagination endpoint answered. */
|
|
8
|
+
abstract readPage<TRow extends TableRow>(result: unknown): TablePage<TRow>;
|
|
9
|
+
/** Reads a whole list: an array, or the rows of a page. */
|
|
10
|
+
readList<TRow extends TableRow>(result: unknown): TRow[];
|
|
11
|
+
/** What a mass action sends: the selected keys, and for `description` the new text. */
|
|
12
|
+
abstract massRequest(action: CrudMassActionName, keys: TableKey[], values?: Row): unknown;
|
|
13
|
+
abstract isFlagged(row: TableRow): boolean;
|
|
14
|
+
/** The row with its flag set or cleared. */
|
|
15
|
+
abstract withFlag<TRow extends TableRow>(row: TRow, on: boolean): TRow;
|
|
16
|
+
abstract isPinned(row: TableRow): boolean;
|
|
17
|
+
/** The row with its pin set or cleared. */
|
|
18
|
+
abstract withPin<TRow extends TableRow>(row: TRow, on: boolean): TRow;
|
|
19
|
+
/**
|
|
20
|
+
* Changes an action's answer makes to rows, by key: a mass flag that answers each record's new status. Rows without
|
|
21
|
+
* an entry take the change the action stands for (a toggled flag). Default: none.
|
|
22
|
+
*/
|
|
23
|
+
answeredChanges(result: unknown, rowKey: string): Map<string, Row>;
|
|
24
|
+
/**
|
|
25
|
+
* The keys of the records an action changed, when its answer lists them: a mass trash where some records could not
|
|
26
|
+
* move. `null` when the answer does not say, and every record sent counts as changed. Default `null`.
|
|
27
|
+
*/
|
|
28
|
+
answeredKeys(result: unknown, rowKey: string): Set<string> | null;
|
|
29
|
+
}
|
|
30
|
+
/** The Nexus record status of a flagged record (`ERecordStatus.Flagged`). */
|
|
31
|
+
export declare const NEXUS_FLAGGED_STATUS = 15;
|
|
32
|
+
/** The Nexus record status of an active record (`ERecordStatus.Active`). */
|
|
33
|
+
export declare const NEXUS_ACTIVE_STATUS = 0;
|
|
34
|
+
/**
|
|
35
|
+
* The Nexus backend: pages as `{ searchKeyword, skipCount, maxCount, sorting, sortType }` answered with
|
|
36
|
+
* `{ results, totalCount, filterCount }`, newest first unless a column is sorted; mass actions send the ids
|
|
37
|
+
* (`{ description, accountIds }` for descriptions); a flagged record has `recordStatus` 15, a pinned one `isPinned`.
|
|
38
|
+
*/
|
|
39
|
+
export declare class NexusCrudBackend extends CrudBackend {
|
|
40
|
+
pageRequest(query: TableQuery, sortKey: string | undefined): unknown;
|
|
41
|
+
readPage<TRow extends TableRow>(result: unknown): TablePage<TRow>;
|
|
42
|
+
massRequest(action: CrudMassActionName, keys: TableKey[], values?: Row): unknown;
|
|
43
|
+
isFlagged(row: TableRow): boolean;
|
|
44
|
+
withFlag<TRow extends TableRow>(row: TRow, on: boolean): TRow;
|
|
45
|
+
isPinned(row: TableRow): boolean;
|
|
46
|
+
withPin<TRow extends TableRow>(row: TRow, on: boolean): TRow;
|
|
47
|
+
/** Mass answers list the records they changed, with their new `recordStatus` and `isPinned`. */
|
|
48
|
+
answeredChanges(result: unknown, rowKey: string): Map<string, Row>;
|
|
49
|
+
/** Mass answers list the records they changed (as records, or as ids); other answers say nothing. */
|
|
50
|
+
answeredKeys(result: unknown, rowKey: string): Set<string> | null;
|
|
51
|
+
}
|
|
52
|
+
/** Registers the app's backend conventions, once, beside its API modules. */
|
|
53
|
+
export declare function defineCrudBackend<T extends CrudBackend>(backend: T): T;
|
|
54
|
+
/** The registered backend conventions, or the Nexus ones. */
|
|
55
|
+
export declare function getCrudBackend(): CrudBackend;
|
|
56
|
+
export {};
|