@svgrid/grid 1.2.34 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -5
- package/dist/FlexRender.svelte +96 -96
- package/dist/GridFooter.svelte +178 -178
- package/dist/SvAccordion.svelte +109 -0
- package/dist/SvAccordion.svelte.d.ts +28 -0
- package/dist/SvAlert.svelte +92 -0
- package/dist/SvAlert.svelte.d.ts +27 -0
- package/dist/SvAutoComplete.svelte +47 -18
- package/dist/SvAutoComplete.svelte.d.ts +2 -5
- package/dist/SvAvatar.svelte +75 -0
- package/dist/SvAvatar.svelte.d.ts +14 -0
- package/dist/SvAvatarGroup.svelte +48 -0
- package/dist/SvAvatarGroup.svelte.d.ts +16 -0
- package/dist/SvBadge.svelte +54 -0
- package/dist/SvBadge.svelte.d.ts +23 -0
- package/dist/SvBreadcrumb.svelte +88 -0
- package/dist/SvBreadcrumb.svelte.d.ts +23 -0
- package/dist/SvButtonGroup.svelte +121 -0
- package/dist/SvButtonGroup.svelte.d.ts +16 -0
- package/dist/SvCalendar.svelte +72 -9
- package/dist/SvCalendar.svelte.d.ts +34 -1
- package/dist/SvCard.svelte +71 -0
- package/dist/SvCard.svelte.d.ts +28 -0
- package/dist/SvCarousel.svelte +134 -0
- package/dist/SvCarousel.svelte.d.ts +28 -0
- package/dist/SvCheckBox.svelte +36 -21
- package/dist/SvCheckBox.svelte.d.ts +2 -5
- package/dist/SvChip.svelte +88 -0
- package/dist/SvChip.svelte.d.ts +31 -0
- package/dist/SvCircularProgress.svelte +109 -0
- package/dist/SvCircularProgress.svelte.d.ts +31 -0
- package/dist/SvColorInput.svelte +54 -27
- package/dist/SvColorInput.svelte.d.ts +10 -6
- package/dist/SvComboBox.svelte +119 -33
- package/dist/SvComboBox.svelte.d.ts +18 -6
- package/dist/SvCommand.svelte +197 -0
- package/dist/SvCommand.svelte.d.ts +26 -0
- package/dist/SvContextMenu.svelte +96 -0
- package/dist/SvContextMenu.svelte.d.ts +25 -0
- package/dist/SvCountryInput.svelte +56 -21
- package/dist/SvCountryInput.svelte.d.ts +9 -5
- package/dist/SvDateRangeInput.svelte +256 -0
- package/dist/SvDateRangeInput.svelte.d.ts +37 -0
- package/dist/SvDateTimePicker.svelte +57 -20
- package/dist/SvDateTimePicker.svelte.d.ts +12 -4
- package/dist/SvDivider.svelte +61 -0
- package/dist/SvDivider.svelte.d.ts +22 -0
- package/dist/SvDrawer.svelte +226 -0
- package/dist/SvDrawer.svelte.d.ts +43 -0
- package/dist/SvDropDownList.svelte +114 -35
- package/dist/SvDropDownList.svelte.d.ts +7 -6
- package/dist/SvDurationInput.svelte +108 -0
- package/dist/SvDurationInput.svelte.d.ts +15 -0
- package/dist/SvEmptyState.svelte +59 -0
- package/dist/SvEmptyState.svelte.d.ts +25 -0
- package/dist/SvField.svelte +85 -0
- package/dist/SvField.svelte.d.ts +30 -0
- package/dist/SvFileUpload.svelte +169 -0
- package/dist/SvFileUpload.svelte.d.ts +24 -0
- package/dist/SvForm.svelte +7 -1
- package/dist/SvForm.svelte.d.ts +3 -0
- package/dist/SvGauge.svelte +7 -1
- package/dist/SvGauge.svelte.d.ts +11 -0
- package/dist/SvGrid.controller.svelte.d.ts +6 -0
- package/dist/SvGrid.controller.svelte.js +64 -9
- package/dist/SvGrid.css +2376 -2343
- package/dist/SvGrid.svelte +2764 -2651
- package/dist/SvGrid.types.d.ts +366 -0
- package/dist/SvGridBoard.svelte +2281 -0
- package/dist/SvGridBoard.svelte.d.ts +31 -0
- package/dist/SvGridDropdown.svelte +666 -665
- package/dist/SvGridSelect.svelte +255 -0
- package/dist/SvGridSelect.svelte.d.ts +32 -0
- package/dist/SvGroupCell.svelte +109 -0
- package/dist/SvGroupCell.svelte.d.ts +41 -0
- package/dist/SvListBox.svelte +123 -36
- package/dist/SvListBox.svelte.d.ts +12 -8
- package/dist/SvMaskedInput.svelte +53 -13
- package/dist/SvMaskedInput.svelte.d.ts +16 -6
- package/dist/SvMenu.svelte +101 -0
- package/dist/SvMenu.svelte.d.ts +26 -0
- package/dist/SvMenuList.svelte +143 -0
- package/dist/SvMenuList.svelte.d.ts +24 -0
- package/dist/SvModal.svelte +220 -0
- package/dist/SvModal.svelte.d.ts +31 -0
- package/dist/SvMultiSelect.svelte +277 -0
- package/dist/SvMultiSelect.svelte.d.ts +31 -0
- package/dist/SvNavPane.svelte +314 -0
- package/dist/SvNavPane.svelte.d.ts +59 -0
- package/dist/SvNumberInput.svelte +23 -9
- package/dist/SvNumberInput.svelte.d.ts +2 -0
- package/dist/SvOtpInput.svelte +157 -0
- package/dist/SvOtpInput.svelte.d.ts +16 -0
- package/dist/SvPagination.svelte +100 -0
- package/dist/SvPagination.svelte.d.ts +16 -0
- package/dist/SvPasswordInput.svelte +42 -12
- package/dist/SvPasswordInput.svelte.d.ts +13 -6
- package/dist/SvPhoneInput.svelte +43 -11
- package/dist/SvPhoneInput.svelte.d.ts +10 -11
- package/dist/SvPopover.svelte +127 -0
- package/dist/SvPopover.svelte.d.ts +35 -0
- package/dist/SvProgress.svelte +116 -0
- package/dist/SvProgress.svelte.d.ts +32 -0
- package/dist/SvRadioGroup.svelte +40 -22
- package/dist/SvRadioGroup.svelte.d.ts +2 -10
- package/dist/SvRating.svelte +51 -30
- package/dist/SvRating.svelte.d.ts +10 -7
- package/dist/SvRichText.svelte +192 -0
- package/dist/SvRichText.svelte.d.ts +16 -0
- package/dist/SvRowGroupPanel.svelte +147 -0
- package/dist/SvRowGroupPanel.svelte.d.ts +17 -0
- package/dist/SvScrollArea.svelte +50 -0
- package/dist/SvScrollArea.svelte.d.ts +22 -0
- package/dist/SvSkeleton.svelte +76 -0
- package/dist/SvSkeleton.svelte.d.ts +25 -0
- package/dist/SvSlider.svelte +41 -9
- package/dist/SvSlider.svelte.d.ts +4 -4
- package/dist/SvSparkline.svelte +77 -0
- package/dist/SvSparkline.svelte.d.ts +28 -0
- package/dist/SvSplitter.svelte +138 -0
- package/dist/SvSplitter.svelte.d.ts +32 -0
- package/dist/SvStat.svelte +86 -0
- package/dist/SvStat.svelte.d.ts +30 -0
- package/dist/SvStepper.svelte +91 -0
- package/dist/SvStepper.svelte.d.ts +24 -0
- package/dist/SvSwitchButton.svelte +39 -24
- package/dist/SvSwitchButton.svelte.d.ts +2 -5
- package/dist/SvTabs.svelte +147 -28
- package/dist/SvTabs.svelte.d.ts +16 -4
- package/dist/SvTagsInput.svelte +59 -22
- package/dist/SvTagsInput.svelte.d.ts +9 -4
- package/dist/SvTextArea.svelte +113 -0
- package/dist/SvTextArea.svelte.d.ts +18 -0
- package/dist/SvTextInput.svelte +115 -0
- package/dist/SvTextInput.svelte.d.ts +20 -0
- package/dist/SvTimePicker.svelte +19 -5
- package/dist/SvTimePicker.svelte.d.ts +12 -0
- package/dist/SvTimeline.svelte +75 -0
- package/dist/SvTimeline.svelte.d.ts +24 -0
- package/dist/SvToaster.svelte +124 -0
- package/dist/SvToaster.svelte.d.ts +10 -0
- package/dist/SvToggleButton.svelte +37 -12
- package/dist/SvToggleButton.svelte.d.ts +2 -4
- package/dist/SvTooltip.svelte +113 -0
- package/dist/SvTooltip.svelte.d.ts +22 -0
- package/dist/SvTour.svelte +151 -0
- package/dist/SvTour.svelte.d.ts +25 -0
- package/dist/SvTree.svelte +286 -44
- package/dist/SvTree.svelte.d.ts +26 -9
- package/dist/SvTreeSelect.svelte +222 -0
- package/dist/SvTreeSelect.svelte.d.ts +24 -0
- package/dist/a11y/dismissable.d.ts +32 -0
- package/dist/a11y/dismissable.js +54 -0
- package/dist/a11y/focus-trap.d.ts +39 -0
- package/dist/a11y/focus-trap.js +142 -0
- package/dist/a11y/live-region.d.ts +8 -0
- package/dist/a11y/live-region.js +60 -0
- package/dist/a11y/scroll-lock.d.ts +4 -0
- package/dist/a11y/scroll-lock.js +41 -0
- package/dist/a11y.d.ts +3 -1
- package/dist/a11y.js +1 -1
- package/dist/avatar.d.ts +10 -0
- package/dist/avatar.js +22 -0
- package/dist/builtin-editors.d.ts +5 -0
- package/dist/builtin-editors.js +55 -0
- package/dist/cdn/svgrid.js +16022 -8235
- package/dist/cdn/svgrid.svelte-external.js +14543 -6765
- package/dist/columns.js +3 -2
- package/dist/core.d.ts +7 -1
- package/dist/countries.d.ts +12 -0
- package/dist/countries.js +24 -0
- package/dist/createAccordion.svelte.d.ts +71 -0
- package/dist/createAccordion.svelte.js +99 -0
- package/dist/createAutocomplete.svelte.d.ts +13 -4
- package/dist/createAutocomplete.svelte.js +11 -1
- package/dist/createButtonGroup.svelte.d.ts +62 -0
- package/dist/createButtonGroup.svelte.js +154 -0
- package/dist/createCalendar.svelte.d.ts +5 -0
- package/dist/createCalendar.svelte.js +2 -0
- package/dist/createCheckbox.svelte.d.ts +9 -17
- package/dist/createCheckbox.svelte.js +28 -1
- package/dist/createColorInput.svelte.d.ts +12 -22
- package/dist/createColorInput.svelte.js +13 -0
- package/dist/createCombobox.svelte.d.ts +16 -5
- package/dist/createCombobox.svelte.js +13 -2
- package/dist/createCountryInput.svelte.d.ts +14 -3
- package/dist/createCountryInput.svelte.js +12 -2
- package/dist/createDateTimePicker.svelte.d.ts +17 -2
- package/dist/createDateTimePicker.svelte.js +12 -2
- package/dist/createDropdownList.svelte.d.ts +14 -5
- package/dist/createDropdownList.svelte.js +50 -1
- package/dist/createFileUpload.svelte.d.ts +68 -0
- package/dist/createFileUpload.svelte.js +122 -0
- package/dist/createListbox.svelte.d.ts +10 -1
- package/dist/createListbox.svelte.js +48 -1
- package/dist/createMaskedInput.svelte.d.ts +11 -2
- package/dist/createMaskedInput.svelte.js +11 -1
- package/dist/createNumberInput.svelte.d.ts +1 -0
- package/dist/createNumberInput.svelte.js +1 -0
- package/dist/createPasswordInput.svelte.d.ts +19 -21
- package/dist/createPasswordInput.svelte.js +16 -4
- package/dist/createPhoneInput.svelte.d.ts +26 -7
- package/dist/createPhoneInput.svelte.js +24 -3
- package/dist/createPopoverSelect.svelte.d.ts +86 -0
- package/dist/createPopoverSelect.svelte.js +196 -0
- package/dist/createRadioGroup.svelte.d.ts +9 -0
- package/dist/createRadioGroup.svelte.js +11 -1
- package/dist/createRating.svelte.d.ts +11 -0
- package/dist/createRating.svelte.js +12 -1
- package/dist/createSlider.svelte.d.ts +2 -0
- package/dist/createSlider.svelte.js +16 -5
- package/dist/createSplitter.svelte.d.ts +67 -0
- package/dist/createSplitter.svelte.js +99 -0
- package/dist/createSwitch.svelte.d.ts +9 -16
- package/dist/createSwitch.svelte.js +28 -1
- package/dist/createTabs.svelte.d.ts +7 -0
- package/dist/createTabs.svelte.js +17 -2
- package/dist/createTagsInput.svelte.d.ts +13 -1
- package/dist/createTagsInput.svelte.js +12 -3
- package/dist/createToggle.svelte.d.ts +9 -0
- package/dist/createToggle.svelte.js +12 -1
- package/dist/createTree.svelte.d.ts +19 -0
- package/dist/createTree.svelte.js +108 -15
- package/dist/debounce.d.ts +11 -0
- package/dist/debounce.js +30 -0
- package/dist/duration.d.ts +15 -0
- package/dist/duration.js +51 -0
- package/dist/editor-contract.d.ts +62 -7
- package/dist/editor-contract.js +45 -5
- package/dist/editor-registry.d.ts +67 -0
- package/dist/editor-registry.js +38 -0
- package/dist/editors/cell-editors.d.ts +1 -1
- package/dist/editors/cell-editors.js +5 -0
- package/dist/fuzzy.d.ts +3 -0
- package/dist/fuzzy.js +41 -0
- package/dist/grid-select.d.ts +9 -0
- package/dist/grid-select.js +8 -0
- package/dist/index.d.ts +79 -6
- package/dist/index.js +78 -4
- package/dist/keyboard-handlers.js +19 -0
- package/dist/list-option.d.ts +36 -0
- package/dist/list-option.js +68 -0
- package/dist/otp.d.ts +16 -0
- package/dist/otp.js +26 -0
- package/dist/paginate.d.ts +20 -0
- package/dist/paginate.js +40 -0
- package/dist/popover.d.ts +15 -1
- package/dist/popover.js +29 -3
- package/dist/recurrence.d.ts +36 -0
- package/dist/recurrence.js +90 -0
- package/dist/server-data-source.d.ts +89 -0
- package/dist/server-data-source.js +4 -0
- package/dist/server-group-model.d.ts +98 -0
- package/dist/server-group-model.js +263 -0
- package/dist/toast-store.svelte.d.ts +41 -0
- package/dist/toast-store.svelte.js +113 -0
- package/dist/tree-select.d.ts +19 -0
- package/dist/tree-select.js +40 -0
- package/dist/ui-app.types.d.ts +80 -0
- package/dist/ui-app.types.js +7 -0
- package/dist/validators.d.ts +43 -0
- package/dist/validators.js +53 -0
- package/dist/virtual.d.ts +35 -0
- package/dist/virtual.js +34 -0
- package/package.json +85 -85
- package/src/FlexRender.svelte +96 -96
- package/src/GridFooter.svelte +178 -178
- package/src/SvAccordion.svelte +109 -0
- package/src/SvAccordion.test.ts +66 -0
- package/src/SvAlert.svelte +92 -0
- package/src/SvAutoComplete.svelte +47 -18
- package/src/SvAvatar.svelte +75 -0
- package/src/SvAvatarGroup.svelte +48 -0
- package/src/SvBadge.svelte +54 -0
- package/src/SvBreadcrumb.svelte +88 -0
- package/src/SvButtonGroup.svelte +121 -0
- package/src/SvButtonGroup.test.ts +72 -0
- package/src/SvCalendar.svelte +72 -9
- package/src/SvCard.svelte +71 -0
- package/src/SvCarousel.svelte +134 -0
- package/src/SvCheckBox.svelte +36 -21
- package/src/SvChip.svelte +88 -0
- package/src/SvCircularProgress.svelte +109 -0
- package/src/SvColorInput.svelte +54 -27
- package/src/SvComboBox.svelte +119 -33
- package/src/SvCommand.svelte +197 -0
- package/src/SvContextMenu.svelte +96 -0
- package/src/SvCountryInput.svelte +56 -21
- package/src/SvDateRangeInput.svelte +256 -0
- package/src/SvDateRangeInput.test.ts +80 -0
- package/src/SvDateTimePicker.svelte +57 -20
- package/src/SvDivider.svelte +61 -0
- package/src/SvDrawer.svelte +226 -0
- package/src/SvDrawer.test.ts +125 -0
- package/src/SvDropDownList.svelte +114 -35
- package/src/SvDurationInput.svelte +108 -0
- package/src/SvEmptyState.svelte +59 -0
- package/src/SvField.svelte +85 -0
- package/src/SvFileUpload.svelte +169 -0
- package/src/SvFileUpload.test.ts +59 -0
- package/src/SvForm.svelte +7 -1
- package/src/SvGauge.svelte +7 -1
- package/src/SvGrid.controller.svelte.ts +2553 -2494
- package/src/SvGrid.css +2376 -2343
- package/src/SvGrid.svelte +2764 -2651
- package/src/SvGrid.types.ts +944 -570
- package/src/SvGridBoard.svelte +2281 -0
- package/src/SvGridDropdown.svelte +666 -665
- package/src/SvGridSelect.svelte +255 -0
- package/src/SvGridSelect.test.ts +100 -0
- package/src/SvGroupCell.svelte +109 -0
- package/src/SvListBox.svelte +123 -36
- package/src/SvMaskedInput.svelte +53 -13
- package/src/SvMenu.svelte +101 -0
- package/src/SvMenu.test.ts +73 -0
- package/src/SvMenuList.svelte +143 -0
- package/src/SvModal.svelte +220 -0
- package/src/SvMultiSelect.svelte +277 -0
- package/src/SvMultiSelect.test.ts +167 -0
- package/src/SvNavPane.svelte +314 -0
- package/src/SvNavPane.test.ts +70 -0
- package/src/SvNumberInput.svelte +23 -9
- package/src/SvOtpInput.svelte +157 -0
- package/src/SvPagination.svelte +100 -0
- package/src/SvPasswordInput.svelte +42 -12
- package/src/SvPhoneInput.svelte +43 -11
- package/src/SvPopover.svelte +127 -0
- package/src/SvProgress.svelte +116 -0
- package/src/SvRadioGroup.svelte +40 -22
- package/src/SvRating.svelte +51 -30
- package/src/SvRichText.svelte +192 -0
- package/src/SvRowGroupPanel.svelte +147 -0
- package/src/SvScrollArea.svelte +50 -0
- package/src/SvSkeleton.svelte +76 -0
- package/src/SvSlider.svelte +41 -9
- package/src/SvSparkline.svelte +77 -0
- package/src/SvSplitter.svelte +138 -0
- package/src/SvSplitter.test.ts +55 -0
- package/src/SvStat.svelte +86 -0
- package/src/SvStepper.svelte +91 -0
- package/src/SvSwitchButton.svelte +39 -24
- package/src/SvTabs.svelte +147 -28
- package/src/SvTagsInput.svelte +59 -22
- package/src/SvTextArea.svelte +113 -0
- package/src/SvTextInput.svelte +115 -0
- package/src/SvTimePicker.svelte +19 -5
- package/src/SvTimeline.svelte +75 -0
- package/src/SvToaster.svelte +124 -0
- package/src/SvToaster.test.ts +63 -0
- package/src/SvToggleButton.svelte +37 -12
- package/src/SvTooltip.svelte +113 -0
- package/src/SvTour.svelte +151 -0
- package/src/SvTree.svelte +286 -44
- package/src/SvTreeSelect.svelte +222 -0
- package/src/SvTreeSelect.test.ts +87 -0
- package/src/a11y/dismissable.test.ts +95 -0
- package/src/a11y/dismissable.ts +94 -0
- package/src/a11y/focus-trap.test.ts +110 -0
- package/src/a11y/focus-trap.ts +134 -0
- package/src/a11y/live-region.test.ts +40 -0
- package/src/a11y/live-region.ts +65 -0
- package/src/a11y/scroll-lock.test.ts +38 -0
- package/src/a11y/scroll-lock.ts +41 -0
- package/src/a11y.contract.test.ts +49 -49
- package/src/a11y.test.ts +59 -59
- package/src/a11y.ts +61 -59
- package/src/avatar.test.ts +32 -0
- package/src/avatar.ts +21 -0
- package/src/board.test.ts +927 -0
- package/src/build-api.ts +798 -798
- package/src/builtin-editors.grid.test.ts +85 -0
- package/src/builtin-editors.test.ts +58 -0
- package/src/builtin-editors.ts +56 -0
- package/src/cell-formatting.ts +169 -169
- package/src/cell-render.ts +469 -469
- package/src/collaboration.test.ts +104 -104
- package/src/collaboration.ts +167 -167
- package/src/columns.ts +3 -2
- package/src/core.performance.test.ts +30 -30
- package/src/core.ts +1111 -1101
- package/src/countries.ts +25 -0
- package/src/createAccordion.svelte.ts +126 -0
- package/src/createAutocomplete.svelte.ts +18 -1
- package/src/createButtonGroup.svelte.ts +166 -0
- package/src/createCalendar.svelte.ts +6 -0
- package/src/createCheckbox.svelte.ts +23 -1
- package/src/createColorInput.svelte.ts +20 -0
- package/src/createCombobox.svelte.ts +22 -2
- package/src/createCountryInput.svelte.ts +21 -2
- package/src/createDateTimePicker.svelte.ts +23 -2
- package/src/createDropdownList.svelte.ts +31 -2
- package/src/createFileUpload.svelte.ts +144 -0
- package/src/createGrid.svelte.ts +42 -42
- package/src/createGrid.test.ts +10 -10
- package/src/createGridState.svelte.ts +17 -17
- package/src/createListbox.svelte.ts +35 -3
- package/src/createMaskedInput.svelte.ts +18 -1
- package/src/createNumberInput.svelte.ts +2 -0
- package/src/createPasswordInput.svelte.ts +31 -5
- package/src/createPhoneInput.svelte.ts +38 -4
- package/src/createPopoverSelect.svelte.ts +208 -0
- package/src/createRadioGroup.svelte.ts +23 -1
- package/src/createRating.svelte.ts +26 -1
- package/src/createSlider.svelte.ts +14 -3
- package/src/createSplitter.svelte.ts +119 -0
- package/src/createSwitch.svelte.ts +26 -3
- package/src/createTabs.svelte.ts +20 -3
- package/src/createTagsInput.svelte.ts +24 -3
- package/src/createToggle.svelte.ts +25 -2
- package/src/createTree.svelte.ts +104 -14
- package/src/debounce.test.ts +50 -0
- package/src/debounce.ts +41 -0
- package/src/duration.test.ts +41 -0
- package/src/duration.ts +51 -0
- package/src/editing.test.ts +859 -859
- package/src/editing.ts +675 -675
- package/src/editor-contract.ts +87 -8
- package/src/editor-registry.grid.test.ts +98 -0
- package/src/editor-registry.test.ts +86 -0
- package/src/editor-registry.ts +106 -0
- package/src/editors/cell-editors.ts +10 -1
- package/src/export-data-api.test.ts +126 -126
- package/src/export-format.test.ts +107 -107
- package/src/export-format.ts +598 -598
- package/src/flex-render.ts +3 -3
- package/src/fuzzy.test.ts +40 -0
- package/src/fuzzy.ts +39 -0
- package/src/grid-select.test.ts +28 -0
- package/src/grid-select.ts +20 -0
- package/src/index.ts +463 -318
- package/src/keyboard-handlers.ts +20 -0
- package/src/keyboard.test.ts +59 -59
- package/src/keyboard.ts +97 -97
- package/src/list-option.ts +74 -0
- package/src/list-power.test.ts +71 -0
- package/src/menus.ts +582 -582
- package/src/merge-objects.ts +48 -48
- package/src/otp.test.ts +28 -0
- package/src/otp.ts +28 -0
- package/src/overlays.test.ts +85 -0
- package/src/paginate.test.ts +41 -0
- package/src/paginate.ts +61 -0
- package/src/phone-valid.test.ts +26 -0
- package/src/popover.ts +31 -3
- package/src/progress.test.ts +55 -0
- package/src/recurrence.test.ts +91 -0
- package/src/recurrence.ts +111 -0
- package/src/render-component.ts +28 -28
- package/src/selection.test.ts +754 -754
- package/src/selection.ts +600 -600
- package/src/server-data-source.test.ts +289 -289
- package/src/server-data-source.ts +413 -311
- package/src/server-group-model.test.ts +294 -0
- package/src/server-group-model.ts +370 -0
- package/src/sparkline.test.ts +68 -68
- package/src/sparkline.ts +169 -169
- package/src/spreadsheet.test.ts +489 -489
- package/src/spreadsheet.ts +304 -304
- package/src/static-functions.ts +11 -11
- package/src/subscribe.ts +38 -38
- package/src/svgrid-wrapper.types.ts +439 -439
- package/src/svgrid.behavior.test.ts +706 -706
- package/src/svgrid.features.test.ts +157 -157
- package/src/svgrid.new-features.wrapper.test.ts +251 -251
- package/src/svgrid.responsive.test.ts +72 -0
- package/src/svgrid.wrapper.test.ts +40 -40
- package/src/toast-store.svelte.ts +148 -0
- package/src/toast-store.test.ts +86 -0
- package/src/tree-edit.test.ts +49 -0
- package/src/tree-filter.test.ts +56 -0
- package/src/tree-select.test.ts +48 -0
- package/src/tree-select.ts +62 -0
- package/src/ui-app.test.ts +130 -0
- package/src/ui-app.types.ts +88 -0
- package/src/ui-display.test.ts +79 -0
- package/src/ui-inputs.test.ts +1 -1
- package/src/ui-status.test.ts +141 -0
- package/src/ui-tier1.test.ts +142 -0
- package/src/ui-tier2.test.ts +124 -0
- package/src/ui-tier3.test.ts +139 -0
- package/src/validators.test.ts +58 -0
- package/src/validators.ts +105 -0
- package/src/virtual.test.ts +72 -0
- package/src/virtual.ts +67 -0
- package/src/virtualization/column-virtualizer.test.ts +27 -27
- package/src/virtualization/column-virtualizer.ts +30 -30
- package/src/virtualization/svelte-virtualizer.svelte.ts +26 -26
- package/src/virtualization/types.ts +30 -30
- package/src/virtualization/virtualizer.test.ts +47 -47
- package/src/virtualization/virtualizer.ts +296 -296
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { createServerGroupModel } from './server-group-model'
|
|
3
|
+
import type { ServerDataSource, ServerDisplayRow, ServerGroupRow } from './server-data-source'
|
|
4
|
+
|
|
5
|
+
type Row = { country: string; city: string; amount: number }
|
|
6
|
+
|
|
7
|
+
const DATA: Row[] = [
|
|
8
|
+
{ country: 'DE', city: 'Berlin', amount: 10 },
|
|
9
|
+
{ country: 'DE', city: 'Berlin', amount: 5 },
|
|
10
|
+
{ country: 'DE', city: 'Munich', amount: 7 },
|
|
11
|
+
{ country: 'FR', city: 'Paris', amount: 3 },
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A fake backend that groups + aggregates DATA per the request's groupBy /
|
|
16
|
+
* groupKeys, exactly as a real SQL GROUP BY would - returns group rows for a
|
|
17
|
+
* group level and leaf rows at the bottom.
|
|
18
|
+
*/
|
|
19
|
+
function makeGroupSource(data: Row[], delay = 0): ServerDataSource<Row> & { calls: number } {
|
|
20
|
+
return {
|
|
21
|
+
calls: 0,
|
|
22
|
+
async getRows(req) {
|
|
23
|
+
this.calls += 1
|
|
24
|
+
if (delay) await new Promise((r) => setTimeout(r, delay))
|
|
25
|
+
const subset = data.filter((r) =>
|
|
26
|
+
req.groupKeys.every((k, i) => String((r as Record<string, unknown>)[req.groupBy[i]!]) === k),
|
|
27
|
+
)
|
|
28
|
+
const level = req.groupKeys.length
|
|
29
|
+
if (level < req.groupBy.length) {
|
|
30
|
+
const field = req.groupBy[level]!
|
|
31
|
+
const map = new Map<string, Record<string, unknown>>()
|
|
32
|
+
for (const r of subset) {
|
|
33
|
+
const key = String((r as Record<string, unknown>)[field])
|
|
34
|
+
const agg = map.get(key) ?? { [field]: (r as Record<string, unknown>)[field] }
|
|
35
|
+
for (const a of req.aggregations) {
|
|
36
|
+
const cur = (agg[a.col] as number) ?? 0
|
|
37
|
+
agg[a.col] = a.fn === 'count' ? cur + 1 : cur + ((r as Record<string, unknown>)[a.col] as number)
|
|
38
|
+
}
|
|
39
|
+
map.set(key, agg)
|
|
40
|
+
}
|
|
41
|
+
const groups = [...map.values()]
|
|
42
|
+
return { rows: groups.slice(req.startRow, req.endRow) as unknown as Row[], rowCount: groups.length }
|
|
43
|
+
}
|
|
44
|
+
return { rows: subset.slice(req.startRow, req.endRow), rowCount: subset.length }
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const flush = () => new Promise((r) => setTimeout(r, 0))
|
|
50
|
+
const groups = (rows: ServerDisplayRow<Row>[]) => rows.filter((r): r is ServerGroupRow<Row> => r.kind === 'group')
|
|
51
|
+
|
|
52
|
+
describe('createServerGroupModel', () => {
|
|
53
|
+
it('refresh() loads the top-level groups with aggregates', async () => {
|
|
54
|
+
const src = makeGroupSource(DATA)
|
|
55
|
+
let state = createServerGroupModel<Row>(src, {
|
|
56
|
+
groupBy: ['country', 'city'],
|
|
57
|
+
aggregations: [{ col: 'amount', fn: 'sum' }],
|
|
58
|
+
onChange: (s) => (state = s as never),
|
|
59
|
+
}).getState()
|
|
60
|
+
const ctl = createServerGroupModel<Row>(src, {
|
|
61
|
+
groupBy: ['country', 'city'],
|
|
62
|
+
aggregations: [{ col: 'amount', fn: 'sum' }],
|
|
63
|
+
onChange: (s) => (state = s as never),
|
|
64
|
+
})
|
|
65
|
+
ctl.refresh()
|
|
66
|
+
await flush()
|
|
67
|
+
const g = groups(state.displayRows)
|
|
68
|
+
expect(g.map((r) => r.key)).toEqual(['DE', 'FR'])
|
|
69
|
+
expect(g.every((r) => r.level === 0 && !r.expanded)).toBe(true)
|
|
70
|
+
expect(g[0]!.aggregates.amount).toBe(22) // 10 + 5 + 7
|
|
71
|
+
expect(g[1]!.aggregates.amount).toBe(3)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('expanding a group lazily fetches and splices its child groups', async () => {
|
|
75
|
+
const src = makeGroupSource(DATA)
|
|
76
|
+
let state = { displayRows: [] as ServerDisplayRow<Row>[] } as never
|
|
77
|
+
const ctl = createServerGroupModel<Row>(src, {
|
|
78
|
+
groupBy: ['country', 'city'],
|
|
79
|
+
aggregations: [{ col: 'amount', fn: 'sum' }],
|
|
80
|
+
onChange: (s) => (state = s as never),
|
|
81
|
+
})
|
|
82
|
+
ctl.refresh()
|
|
83
|
+
await flush()
|
|
84
|
+
const callsBefore = src.calls
|
|
85
|
+
ctl.expandGroup(['DE'])
|
|
86
|
+
await flush()
|
|
87
|
+
expect(src.calls).toBe(callsBefore + 1) // one fetch for DE's children
|
|
88
|
+
const rows = (state as { displayRows: ServerDisplayRow<Row>[] }).displayRows
|
|
89
|
+
// DE (expanded), Berlin, Munich, FR
|
|
90
|
+
expect(rows.map((r) => (r.kind === 'group' ? r.key : 'leaf'))).toEqual(['DE', 'Berlin', 'Munich', 'FR'])
|
|
91
|
+
const berlin = rows.find((r) => r.kind === 'group' && r.key === 'Berlin') as ServerGroupRow<Row>
|
|
92
|
+
expect(berlin.level).toBe(1)
|
|
93
|
+
expect(berlin.aggregates.amount).toBe(15) // 10 + 5
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('expanding a bottom-level group returns leaf rows', async () => {
|
|
97
|
+
const src = makeGroupSource(DATA)
|
|
98
|
+
let state = { displayRows: [] as ServerDisplayRow<Row>[] } as never
|
|
99
|
+
const ctl = createServerGroupModel<Row>(src, {
|
|
100
|
+
groupBy: ['country', 'city'],
|
|
101
|
+
onChange: (s) => (state = s as never),
|
|
102
|
+
})
|
|
103
|
+
ctl.refresh()
|
|
104
|
+
await flush()
|
|
105
|
+
ctl.expandGroup(['DE'])
|
|
106
|
+
await flush()
|
|
107
|
+
ctl.expandGroup(['DE', 'Berlin'])
|
|
108
|
+
await flush()
|
|
109
|
+
const rows = (state as { displayRows: ServerDisplayRow<Row>[] }).displayRows
|
|
110
|
+
const leaves = rows.filter((r) => r.kind === 'leaf')
|
|
111
|
+
expect(leaves).toHaveLength(2) // the two Berlin rows
|
|
112
|
+
expect(leaves.every((r) => r.level === 2)).toBe(true)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('collapse removes the children again', async () => {
|
|
116
|
+
const src = makeGroupSource(DATA)
|
|
117
|
+
let state = { displayRows: [] as ServerDisplayRow<Row>[] } as never
|
|
118
|
+
const ctl = createServerGroupModel<Row>(src, {
|
|
119
|
+
groupBy: ['country', 'city'],
|
|
120
|
+
onChange: (s) => (state = s as never),
|
|
121
|
+
})
|
|
122
|
+
ctl.refresh()
|
|
123
|
+
await flush()
|
|
124
|
+
ctl.expandGroup(['DE'])
|
|
125
|
+
await flush()
|
|
126
|
+
ctl.collapseGroup(['DE'])
|
|
127
|
+
const rows = ctl.getState().displayRows
|
|
128
|
+
expect(rows.map((r) => (r.kind === 'group' ? r.key : 'leaf'))).toEqual(['DE', 'FR'])
|
|
129
|
+
expect(ctl.isExpanded(['DE'])).toBe(false)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('setFilter resets the tree and refetches from the top', async () => {
|
|
133
|
+
const src = makeGroupSource(DATA)
|
|
134
|
+
const ctl = createServerGroupModel<Row>(src, { groupBy: ['country'], onChange: () => {} })
|
|
135
|
+
ctl.refresh()
|
|
136
|
+
await flush()
|
|
137
|
+
const before = src.calls
|
|
138
|
+
ctl.setFilter({ global: 'x' })
|
|
139
|
+
await flush()
|
|
140
|
+
expect(src.calls).toBeGreaterThan(before)
|
|
141
|
+
expect(ctl.getState().filterModel).toEqual({ global: 'x' })
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('a stale expand response is dropped after a reset (race safety)', async () => {
|
|
145
|
+
const src = makeGroupSource(DATA, 20) // slow
|
|
146
|
+
let state = { displayRows: [] as ServerDisplayRow<Row>[] } as never
|
|
147
|
+
const ctl = createServerGroupModel<Row>(src, {
|
|
148
|
+
groupBy: ['country', 'city'],
|
|
149
|
+
onChange: (s) => (state = s as never),
|
|
150
|
+
})
|
|
151
|
+
ctl.refresh()
|
|
152
|
+
await new Promise((r) => setTimeout(r, 30))
|
|
153
|
+
ctl.expandGroup(['DE']) // in-flight, slow
|
|
154
|
+
ctl.setGroupBy(['city']) // reset (bumps generation) before DE's children land
|
|
155
|
+
await new Promise((r) => setTimeout(r, 60))
|
|
156
|
+
const rows = (state as { displayRows: ServerDisplayRow<Row>[] }).displayRows
|
|
157
|
+
// Regrouped by city at the top; no stale DE subtree leaked in.
|
|
158
|
+
expect(rows.every((r) => r.level === 0)).toBe(true)
|
|
159
|
+
expect(rows.some((r) => r.kind === 'group' && r.key === 'DE')).toBe(false)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('pages a group\'s children in blocks (load more)', async () => {
|
|
163
|
+
const data: Row[] = [
|
|
164
|
+
{ country: 'DE', city: 'B', amount: 1 },
|
|
165
|
+
{ country: 'DE', city: 'M', amount: 2 },
|
|
166
|
+
{ country: 'DE', city: 'H', amount: 3 },
|
|
167
|
+
{ country: 'DE', city: 'K', amount: 4 },
|
|
168
|
+
]
|
|
169
|
+
const src = makeGroupSource(data)
|
|
170
|
+
let state = { displayRows: [] as ServerDisplayRow<Row>[] } as never
|
|
171
|
+
const ctl = createServerGroupModel<Row>(src, {
|
|
172
|
+
groupBy: ['country'],
|
|
173
|
+
pageSize: 2,
|
|
174
|
+
onChange: (s) => (state = s as never),
|
|
175
|
+
})
|
|
176
|
+
ctl.refresh()
|
|
177
|
+
await flush()
|
|
178
|
+
ctl.expandGroup(['DE'])
|
|
179
|
+
await flush()
|
|
180
|
+
let rows = (state as { displayRows: ServerDisplayRow<Row>[] }).displayRows
|
|
181
|
+
expect(rows.filter((r) => r.kind === 'leaf')).toHaveLength(2) // first block
|
|
182
|
+
const more = rows.find((r) => r.kind === 'more')
|
|
183
|
+
expect(more && more.kind === 'more' && more.remaining).toBe(2)
|
|
184
|
+
|
|
185
|
+
ctl.loadMoreChildren(['DE'])
|
|
186
|
+
await flush()
|
|
187
|
+
rows = (state as { displayRows: ServerDisplayRow<Row>[] }).displayRows
|
|
188
|
+
expect(rows.filter((r) => r.kind === 'leaf')).toHaveLength(4) // all loaded
|
|
189
|
+
expect(rows.some((r) => r.kind === 'more')).toBe(false) // no more affordance
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('emits a subtotal footer row after an expanded group when groupFooters is on', async () => {
|
|
193
|
+
const src = makeGroupSource(DATA)
|
|
194
|
+
let state = { displayRows: [] as ServerDisplayRow<Row>[] } as never
|
|
195
|
+
const ctl = createServerGroupModel<Row>(src, {
|
|
196
|
+
groupBy: ['country', 'city'],
|
|
197
|
+
aggregations: [{ col: 'amount', fn: 'sum' }],
|
|
198
|
+
groupFooters: true,
|
|
199
|
+
onChange: (s) => (state = s as never),
|
|
200
|
+
})
|
|
201
|
+
ctl.refresh()
|
|
202
|
+
await flush()
|
|
203
|
+
ctl.expandGroup(['DE'])
|
|
204
|
+
await flush()
|
|
205
|
+
const rows = (state as { displayRows: ServerDisplayRow<Row>[] }).displayRows
|
|
206
|
+
const footers = rows.filter((r) => r.kind === 'footer')
|
|
207
|
+
expect(footers).toHaveLength(1)
|
|
208
|
+
expect(footers[0]!.kind === 'footer' && footers[0]!.key).toBe('DE')
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('shows skeleton rows while a group\'s first block is loading', async () => {
|
|
212
|
+
const src = makeGroupSource(DATA, 20) // slow
|
|
213
|
+
let state = { displayRows: [] as ServerDisplayRow<Row>[] } as never
|
|
214
|
+
const ctl = createServerGroupModel<Row>(src, {
|
|
215
|
+
groupBy: ['country', 'city'],
|
|
216
|
+
skeletonRows: 3,
|
|
217
|
+
onChange: (s) => (state = s as never),
|
|
218
|
+
})
|
|
219
|
+
ctl.refresh()
|
|
220
|
+
await new Promise((r) => setTimeout(r, 35)) // root loaded
|
|
221
|
+
ctl.expandGroup(['DE']) // fetch in flight -> synchronous emit with skeletons
|
|
222
|
+
const loading = (state as { displayRows: ServerDisplayRow<Row>[] }).displayRows
|
|
223
|
+
expect(loading.filter((r) => r.kind === 'skeleton')).toHaveLength(3)
|
|
224
|
+
await new Promise((r) => setTimeout(r, 40)) // DE loads
|
|
225
|
+
expect((state as { displayRows: ServerDisplayRow<Row>[] }).displayRows.some((r) => r.kind === 'skeleton')).toBe(false)
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
type Node = { id: string; parentId: string | null; name: string; kids: boolean }
|
|
230
|
+
const TREE: Node[] = [
|
|
231
|
+
{ id: '1', parentId: null, name: 'Root A', kids: true },
|
|
232
|
+
{ id: '2', parentId: null, name: 'Root B', kids: false },
|
|
233
|
+
{ id: '1a', parentId: '1', name: 'Child A1', kids: true },
|
|
234
|
+
{ id: '1b', parentId: '1', name: 'Child A2', kids: false },
|
|
235
|
+
{ id: '1a1', parentId: '1a', name: 'Leaf A1a', kids: false },
|
|
236
|
+
]
|
|
237
|
+
|
|
238
|
+
/** A fake self-referential tree backend: return the children of the node at the end of groupKeys. */
|
|
239
|
+
function makeTreeSource(nodes: Node[]): ServerDataSource<Node> & { calls: number } {
|
|
240
|
+
return {
|
|
241
|
+
calls: 0,
|
|
242
|
+
async getRows(req) {
|
|
243
|
+
this.calls += 1
|
|
244
|
+
const parent = req.groupKeys.length ? req.groupKeys[req.groupKeys.length - 1] : null
|
|
245
|
+
const children = nodes.filter((n) => (n.parentId ?? null) === (parent ?? null))
|
|
246
|
+
return { rows: children, rowCount: children.length }
|
|
247
|
+
},
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
describe('createServerGroupModel - treeData', () => {
|
|
252
|
+
const opts = (onChange: (s: unknown) => void) => ({
|
|
253
|
+
treeData: true,
|
|
254
|
+
getRowId: (n: Node) => n.id,
|
|
255
|
+
hasChildren: (n: Node) => n.kids,
|
|
256
|
+
onChange: onChange as never,
|
|
257
|
+
})
|
|
258
|
+
const nameOf = (r: ServerDisplayRow<Node>) => ('data' in r ? r.data.name : `(${r.kind})`)
|
|
259
|
+
|
|
260
|
+
it('throws without getRowId/hasChildren', () => {
|
|
261
|
+
expect(() => createServerGroupModel<Node>(makeTreeSource(TREE), { treeData: true, onChange: () => {} })).toThrow()
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('loads roots, marking expandable nodes as groups and leaves as leaves', async () => {
|
|
265
|
+
const src = makeTreeSource(TREE)
|
|
266
|
+
let state = { displayRows: [] as ServerDisplayRow<Node>[] } as never
|
|
267
|
+
const ctl = createServerGroupModel<Node>(src, opts((s) => (state = s as never)))
|
|
268
|
+
ctl.refresh()
|
|
269
|
+
await flush()
|
|
270
|
+
const rows = (state as { displayRows: ServerDisplayRow<Node>[] }).displayRows
|
|
271
|
+
expect(rows.map((r) => [r.kind, nameOf(r)])).toEqual([
|
|
272
|
+
['group', 'Root A'], // kids: true
|
|
273
|
+
['leaf', 'Root B'], // kids: false
|
|
274
|
+
])
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('expanding a node fetches and splices its children by id path', async () => {
|
|
278
|
+
const src = makeTreeSource(TREE)
|
|
279
|
+
let state = { displayRows: [] as ServerDisplayRow<Node>[] } as never
|
|
280
|
+
const ctl = createServerGroupModel<Node>(src, opts((s) => (state = s as never)))
|
|
281
|
+
ctl.refresh()
|
|
282
|
+
await flush()
|
|
283
|
+
ctl.expandGroup(['1'])
|
|
284
|
+
await flush()
|
|
285
|
+
let rows = (state as { displayRows: ServerDisplayRow<Node>[] }).displayRows
|
|
286
|
+
expect(rows.map(nameOf)).toEqual(['Root A', 'Child A1', 'Child A2', 'Root B'])
|
|
287
|
+
// drill one more level: Child A1 -> Leaf A1a
|
|
288
|
+
ctl.expandGroup(['1', '1a'])
|
|
289
|
+
await flush()
|
|
290
|
+
rows = (state as { displayRows: ServerDisplayRow<Node>[] }).displayRows
|
|
291
|
+
expect(rows.map(nameOf)).toEqual(['Root A', 'Child A1', 'Leaf A1a', 'Child A2', 'Root B'])
|
|
292
|
+
expect(rows.find((r) => nameOf(r) === 'Leaf A1a')!.level).toBe(2)
|
|
293
|
+
})
|
|
294
|
+
})
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side GROUP row model (SSRM grouping) - the first-class
|
|
3
|
+
* companion to `createServerDataSource`. Grouping, aggregation, and lazy
|
|
4
|
+
* expansion all flow through the SAME `ServerDataSource.getRows` contract:
|
|
5
|
+
*
|
|
6
|
+
* - The request carries `groupBy` (the columns grouped on) and `groupKeys`
|
|
7
|
+
* (the path of the group being expanded). While `groupKeys.length <
|
|
8
|
+
* groupBy.length` the server returns GROUP rows (one per distinct key at
|
|
9
|
+
* that level, carrying the key + aggregate values); at the bottom level it
|
|
10
|
+
* returns the leaf rows under that path.
|
|
11
|
+
* - Expanding a group is just another `getRows` with `groupKeys` set to that
|
|
12
|
+
* group's path. This controller owns the group tree: expand / collapse,
|
|
13
|
+
* per-node lazy fetch + cache, race-safety, and flattening to a display
|
|
14
|
+
* list the grid renders (group rows with an expander, leaves underneath).
|
|
15
|
+
*
|
|
16
|
+
* Headless and framework-agnostic on purpose: render from `state.displayRows`
|
|
17
|
+
* and call `toggleGroup` from the group cell's expander.
|
|
18
|
+
*/
|
|
19
|
+
import type {
|
|
20
|
+
ServerAggregation,
|
|
21
|
+
ServerDataSource,
|
|
22
|
+
ServerDisplayRow,
|
|
23
|
+
ServerFilterModel,
|
|
24
|
+
ServerGroupRow,
|
|
25
|
+
ServerSortModel,
|
|
26
|
+
} from './server-data-source'
|
|
27
|
+
|
|
28
|
+
export type ServerGroupState<TData> = {
|
|
29
|
+
/** The flattened tree: top-level groups, with expanded groups' children spliced in. */
|
|
30
|
+
displayRows: ServerDisplayRow<TData>[]
|
|
31
|
+
groupBy: string[]
|
|
32
|
+
/** True during the initial top-level fetch (before the root has ever loaded). */
|
|
33
|
+
loading: boolean
|
|
34
|
+
/** The rejection from the last failed top-level fetch, else null. */
|
|
35
|
+
error: unknown
|
|
36
|
+
sortModel: ServerSortModel
|
|
37
|
+
filterModel: ServerFilterModel
|
|
38
|
+
/** Ids (JSON of the path) of the currently expanded groups. */
|
|
39
|
+
expandedGroups: string[]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type ServerGroupControllerOptions<TData> = {
|
|
43
|
+
/** Columns to group on, outer to inner. Empty in `treeData` mode. */
|
|
44
|
+
groupBy?: string[]
|
|
45
|
+
/** Value columns to roll up per group. */
|
|
46
|
+
aggregations?: ServerAggregation[]
|
|
47
|
+
/** Max children fetched per group in one request. Default 200. */
|
|
48
|
+
pageSize?: number
|
|
49
|
+
/**
|
|
50
|
+
* Self-referential tree mode: instead of grouping flat rows by columns, each
|
|
51
|
+
* row is a node whose children are fetched on expand. Requires `getRowId`
|
|
52
|
+
* (the node id, which builds the `groupKeys` path) and `hasChildren` (whether
|
|
53
|
+
* a node is expandable). `getRows` receives `groupKeys` = the path to the
|
|
54
|
+
* node being expanded, and returns that node's direct children.
|
|
55
|
+
*/
|
|
56
|
+
treeData?: boolean
|
|
57
|
+
/** Node id, used to build the `groupKeys` path. Required in `treeData` mode. */
|
|
58
|
+
getRowId?: (row: TData) => string
|
|
59
|
+
/** Whether a node can be expanded. Required in `treeData` mode. */
|
|
60
|
+
hasChildren?: (row: TData) => boolean
|
|
61
|
+
/** Emit a subtotal "footer" row after each expanded group's children. */
|
|
62
|
+
groupFooters?: boolean
|
|
63
|
+
/** Placeholder rows shown while a group's first block is loading. Default 3. */
|
|
64
|
+
skeletonRows?: number
|
|
65
|
+
onChange: (state: ServerGroupState<TData>) => void
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type ServerGroupController<TData> = {
|
|
69
|
+
/** (Re-)fetch the tree from the top. Call once to load the first level. */
|
|
70
|
+
refresh(): void
|
|
71
|
+
setSort(sortModel: ServerSortModel): void
|
|
72
|
+
setFilter(filterModel: ServerFilterModel): void
|
|
73
|
+
/** Change the grouping columns. Collapses everything and reloads the top level. */
|
|
74
|
+
setGroupBy(groupBy: string[]): void
|
|
75
|
+
/** Expand or collapse a group row (from its expander). */
|
|
76
|
+
toggleGroup(row: ServerGroupRow<TData>): void
|
|
77
|
+
expandGroup(path: string[]): void
|
|
78
|
+
collapseGroup(path: string[]): void
|
|
79
|
+
/** Fetch the next block of a group's children (intra-group paging). */
|
|
80
|
+
loadMoreChildren(path: string[]): void
|
|
81
|
+
isExpanded(path: string[]): boolean
|
|
82
|
+
getState(): ServerGroupState<TData>
|
|
83
|
+
/** Stop accepting in-flight responses. Call on unmount. */
|
|
84
|
+
dispose(): void
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Map a `ServerGroupState.displayRows` to grid rows: each row's data spread at
|
|
89
|
+
* the top level (so normal `field` columns show group subtotals on group rows
|
|
90
|
+
* and cell values on leaves) plus a `__group` marker the built-in `SvGroupCell`
|
|
91
|
+
* reads to draw the expander + indentation. Feed the result to `<SvGrid data>`.
|
|
92
|
+
*/
|
|
93
|
+
export type ServerGroupGridRow<TData> = TData & { __group: ServerDisplayRow<TData> }
|
|
94
|
+
export function serverGroupRows<TData>(
|
|
95
|
+
state: ServerGroupState<TData> | undefined | null,
|
|
96
|
+
): ServerGroupGridRow<TData>[] {
|
|
97
|
+
return (state?.displayRows ?? []).map(
|
|
98
|
+
(r) => ({ ...('data' in r ? (r.data as object) : {}), __group: r }) as ServerGroupGridRow<TData>,
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Build the `serverGroup` prop for `<SvGrid>` from a controller, so the grid's
|
|
104
|
+
* built-in tree keyboard (ArrowRight / ArrowLeft expand / collapse) and treegrid
|
|
105
|
+
* a11y work with no app-level key handling. Reads the `__group` marker that
|
|
106
|
+
* `serverGroupRows` attaches. Usage: `<SvGrid ... serverGroup={serverGroupNav(ctl)} />`.
|
|
107
|
+
*/
|
|
108
|
+
export function serverGroupNav<TData>(ctl: ServerGroupController<TData>) {
|
|
109
|
+
return {
|
|
110
|
+
isGroup: (row: ServerGroupGridRow<TData>) => row.__group?.kind === 'group',
|
|
111
|
+
level: (row: ServerGroupGridRow<TData>) => row.__group?.level ?? 0,
|
|
112
|
+
expanded: (row: ServerGroupGridRow<TData>) =>
|
|
113
|
+
row.__group?.kind === 'group' ? row.__group.expanded : false,
|
|
114
|
+
onToggle: (row: ServerGroupGridRow<TData>) => {
|
|
115
|
+
if (row.__group?.kind === 'group') ctl.toggleGroup(row.__group)
|
|
116
|
+
else if (row.__group?.kind === 'more') ctl.loadMoreChildren(row.__group.path)
|
|
117
|
+
},
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const keyOf = (path: string[]): string => JSON.stringify(path)
|
|
122
|
+
|
|
123
|
+
type ChildDescriptor<TData> =
|
|
124
|
+
| { kind: 'group'; path: string[]; field: string; key: string; level: number; aggregates: Record<string, unknown>; data: TData }
|
|
125
|
+
| { kind: 'leaf'; id: string; level: number; data: TData }
|
|
126
|
+
|
|
127
|
+
type GroupNode<TData> = {
|
|
128
|
+
loaded: boolean
|
|
129
|
+
loading: boolean
|
|
130
|
+
error: unknown
|
|
131
|
+
count: number
|
|
132
|
+
children: ChildDescriptor<TData>[]
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function createServerGroupModel<TData>(
|
|
136
|
+
source: ServerDataSource<TData>,
|
|
137
|
+
options: ServerGroupControllerOptions<TData>,
|
|
138
|
+
): ServerGroupController<TData> {
|
|
139
|
+
let groupBy = [...(options.groupBy ?? [])]
|
|
140
|
+
const aggregations = options.aggregations ?? []
|
|
141
|
+
const pageSize = options.pageSize ?? 200
|
|
142
|
+
const treeData = !!options.treeData
|
|
143
|
+
if (treeData && (!options.getRowId || !options.hasChildren)) {
|
|
144
|
+
throw new Error('createServerGroupModel: treeData requires getRowId and hasChildren')
|
|
145
|
+
}
|
|
146
|
+
const getRowId = options.getRowId
|
|
147
|
+
const hasChildren = options.hasChildren
|
|
148
|
+
const groupFooters = !!options.groupFooters
|
|
149
|
+
const skeletonRows = Math.max(0, options.skeletonRows ?? 3)
|
|
150
|
+
|
|
151
|
+
let sortModel: ServerSortModel = []
|
|
152
|
+
let filterModel: ServerFilterModel = {}
|
|
153
|
+
const nodes = new Map<string, GroupNode<TData>>()
|
|
154
|
+
const expanded = new Set<string>() // path keys
|
|
155
|
+
// Generation guard: bumped on any tree reset so every fetch in flight for the
|
|
156
|
+
// previous shape is discarded when it lands (the classic stale-response bug).
|
|
157
|
+
let generation = 0
|
|
158
|
+
let disposed = false
|
|
159
|
+
|
|
160
|
+
const rootKey = keyOf([])
|
|
161
|
+
|
|
162
|
+
function ensureNode(key: string): GroupNode<TData> {
|
|
163
|
+
let node = nodes.get(key)
|
|
164
|
+
if (!node) {
|
|
165
|
+
node = { loaded: false, loading: false, error: null, count: 0, children: [] }
|
|
166
|
+
nodes.set(key, node)
|
|
167
|
+
}
|
|
168
|
+
return node
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Walk the loaded tree from the root, splicing expanded groups' children in.
|
|
172
|
+
function flatten(): ServerDisplayRow<TData>[] {
|
|
173
|
+
const out: ServerDisplayRow<TData>[] = []
|
|
174
|
+
const walk = (path: string[]): void => {
|
|
175
|
+
const node = nodes.get(keyOf(path))
|
|
176
|
+
if (!node?.loaded) return
|
|
177
|
+
for (const child of node.children) {
|
|
178
|
+
if (child.kind === 'leaf') {
|
|
179
|
+
out.push({ kind: 'leaf', id: child.id, level: child.level, data: child.data })
|
|
180
|
+
continue
|
|
181
|
+
}
|
|
182
|
+
const cid = keyOf(child.path)
|
|
183
|
+
const cnode = nodes.get(cid)
|
|
184
|
+
const isExp = expanded.has(cid)
|
|
185
|
+
out.push({
|
|
186
|
+
kind: 'group',
|
|
187
|
+
id: cid,
|
|
188
|
+
path: child.path,
|
|
189
|
+
field: child.field,
|
|
190
|
+
key: child.key,
|
|
191
|
+
level: child.level,
|
|
192
|
+
expanded: isExp,
|
|
193
|
+
loading: !!cnode?.loading,
|
|
194
|
+
aggregates: child.aggregates,
|
|
195
|
+
data: child.data,
|
|
196
|
+
})
|
|
197
|
+
if (isExp) {
|
|
198
|
+
if (cnode?.loaded) {
|
|
199
|
+
walk(child.path)
|
|
200
|
+
// Subtotal row after the group's children.
|
|
201
|
+
if (groupFooters) {
|
|
202
|
+
out.push({
|
|
203
|
+
kind: 'footer',
|
|
204
|
+
id: cid + 'footer',
|
|
205
|
+
level: child.level + 1,
|
|
206
|
+
path: child.path,
|
|
207
|
+
key: child.key,
|
|
208
|
+
aggregates: child.aggregates,
|
|
209
|
+
data: child.data,
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
} else if (cnode?.loading) {
|
|
213
|
+
// First block in flight: placeholder skeleton rows.
|
|
214
|
+
for (let i = 0; i < skeletonRows; i += 1) {
|
|
215
|
+
out.push({ kind: 'skeleton', id: cid + 'sk' + i, level: child.level + 1 })
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Intra-group paging: emit a "load more" affordance when only part of this
|
|
221
|
+
// node's children have been fetched.
|
|
222
|
+
if (node.children.length < node.count) {
|
|
223
|
+
out.push({
|
|
224
|
+
kind: 'more',
|
|
225
|
+
id: keyOf([...path, 'more']),
|
|
226
|
+
level: path.length,
|
|
227
|
+
path,
|
|
228
|
+
remaining: node.count - node.children.length,
|
|
229
|
+
loading: node.loading,
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
walk([])
|
|
234
|
+
return out
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function emit(): void {
|
|
238
|
+
if (disposed) return
|
|
239
|
+
const root = nodes.get(rootKey)
|
|
240
|
+
options.onChange({
|
|
241
|
+
displayRows: flatten(),
|
|
242
|
+
groupBy: [...groupBy],
|
|
243
|
+
loading: !!root?.loading && !root?.loaded,
|
|
244
|
+
error: root?.error ?? null,
|
|
245
|
+
sortModel,
|
|
246
|
+
filterModel,
|
|
247
|
+
expandedGroups: [...expanded],
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// append=false loads the first block of a node's children (on expand);
|
|
252
|
+
// append=true fetches the NEXT block and concatenates (intra-group paging).
|
|
253
|
+
async function fetchNode(path: string[], append = false): Promise<void> {
|
|
254
|
+
if (disposed) return
|
|
255
|
+
const gen = generation
|
|
256
|
+
const key = keyOf(path)
|
|
257
|
+
const node = ensureNode(key)
|
|
258
|
+
if (node.loading) return // a fetch for this node is already in flight
|
|
259
|
+
node.loading = true
|
|
260
|
+
node.error = null
|
|
261
|
+
emit()
|
|
262
|
+
const level = path.length
|
|
263
|
+
const groupField = groupBy[level]
|
|
264
|
+
const startRow = append ? node.children.length : 0
|
|
265
|
+
try {
|
|
266
|
+
const res = await source.getRows({
|
|
267
|
+
startRow,
|
|
268
|
+
endRow: startRow + pageSize,
|
|
269
|
+
pageIndex: Math.floor(startRow / pageSize),
|
|
270
|
+
pageSize,
|
|
271
|
+
sortModel,
|
|
272
|
+
filterModel,
|
|
273
|
+
groupBy,
|
|
274
|
+
groupKeys: path,
|
|
275
|
+
aggregations,
|
|
276
|
+
})
|
|
277
|
+
if (disposed || gen !== generation) return // stale: the tree was reset
|
|
278
|
+
node.count = res.rowCount
|
|
279
|
+
const block = res.rows.map((data, i): ChildDescriptor<TData> => {
|
|
280
|
+
// A row is an expandable "group" when: (tree mode) the source says it has
|
|
281
|
+
// children, or (grouping mode) we are above the innermost groupBy level.
|
|
282
|
+
const isGroup = treeData ? hasChildren!(data) : level < groupBy.length
|
|
283
|
+
if (isGroup) {
|
|
284
|
+
const gk = treeData
|
|
285
|
+
? getRowId!(data)
|
|
286
|
+
: ((v) => (v == null ? '' : String(v)))((data as Record<string, unknown>)[groupField!])
|
|
287
|
+
const aggregates: Record<string, unknown> = {}
|
|
288
|
+
for (const a of aggregations) aggregates[a.col] = (data as Record<string, unknown>)[a.col]
|
|
289
|
+
return { kind: 'group', path: [...path, gk], field: treeData ? '' : groupField!, key: gk, level, aggregates, data }
|
|
290
|
+
}
|
|
291
|
+
const id = treeData && getRowId ? `${key}:${getRowId(data)}` : `${key}:${startRow + i}`
|
|
292
|
+
return { kind: 'leaf', id, level, data }
|
|
293
|
+
})
|
|
294
|
+
node.children = append ? [...node.children, ...block] : block
|
|
295
|
+
node.loaded = true
|
|
296
|
+
node.loading = false
|
|
297
|
+
emit()
|
|
298
|
+
} catch (err) {
|
|
299
|
+
if (disposed || gen !== generation) return
|
|
300
|
+
node.error = err
|
|
301
|
+
if (!append) node.loaded = false
|
|
302
|
+
node.loading = false
|
|
303
|
+
emit()
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Clear the cache and reload the root plus any still-expanded paths (in
|
|
308
|
+
// parallel; flatten only descends into loaded nodes, so children appear as
|
|
309
|
+
// each level lands).
|
|
310
|
+
function resetTree(): void {
|
|
311
|
+
generation += 1
|
|
312
|
+
nodes.clear()
|
|
313
|
+
void fetchNode([])
|
|
314
|
+
for (const k of expanded) void fetchNode(JSON.parse(k) as string[])
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function expandGroup(path: string[]): void {
|
|
318
|
+
const key = keyOf(path)
|
|
319
|
+
if (expanded.has(key)) return
|
|
320
|
+
expanded.add(key)
|
|
321
|
+
const node = nodes.get(key)
|
|
322
|
+
if (!node?.loaded && !node?.loading) void fetchNode(path)
|
|
323
|
+
else emit()
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function collapseGroup(path: string[]): void {
|
|
327
|
+
if (expanded.delete(keyOf(path))) emit()
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
refresh: resetTree,
|
|
332
|
+
setSort(next) {
|
|
333
|
+
sortModel = next
|
|
334
|
+
resetTree()
|
|
335
|
+
},
|
|
336
|
+
setFilter(next) {
|
|
337
|
+
filterModel = next
|
|
338
|
+
resetTree()
|
|
339
|
+
},
|
|
340
|
+
setGroupBy(next) {
|
|
341
|
+
groupBy = [...next]
|
|
342
|
+
expanded.clear()
|
|
343
|
+
resetTree()
|
|
344
|
+
},
|
|
345
|
+
toggleGroup(row) {
|
|
346
|
+
if (expanded.has(row.id)) collapseGroup(row.path)
|
|
347
|
+
else expandGroup(row.path)
|
|
348
|
+
},
|
|
349
|
+
expandGroup,
|
|
350
|
+
collapseGroup,
|
|
351
|
+
loadMoreChildren: (path) => void fetchNode(path, true),
|
|
352
|
+
isExpanded: (path) => expanded.has(keyOf(path)),
|
|
353
|
+
getState() {
|
|
354
|
+
const root = nodes.get(rootKey)
|
|
355
|
+
return {
|
|
356
|
+
displayRows: flatten(),
|
|
357
|
+
groupBy: [...groupBy],
|
|
358
|
+
loading: !!root?.loading && !root?.loaded,
|
|
359
|
+
error: root?.error ?? null,
|
|
360
|
+
sortModel,
|
|
361
|
+
filterModel,
|
|
362
|
+
expandedGroups: [...expanded],
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
dispose() {
|
|
366
|
+
disposed = true
|
|
367
|
+
generation += 1
|
|
368
|
+
},
|
|
369
|
+
}
|
|
370
|
+
}
|