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,596 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { iconAlertCircle, iconArrowAutofitWidth, iconArrowBarToLeft, iconArrowBarToRight, iconArrowDown, iconArrowLeft, iconArrowRight, iconArrowUp, iconArrowsSort, iconChevronDown, iconDatabase, iconDotsVertical, iconEyeOff, iconDownload, iconFileExport, iconFileImport, iconPinnedOff, iconPrinter, iconRefresh, iconRestore, iconSearch, iconSearchOff, iconSortAscending, iconSortDescending, iconX, InlineIcon, } from "nexus-icons";
|
|
4
|
+
import { useCallback, useEffect, useId, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
5
|
+
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
6
|
+
import { useBrowserTimeZone, useDatePreferences } from "../../Helpers/DatePreferences.js";
|
|
7
|
+
import { fitColumnWidth, headLeafCells, startColumnDrag, startColumnResize } from "../../Helpers/TableColumns.js";
|
|
8
|
+
import { exportTableCsv, printTable } from "../../Helpers/TableExport.js";
|
|
9
|
+
import { canPinColumn, columnAlign, columnValue, cssLength, DEFAULT_MAX_SELECTION, DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZES, formatCellText, headBands, MIN_COLUMN_WIDTH, orderColumns, TABLE_ACTIONS_COLUMN, TABLE_INDEX_COLUMN, viewColumns, } from "../../Helpers/TableHelpers.js";
|
|
10
|
+
import { TableStore } from "../../Helpers/TableStore.js";
|
|
11
|
+
import { Button } from "../Documents/Button.js";
|
|
12
|
+
import { Menu } from "../Documents/Menu.js";
|
|
13
|
+
import { SplitButton } from "../Documents/SplitButton.js";
|
|
14
|
+
import { TextBox } from "../Inputs/TextBox.js";
|
|
15
|
+
import { toast } from "../Layouts/Toaster.js";
|
|
16
|
+
import { downloadExcelTemplate, exportExcel } from "../../Services/ExcelApi.js";
|
|
17
|
+
import { openExcelImport } from "../Forms/ExcelImport.js";
|
|
18
|
+
import { TableColumnsPanel } from "./DataTableColumns.js";
|
|
19
|
+
import { columnStyle, formatCount, TableColumnGroup, TableFooter, TableIndexCell, TableMessageRow, TablePageSize, TablePinnedHead, TableRowView, TableSkeletonRows, } from "./DataTableParts.js";
|
|
20
|
+
/*
|
|
21
|
+
* DataTable: rows in a table with a search, sorting by column, pages, a selection with actions on the selected rows,
|
|
22
|
+
* actions on each row, and columns the user arranges (shown or hidden, in any order, resized, pinned to a side). Rows
|
|
23
|
+
* given as `pinnedRows` sit in a section above the page that folds open and shut. Rows are given, loaded once, or
|
|
24
|
+
* loaded a page at a time; what the table shows lives in a `TableStore`, which pages (and the CRUD component) change
|
|
25
|
+
* from outside.
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* The store of a table, for the page: pass it as `table`, then call `table.reload()`, `table.upsert(row)`, or
|
|
29
|
+
* `table.getSelected()` from anywhere.
|
|
30
|
+
*/
|
|
31
|
+
export function useTable() {
|
|
32
|
+
const [store] = useState(() => new TableStore());
|
|
33
|
+
return store;
|
|
34
|
+
}
|
|
35
|
+
const STATE_PREFIX = "nx-table:";
|
|
36
|
+
function readSaved(key) {
|
|
37
|
+
if (!key || typeof localStorage === "undefined")
|
|
38
|
+
return null;
|
|
39
|
+
try {
|
|
40
|
+
const text = localStorage.getItem(STATE_PREFIX + key);
|
|
41
|
+
return text ? JSON.parse(text) : null;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Settings are written in two places (the query and the columns together, the pinned section on its own click), so a
|
|
48
|
+
// write keeps whatever it does not mention.
|
|
49
|
+
function writeSaved(key, changes) {
|
|
50
|
+
try {
|
|
51
|
+
localStorage.setItem(STATE_PREFIX + key, JSON.stringify({ ...readSaved(key), ...changes }));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Storage full or blocked: the table works without it.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Each row's actions and marks, made once per row object, so rows that did not change do not render again. */
|
|
58
|
+
function usePerRow(make) {
|
|
59
|
+
const cache = useMemo(() => new WeakMap(), [make]);
|
|
60
|
+
return useCallback((row) => {
|
|
61
|
+
if (!make)
|
|
62
|
+
return undefined;
|
|
63
|
+
if (cache.has(row))
|
|
64
|
+
return cache.get(row);
|
|
65
|
+
const value = make(row);
|
|
66
|
+
cache.set(row, value);
|
|
67
|
+
return value;
|
|
68
|
+
}, [cache, make]);
|
|
69
|
+
}
|
|
70
|
+
const sameNumbers = (a, b) => {
|
|
71
|
+
const keys = Object.keys(a);
|
|
72
|
+
return keys.length === Object.keys(b).length && keys.every(key => a[key] === b[key]);
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* A data table. Columns are params (`{ key, label, format, ... }`); rows are given (`rows`), loaded once (`load`), or
|
|
76
|
+
* loaded a page at a time (`load` with `paging="server"`, which sends the search, the sort, and the page). The user
|
|
77
|
+
* arranges the columns: the Columns menu shows and hides them, a heading drags into another place, its edge changes its
|
|
78
|
+
* width, and its menu pins it left or right; with a `stateKey` the table remembers all of it on the device.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* <DataTable
|
|
82
|
+
* aria-label="Guests"
|
|
83
|
+
* columns={[{ key: "name", label: "Name" }, { key: "visits", label: "Visits", format: "number" }]}
|
|
84
|
+
* load={(query, signal) => fetchGuests(query, signal)}
|
|
85
|
+
* paging="server"
|
|
86
|
+
* selectable
|
|
87
|
+
* massActions={[{ id: "email", label: "Email", onSelect: rows => email(rows) }]}
|
|
88
|
+
* rowActions={row => [{ id: "edit", label: "Edit", icon: <InlineIcon icon={iconPencil} />, inline: true, onSelect: edit }]}
|
|
89
|
+
* />
|
|
90
|
+
*/
|
|
91
|
+
export function DataTable(props) {
|
|
92
|
+
const { columns, rows: givenRows, load, loadKey, rowKey = "id", rowLabel, table: givenTable, searchable = true, searchPlaceholder = "Search…", toolbar, actions, columnsMenu = true, selectable = false, maxSelection = DEFAULT_MAX_SELECTION, onSelectionChange, massActions, rowActions, onRowOpen, openOnClick = false, pinnedRows, pinnedLoading = false, pinnedLabel = "Pinned", pinnedMax, onReload, reorderable = true, resizable = true, pinnable = true, moreActions, exportable = true, printable = true, exportName, onImport, excel, pageSize = DEFAULT_PAGE_SIZE, pageSizes = DEFAULT_PAGE_SIZES, defaultSort = null, stateKey, emptyMessage = "No records yet.", emptyAction, maxHeight, fill = false, size = "md", id, className, style, ref, } = props;
|
|
93
|
+
const paging = givenRows ? "client" : (props.paging ?? "client");
|
|
94
|
+
const reloadable = props.reloadable ?? Boolean(load);
|
|
95
|
+
const searchDelay = props.searchDelay ?? (paging === "server" ? 300 : 0);
|
|
96
|
+
// A table with checkboxes numbers its rows: the number becomes the checkbox as soon as the row is under the pointer.
|
|
97
|
+
const numbered = props.numbered ?? selectable;
|
|
98
|
+
const [own] = useState(() => (givenTable ? null : new TableStore(pageSize)));
|
|
99
|
+
const table = givenTable ?? own;
|
|
100
|
+
table.configure({ columns, rowKey, rows: givenRows, load, paging, maxSelection, onSelectionChange });
|
|
101
|
+
useImperativeHandle(ref, () => table, [table]);
|
|
102
|
+
const state = useSyncExternalStore(table.subscribe, table.getState, table.getState);
|
|
103
|
+
const preferences = useDatePreferences();
|
|
104
|
+
const timeZone = useBrowserTimeZone();
|
|
105
|
+
const cellOptions = useMemo(() => ({ preferences, timeZone: timeZone ?? undefined }), [preferences, timeZone]);
|
|
106
|
+
const tableId = useId();
|
|
107
|
+
// The search box is uncontrolled: a new key clears it when a new list starts over.
|
|
108
|
+
const [searchGeneration, setSearchGeneration] = useState(0);
|
|
109
|
+
// The pinned section starts folded shut, then takes the choice this device remembers (a layout effect: no flicker).
|
|
110
|
+
const [pinsOpen, setPinsOpen] = useState(false);
|
|
111
|
+
const searchTimer = useRef(undefined);
|
|
112
|
+
const started = useRef(false);
|
|
113
|
+
const loadedKey = useRef(loadKey);
|
|
114
|
+
// First rows: saved settings first, then the rows given or the first load. A layout effect, so rows that are at hand
|
|
115
|
+
// (given, or a cached list) paint with the table, not a frame after it.
|
|
116
|
+
useLayoutEffect(() => {
|
|
117
|
+
if (!started.current) {
|
|
118
|
+
started.current = true;
|
|
119
|
+
const saved = readSaved(stateKey);
|
|
120
|
+
table.restore({
|
|
121
|
+
pageSize: saved?.pageSize && pageSizes.includes(saved.pageSize) ? saved.pageSize : pageSize,
|
|
122
|
+
sort: saved?.sort !== undefined ? saved.sort : defaultSort,
|
|
123
|
+
hidden: saved?.hidden ?? columns.filter(column => column.hidden).map(column => column.key),
|
|
124
|
+
order: saved?.order,
|
|
125
|
+
widths: saved?.widths,
|
|
126
|
+
pinned: saved?.pinned,
|
|
127
|
+
});
|
|
128
|
+
if (saved?.pinsOpen)
|
|
129
|
+
setPinsOpen(true);
|
|
130
|
+
if (givenRows)
|
|
131
|
+
table.setRows(givenRows);
|
|
132
|
+
else
|
|
133
|
+
void table.reload();
|
|
134
|
+
}
|
|
135
|
+
else
|
|
136
|
+
table.resume();
|
|
137
|
+
// Leaving stops a load that runs; coming back (Strict Mode's second mount) starts it again.
|
|
138
|
+
return () => table.dispose();
|
|
139
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
140
|
+
}, []);
|
|
141
|
+
// Rows given by the page, again after each change of the array.
|
|
142
|
+
useLayoutEffect(() => {
|
|
143
|
+
if (started.current && givenRows)
|
|
144
|
+
table.setRows(givenRows);
|
|
145
|
+
}, [table, givenRows]);
|
|
146
|
+
// Another list behind the same table: start over from the first page, with no search and no selection.
|
|
147
|
+
useLayoutEffect(() => {
|
|
148
|
+
if (loadedKey.current === loadKey)
|
|
149
|
+
return;
|
|
150
|
+
loadedKey.current = loadKey;
|
|
151
|
+
clearTimeout(searchTimer.current);
|
|
152
|
+
setSearchGeneration(count => count + 1);
|
|
153
|
+
void table.reload({ reset: true });
|
|
154
|
+
}, [table, loadKey]);
|
|
155
|
+
useEffect(() => () => clearTimeout(searchTimer.current), []);
|
|
156
|
+
// Saved settings follow the user's choices.
|
|
157
|
+
const hiddenList = useMemo(() => [...state.hidden].sort(), [state.hidden]);
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (!stateKey || !started.current)
|
|
160
|
+
return;
|
|
161
|
+
writeSaved(stateKey, {
|
|
162
|
+
pageSize: state.query.pageSize,
|
|
163
|
+
sort: state.query.sort,
|
|
164
|
+
hidden: hiddenList,
|
|
165
|
+
order: state.order.length ? [...state.order] : undefined,
|
|
166
|
+
widths: Object.keys(state.widths).length ? state.widths : undefined,
|
|
167
|
+
pinned: Object.keys(state.pinned).length ? state.pinned : undefined,
|
|
168
|
+
});
|
|
169
|
+
}, [stateKey, state.query.pageSize, state.query.sort, hiddenList, state.order, state.widths, state.pinned]);
|
|
170
|
+
const visible = useMemo(() => columns.filter(column => !state.hidden.has(column.key)), [columns, state.hidden]);
|
|
171
|
+
// Every column in the order the user put them in, hidden ones among them: what the Columns panel lists.
|
|
172
|
+
const ordered = useMemo(() => orderColumns(columns, state.order), [columns, state.order]);
|
|
173
|
+
const labelOf = useCallback((row) => {
|
|
174
|
+
if (rowLabel)
|
|
175
|
+
return rowLabel(row);
|
|
176
|
+
const first = columns[0];
|
|
177
|
+
const text = first ? formatCellText(first, columnValue(first, row), cellOptions) : "";
|
|
178
|
+
return text || `row ${table.keyOf(row)}`;
|
|
179
|
+
}, [rowLabel, columns, cellOptions, table]);
|
|
180
|
+
const actionsOf = usePerRow(rowActions);
|
|
181
|
+
const hasActions = Boolean(rowActions);
|
|
182
|
+
const lastToggled = useRef(null);
|
|
183
|
+
const warnFull = useCallback(() => toast.warning(`You can select up to ${formatCount(maxSelection)} rows.`), [maxSelection]);
|
|
184
|
+
// Shift+click selects every row between the last one clicked and this one.
|
|
185
|
+
const onToggle = useCallback((row, on, event) => {
|
|
186
|
+
const rows = table.getState().rows;
|
|
187
|
+
const index = rows.indexOf(row);
|
|
188
|
+
const from = lastToggled.current;
|
|
189
|
+
const range = event.shiftKey && from !== null && from < rows.length && index >= 0 ? rows.slice(Math.min(from, index), Math.max(from, index) + 1) : [row];
|
|
190
|
+
lastToggled.current = index;
|
|
191
|
+
if (table.select(range, on) > 0)
|
|
192
|
+
warnFull();
|
|
193
|
+
}, [table, warnFull]);
|
|
194
|
+
const onAction = useCallback((row, action) => {
|
|
195
|
+
if (!action || action.disabled)
|
|
196
|
+
return;
|
|
197
|
+
const key = table.keyOf(row);
|
|
198
|
+
const result = action.onSelect(row);
|
|
199
|
+
if (result instanceof Promise) {
|
|
200
|
+
table.setBusy([key], true);
|
|
201
|
+
void result.finally(() => table.setBusy([key], false)).catch(() => undefined);
|
|
202
|
+
}
|
|
203
|
+
}, [table]);
|
|
204
|
+
const onSearch = (text) => {
|
|
205
|
+
clearTimeout(searchTimer.current);
|
|
206
|
+
if (searchDelay > 0 && text.trim())
|
|
207
|
+
searchTimer.current = setTimeout(() => table.setSearch(text), searchDelay);
|
|
208
|
+
else
|
|
209
|
+
table.setSearch(text);
|
|
210
|
+
};
|
|
211
|
+
const { rows, query, status, refreshing } = state;
|
|
212
|
+
const pageKeys = rows.map(row => table.keyOf(row));
|
|
213
|
+
const selectedOnPage = pageKeys.filter(key => state.selected.has(key)).length;
|
|
214
|
+
const allOnPage = rows.length > 0 && selectedOnPage === rows.length;
|
|
215
|
+
const selectionFull = state.selected.size >= maxSelection;
|
|
216
|
+
const selecting = selectable && state.selected.size > 0;
|
|
217
|
+
/* The columns as they are drawn: in the user's order, the pinned ones at their side, each with its width */
|
|
218
|
+
const rootRef = useRef(null);
|
|
219
|
+
const scrollRef = useRef(null);
|
|
220
|
+
const tableRef = useRef(null);
|
|
221
|
+
const headRef = useRef(null);
|
|
222
|
+
const colRefs = useMemo(() => new Map(), []);
|
|
223
|
+
const [offsets, setOffsets] = useState({});
|
|
224
|
+
const sized = Object.keys(state.widths).length > 0;
|
|
225
|
+
const views = useMemo(() => viewColumns(visible, { order: state.order, pinned: state.pinned, widths: state.widths, offsets, reorderable, resizable }), [visible, state.order, state.pinned, state.widths, offsets, reorderable, resizable]);
|
|
226
|
+
const indexOn = selectable || numbered;
|
|
227
|
+
// The bands above the headings. With no group anywhere the header is the one row it always was.
|
|
228
|
+
const bands = useMemo(() => headBands(views), [views]);
|
|
229
|
+
const grouped = bands.some(band => band.group !== null);
|
|
230
|
+
/** How many rows the header has: a heading with no band above it is as tall as both. */
|
|
231
|
+
const headRows = grouped ? 2 : undefined;
|
|
232
|
+
// The shadow while columns scroll under a pinned side belongs to its last column, which may be one of the table's own.
|
|
233
|
+
const pinnedLeft = views.some(view => view.pin === "left");
|
|
234
|
+
const pinnedRight = views.some(view => view.pin === "right");
|
|
235
|
+
const frame = useMemo(() => ({
|
|
236
|
+
index: indexOn ? { offset: offsets[TABLE_INDEX_COLUMN] ?? 0, edge: !pinnedLeft } : null,
|
|
237
|
+
actions: hasActions ? { offset: offsets[TABLE_ACTIONS_COLUMN] ?? 0, edge: !pinnedRight } : null,
|
|
238
|
+
}), [indexOn, hasActions, offsets, pinnedLeft, pinnedRight]);
|
|
239
|
+
const cellCount = views.length;
|
|
240
|
+
const span = cellCount + (frame.index ? 1 : 0) + (frame.actions ? 1 : 0) + (sized ? 1 : 0);
|
|
241
|
+
// How far the pinned cells sit from their edge, measured from the headings before them. It runs after every render
|
|
242
|
+
// (before the browser paints) and settles at once, since nothing changes when the numbers are the same.
|
|
243
|
+
useLayoutEffect(() => {
|
|
244
|
+
const cells = headLeafCells(headRef.current);
|
|
245
|
+
const root = rootRef.current;
|
|
246
|
+
if (cells.length === 0)
|
|
247
|
+
return;
|
|
248
|
+
const next = {};
|
|
249
|
+
let left = 0;
|
|
250
|
+
for (const cell of cells) {
|
|
251
|
+
if (cell.getAttribute("data-pin") !== "left")
|
|
252
|
+
break;
|
|
253
|
+
const key = cell.getAttribute("data-col");
|
|
254
|
+
if (key)
|
|
255
|
+
next[key] = left;
|
|
256
|
+
left += cell.getBoundingClientRect().width;
|
|
257
|
+
}
|
|
258
|
+
let right = 0;
|
|
259
|
+
for (let index = cells.length - 1; index >= 0; index--) {
|
|
260
|
+
const cell = cells[index];
|
|
261
|
+
if (cell.getAttribute("data-pin") !== "right")
|
|
262
|
+
break;
|
|
263
|
+
const key = cell.getAttribute("data-col");
|
|
264
|
+
if (key)
|
|
265
|
+
next[key] = right;
|
|
266
|
+
right += cell.getBoundingClientRect().width;
|
|
267
|
+
}
|
|
268
|
+
// The bar of the selected rows starts after the checkbox column, whatever width it took.
|
|
269
|
+
root?.style.setProperty("--nx-table-index-w", `${Math.round(left)}px`);
|
|
270
|
+
setOffsets(previous => (sameNumbers(previous, next) ? previous : next));
|
|
271
|
+
});
|
|
272
|
+
// Shadows at the pinned columns while the others scroll under them: set on the frame, with no render.
|
|
273
|
+
useLayoutEffect(() => {
|
|
274
|
+
const scroller = scrollRef.current;
|
|
275
|
+
const frameElement = scroller?.parentElement;
|
|
276
|
+
if (!scroller || !frameElement)
|
|
277
|
+
return;
|
|
278
|
+
const update = () => {
|
|
279
|
+
const left = Math.abs(scroller.scrollLeft);
|
|
280
|
+
frameElement.toggleAttribute("data-scroll-start", left > 1);
|
|
281
|
+
frameElement.toggleAttribute("data-scroll-end", left < scroller.scrollWidth - scroller.clientWidth - 1);
|
|
282
|
+
};
|
|
283
|
+
update();
|
|
284
|
+
scroller.addEventListener("scroll", update, { passive: true });
|
|
285
|
+
const observer = new ResizeObserver(update);
|
|
286
|
+
observer.observe(scroller);
|
|
287
|
+
if (scroller.firstElementChild)
|
|
288
|
+
observer.observe(scroller.firstElementChild);
|
|
289
|
+
return () => {
|
|
290
|
+
scroller.removeEventListener("scroll", update);
|
|
291
|
+
observer.disconnect();
|
|
292
|
+
};
|
|
293
|
+
}, []);
|
|
294
|
+
const headerCheck = useRef(null);
|
|
295
|
+
useLayoutEffect(() => {
|
|
296
|
+
if (headerCheck.current)
|
|
297
|
+
headerCheck.current.indeterminate = selectedOnPage > 0 && !allOnPage;
|
|
298
|
+
});
|
|
299
|
+
/* Resizing a column: the table freezes the widths it has, then only the dragged edge moves */
|
|
300
|
+
const measureWidths = useCallback(() => {
|
|
301
|
+
const widths = {};
|
|
302
|
+
for (const cell of headLeafCells(headRef.current)) {
|
|
303
|
+
const key = cell.getAttribute("data-col");
|
|
304
|
+
if (key)
|
|
305
|
+
widths[key] = Math.round(cell.getBoundingClientRect().width);
|
|
306
|
+
}
|
|
307
|
+
return widths;
|
|
308
|
+
}, []);
|
|
309
|
+
const freezeWidths = useCallback(() => {
|
|
310
|
+
if (Object.keys(table.getState().widths).length === 0)
|
|
311
|
+
table.setWidths(measureWidths());
|
|
312
|
+
}, [table, measureWidths]);
|
|
313
|
+
// A column never goes under the width its params ask for, whether the user drags its edge or fits its content.
|
|
314
|
+
const smallest = (heading) => Math.max(MIN_COLUMN_WIDTH, (heading && Number.parseFloat(getComputedStyle(heading).minWidth)) || 0);
|
|
315
|
+
const fitColumn = useCallback((key) => {
|
|
316
|
+
const element = tableRef.current;
|
|
317
|
+
if (!element)
|
|
318
|
+
return;
|
|
319
|
+
const heading = element.querySelector(`thead [data-col="${key}"]`);
|
|
320
|
+
const width = Math.max(fitColumnWidth(element, key), smallest(heading));
|
|
321
|
+
table.setWidths({ ...(Object.keys(table.getState().widths).length ? {} : measureWidths()), [key]: width });
|
|
322
|
+
}, [table, measureWidths]);
|
|
323
|
+
const onColumnPointerDown = (event, view) => {
|
|
324
|
+
if (event.button !== 0 || !event.isPrimary)
|
|
325
|
+
return;
|
|
326
|
+
const heading = event.target.closest("th");
|
|
327
|
+
if (!heading)
|
|
328
|
+
return;
|
|
329
|
+
const target = event.target;
|
|
330
|
+
if (target.closest(".nx-table__resizer, .nx-table__col-menu"))
|
|
331
|
+
return;
|
|
332
|
+
if (!view.movable)
|
|
333
|
+
return;
|
|
334
|
+
startColumnDrag(event.nativeEvent, {
|
|
335
|
+
heading,
|
|
336
|
+
scroller: scrollRef.current,
|
|
337
|
+
root: rootRef.current,
|
|
338
|
+
onDrop: (key, beforeKey) => table.moveColumn(key, beforeKey),
|
|
339
|
+
});
|
|
340
|
+
};
|
|
341
|
+
const onResizerPointerDown = (event, key) => {
|
|
342
|
+
if (event.button !== 0 || !event.isPrimary)
|
|
343
|
+
return;
|
|
344
|
+
const heading = event.target.closest("th");
|
|
345
|
+
if (!heading)
|
|
346
|
+
return;
|
|
347
|
+
event.stopPropagation();
|
|
348
|
+
// The widths freeze on the press, so the column element is there to write to on the first move.
|
|
349
|
+
freezeWidths();
|
|
350
|
+
startColumnResize(event.nativeEvent, {
|
|
351
|
+
heading,
|
|
352
|
+
root: rootRef.current,
|
|
353
|
+
min: smallest(heading),
|
|
354
|
+
onResize: width => {
|
|
355
|
+
const col = colRefs.get(key);
|
|
356
|
+
if (col)
|
|
357
|
+
col.style.width = `${width}px`;
|
|
358
|
+
},
|
|
359
|
+
onCommit: width => table.setWidth(key, width),
|
|
360
|
+
});
|
|
361
|
+
};
|
|
362
|
+
/* The bar above the table, and the options menu */
|
|
363
|
+
const outputName = exportName ?? props["aria-label"] ?? "Table";
|
|
364
|
+
const output = () => {
|
|
365
|
+
const shown = views.map(view => view.column);
|
|
366
|
+
const rowsOut = table.getViewRows();
|
|
367
|
+
const note = [`${formatCount(rowsOut.length)} ${rowsOut.length === 1 ? "row" : "rows"}`, query.search && `search: ${query.search}`].filter(Boolean).join(" · ");
|
|
368
|
+
return { columns: shown, rows: rowsOut, cellOptions, name: outputName, note };
|
|
369
|
+
};
|
|
370
|
+
const serverPage = paging === "server";
|
|
371
|
+
/** What an export to Excel asks the server for: the search and the sort on screen, and the page's own filters. */
|
|
372
|
+
const exportQuery = () => {
|
|
373
|
+
const column = query.sort ? columns.find(candidate => candidate.key === query.sort.key) : undefined;
|
|
374
|
+
return {
|
|
375
|
+
params: excel?.params,
|
|
376
|
+
subject: excel?.subject,
|
|
377
|
+
rows: state.matches,
|
|
378
|
+
query: {
|
|
379
|
+
search: query.search || null,
|
|
380
|
+
sort: column ? (column.sortKey ?? column.key) : null,
|
|
381
|
+
direction: query.sort?.direction,
|
|
382
|
+
filters: excel?.filters?.(),
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
};
|
|
386
|
+
/** After an import: the rows it wrote are the server's, so the table reads its page again. */
|
|
387
|
+
const onImported = () => {
|
|
388
|
+
void table.reload({ quiet: true });
|
|
389
|
+
excel?.onImported?.();
|
|
390
|
+
};
|
|
391
|
+
const optionItems = [
|
|
392
|
+
...(exportable && status === "ready" && rows.length > 0
|
|
393
|
+
? [{ id: "export", label: serverPage ? "Export this page (CSV)" : "Export as CSV", icon: _jsx(InlineIcon, { icon: iconFileExport }), onSelect: () => exportTableCsv(output()) }]
|
|
394
|
+
: []),
|
|
395
|
+
...(printable && status === "ready" && rows.length > 0
|
|
396
|
+
? [{ id: "print", label: serverPage ? "Print this page" : "Print", icon: _jsx(InlineIcon, { icon: iconPrinter }), onSelect: () => printTable(output()) }]
|
|
397
|
+
: []),
|
|
398
|
+
// A spreadsheet: the template to fill, the file to bring back, and an export the server writes from what is on
|
|
399
|
+
// screen. Its own Import replaces the page's `onImport` hook.
|
|
400
|
+
...(excel
|
|
401
|
+
? [
|
|
402
|
+
{ id: "excel-template", label: "Download template", description: `A spreadsheet to fill in`, icon: _jsx(InlineIcon, { icon: iconDownload }), separator: true, onSelect: () => void downloadExcelTemplate(excel.endpoints, { params: excel.params }) },
|
|
403
|
+
{ id: "excel-import", label: "Import from a spreadsheet", icon: _jsx(InlineIcon, { icon: iconFileImport }), onSelect: () => openExcelImport({ endpoints: excel.endpoints, subject: excel.subject, params: excel.params, onImported: onImported }) },
|
|
404
|
+
...(excel.endpoints.export
|
|
405
|
+
? [{ id: "excel-export", label: "Export to Excel", description: "Everything the search leaves, from the server", icon: _jsx(InlineIcon, { icon: iconFileExport }), onSelect: () => void exportExcel(excel.endpoints, exportQuery()) }]
|
|
406
|
+
: []),
|
|
407
|
+
]
|
|
408
|
+
: onImport
|
|
409
|
+
? [{ id: "import", label: "Import", icon: _jsx(InlineIcon, { icon: iconFileImport }), onSelect: onImport }]
|
|
410
|
+
: []),
|
|
411
|
+
...(moreActions ?? []),
|
|
412
|
+
// Reset belongs in the Columns panel, beside what it resets; it is offered here only for a table without one.
|
|
413
|
+
...(!columnsMenu && table.hasColumnChanges()
|
|
414
|
+
? [{ id: "reset-columns", label: "Reset columns", description: "Their order, widths, and pins", icon: _jsx(InlineIcon, { icon: iconRestore }), separator: true, onSelect: () => table.resetColumns() }]
|
|
415
|
+
: []),
|
|
416
|
+
];
|
|
417
|
+
const optionEntries = optionItems.flatMap((action, index) => [
|
|
418
|
+
...(action.separator && index > 0 ? [{ separator: true }] : []),
|
|
419
|
+
{ value: action.id, label: action.label, icon: action.icon, description: action.description, tone: action.tone, disabled: action.disabled },
|
|
420
|
+
]);
|
|
421
|
+
const pageSizePicker = _jsx(TablePageSize, { pageSize: query.pageSize, pageSizes: pageSizes, compact: searchable, onChange: value => table.setPageSize(value) });
|
|
422
|
+
let body;
|
|
423
|
+
if (status === "loading") {
|
|
424
|
+
body = _jsx(TableSkeletonRows, { count: Math.min(query.pageSize, 8), cells: cellCount, frame: frame, sized: sized });
|
|
425
|
+
}
|
|
426
|
+
else if (status === "error") {
|
|
427
|
+
body = (_jsx(TableMessageRow, { span: span, tone: "danger", icon: iconAlertCircle, title: "The rows could not be loaded.", detail: state.error, children: _jsx(Button, { size: "sm", icon: _jsx(InlineIcon, { icon: iconRefresh }), onClick: () => void table.reload({ quiet: false }), children: "Retry" }) }));
|
|
428
|
+
}
|
|
429
|
+
else if (rows.length === 0 && query.search) {
|
|
430
|
+
body = (_jsx(TableMessageRow, { span: span, icon: iconSearchOff, title: `Nothing matches “${query.search}”.`, detail: "Check the spelling, or search for something shorter.", children: _jsx(Button, { size: "sm", icon: _jsx(InlineIcon, { icon: iconX }), onClick: () => {
|
|
431
|
+
clearTimeout(searchTimer.current);
|
|
432
|
+
setSearchGeneration(count => count + 1);
|
|
433
|
+
table.setSearch("");
|
|
434
|
+
}, children: "Clear search" }) }));
|
|
435
|
+
}
|
|
436
|
+
else if (rows.length === 0) {
|
|
437
|
+
body = (_jsx(TableMessageRow, { span: span, icon: iconDatabase, title: emptyMessage, children: emptyAction }));
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
body = rows.map((row, index) => {
|
|
441
|
+
const key = pageKeys[index];
|
|
442
|
+
const selected = state.selected.has(key);
|
|
443
|
+
return (_jsx(TableRowView, { row: row, rowKey: key, number: state.offset + index + 1, label: labelOf(row), columns: views, cellOptions: cellOptions, selectable: selectable, selected: selected, selectionFull: selectionFull && !selected, busy: state.busy.has(key), flashed: state.flashed.has(key), numbered: numbered, frame: frame, sized: sized, actions: hasActions ? (actionsOf(row) ?? []) : null, onToggle: onToggle, onAction: onAction, onOpen: onRowOpen, openOnClick: openOnClick }, key || index));
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
/* The pinned section above the page: the records the user pinned, folded open or shut, remembered on this device */
|
|
447
|
+
// A section with nothing in it and nothing on its way would be an empty heading, so it does not show at all.
|
|
448
|
+
const pins = pinnedRows && (pinnedRows.length > 0 || pinnedLoading) ? pinnedRows : null;
|
|
449
|
+
const togglePins = () => {
|
|
450
|
+
const next = !pinsOpen;
|
|
451
|
+
setPinsOpen(next);
|
|
452
|
+
if (stateKey)
|
|
453
|
+
writeSaved(stateKey, { pinsOpen: next });
|
|
454
|
+
};
|
|
455
|
+
const pinnedBody = pinsOpen &&
|
|
456
|
+
pins?.map((row, index) => {
|
|
457
|
+
const key = table.keyOf(row);
|
|
458
|
+
const selected = state.selected.has(key);
|
|
459
|
+
return (_jsx(TableRowView, { row: row, rowKey: key, number: index + 1, label: labelOf(row), columns: views, cellOptions: cellOptions, selectable: selectable, selected: selected, selectionFull: selectionFull && !selected, busy: state.busy.has(key), flashed: state.flashed.has(key), numbered: numbered, pinned: true, frame: frame, sized: sized, actions: hasActions ? (actionsOf(row) ?? []) : null, onToggle: onToggle, onAction: onAction, onOpen: onRowOpen, openOnClick: openOnClick }, `pin:${key || index}`));
|
|
460
|
+
});
|
|
461
|
+
const selectedRows = selecting ? [...state.selected.values()] : [];
|
|
462
|
+
const mass = selectedRows.length ? (typeof massActions === "function" ? massActions(selectedRows) : (massActions ?? [])) : [];
|
|
463
|
+
// A column's own heading, wherever it stands: across both rows when no band is above it, in the row under its band when one is.
|
|
464
|
+
const headingCell = (view, rowSpan) => (_jsx(HeaderCell, { view: view, sized: sized, rowSpan: rowSpan, sort: query.sort, disabled: status !== "ready" || rows.length < 2, covered: selectedRows.length > 0, canPin: canPinColumn(view.column, { pinnable }), canHide: view.column.hideable !== false && visible.length > 1, neighbours: moveTargets(views, view.column.key), onSort: direction => table.setSort(direction ? { key: view.column.key, direction } : null), onPin: side => table.setPinned(view.column.key, side), onMove: beforeKey => table.moveColumn(view.column.key, beforeKey), onFit: () => fitColumn(view.column.key), onHide: () => table.setHidden(view.column.key, true), onPointerDown: event => onColumnPointerDown(event, view), onResizerDown: event => onResizerPointerDown(event, view.column.key) }, view.column.key));
|
|
465
|
+
return (_jsxs("div", { ref: rootRef, id: id, className: cx("nx-table", `nx-table--${size}`, fill && "nx-table--fill", className), style: style, "data-head-rows": headRows, "data-status": status, "data-refreshing": refreshing || undefined, "data-selecting": selecting || undefined, "data-sized": sized || undefined, children: [_jsxs("div", { className: "nx-table__bar", children: [toolbar && _jsx("div", { className: "nx-table__bar-start", children: toolbar }), _jsxs("div", { className: "nx-table__bar-end", children: [searchable ? (_jsx(TextBox, { type: "search", size: "sm", className: "nx-table__search", actionsStart: pageSizePicker, icon: _jsx(InlineIcon, { icon: iconSearch }), placeholder: searchPlaceholder, "aria-label": searchPlaceholder.replace(/…$/, ""), "aria-controls": tableId, defaultValue: query.search, clearable: true, onValueChange: onSearch, onKeyDown: event => {
|
|
466
|
+
if (event.key === "Enter") {
|
|
467
|
+
clearTimeout(searchTimer.current);
|
|
468
|
+
table.setSearch(event.currentTarget.value);
|
|
469
|
+
}
|
|
470
|
+
} }, searchGeneration)) : (pageSizePicker), columnsMenu && columns.length > 1 && (_jsx(TableColumnsPanel, { columns: ordered, hidden: state.hidden, pinned: state.pinned, reorderable: reorderable, size: "sm", changed: table.hasColumnChanges(), onToggle: (key, hide) => table.setHidden(key, hide), onOrder: keys => table.setOrder(keys), onReset: () => table.resetColumns() })), reloadable && (_jsx(Button, { size: "sm", icon: _jsx(InlineIcon, { icon: iconRefresh, className: refreshing ? "nx-spin" : undefined }), "aria-label": "Reload", title: "Reload", disabled: status === "loading", onClick: () => {
|
|
471
|
+
onReload?.();
|
|
472
|
+
void table.reload({ quiet: true });
|
|
473
|
+
} })), optionEntries.length > 0 && (_jsx(Menu, { items: optionEntries, size: "sm", align: "end", "aria-label": "More options", onSelect: value => optionItems.find(action => action.id === value)?.onSelect(), children: _jsx(Button, { size: "sm", icon: _jsx(InlineIcon, { icon: iconDotsVertical }), "aria-label": "More options", title: "More options", "data-table-options": true }) })), actions] })] }), _jsxs("div", { className: "nx-table__frame", children: [selectedRows.length > 0 && (_jsx(SelectionBar, { count: selectedRows.length, actions: mass, rows: selectedRows, onClear: () => table.clearSelection(), controls: tableId })), _jsx("div", { className: "nx-table__progress", "aria-hidden": "true" }), _jsx("div", { ref: scrollRef, className: "nx-table__scroll", style: fill ? undefined : { maxHeight: cssLength(maxHeight) }, children: _jsxs("table", { ref: tableRef, id: tableId, className: "nx-table__table", "aria-label": props["aria-label"], "aria-labelledby": props["aria-labelledby"], "aria-busy": status === "loading" || refreshing || undefined, children: [sized && _jsx(TableColumnGroup, { columns: views, widths: state.widths, frame: frame, colRefs: colRefs }), _jsxs("thead", { ref: headRef, className: "nx-table__thead", children: [_jsxs("tr", { className: "nx-table__head", children: [frame.index && (_jsx(TableIndexCell, { heading: true, rowSpan: headRows, own: frame.index, number: "#", numbered: numbered, selectable: selectable, checkbox: selectable && (_jsx("label", { className: "nx-choice nx-choice--sm nx-table__check", children: _jsx("input", { ref: headerCheck, type: "checkbox", className: "nx-choice__control nx-choice__control--checkbox", checked: allOnPage, disabled: rows.length === 0 || status !== "ready", "aria-label": "Select every row on this page", onChange: event => {
|
|
474
|
+
lastToggled.current = null;
|
|
475
|
+
if (table.selectPage(event.currentTarget.checked) > 0)
|
|
476
|
+
warnFull();
|
|
477
|
+
} }) })) })), bands.map(band => band.group === null ? (headingCell(band.columns[0], headRows)) : (_jsx(GroupHeading, { band: band }, `group:${band.group}:${band.columns[0].column.key}`))), sized && _jsx("td", { className: "nx-table__heading nx-table__cell--filler", rowSpan: headRows }), frame.actions && (_jsx("th", { scope: "col", className: "nx-table__heading nx-table__cell--actions", rowSpan: headRows, "data-col": TABLE_ACTIONS_COLUMN, "data-pin": "right", "data-pin-edge": frame.actions.edge || undefined, style: { right: frame.actions.offset }, children: _jsx("span", { className: "nx-visually-hidden", children: "Actions" }) }))] }), grouped && _jsx("tr", { className: "nx-table__head nx-table__head--leaves", children: bands.filter(band => band.group !== null).flatMap(band => band.columns.map(view => headingCell(view))) })] }), pins && (_jsxs("tbody", { className: "nx-table__pins", "data-open": pinsOpen || undefined, children: [_jsx(TablePinnedHead, { span: span, label: pinnedLabel, count: pins.length, max: pinnedMax, loading: pinnedLoading, open: pinsOpen, onToggle: togglePins }), pinnedBody] })), _jsx("tbody", { children: body })] }) })] }), _jsx(TableFooter, { state: state, disabled: refreshing, onPage: page => table.setPage(page) })] }));
|
|
478
|
+
}
|
|
479
|
+
/** Where "Move left" and "Move right" put a column: the key it goes before, or null for the end of its side. */
|
|
480
|
+
function moveTargets(views, key) {
|
|
481
|
+
// A column moves among the columns on its own side, past the ones that stay put.
|
|
482
|
+
const pin = views.find(view => view.column.key === key)?.pin ?? null;
|
|
483
|
+
const group = views.filter(view => view.pin === pin && view.movable);
|
|
484
|
+
const at = group.findIndex(view => view.column.key === key);
|
|
485
|
+
if (at < 0)
|
|
486
|
+
return { left: undefined, right: undefined };
|
|
487
|
+
return {
|
|
488
|
+
left: at > 0 ? group[at - 1].column.key : undefined,
|
|
489
|
+
right: at < group.length - 1 ? (group[at + 2]?.column.key ?? null) : undefined,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* The heading of a band: one cell centred over the columns of its run, which reads as a heading and not as a column —
|
|
494
|
+
* it sorts nothing, has no menu, and has no edge to drag. A band over pinned columns stays with them.
|
|
495
|
+
*/
|
|
496
|
+
function GroupHeading({ band }) {
|
|
497
|
+
const { columns } = band;
|
|
498
|
+
const pin = columns[0].pin;
|
|
499
|
+
// Pinned: the band sits where the outermost column of its own side sits.
|
|
500
|
+
const style = pin === "left" ? { left: columns[0].offset } : pin === "right" ? { right: columns[columns.length - 1].offset } : undefined;
|
|
501
|
+
return (_jsx("th", { scope: "colgroup", className: "nx-table__heading nx-table__heading--group", colSpan: columns.length, "data-group": band.group, "data-pin": pin ?? undefined, "data-pin-edge": (pin && columns.some(view => view.edge)) || undefined, style: style, children: _jsx("span", { className: "nx-table__group", children: band.group }) }));
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* A column's heading: a button that sorts (the direction as an arrow and in `aria-sort`), a menu that pins, moves,
|
|
505
|
+
* fits, and hides the column, and an edge to drag for its width. The heading itself drags into another place.
|
|
506
|
+
* While the selection bar covers the headings (`covered`), Tab skips what is under it.
|
|
507
|
+
*/
|
|
508
|
+
function HeaderCell({ view, sized, rowSpan, sort, disabled, covered, canPin, canHide, neighbours, onSort, onPin, onMove, onFit, onHide, onPointerDown, onResizerDown }) {
|
|
509
|
+
const { column } = view;
|
|
510
|
+
const direction = sort?.key === column.key ? sort.direction : null;
|
|
511
|
+
const align = columnAlign(column);
|
|
512
|
+
const sortable = column.sortable !== false;
|
|
513
|
+
const entries = [];
|
|
514
|
+
if (sortable) {
|
|
515
|
+
// The heading's arrow says which way it is sorted now; the menu offers a direction straight away.
|
|
516
|
+
entries.push({ value: "asc", label: "Sort ascending", icon: _jsx(InlineIcon, { icon: iconSortAscending }), disabled: direction === "asc" });
|
|
517
|
+
entries.push({ value: "desc", label: "Sort descending", icon: _jsx(InlineIcon, { icon: iconSortDescending }), disabled: direction === "desc" });
|
|
518
|
+
if (direction)
|
|
519
|
+
entries.push({ value: "no-sort", label: "Clear sort", icon: _jsx(InlineIcon, { icon: iconArrowsSort }) });
|
|
520
|
+
}
|
|
521
|
+
if (canPin) {
|
|
522
|
+
if (entries.length)
|
|
523
|
+
entries.push({ separator: true });
|
|
524
|
+
if (view.pin !== "left")
|
|
525
|
+
entries.push({ value: "pin-left", label: "Pin left", icon: _jsx(InlineIcon, { icon: iconArrowBarToLeft }) });
|
|
526
|
+
if (view.pin !== "right")
|
|
527
|
+
entries.push({ value: "pin-right", label: "Pin right", icon: _jsx(InlineIcon, { icon: iconArrowBarToRight }) });
|
|
528
|
+
if (view.pin)
|
|
529
|
+
entries.push({ value: "unpin", label: "Unpin", icon: _jsx(InlineIcon, { icon: iconPinnedOff }) });
|
|
530
|
+
}
|
|
531
|
+
if (view.movable && (neighbours.left !== undefined || neighbours.right !== undefined)) {
|
|
532
|
+
if (entries.length)
|
|
533
|
+
entries.push({ separator: true });
|
|
534
|
+
entries.push({ value: "move-left", label: "Move left", icon: _jsx(InlineIcon, { icon: iconArrowLeft }), disabled: neighbours.left === undefined });
|
|
535
|
+
entries.push({ value: "move-right", label: "Move right", icon: _jsx(InlineIcon, { icon: iconArrowRight }), disabled: neighbours.right === undefined });
|
|
536
|
+
}
|
|
537
|
+
if (view.resizable || canHide) {
|
|
538
|
+
if (entries.length)
|
|
539
|
+
entries.push({ separator: true });
|
|
540
|
+
if (view.resizable)
|
|
541
|
+
entries.push({ value: "fit", label: "Fit to content", icon: _jsx(InlineIcon, { icon: iconArrowAutofitWidth }) });
|
|
542
|
+
if (canHide)
|
|
543
|
+
entries.push({ value: "hide", label: "Hide column", icon: _jsx(InlineIcon, { icon: iconEyeOff }) });
|
|
544
|
+
}
|
|
545
|
+
function choose(value) {
|
|
546
|
+
if (value === "asc" || value === "desc")
|
|
547
|
+
onSort(value);
|
|
548
|
+
else if (value === "no-sort")
|
|
549
|
+
onSort(null);
|
|
550
|
+
else if (value === "pin-left")
|
|
551
|
+
onPin("left");
|
|
552
|
+
else if (value === "pin-right")
|
|
553
|
+
onPin("right");
|
|
554
|
+
else if (value === "unpin")
|
|
555
|
+
onPin(null);
|
|
556
|
+
else if (value === "move-left")
|
|
557
|
+
onMove(neighbours.left ?? null);
|
|
558
|
+
else if (value === "move-right")
|
|
559
|
+
onMove(neighbours.right ?? null);
|
|
560
|
+
else if (value === "fit")
|
|
561
|
+
onFit();
|
|
562
|
+
else if (value === "hide")
|
|
563
|
+
onHide();
|
|
564
|
+
}
|
|
565
|
+
const label = _jsx("span", { className: "nx-table__heading-label", children: column.label });
|
|
566
|
+
return (_jsxs("th", { scope: "col", className: cx("nx-table__heading", `nx-table__cell--${align}`), rowSpan: rowSpan, "data-col": column.key, "data-pin": view.pin ?? undefined, "data-pin-edge": (view.pin && view.edge) || undefined, "data-movable": view.movable || undefined, style: columnStyle(view, sized), "aria-sort": direction === "asc" ? "ascending" : direction === "desc" ? "descending" : undefined, onPointerDown: onPointerDown, children: [_jsxs("div", { className: "nx-table__heading-inner", children: [sortable ? (_jsxs("button", { type: "button", className: "nx-table__sort", "data-sorted": direction ?? undefined, disabled: disabled && !direction, tabIndex: covered ? -1 : undefined, onClick: () => onSort(direction === "asc" ? "desc" : direction === "desc" ? null : "asc"), title: `Sort by ${column.label}`, children: [label, _jsx(InlineIcon, { icon: direction === "asc" ? iconArrowUp : direction === "desc" ? iconArrowDown : iconArrowsSort, className: "nx-table__sort-icon" })] })) : (label), entries.length > 0 && (_jsx(Menu, { items: entries, size: "sm", align: "end", "aria-label": `${column.label}: column`, onSelect: choose, children: _jsx(Button, { variant: "ghost", size: "sm", className: "nx-table__col-menu", icon: _jsx(InlineIcon, { icon: iconChevronDown }), tabIndex: covered ? -1 : undefined, "aria-label": `${column.label}: column options`, title: "Column options" }) }))] }), view.resizable && (_jsx("span", { className: "nx-table__resizer", "aria-hidden": "true", "data-resize": column.key, title: `Drag to resize ${column.label}, double-click to fit`, onPointerDown: onResizerDown, onDoubleClick: onFit }))] }));
|
|
567
|
+
}
|
|
568
|
+
/** The bar over the column headings while rows are selected: the count, one button for the actions, and Clear. */
|
|
569
|
+
function SelectionBar({ count, actions, rows, onClear, controls }) {
|
|
570
|
+
const [running, setRunning] = useState(null);
|
|
571
|
+
const [chosen, setChosen] = useState(undefined);
|
|
572
|
+
const alive = useRef(true);
|
|
573
|
+
// The bar is alive again on every mount, not only the first: React runs this twice in development, and the second
|
|
574
|
+
// mount would otherwise keep the `false` the first one left, so an action's spinner would never stop.
|
|
575
|
+
useEffect(() => {
|
|
576
|
+
alive.current = true;
|
|
577
|
+
return () => {
|
|
578
|
+
alive.current = false;
|
|
579
|
+
};
|
|
580
|
+
}, []);
|
|
581
|
+
const usable = actions.filter(action => !action.disabled);
|
|
582
|
+
const current = actions.find(action => action.id === chosen && !action.disabled) ?? usable[0] ?? actions[0];
|
|
583
|
+
function run(action) {
|
|
584
|
+
if (!action || action.disabled)
|
|
585
|
+
return;
|
|
586
|
+
const result = action.onSelect(rows);
|
|
587
|
+
if (!(result instanceof Promise))
|
|
588
|
+
return;
|
|
589
|
+
setRunning(action.id);
|
|
590
|
+
void result.finally(() => alive.current && setRunning(null)).catch(() => undefined);
|
|
591
|
+
}
|
|
592
|
+
// Every action on the selected rows lives in one button: the one shown runs on a click, the menu holds the rest and
|
|
593
|
+
// makes the one picked the button, the way a split button works everywhere else.
|
|
594
|
+
const options = actions.map(action => ({ value: action.id, label: action.label, icon: action.icon, disabled: action.disabled }));
|
|
595
|
+
return (_jsxs("div", { className: "nx-table__selection", role: "toolbar", "aria-label": "Selected rows", "aria-controls": controls, children: [_jsxs("span", { className: "nx-table__selection-count", "aria-live": "polite", children: [formatCount(count), " selected"] }), _jsx("div", { className: "nx-table__selection-actions", children: actions.length > 1 ? (_jsx(SplitButton, { options: options, value: current?.id, size: "sm", variant: current?.tone === "danger" ? "danger" : "primary", loading: running !== null, menuLabel: "More actions on the selected rows", className: "nx-table__mass", onValueChange: value => setChosen(value), onAction: value => run(actions.find(action => action.id === value)) })) : (actions.map(action => (_jsx(Button, { variant: "primary", size: "sm", icon: action.icon, loading: running === action.id, disabled: action.disabled, "data-action": action.id, title: action.label, className: "nx-table__mass", onClick: () => run(action), children: action.label }, action.id)))) }), _jsx(Button, { variant: "ghost", size: "sm", icon: _jsx(InlineIcon, { icon: iconX }), className: "nx-table__selection-clear", disabled: running !== null, onClick: onClear, children: "Clear" })] }));
|
|
596
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { TableColumn, TablePinSide, TableRow } from "../../Interfaces/TableInterfaces.ts";
|
|
2
|
+
export interface TableColumnsPanelProps<TRow extends TableRow> {
|
|
3
|
+
/** Every column the table has, in the order they are in now - hidden ones among them. */
|
|
4
|
+
columns: readonly TableColumn<TRow>[];
|
|
5
|
+
hidden: ReadonlySet<string>;
|
|
6
|
+
pinned: Readonly<Record<string, TablePinSide>>;
|
|
7
|
+
/** False while the table is not reorderable: the panel then only shows and hides. */
|
|
8
|
+
reorderable?: boolean;
|
|
9
|
+
/** Default `md`. */
|
|
10
|
+
size?: "sm" | "md";
|
|
11
|
+
/** Whether the user has arranged anything, so Reset has something to undo. */
|
|
12
|
+
changed?: boolean;
|
|
13
|
+
onToggle: (key: string, hidden: boolean) => void;
|
|
14
|
+
/** The whole order, by key, after a move. */
|
|
15
|
+
onOrder: (keys: readonly string[]) => void;
|
|
16
|
+
onReset?: () => void;
|
|
17
|
+
"aria-label"?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The button that opens the Columns panel, and the panel itself: a checkbox per column, bands with their columns under
|
|
21
|
+
* them, and a handle to drag a row into another place. The panel is a popover under the button, so it closes on Esc,
|
|
22
|
+
* on a click outside, and on Tab.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* <TableColumnsPanel columns={columns} hidden={state.hidden} pinned={state.pinned}
|
|
26
|
+
* onToggle={(key, hide) => table.setHidden(key, hide)} onOrder={keys => table.setOrder(keys)} />
|
|
27
|
+
*/
|
|
28
|
+
export declare function TableColumnsPanel<TRow extends TableRow>({ columns, hidden, pinned, reorderable, size, changed, onToggle, onOrder, onReset, "aria-label": ariaLabel, }: TableColumnsPanelProps<TRow>): import("react").JSX.Element;
|