nexus-shared 1.1.21 → 3.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/CHANGELOG.md +262 -0
- package/LICENSE +21 -0
- package/README.md +1000 -0
- package/dist/Client.Index.d.ts +55 -0
- package/dist/Client.Index.js +60 -0
- package/dist/Components/Chats/Chat.d.ts +28 -0
- package/dist/Components/Chats/Chat.js +19 -0
- package/dist/Components/Chats/ChatButton.d.ts +26 -0
- package/dist/Components/Chats/ChatButton.js +41 -0
- package/dist/Components/Chats/ChatComposer.d.ts +41 -0
- package/dist/Components/Chats/ChatComposer.js +177 -0
- package/dist/Components/Chats/ChatConversations.d.ts +35 -0
- package/dist/Components/Chats/ChatConversations.js +38 -0
- package/dist/Components/Chats/ChatPanel.d.ts +66 -0
- package/dist/Components/Chats/ChatPanel.js +94 -0
- package/dist/Components/Chats/ChatParts.d.ts +87 -0
- package/dist/Components/Chats/ChatParts.js +100 -0
- package/dist/Components/Chats/ChatThread.d.ts +22 -0
- package/dist/Components/Chats/ChatThread.js +96 -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 +242 -0
- package/dist/Components/Documents/SplitButton.d.ts +64 -0
- package/dist/Components/Documents/SplitButton.js +129 -0
- package/dist/Components/Documents/TabButtons.d.ts +80 -0
- package/dist/Components/Documents/TabButtons.js +38 -0
- package/dist/Components/Forms/ApiForm.d.ts +23 -0
- package/dist/Components/Forms/ApiForm.js +100 -0
- package/dist/Components/Forms/Crud.d.ts +17 -0
- package/dist/Components/Forms/Crud.js +708 -0
- package/dist/Components/Forms/CrudParts.d.ts +15 -0
- package/dist/Components/Forms/CrudParts.js +36 -0
- package/dist/Components/Forms/ExcelImport.d.ts +42 -0
- package/dist/Components/Forms/ExcelImport.js +190 -0
- package/dist/Components/Forms/Form.d.ts +52 -0
- package/dist/Components/Forms/Form.js +323 -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 +400 -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 +172 -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 +37 -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 +117 -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 +115 -0
- package/dist/Components/Inputs/RowsInput.js +308 -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 +27 -0
- package/dist/Components/Inputs/TabularForm.js +348 -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 +240 -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 +49 -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 +596 -0
- package/dist/Components/Viewers/DataTableColumns.d.ts +28 -0
- package/dist/Components/Viewers/DataTableColumns.js +243 -0
- package/dist/Components/Viewers/DataTableParts.d.ts +138 -0
- package/dist/Components/Viewers/DataTableParts.js +157 -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 +96 -0
- package/dist/Helpers/ApiClient.js +200 -0
- package/dist/Helpers/ApiFormHelpers.d.ts +49 -0
- package/dist/Helpers/ApiFormHelpers.js +120 -0
- package/dist/Helpers/ApiModules.d.ts +131 -0
- package/dist/Helpers/ApiModules.js +195 -0
- package/dist/Helpers/ApiResponses.d.ts +51 -0
- package/dist/Helpers/ApiResponses.js +250 -0
- package/dist/Helpers/ApiRoutes.d.ts +140 -0
- package/dist/Helpers/ApiRoutes.js +183 -0
- package/dist/Helpers/ApiToasts.d.ts +21 -0
- package/dist/Helpers/ApiToasts.js +22 -0
- package/dist/Helpers/ChatBackend.d.ts +134 -0
- package/dist/Helpers/ChatBackend.js +332 -0
- package/dist/Helpers/ChatHelpers.d.ts +189 -0
- package/dist/Helpers/ChatHelpers.js +486 -0
- package/dist/Helpers/ChatHooks.d.ts +81 -0
- package/dist/Helpers/ChatHooks.js +175 -0
- package/dist/Helpers/ChatStore.d.ts +132 -0
- package/dist/Helpers/ChatStore.js +394 -0
- package/dist/Helpers/ChatSummaries.d.ts +39 -0
- package/dist/Helpers/ChatSummaries.js +118 -0
- package/dist/Helpers/ClassHelpers.d.ts +3 -0
- package/dist/Helpers/ClassHelpers.js +4 -0
- package/dist/Helpers/CrudBackend.d.ts +98 -0
- package/dist/Helpers/CrudBackend.js +214 -0
- package/dist/Helpers/CrudHelpers.d.ts +111 -0
- package/dist/Helpers/CrudHelpers.js +267 -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/ExcelBackend.d.ts +106 -0
- package/dist/Helpers/ExcelBackend.js +290 -0
- package/dist/Helpers/ExcelHelpers.d.ts +98 -0
- package/dist/Helpers/ExcelHelpers.js +302 -0
- package/dist/Helpers/FormDrafts.d.ts +18 -0
- package/dist/Helpers/FormDrafts.js +76 -0
- package/dist/Helpers/FormStore.d.ts +233 -0
- package/dist/Helpers/FormStore.js +752 -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 +252 -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 +68 -0
- package/dist/Helpers/PopoverHelpers.js +195 -0
- package/dist/Helpers/RichTextHelpers.d.ts +77 -0
- package/dist/Helpers/RichTextHelpers.js +1072 -0
- package/dist/Helpers/RowsStore.d.ts +166 -0
- package/dist/Helpers/RowsStore.js +606 -0
- package/dist/Helpers/TableColumns.d.ts +47 -0
- package/dist/Helpers/TableColumns.js +282 -0
- package/dist/Helpers/TableExport.d.ts +30 -0
- package/dist/Helpers/TableExport.js +97 -0
- package/dist/Helpers/TableHelpers.d.ts +110 -0
- package/dist/Helpers/TableHelpers.js +301 -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 +174 -0
- package/dist/Interfaces/ApiInterfaces.js +30 -0
- package/dist/Interfaces/ChatInterfaces.d.ts +240 -0
- package/{src/sso-client.tsx → dist/Interfaces/ChatInterfaces.js} +1 -1
- package/dist/Interfaces/CrudInterfaces.d.ts +232 -0
- package/dist/Interfaces/CrudInterfaces.js +1 -0
- package/dist/Interfaces/DateInterfaces.d.ts +154 -0
- package/dist/Interfaces/DateInterfaces.js +29 -0
- package/dist/Interfaces/ExcelInterfaces.d.ts +215 -0
- package/dist/Interfaces/ExcelInterfaces.js +45 -0
- package/dist/Interfaces/FormInterfaces.d.ts +402 -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 +351 -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 +95 -0
- package/dist/Services/BrowserApi.js +129 -0
- package/dist/Services/ChatApi.d.ts +37 -0
- package/dist/Services/ChatApi.js +98 -0
- package/dist/Services/ExcelApi.d.ts +46 -0
- package/dist/Services/ExcelApi.js +196 -0
- package/dist/Services/ServerApi.d.ts +54 -0
- package/dist/Services/ServerApi.js +78 -0
- package/dist/Services/ThemeService.d.ts +17 -0
- package/dist/Services/ThemeService.js +83 -0
- package/dist/Shared.Index.d.ts +51 -0
- package/dist/Shared.Index.js +55 -0
- package/package.json +85 -33
- package/src/Styles/Nexus.Base.css +70 -0
- package/src/Styles/Nexus.Button.css +153 -0
- package/src/Styles/Nexus.Calendar.css +275 -0
- package/src/Styles/Nexus.Chat.css +1489 -0
- package/src/Styles/Nexus.Choice.css +345 -0
- package/src/Styles/Nexus.Crud.css +161 -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.Excel.css +370 -0
- package/src/Styles/Nexus.Form.css +321 -0
- package/src/Styles/Nexus.Index.css +36 -0
- package/src/Styles/Nexus.Input.css +637 -0
- package/src/Styles/Nexus.Markdown.css +265 -0
- package/src/Styles/Nexus.Menu.css +259 -0
- package/src/Styles/Nexus.Popup.css +731 -0
- package/src/Styles/Nexus.RichText.css +463 -0
- package/src/Styles/Nexus.Rows.css +596 -0
- package/src/Styles/Nexus.Slider.css +278 -0
- package/src/Styles/Nexus.Tab.Buttons.css +419 -0
- package/src/Styles/Nexus.Table.css +1296 -0
- package/src/Styles/Nexus.Theme.Picker.css +232 -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 -29
- package/src/client.ts +0 -31
- package/src/components/documents/button.tsx +0 -133
- 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 -64
- 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 -123
- 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 -53
- 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 -163
- 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.grid-view.css +0 -42
- 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,708 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { iconEye, iconFlag, iconFlagOff, iconList, iconLock, iconNotes, iconPencil, iconPin, iconPinnedOff, iconPlus, iconRestore, iconTrash, iconTrashX, InlineIcon, } from "nexus-icons";
|
|
4
|
+
import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
5
|
+
import { abortError } from "../../Helpers/ApiFormHelpers.js";
|
|
6
|
+
import { unwrapApiResult } from "../../Helpers/ApiResponses.js";
|
|
7
|
+
import { getCrudBackend } from "../../Helpers/CrudBackend.js";
|
|
8
|
+
import { actionAllowed, availableModes, CRUD_CACHE_LIFETIME, CRUD_MODES, CRUD_ROW_ACTIONS, CrudCache, crudCacheMode, crudListEndpoint, crudMassActions, crudMessageAction, crudRowActions, crudRowEndpoint, getCrudSettings, leavesList, massActionAllowed, patchCachedList, pinRoom, } from "../../Helpers/CrudHelpers.js";
|
|
9
|
+
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
10
|
+
import { FormStore } from "../../Helpers/FormStore.js";
|
|
11
|
+
import { pluralize } from "../../Helpers/MessageBuilder.js";
|
|
12
|
+
import { getPermissions, permissionsVersion, subscribePermissions } from "../../Helpers/Permissions.js";
|
|
13
|
+
import { columnValue, formatCellText, readPath } from "../../Helpers/TableHelpers.js";
|
|
14
|
+
import { api as browserApi } from "../../Services/BrowserApi.js";
|
|
15
|
+
import { Button } from "../Documents/Button.js";
|
|
16
|
+
import { TabButtons } from "../Documents/TabButtons.js";
|
|
17
|
+
import { openPopup } from "../Layouts/PopupHost.js";
|
|
18
|
+
import { toast } from "../Layouts/Toaster.js";
|
|
19
|
+
import { DataTable, useTable } from "../Viewers/DataTable.js";
|
|
20
|
+
import { ApiForm } from "./ApiForm.js";
|
|
21
|
+
import { CrudDetails } from "./CrudParts.js";
|
|
22
|
+
import { confirmDiscard, FormButtons } from "./SubmitForm.js";
|
|
23
|
+
/*
|
|
24
|
+
* Crud: a module's records in one component. It takes a controller's endpoints, the table's columns, and the form's
|
|
25
|
+
* inputs, and handles the rest: the lists (all, flagged, trash, deleted) the user may see, the actions they
|
|
26
|
+
* may take, the records the user pinned in a section above the list, Add and Edit popups built on ApiForm, a details
|
|
27
|
+
* popup, actions on many rows, a cache of lists while the page is open, and rows updated from each answer instead of
|
|
28
|
+
* reloading.
|
|
29
|
+
*/
|
|
30
|
+
const ALL_ACTIONS = ["create", "view", "edit", "flag", "pin", "trash", "recover", "delete", "trashFromDelete", "description"];
|
|
31
|
+
const MODE_INFO = {
|
|
32
|
+
normal: { label: "All", icon: iconList, empty: plural => `No ${plural} yet.` },
|
|
33
|
+
flag: { label: "Flagged", icon: iconFlag, empty: plural => `No flagged ${plural}.` },
|
|
34
|
+
trash: { label: "Trash", icon: iconTrash, empty: () => "The trash is empty." },
|
|
35
|
+
deleted: { label: "Deleted", icon: iconTrashX, empty: plural => `No deleted ${plural}.` },
|
|
36
|
+
};
|
|
37
|
+
/** Actions that take a record out of reach are asked about first; flags, pins and recover are undone by a click. */
|
|
38
|
+
const ASKED = new Set(["trash", "delete", "trashFromDelete"]);
|
|
39
|
+
/** Actions that take a record out of the active lists, so it is no longer among the pinned records either. */
|
|
40
|
+
const LEFT_ACTIVE = new Set(["trash", "delete", "trashFromDelete"]);
|
|
41
|
+
/** The words of an action that flags some records and unflags others (a mass toggle of a mixed selection). */
|
|
42
|
+
const TOGGLED_FLAG = { verb: "change the flags of {object}", done: "updated", doing: "Changing the flags of {object}", object: "the records" };
|
|
43
|
+
const capitalize = (text) => (text ? text[0].toUpperCase() + text.slice(1) : text);
|
|
44
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
45
|
+
function isInputList(inputs) {
|
|
46
|
+
return Array.isArray(inputs);
|
|
47
|
+
}
|
|
48
|
+
/** The subject the endpoints were given: `apiController(module, "Guest", "guest")` → "guest". */
|
|
49
|
+
function endpointSubject(api) {
|
|
50
|
+
const endpoints = [api.create, api.update, api.details, ...Object.values(api.lists ?? {}), ...Object.values(api.pages ?? {})];
|
|
51
|
+
return endpoints.find(endpoint => endpoint?.subject)?.subject;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The body of an Add, Edit or details popup. Without an `aside` it is the form (or the details) and nothing else, so
|
|
55
|
+
* a popup that wants no panel carries no wrapper at all. With one, the two become columns that scroll on their own,
|
|
56
|
+
* and the form is first in the DOM — which is also the order they stack in when there is no room for two. A details
|
|
57
|
+
* popup is marked, because there the panel is what the popup is for and takes the larger share.
|
|
58
|
+
*/
|
|
59
|
+
function CrudFormBody({ aside, details, children }) {
|
|
60
|
+
if (!aside)
|
|
61
|
+
return children;
|
|
62
|
+
return (_jsxs("div", { className: cx("nx-crud-aside", details && "nx-crud-aside--details"), children: [_jsx("div", { className: "nx-crud-aside__main", children: children }), _jsx("aside", { className: "nx-crud-aside__side", children: aside })] }));
|
|
63
|
+
}
|
|
64
|
+
/** Whether the details endpoint's record says anything the list's row did not. Plain JSON, compared field by field. */
|
|
65
|
+
function addsToRow(row, full) {
|
|
66
|
+
return Object.entries(full).some(([name, value]) => JSON.stringify(value) !== JSON.stringify(row[name]));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* A CRUD page in one component: a data table of a controller's records with its lists, Add and Edit popups, details,
|
|
70
|
+
* and actions on one record or many, each shown only when its endpoint exists and the user may call it.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* const GUESTS = MODULES.controller(NexusModule.Reservation, "Guest", "guest");
|
|
74
|
+
*
|
|
75
|
+
* <Crud<Guest>
|
|
76
|
+
* api={GUESTS}
|
|
77
|
+
* columns={[{ key: "fullName", label: "Name" }, { key: "email", label: "Email" }, { key: "visits", label: "Visits", format: "number" }]}
|
|
78
|
+
* inputs={GUEST_INPUTS}
|
|
79
|
+
* paging="server"
|
|
80
|
+
* />
|
|
81
|
+
*/
|
|
82
|
+
export function Crud(props) {
|
|
83
|
+
const { api, columns, rowKey = "id", paging = "client", table: tableOptions, id, className, style, ref } = props;
|
|
84
|
+
const latest = useRef(props);
|
|
85
|
+
latest.current = props;
|
|
86
|
+
const subject = props.subject ?? endpointSubject(api) ?? "record";
|
|
87
|
+
const plural = props.subjectPlural ?? pluralize(subject);
|
|
88
|
+
const name = props.name ?? subject;
|
|
89
|
+
const backend = getCrudBackend();
|
|
90
|
+
// Permissions: every list and action asks, again whenever the user's rights change.
|
|
91
|
+
const permissionsStamp = useSyncExternalStore(subscribePermissions, permissionsVersion, permissionsVersion);
|
|
92
|
+
const permissions = getPermissions();
|
|
93
|
+
const ready = permissions.ready;
|
|
94
|
+
const wantedModes = props.modes ?? CRUD_MODES;
|
|
95
|
+
const wantedActions = props.actions ?? ALL_ACTIONS;
|
|
96
|
+
const can = useCallback((endpoint) => permissions.can(endpoint), [permissions, permissionsStamp]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
97
|
+
const modes = useMemo(() => (ready ? availableModes(api, paging, wantedModes, can) : []), [ready, api, paging, wantedModes, can]);
|
|
98
|
+
const allowed = useMemo(() => {
|
|
99
|
+
const map = {};
|
|
100
|
+
for (const action of ALL_ACTIONS)
|
|
101
|
+
map[action] = ready && actionAllowed(action, api, wantedActions, can);
|
|
102
|
+
return map;
|
|
103
|
+
}, [ready, api, wantedActions, can]);
|
|
104
|
+
const massAllowed = useMemo(() => {
|
|
105
|
+
const map = {};
|
|
106
|
+
for (const action of ["flag", "pin", "trash", "recover", "delete", "trashFromDelete", "description"])
|
|
107
|
+
map[action] = ready && massActionAllowed(action, api, wantedActions, can);
|
|
108
|
+
return map;
|
|
109
|
+
}, [ready, api, wantedActions, can]);
|
|
110
|
+
// A controller without a trash deletes from its lists; a trash the user may not use never becomes Delete.
|
|
111
|
+
const trashless = !api.trash || !wantedActions.includes("trash");
|
|
112
|
+
const massTrashless = !api.mass?.trash || !wantedActions.includes("trash");
|
|
113
|
+
const [chosenMode, setChosenMode] = useState(props.defaultMode ?? null);
|
|
114
|
+
const mode = chosenMode && modes.includes(chosenMode) ? chosenMode : modes[0];
|
|
115
|
+
const modeRef = useRef(mode);
|
|
116
|
+
modeRef.current = mode;
|
|
117
|
+
const table = useTable();
|
|
118
|
+
const lifetime = props.cache === false ? 0 : (props.cache?.lifetime ?? CRUD_CACHE_LIFETIME);
|
|
119
|
+
const [cache] = useState(() => new CrudCache(lifetime));
|
|
120
|
+
// The cache lives while the page is open, as the old CRUD's did.
|
|
121
|
+
useEffect(() => () => cache.clear(), [cache]);
|
|
122
|
+
const rawKey = (row) => readPath(row, rowKey);
|
|
123
|
+
const labelOf = useCallback((row) => {
|
|
124
|
+
const custom = latest.current.rowLabel;
|
|
125
|
+
if (custom)
|
|
126
|
+
return custom(row);
|
|
127
|
+
const first = latest.current.columns[0];
|
|
128
|
+
const text = first ? formatCellText(first, columnValue(first, row)) : "";
|
|
129
|
+
return text || `${capitalize(subject)} ${table.keyOf(row)}`;
|
|
130
|
+
}, [subject, table]);
|
|
131
|
+
/* The records the user pinned: their own endpoint, a section above the list, and a limit every CRUD shares */
|
|
132
|
+
const maxPins = getCrudSettings().maxPins;
|
|
133
|
+
const pinsEndpoint = api.pins;
|
|
134
|
+
const pinsOn = ready && Boolean(pinsEndpoint) && wantedActions.includes("pin") && can(pinsEndpoint);
|
|
135
|
+
const [pinned, setPinned] = useState(null);
|
|
136
|
+
const [pinsLoading, setPinsLoading] = useState(false);
|
|
137
|
+
const [pinsStamp, setPinsStamp] = useState(0);
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (!pinsOn || !pinsEndpoint) {
|
|
140
|
+
setPinned(null);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const controller = new AbortController();
|
|
144
|
+
setPinsLoading(true);
|
|
145
|
+
const done = (rows) => {
|
|
146
|
+
if (controller.signal.aborted)
|
|
147
|
+
return;
|
|
148
|
+
if (rows)
|
|
149
|
+
setPinned(rows);
|
|
150
|
+
setPinsLoading(false);
|
|
151
|
+
};
|
|
152
|
+
void browserApi.send(pinsEndpoint, { signal: controller.signal, toast: false, subject: plural }).then(answer => {
|
|
153
|
+
if (answer.aborted)
|
|
154
|
+
return;
|
|
155
|
+
// The rows come from the pinned list, so they are pinned whatever the backend sends on each one.
|
|
156
|
+
done(answer.ok ? backend.readList(answer.result).map(row => backend.withPin(row, true)) : []);
|
|
157
|
+
},
|
|
158
|
+
// The section is a shortcut to records the list holds as well, so a failed load leaves it out quietly.
|
|
159
|
+
() => done([]));
|
|
160
|
+
return () => controller.abort();
|
|
161
|
+
}, [pinsOn, pinsEndpoint, backend, plural, pinsStamp]);
|
|
162
|
+
const reloadPins = useCallback(() => setPinsStamp(stamp => stamp + 1), []);
|
|
163
|
+
/** The section after a pin or an unpin: records just pinned go on top, unpinned ones go. */
|
|
164
|
+
function applyPins(rows) {
|
|
165
|
+
setPinned(current => {
|
|
166
|
+
if (!current)
|
|
167
|
+
return current;
|
|
168
|
+
const changed = new Map(rows.map(row => [table.keyOf(row), row]));
|
|
169
|
+
const kept = current.map(row => changed.get(table.keyOf(row)) ?? row).filter(row => backend.isPinned(row));
|
|
170
|
+
const keys = new Set(kept.map(row => table.keyOf(row)));
|
|
171
|
+
return [...rows.filter(row => backend.isPinned(row) && !keys.has(table.keyOf(row))), ...kept];
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
/** A pinned record's copy in the section, after a change that was not a pin: a flag, a description, a save. */
|
|
175
|
+
function patchPins(rows) {
|
|
176
|
+
setPinned(current => {
|
|
177
|
+
if (!current)
|
|
178
|
+
return current;
|
|
179
|
+
const changed = new Map(rows.map(row => [table.keyOf(row), row]));
|
|
180
|
+
if (!current.some(row => changed.has(table.keyOf(row))))
|
|
181
|
+
return current;
|
|
182
|
+
// The record is still pinned: only its other values changed, and a list may not send the pin at all.
|
|
183
|
+
return current.map(row => {
|
|
184
|
+
const next = changed.get(table.keyOf(row));
|
|
185
|
+
return next ? backend.withPin(next, true) : row;
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/** Records that left the active lists are no longer among the pinned ones. */
|
|
190
|
+
function dropPins(keys) {
|
|
191
|
+
if (!keys.length)
|
|
192
|
+
return;
|
|
193
|
+
const gone = new Set(keys);
|
|
194
|
+
setPinned(current => (current && current.some(row => gone.has(table.keyOf(row))) ? current.filter(row => !gone.has(table.keyOf(row))) : current));
|
|
195
|
+
}
|
|
196
|
+
/** Nothing can be pinned while the limit is reached: lowering the limit never unpins, it only waits to be met. */
|
|
197
|
+
const pinsFull = () => `You can pin up to ${maxPins} ${plural}. Unpin one to pin another.`;
|
|
198
|
+
/* Loading */
|
|
199
|
+
const load = useCallback((query, signal) => {
|
|
200
|
+
if (!mode)
|
|
201
|
+
return new Promise(() => undefined);
|
|
202
|
+
const endpoint = crudListEndpoint(api, mode, paging);
|
|
203
|
+
const sortColumn = query.sort ? latest.current.columns.find(column => column.key === query.sort.key) : undefined;
|
|
204
|
+
const key = paging === "server" ? `${mode}|${query.search}|${query.sort ? `${query.sort.key}:${query.sort.direction}` : ""}|${query.page}|${query.pageSize}` : mode;
|
|
205
|
+
const kept = cache.get(key);
|
|
206
|
+
if (kept)
|
|
207
|
+
return kept;
|
|
208
|
+
const options = { signal, toast: false, subject: plural };
|
|
209
|
+
if (paging === "server")
|
|
210
|
+
options.body = backend.pageRequest(query, sortColumn?.sortKey ?? sortColumn?.key);
|
|
211
|
+
return browserApi.send(endpoint, options).then(answer => {
|
|
212
|
+
if (answer.aborted)
|
|
213
|
+
throw abortError();
|
|
214
|
+
if (!answer.ok)
|
|
215
|
+
throw new Error(answer.message ?? `Could not load the ${plural}.`);
|
|
216
|
+
const result = paging === "server" ? backend.readPage(answer.result) : backend.readList(answer.result);
|
|
217
|
+
cache.set(key, result);
|
|
218
|
+
return result;
|
|
219
|
+
});
|
|
220
|
+
}, [mode, api, paging, cache, backend, plural]);
|
|
221
|
+
/** Which list a record belongs in, as this backend reads the record. */
|
|
222
|
+
const member = (row, of) => backend.listMembership(row, of);
|
|
223
|
+
/**
|
|
224
|
+
* The lists kept in the cache, after a change: **every one of them is patched**, from the record the answer
|
|
225
|
+
* carried. A record flagged joins the flagged list, a record trashed leaves the active list and joins the trash,
|
|
226
|
+
* and coming back to any of them costs no request. Only a list the record cannot answer for (`changes` empty, or
|
|
227
|
+
* a backend with no `listMembership`) is dropped, and that one loads when it is shown.
|
|
228
|
+
*
|
|
229
|
+
* A server page is not patched when a record joined it or left it: its order, its last row and its counts are the
|
|
230
|
+
* server's, so that page is dropped and the next one loads. One whose rows only changed is written in place.
|
|
231
|
+
*/
|
|
232
|
+
function syncCaches(changes) {
|
|
233
|
+
const shown = modeRef.current;
|
|
234
|
+
if (changes.length)
|
|
235
|
+
cache.map((key, value) => patchCachedList(value, crudCacheMode(key), changes, row => table.keyOf(row), member));
|
|
236
|
+
else
|
|
237
|
+
cache.clear();
|
|
238
|
+
// The list on screen is the table's own, every row of it, so it is written back whole rather than patched.
|
|
239
|
+
if (paging === "client" && shown)
|
|
240
|
+
cache.update(shown, table.getRows());
|
|
241
|
+
}
|
|
242
|
+
function afterChange(action, rows, changes) {
|
|
243
|
+
syncCaches(changes);
|
|
244
|
+
const shown = modeRef.current;
|
|
245
|
+
if (shown)
|
|
246
|
+
latest.current.onChange?.({ action, rows, mode: shown });
|
|
247
|
+
}
|
|
248
|
+
function applyChange(action, rows, options = {}) {
|
|
249
|
+
const { result, mass = false, markOn, values } = options;
|
|
250
|
+
const shown = modeRef.current;
|
|
251
|
+
if (!shown)
|
|
252
|
+
return;
|
|
253
|
+
const answered = rows.length > 1 ? backend.answeredKeys(result, rowKey) : null;
|
|
254
|
+
const done = answered ? rows.filter(row => answered.has(table.keyOf(row))) : rows;
|
|
255
|
+
const changes = backend.answeredChanges(result, rowKey);
|
|
256
|
+
// What the answer said about the one record acted on: the record, or the fields that changed - a mark's new
|
|
257
|
+
// status. It is what lets the other lists be patched rather than thrown away.
|
|
258
|
+
const one = mass ? null : backend.answeredRow(result);
|
|
259
|
+
const moved = [];
|
|
260
|
+
if (action === "flag") {
|
|
261
|
+
const leaving = [];
|
|
262
|
+
const updated = [];
|
|
263
|
+
for (const row of done) {
|
|
264
|
+
const key = table.keyOf(row);
|
|
265
|
+
const current = table.getRow(key) ?? row;
|
|
266
|
+
const on = markOn ?? !backend.isFlagged(current);
|
|
267
|
+
let next = backend.withFlag(current, on);
|
|
268
|
+
const change = changes.get(key);
|
|
269
|
+
if (change)
|
|
270
|
+
next = { ...next, ...change };
|
|
271
|
+
if (one)
|
|
272
|
+
next = { ...next, ...one };
|
|
273
|
+
if (leavesList(action, shown, backend.isFlagged(next)))
|
|
274
|
+
leaving.push(key);
|
|
275
|
+
else
|
|
276
|
+
table.upsert(next, { flash: false });
|
|
277
|
+
updated.push(next);
|
|
278
|
+
moved.push({ key, row: next, before: current });
|
|
279
|
+
}
|
|
280
|
+
table.remove(leaving);
|
|
281
|
+
patchPins(updated);
|
|
282
|
+
afterChange(action, updated, moved);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (action === "pin") {
|
|
286
|
+
const updated = [];
|
|
287
|
+
for (const row of done) {
|
|
288
|
+
const key = table.keyOf(row);
|
|
289
|
+
const current = table.getRow(key) ?? row;
|
|
290
|
+
const on = markOn ?? !backend.isPinned(current);
|
|
291
|
+
let next = backend.withPin(current, on);
|
|
292
|
+
const change = changes.get(key);
|
|
293
|
+
if (change)
|
|
294
|
+
next = { ...next, ...change };
|
|
295
|
+
if (one)
|
|
296
|
+
next = { ...next, ...one };
|
|
297
|
+
// A pin changes the section above the list, never the list itself: the record stays where it is, on its page.
|
|
298
|
+
if (table.getRow(key))
|
|
299
|
+
table.upsert(next, { flash: false });
|
|
300
|
+
updated.push(next);
|
|
301
|
+
moved.push({ key, row: next, before: current });
|
|
302
|
+
}
|
|
303
|
+
applyPins(updated);
|
|
304
|
+
afterChange(action, updated, moved);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (action === "description") {
|
|
308
|
+
const updated = [];
|
|
309
|
+
for (const row of done) {
|
|
310
|
+
const key = table.keyOf(row);
|
|
311
|
+
const current = table.getRow(key) ?? row;
|
|
312
|
+
const next = { ...current, ...values };
|
|
313
|
+
table.upsert(next);
|
|
314
|
+
updated.push(next);
|
|
315
|
+
moved.push({ key, row: next, before: current });
|
|
316
|
+
}
|
|
317
|
+
patchPins(updated);
|
|
318
|
+
afterChange(action, updated, moved);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
// Trash, recover, delete and the move back to the trash take the record to another band, and only the answer
|
|
322
|
+
// says which. Every record has to say so for the other lists to be patched; one that does not leaves `moved`
|
|
323
|
+
// empty, and they load again, which is what they all did before.
|
|
324
|
+
const keys = done.map(row => table.keyOf(row));
|
|
325
|
+
const updated = [];
|
|
326
|
+
for (const row of done) {
|
|
327
|
+
const key = table.keyOf(row);
|
|
328
|
+
const current = table.getRow(key) ?? row;
|
|
329
|
+
const change = changes.get(key) ?? one;
|
|
330
|
+
const next = change ? { ...current, ...change } : current;
|
|
331
|
+
if (change)
|
|
332
|
+
moved.push({ key, row: next, before: current });
|
|
333
|
+
updated.push(next);
|
|
334
|
+
}
|
|
335
|
+
if (moved.length !== done.length)
|
|
336
|
+
moved.length = 0;
|
|
337
|
+
if (leavesList(action, shown))
|
|
338
|
+
table.remove(keys);
|
|
339
|
+
if (LEFT_ACTIVE.has(action))
|
|
340
|
+
dropPins(keys);
|
|
341
|
+
afterChange(action, updated, moved);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Records a page's own action changed, written exactly where the built-in ones write theirs. The handle's `apply`.
|
|
345
|
+
*/
|
|
346
|
+
function applyRows(rows, gone = false) {
|
|
347
|
+
const shown = modeRef.current;
|
|
348
|
+
if (!shown || rows.length === 0)
|
|
349
|
+
return;
|
|
350
|
+
const moved = [];
|
|
351
|
+
const updated = [];
|
|
352
|
+
const leaving = [];
|
|
353
|
+
let pinned = false;
|
|
354
|
+
for (const given of rows) {
|
|
355
|
+
const next = { ...(table.getRow(table.keyOf(given)) ?? {}), ...given };
|
|
356
|
+
const key = table.keyOf(next);
|
|
357
|
+
if (!key)
|
|
358
|
+
continue;
|
|
359
|
+
const before = table.getRow(key);
|
|
360
|
+
// The record decides whether it is still in the list on screen; a backend that cannot say keeps it there.
|
|
361
|
+
if (gone || member(next, shown) === false)
|
|
362
|
+
leaving.push(key);
|
|
363
|
+
else if (before)
|
|
364
|
+
table.upsert(next, { flash: false });
|
|
365
|
+
if (before && backend.isPinned(next) !== backend.isPinned(before))
|
|
366
|
+
pinned = true;
|
|
367
|
+
updated.push(next);
|
|
368
|
+
moved.push({ key, row: next, before, gone });
|
|
369
|
+
}
|
|
370
|
+
table.remove(leaving);
|
|
371
|
+
if (gone)
|
|
372
|
+
dropPins(leaving);
|
|
373
|
+
else if (pinned)
|
|
374
|
+
applyPins(updated);
|
|
375
|
+
else
|
|
376
|
+
patchPins(updated);
|
|
377
|
+
syncCaches(moved);
|
|
378
|
+
}
|
|
379
|
+
/* Row actions */
|
|
380
|
+
async function runRowAction(action, row) {
|
|
381
|
+
const endpoint = crudRowEndpoint(api, action);
|
|
382
|
+
if (!endpoint)
|
|
383
|
+
return;
|
|
384
|
+
const key = rawKey(row);
|
|
385
|
+
const markOn = action === "flag" ? !backend.isFlagged(row) : action === "pin" ? !backend.isPinned(row) : undefined;
|
|
386
|
+
if (action === "pin" && markOn && pinRoom(pinned?.length) < 1) {
|
|
387
|
+
toast.warning(pinsFull());
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const options = { params: { id: key, [rowKey]: key }, subject: labelOf(row), action: crudMessageAction(action, markOn) };
|
|
391
|
+
const answer = ASKED.has(action) ? await browserApi.confirm(endpoint, options) : await browserApi.send(endpoint, options);
|
|
392
|
+
if (answer?.ok)
|
|
393
|
+
applyChange(action, [row], { result: answer.result, markOn });
|
|
394
|
+
}
|
|
395
|
+
async function runMassAction(action, rows) {
|
|
396
|
+
if (action === "description") {
|
|
397
|
+
openDescription(rows);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const endpoint = api.mass?.[action];
|
|
401
|
+
if (!endpoint)
|
|
402
|
+
return;
|
|
403
|
+
let markOn;
|
|
404
|
+
let sent = rows;
|
|
405
|
+
let over = 0;
|
|
406
|
+
if (action === "flag") {
|
|
407
|
+
const on = rows.filter(row => backend.isFlagged(row)).length;
|
|
408
|
+
markOn = on === 0 ? true : on === rows.length ? false : undefined;
|
|
409
|
+
}
|
|
410
|
+
if (action === "pin") {
|
|
411
|
+
// Every selected record pinned means Unpin; otherwise the ones that are not pinned yet are pinned, as many as
|
|
412
|
+
// the limit leaves room for, in the order the user picked them. The rest stay as they are.
|
|
413
|
+
const adding = rows.filter(row => !backend.isPinned(row));
|
|
414
|
+
markOn = adding.length > 0;
|
|
415
|
+
if (markOn) {
|
|
416
|
+
const room = pinRoom(pinned?.length);
|
|
417
|
+
if (room < 1) {
|
|
418
|
+
toast.warning(pinsFull());
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
sent = adding.slice(0, room);
|
|
422
|
+
over = adding.length - sent.length;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
const messageAction = action === "flag" && markOn === undefined ? TOGGLED_FLAG : crudMessageAction(action, markOn);
|
|
426
|
+
const options = { body: backend.massRequest(action, sent.map(rawKey)), subject, plural: props.subjectPlural, count: sent.length, action: messageAction };
|
|
427
|
+
const answer = ASKED.has(action) ? await browserApi.confirm(endpoint, options) : await browserApi.send(endpoint, options);
|
|
428
|
+
if (!answer?.ok)
|
|
429
|
+
return;
|
|
430
|
+
applyChange(action, sent, { result: answer.result, mass: true, markOn });
|
|
431
|
+
table.clearSelection();
|
|
432
|
+
if (over > 0)
|
|
433
|
+
toast.warning(`${sent.length} of ${sent.length + over} ${plural} pinned: you can pin up to ${maxPins}.`);
|
|
434
|
+
}
|
|
435
|
+
/* Popups */
|
|
436
|
+
const addPopup = useRef(null);
|
|
437
|
+
async function inputsFor(action, row) {
|
|
438
|
+
const given = latest.current.inputs;
|
|
439
|
+
if (typeof given === "function")
|
|
440
|
+
return given(action, row);
|
|
441
|
+
if (isInputList(given))
|
|
442
|
+
return given;
|
|
443
|
+
return (action === "create" ? (given.create ?? given.edit) : (given.edit ?? given.create)) ?? [];
|
|
444
|
+
}
|
|
445
|
+
async function openForm(action, row, values) {
|
|
446
|
+
const endpoint = action === "create" ? api.create : api.update;
|
|
447
|
+
if (!endpoint)
|
|
448
|
+
return;
|
|
449
|
+
// An Add form with changes comes back as it was; an untouched one is replaced by a fresh form.
|
|
450
|
+
if (action === "create" && addPopup.current?.form.isChanged()) {
|
|
451
|
+
addPopup.current.handle.restore();
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const inputs = await inputsFor(action, row);
|
|
455
|
+
const options = latest.current.form ?? {};
|
|
456
|
+
const form = new FormStore();
|
|
457
|
+
const key = row ? table.keyOf(row) : "";
|
|
458
|
+
const label = row ? labelOf(row) : undefined;
|
|
459
|
+
const newValues = typeof options.newValues === "function" ? options.newValues() : options.newValues;
|
|
460
|
+
const loader = options.load;
|
|
461
|
+
const loadValue = action === "edit" && row && loader && loader !== "row"
|
|
462
|
+
? loader === "details"
|
|
463
|
+
? api.details
|
|
464
|
+
? (signal) => browserApi.get(api.details, { params: { id: rawKey(row), [rowKey]: rawKey(row) }, signal, toast: false }).then(answer => ({ ...row, ...unwrapApiResult(answer) }))
|
|
465
|
+
: undefined
|
|
466
|
+
: (signal) => loader(row, signal).then(full => ({ ...row, ...full }))
|
|
467
|
+
: undefined;
|
|
468
|
+
const handle = openPopup({
|
|
469
|
+
key: action === "create" ? `${name}-new` : `${name}-edit-${key}`,
|
|
470
|
+
reopen: action === "create" ? "replace" : "restore",
|
|
471
|
+
title: action === "create" ? `Add ${subject}` : `Edit ${subject}`,
|
|
472
|
+
description: label,
|
|
473
|
+
icon: _jsx(InlineIcon, { icon: action === "create" ? iconPlus : iconPencil }),
|
|
474
|
+
size: options.size ?? (inputs.length > 8 ? "lg" : "md"),
|
|
475
|
+
closeOnBackdrop: false,
|
|
476
|
+
beforeClose: async () => {
|
|
477
|
+
const discard = await confirmDiscard(form);
|
|
478
|
+
if (discard)
|
|
479
|
+
form.dropDraft();
|
|
480
|
+
return discard;
|
|
481
|
+
},
|
|
482
|
+
content: popup => (_jsx(CrudFormBody, { aside: typeof options.aside === "function" ? options.aside({ form, action, row }) : options.aside, children: _jsx(ApiForm, { form: form, inputs: inputs, buttons: false, defaultValue: (action === "create" ? { ...newValues, ...values } : (row ?? undefined)), loadValue: loadValue, api: endpoint, transform: options.transform ? formValues => options.transform(formValues, { action, row }) : undefined, validate: options.validate, onFieldChange: options.onFieldChange, design: options.design, draftKey: options.draftKey ? `${options.draftKey}:${name}:${action === "create" ? "new" : key}` : undefined, "aria-label": action === "create" ? `Add ${subject}` : `Edit ${label}`, onSuccess: (result, saved) => {
|
|
483
|
+
onSaved(action, row, result, saved);
|
|
484
|
+
popup.close();
|
|
485
|
+
} }) })),
|
|
486
|
+
footer: popup => _jsx(FormButtons, { form: form, submitLabel: action === "create" ? `Add ${subject}` : "Save", onCancel: () => popup.close() }),
|
|
487
|
+
});
|
|
488
|
+
if (action === "create") {
|
|
489
|
+
const entry = { handle, form };
|
|
490
|
+
addPopup.current = entry;
|
|
491
|
+
void handle.closed.then(() => {
|
|
492
|
+
if (addPopup.current === entry)
|
|
493
|
+
addPopup.current = null;
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* The record a `Create` answered only the key of, read back on its own: the row goes in at once with what the form
|
|
499
|
+
* sent, and the fields the server owns - its status, its times, anything it worked out - land a moment later. One
|
|
500
|
+
* small request, where loading the list again was a request for every record on it.
|
|
501
|
+
*/
|
|
502
|
+
async function fillSaved(key, record) {
|
|
503
|
+
const endpoint = api.details;
|
|
504
|
+
if (!endpoint)
|
|
505
|
+
return;
|
|
506
|
+
const id = readPath(record, rowKey);
|
|
507
|
+
const answer = await browserApi.get(endpoint, { params: { id, [rowKey]: id }, toast: false });
|
|
508
|
+
const full = unwrapApiResult(answer);
|
|
509
|
+
if (!isRecord(full))
|
|
510
|
+
return;
|
|
511
|
+
const before = table.getRow(key);
|
|
512
|
+
const next = { ...(before ?? record), ...full };
|
|
513
|
+
if (before)
|
|
514
|
+
table.upsert(next, { flash: false });
|
|
515
|
+
syncCaches([{ key, row: next, before }]);
|
|
516
|
+
}
|
|
517
|
+
function onSaved(action, row, result, values) {
|
|
518
|
+
// The record as the server now has it: what was sent, over the row it came from, with whatever the answer added.
|
|
519
|
+
// A backend that answers the record it wrote leaves nothing to load at all.
|
|
520
|
+
const answered = backend.answeredRow(result);
|
|
521
|
+
const record = { ...row, ...values, ...answered };
|
|
522
|
+
const shown = modeRef.current;
|
|
523
|
+
if (action === "edit") {
|
|
524
|
+
const key = table.keyOf(record);
|
|
525
|
+
const before = table.getRow(key);
|
|
526
|
+
table.upsert(record);
|
|
527
|
+
patchPins([record]);
|
|
528
|
+
afterChange("edit", [record], key ? [{ key, row: record, before }] : []);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
// A new record belongs to the list of every record. An answer that is only the new key still gives it one, so
|
|
532
|
+
// the row goes in and the record is read back on its own; without even a key the list has to load again.
|
|
533
|
+
if (!answered && !readPath(record, rowKey)) {
|
|
534
|
+
const given = backend.answeredKey(result);
|
|
535
|
+
if (given !== null && !rowKey.includes("."))
|
|
536
|
+
record[rowKey] = given;
|
|
537
|
+
}
|
|
538
|
+
const key = table.keyOf(record);
|
|
539
|
+
if (shown === "normal") {
|
|
540
|
+
if (key)
|
|
541
|
+
table.upsert(record);
|
|
542
|
+
else
|
|
543
|
+
void table.reload({ quiet: true });
|
|
544
|
+
}
|
|
545
|
+
afterChange("create", [record], key ? [{ key, row: record, before: undefined }] : []);
|
|
546
|
+
if (key && !answered && api.details)
|
|
547
|
+
void fillSaved(key, record);
|
|
548
|
+
}
|
|
549
|
+
function openDetails(row) {
|
|
550
|
+
const details = latest.current.details;
|
|
551
|
+
if (details === false)
|
|
552
|
+
return;
|
|
553
|
+
const key = table.keyOf(row);
|
|
554
|
+
const canEdit = allowed.edit && modeRef.current !== undefined && CRUD_ROW_ACTIONS[modeRef.current].includes("edit");
|
|
555
|
+
const endpoint = api.details;
|
|
556
|
+
// The panel's record: the row at once, so it has something to show while the full one loads, and that one after.
|
|
557
|
+
const shown = typeof details?.aside === "function" ? new FormStore() : null;
|
|
558
|
+
shown?.load(row, { draft: false });
|
|
559
|
+
const aside = typeof details?.aside === "function" ? details.aside({ form: shown, row }) : details?.aside;
|
|
560
|
+
openPopup({
|
|
561
|
+
key: `${name}-view-${key}`,
|
|
562
|
+
reopen: "restore",
|
|
563
|
+
title: `${capitalize(subject)} ${details?.label ?? "details"}`,
|
|
564
|
+
description: labelOf(row),
|
|
565
|
+
icon: _jsx(InlineIcon, { icon: iconEye }),
|
|
566
|
+
size: details?.size ?? "md",
|
|
567
|
+
// Focus goes where it goes without a panel, the footer, rather than to the first button the panel happens to
|
|
568
|
+
// draw: a panel that renders at once (a Copy button) would otherwise take it, and one that loads would not.
|
|
569
|
+
initialFocus: aside ? ".nx-popup__foot button" : undefined,
|
|
570
|
+
content: (_jsx(CrudFormBody, { aside: aside, details: true, children: _jsx(CrudDetails, { row: row, columns: details?.columns ?? latest.current.columns, render: details?.render, load: endpoint
|
|
571
|
+
? signal => browserApi
|
|
572
|
+
.get(endpoint, { params: { id: rawKey(row), [rowKey]: rawKey(row) }, signal, toast: false })
|
|
573
|
+
.then(unwrapApiResult)
|
|
574
|
+
.then(full => {
|
|
575
|
+
// Only when it adds something: a panel that renders from the record would otherwise ask again
|
|
576
|
+
// for exactly what it is already showing.
|
|
577
|
+
if (shown && isRecord(full) && addsToRow(row, full))
|
|
578
|
+
shown.load({ ...row, ...full }, { draft: false });
|
|
579
|
+
return full;
|
|
580
|
+
})
|
|
581
|
+
: undefined }) })),
|
|
582
|
+
footer: popup => (_jsxs(_Fragment, { children: [canEdit && (_jsx(Button, { icon: _jsx(InlineIcon, { icon: iconPencil }), onClick: () => {
|
|
583
|
+
popup.close();
|
|
584
|
+
void openForm("edit", table.getRow(key) ?? row);
|
|
585
|
+
}, children: "Edit" })), _jsx(Button, { variant: "primary", onClick: () => popup.close(), children: "Close" })] })),
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
function openDescription(rows) {
|
|
589
|
+
const endpoint = api.mass?.description;
|
|
590
|
+
if (!endpoint)
|
|
591
|
+
return;
|
|
592
|
+
const form = new FormStore();
|
|
593
|
+
const count = rows.length;
|
|
594
|
+
openPopup({
|
|
595
|
+
key: `${name}-description`,
|
|
596
|
+
title: "Description",
|
|
597
|
+
description: `${count} ${count === 1 ? subject : plural}`,
|
|
598
|
+
icon: _jsx(InlineIcon, { icon: iconNotes }),
|
|
599
|
+
size: "sm",
|
|
600
|
+
closeOnBackdrop: false,
|
|
601
|
+
beforeClose: () => confirmDiscard(form),
|
|
602
|
+
content: popup => (_jsx(ApiForm, { form: form, buttons: false, inputs: [{ type: "textarea", name: "description", label: "Description", maxLength: 500, rows: 4, showCount: true }], api: endpoint, transform: values => backend.massRequest("description", rows.map(rawKey), values), "aria-label": `Description of ${count} ${count === 1 ? subject : plural}`, onSuccess: (result, values) => {
|
|
603
|
+
applyChange("description", rows, { result, mass: true, values });
|
|
604
|
+
table.clearSelection();
|
|
605
|
+
popup.close();
|
|
606
|
+
} })),
|
|
607
|
+
footer: popup => _jsx(FormButtons, { form: form, submitLabel: "Save", onCancel: () => popup.close() }),
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
// Row actions and the handle call these through a ref, so the table's callbacks stay the same between renders.
|
|
611
|
+
const run = useRef({ runRowAction, runMassAction, openForm, openDetails, applyRows });
|
|
612
|
+
run.current = { runRowAction, runMassAction, openForm, openDetails, applyRows };
|
|
613
|
+
/* The table's callbacks */
|
|
614
|
+
const rowActions = useCallback((row) => {
|
|
615
|
+
if (!mode)
|
|
616
|
+
return [];
|
|
617
|
+
const list = [];
|
|
618
|
+
const primary = mode === "normal" || mode === "flag";
|
|
619
|
+
const flagged = backend.isFlagged(row);
|
|
620
|
+
const isPinned = backend.isPinned(row);
|
|
621
|
+
const info = {
|
|
622
|
+
view: { label: `View ${(latest.current.details || undefined)?.label ?? "details"}`, icon: _jsx(InlineIcon, { icon: iconEye }) },
|
|
623
|
+
edit: { label: "Edit", icon: _jsx(InlineIcon, { icon: iconPencil }), inline: true },
|
|
624
|
+
flag: flagged ? { label: "Unflag", icon: _jsx(InlineIcon, { icon: iconFlagOff }) } : { label: "Flag", icon: _jsx(InlineIcon, { icon: iconFlag }) },
|
|
625
|
+
pin: isPinned ? { label: "Unpin", icon: _jsx(InlineIcon, { icon: iconPinnedOff }) } : { label: "Pin", icon: _jsx(InlineIcon, { icon: iconPin }) },
|
|
626
|
+
trash: { label: "Move to trash", icon: _jsx(InlineIcon, { icon: iconTrash }), tone: "danger" },
|
|
627
|
+
recover: { label: "Recover", description: "Back to the active list", icon: _jsx(InlineIcon, { icon: iconRestore }), inline: true },
|
|
628
|
+
delete: { label: "Delete permanently", icon: _jsx(InlineIcon, { icon: iconTrashX }), tone: "danger" },
|
|
629
|
+
trashFromDelete: { label: "Move back to trash", icon: _jsx(InlineIcon, { icon: iconRestore }), inline: true },
|
|
630
|
+
};
|
|
631
|
+
for (const action of crudRowActions(mode, candidate => allowed[candidate], trashless)) {
|
|
632
|
+
if (action === "view" && latest.current.details === false)
|
|
633
|
+
continue;
|
|
634
|
+
const onSelect = action === "view" ? () => run.current.openDetails(row) : action === "edit" ? () => void run.current.openForm("edit", row) : () => run.current.runRowAction(action, row);
|
|
635
|
+
const entry = info[action];
|
|
636
|
+
list.push({ ...entry, id: action, inline: entry.inline && (primary ? action === "edit" : true), onSelect });
|
|
637
|
+
}
|
|
638
|
+
const extra = latest.current.rowActions?.(row, mode) ?? [];
|
|
639
|
+
return [...list, ...extra];
|
|
640
|
+
}, [mode, allowed, backend, trashless]);
|
|
641
|
+
const massActionsFor = useCallback((rows) => {
|
|
642
|
+
if (!mode)
|
|
643
|
+
return [];
|
|
644
|
+
const list = [];
|
|
645
|
+
const flagged = rows.filter(row => backend.isFlagged(row)).length;
|
|
646
|
+
const unpinned = rows.filter(row => !backend.isPinned(row)).length;
|
|
647
|
+
for (const action of crudMassActions(mode, candidate => massAllowed[candidate], massTrashless)) {
|
|
648
|
+
const onSelect = (selected) => run.current.runMassAction(action, selected);
|
|
649
|
+
const info = {
|
|
650
|
+
flag: flagged === rows.length ? { label: "Unflag", icon: _jsx(InlineIcon, { icon: iconFlagOff }) } : { label: flagged ? "Flag / unflag" : "Flag", icon: _jsx(InlineIcon, { icon: iconFlag }) },
|
|
651
|
+
pin: unpinned === 0 ? { label: "Unpin", icon: _jsx(InlineIcon, { icon: iconPinnedOff }) } : { label: "Pin", icon: _jsx(InlineIcon, { icon: iconPin }) },
|
|
652
|
+
trash: { label: "Move to trash", icon: _jsx(InlineIcon, { icon: iconTrash }), tone: "danger" },
|
|
653
|
+
recover: { label: "Recover", icon: _jsx(InlineIcon, { icon: iconRestore }) },
|
|
654
|
+
delete: { label: "Delete permanently", icon: _jsx(InlineIcon, { icon: iconTrashX }), tone: "danger" },
|
|
655
|
+
trashFromDelete: { label: "Move back to trash", icon: _jsx(InlineIcon, { icon: iconRestore }) },
|
|
656
|
+
description: { label: "Description", icon: _jsx(InlineIcon, { icon: iconNotes }) },
|
|
657
|
+
};
|
|
658
|
+
list.push({ ...info[action], id: action, onSelect });
|
|
659
|
+
}
|
|
660
|
+
const extra = latest.current.massActions ? latest.current.massActions(rows, mode) : [];
|
|
661
|
+
return [...list, ...extra];
|
|
662
|
+
}, [mode, massAllowed, backend, massTrashless]);
|
|
663
|
+
const onRowOpen = useCallback((row) => {
|
|
664
|
+
if (allowed.view && latest.current.details !== false)
|
|
665
|
+
run.current.openDetails(row);
|
|
666
|
+
else if (allowed.edit && mode && CRUD_ROW_ACTIONS[mode].includes("edit"))
|
|
667
|
+
void run.current.openForm("edit", row);
|
|
668
|
+
}, [allowed, mode]);
|
|
669
|
+
useImperativeHandle(ref, () => ({
|
|
670
|
+
table,
|
|
671
|
+
get mode() {
|
|
672
|
+
return modeRef.current ?? "normal";
|
|
673
|
+
},
|
|
674
|
+
setMode: next => setChosenMode(next),
|
|
675
|
+
reload: () => {
|
|
676
|
+
cache.clear();
|
|
677
|
+
reloadPins();
|
|
678
|
+
return table.reload({ quiet: true });
|
|
679
|
+
},
|
|
680
|
+
apply: (rows, options) => run.current.applyRows(rows, options?.gone ?? false),
|
|
681
|
+
create: values => void run.current.openForm("create", null, values),
|
|
682
|
+
edit: row => void run.current.openForm("edit", row),
|
|
683
|
+
view: row => run.current.openDetails(row),
|
|
684
|
+
}), [table, cache, reloadPins]);
|
|
685
|
+
const tableLabel = props["aria-label"] ?? (props["aria-labelledby"] ? undefined : capitalize(plural));
|
|
686
|
+
const configured = CRUD_MODES.some(value => wantedModes.includes(value) && crudListEndpoint(api, value, paging));
|
|
687
|
+
if (!configured) {
|
|
688
|
+
return (_jsx("div", { id: id, className: cx("nx-crud", "nx-crud--denied", className), style: style, role: "alert", children: _jsxs("p", { className: "nx-crud__denied-title", children: ["Nothing to show: no ", paging === "server" ? "`pages`" : "`lists`", " endpoint for the ", plural, "."] }) }));
|
|
689
|
+
}
|
|
690
|
+
if (ready && !mode) {
|
|
691
|
+
return (_jsxs("div", { id: id, className: cx("nx-crud", "nx-crud--denied", className), style: style, role: "status", children: [_jsx(InlineIcon, { icon: iconLock, className: "nx-crud__denied-icon" }), _jsxs("p", { className: "nx-crud__denied-title", children: ["You do not have permission to see ", plural, "."] }), _jsx("p", { className: "nx-crud__denied-detail", children: "Ask an administrator for access." })] }));
|
|
692
|
+
}
|
|
693
|
+
const modeOptions = modes.map(value => ({ value, label: MODE_INFO[value].label, icon: _jsx(InlineIcon, { icon: MODE_INFO[value].icon }) }));
|
|
694
|
+
const addButton = allowed.create ? (_jsxs(Button, { variant: "primary", size: "sm", icon: _jsx(InlineIcon, { icon: iconPlus }), onClick: () => void run.current.openForm("create", null), children: ["Add ", subject] })) : null;
|
|
695
|
+
const selectable = props.massActions !== false && Boolean(mode) && (crudMassActions(mode, action => massAllowed[action], massTrashless).length > 0 || Boolean(props.massActions));
|
|
696
|
+
return (_jsx("div", { id: id, className: cx("nx-crud", className), style: style, "data-mode": mode, children: _jsx(DataTable, { table: table, columns: columns, load: load, loadKey: mode ?? "waiting", paging: paging, rowKey: rowKey, rowLabel: labelOf, "aria-label": tableLabel, "aria-labelledby": props["aria-labelledby"], toolbar: modes.length > 1 && mode ? (_jsx(TabButtons, { size: "sm", labels: "selected", options: modeOptions, value: mode, onChange: setChosenMode, "aria-label": `${capitalize(plural)}: lists` })) : undefined, actions: _jsxs(_Fragment, { children: [props.toolbar, addButton] }), selectable: selectable, massActions: massActionsFor, rowActions: rowActions, pinnedRows: mode === "normal" && pinsOn ? (pinned ?? undefined) : undefined, pinnedLoading: pinsLoading, pinnedLabel: `Pinned ${plural}`, pinnedMax: maxPins, onRowOpen: onRowOpen, openOnClick: tableOptions?.openOnClick, onReload: () => {
|
|
697
|
+
cache.clear();
|
|
698
|
+
reloadPins();
|
|
699
|
+
}, emptyMessage: tableOptions?.emptyMessage ?? (mode ? MODE_INFO[mode].empty(plural) : undefined), emptyAction: mode === "normal" ? addButton : undefined, searchPlaceholder: tableOptions?.searchPlaceholder ?? `Search ${plural}…`, stateKey: tableOptions?.stateKey ?? `crud:${name}`, pageSize: tableOptions?.pageSize, pageSizes: tableOptions?.pageSizes, defaultSort: tableOptions?.defaultSort, numbered: tableOptions?.numbered, maxHeight: tableOptions?.maxHeight, fill: tableOptions?.fill, size: tableOptions?.size, maxSelection: tableOptions?.maxSelection, columnsMenu: tableOptions?.columnsMenu, searchDelay: tableOptions?.searchDelay, reorderable: tableOptions?.reorderable, resizable: tableOptions?.resizable, pinnable: tableOptions?.pinnable, moreActions: tableOptions?.moreActions, exportable: tableOptions?.exportable, printable: tableOptions?.printable, exportName: tableOptions?.exportName, onImport: tableOptions?.onImport, excel: tableOptions?.excel && {
|
|
700
|
+
...tableOptions.excel,
|
|
701
|
+
// A spreadsheet wrote records straight into the database: every list this CRUD kept is out of date.
|
|
702
|
+
onImported: () => {
|
|
703
|
+
cache.clear();
|
|
704
|
+
reloadPins();
|
|
705
|
+
tableOptions.excel?.onImported?.();
|
|
706
|
+
},
|
|
707
|
+
} }) }));
|
|
708
|
+
}
|