@svgrid/grid 1.2.21 → 1.2.23
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/dist/GridFooter.svelte +34 -20
- package/dist/GridFooter.svelte.d.ts +8 -0
- package/dist/SvAutoComplete.svelte +100 -0
- package/dist/SvAutoComplete.svelte.d.ts +16 -0
- package/dist/SvButton.svelte +97 -0
- package/dist/SvButton.svelte.d.ts +23 -0
- package/dist/SvCalendar.svelte +669 -0
- package/dist/SvCalendar.svelte.d.ts +70 -0
- package/dist/SvCheckBox.svelte +81 -0
- package/dist/SvCheckBox.svelte.d.ts +19 -0
- package/dist/SvColorInput.svelte +187 -0
- package/dist/SvColorInput.svelte.d.ts +15 -0
- package/dist/SvComboBox.svelte +130 -0
- package/dist/SvComboBox.svelte.d.ts +15 -0
- package/dist/SvCountryInput.svelte +123 -0
- package/dist/SvCountryInput.svelte.d.ts +14 -0
- package/dist/SvDateTimePicker.svelte +322 -0
- package/dist/SvDateTimePicker.svelte.d.ts +37 -0
- package/dist/SvDropDownList.svelte +143 -0
- package/dist/SvDropDownList.svelte.d.ts +15 -0
- package/dist/SvForm.svelte +147 -0
- package/dist/SvForm.svelte.d.ts +29 -0
- package/dist/SvGauge.svelte +99 -0
- package/dist/SvGauge.svelte.d.ts +31 -0
- package/dist/SvGrid.controller.svelte.d.ts +16 -2
- package/dist/SvGrid.controller.svelte.js +63 -8
- package/dist/SvGrid.css +84 -31
- package/dist/SvGrid.helpers.d.ts +4 -0
- package/dist/SvGrid.helpers.js +40 -12
- package/dist/SvGrid.svelte +84 -5
- package/dist/SvGrid.types.d.ts +36 -0
- package/dist/SvGridDropdown.svelte +16 -30
- package/dist/SvGridDropdown.svelte.d.ts +4 -0
- package/dist/SvListBox.svelte +122 -0
- package/dist/SvListBox.svelte.d.ts +19 -0
- package/dist/SvMaskedInput.svelte +0 -0
- package/dist/SvMaskedInput.svelte.d.ts +15 -0
- package/dist/SvNumberInput.svelte +0 -0
- package/dist/SvNumberInput.svelte.d.ts +22 -0
- package/dist/SvPasswordInput.svelte +110 -0
- package/dist/SvPasswordInput.svelte.d.ts +22 -0
- package/dist/SvPhoneInput.svelte +0 -0
- package/dist/SvPhoneInput.svelte.d.ts +19 -0
- package/dist/SvRadioGroup.svelte +108 -0
- package/dist/SvRadioGroup.svelte.d.ts +23 -0
- package/dist/SvRating.svelte +111 -0
- package/dist/SvRating.svelte.d.ts +20 -0
- package/dist/SvRepeatButton.svelte +89 -0
- package/dist/SvRepeatButton.svelte.d.ts +21 -0
- package/dist/SvSlider.svelte +182 -0
- package/dist/SvSlider.svelte.d.ts +25 -0
- package/dist/SvSwitchButton.svelte +94 -0
- package/dist/SvSwitchButton.svelte.d.ts +19 -0
- package/dist/SvTabs.svelte +122 -0
- package/dist/SvTabs.svelte.d.ts +26 -0
- package/dist/SvTagsInput.svelte +81 -0
- package/dist/SvTagsInput.svelte.d.ts +19 -0
- package/dist/SvTimePicker.svelte +359 -0
- package/dist/SvTimePicker.svelte.d.ts +27 -0
- package/dist/SvToggleButton.svelte +57 -0
- package/dist/SvToggleButton.svelte.d.ts +16 -0
- package/dist/SvTree.svelte +210 -0
- package/dist/SvTree.svelte.d.ts +29 -0
- package/dist/build-api.js +78 -1
- package/dist/cdn/svgrid.js +10022 -5904
- package/dist/cdn/svgrid.svelte-external.js +8531 -4429
- package/dist/cell-render.d.ts +4 -0
- package/dist/cell-render.js +35 -1
- package/dist/clipboard.js +19 -1
- package/dist/collaboration.d.ts +2 -0
- package/dist/collaboration.js +4 -0
- package/dist/core.d.ts +22 -1
- package/dist/countries.d.ts +15 -0
- package/dist/countries.js +61 -0
- package/dist/datetime/date-core.d.ts +88 -0
- package/dist/datetime/date-core.js +165 -0
- package/dist/datetime/date-format.d.ts +39 -0
- package/dist/datetime/date-format.js +371 -0
- package/dist/datetime/date-restrict.d.ts +23 -0
- package/dist/datetime/date-restrict.js +50 -0
- package/dist/datetime/date-selection.d.ts +25 -0
- package/dist/datetime/date-selection.js +94 -0
- package/dist/datetime/mask.d.ts +21 -0
- package/dist/datetime/mask.js +90 -0
- package/dist/editing.d.ts +1 -1
- package/dist/editing.js +7 -0
- package/dist/editors/cell-editors.d.ts +2 -2
- package/dist/editors/cell-editors.js +3 -1
- package/dist/export-format.d.ts +178 -0
- package/dist/export-format.js +419 -0
- package/dist/hyperformula-adapter.d.ts +4 -0
- package/dist/hyperformula-adapter.js +15 -6
- package/dist/index.d.ts +36 -0
- package/dist/index.js +41 -0
- package/dist/list-option.d.ts +10 -0
- package/dist/list-option.js +7 -0
- package/dist/menus.js +18 -2
- package/dist/popover.d.ts +48 -0
- package/dist/popover.js +65 -0
- package/dist/row-resize.d.ts +4 -4
- package/dist/row-resize.js +8 -5
- package/dist/selection.d.ts +6 -0
- package/dist/selection.js +31 -5
- package/dist/server-data-source.d.ts +43 -0
- package/dist/server-data-source.js +100 -0
- package/dist/sparkline.js +8 -4
- package/dist/svgrid-wrapper.types.d.ts +26 -1
- package/package.json +6 -1
- package/src/GridFooter.svelte +34 -20
- package/src/SvAutoComplete.svelte +100 -0
- package/src/SvButton.svelte +97 -0
- package/src/SvCalendar.svelte +669 -0
- package/src/SvCalendar.test.ts +211 -0
- package/src/SvCheckBox.svelte +81 -0
- package/src/SvColorInput.svelte +187 -0
- package/src/SvComboBox.svelte +130 -0
- package/src/SvCountryInput.svelte +123 -0
- package/src/SvDateTimePicker.svelte +322 -0
- package/src/SvDateTimePicker.test.ts +136 -0
- package/src/SvDropDownList.svelte +143 -0
- package/src/SvForm.svelte +147 -0
- package/src/SvGauge.svelte +99 -0
- package/src/SvGrid.controller.svelte.ts +74 -8
- package/src/SvGrid.css +84 -31
- package/src/SvGrid.helpers.ts +41 -12
- package/src/SvGrid.svelte +84 -5
- package/src/SvGrid.types.ts +33 -0
- package/src/SvGridDropdown.svelte +16 -30
- package/src/SvListBox.svelte +122 -0
- package/src/SvMaskedInput.svelte +0 -0
- package/src/SvNumberInput.svelte +0 -0
- package/src/SvPasswordInput.svelte +110 -0
- package/src/SvPhoneInput.svelte +0 -0
- package/src/SvRadioGroup.svelte +108 -0
- package/src/SvRating.svelte +111 -0
- package/src/SvRepeatButton.svelte +89 -0
- package/src/SvSlider.svelte +182 -0
- package/src/SvSwitchButton.svelte +94 -0
- package/src/SvTabs.svelte +122 -0
- package/src/SvTagsInput.svelte +81 -0
- package/src/SvTimePicker.svelte +359 -0
- package/src/SvTimePicker.test.ts +98 -0
- package/src/SvToggleButton.svelte +57 -0
- package/src/SvTree.svelte +210 -0
- package/src/build-api.ts +115 -0
- package/src/cell-render.test.ts +38 -0
- package/src/cell-render.ts +43 -0
- package/src/clipboard.test.ts +18 -0
- package/src/clipboard.ts +24 -1
- package/src/collaboration.test.ts +30 -0
- package/src/collaboration.ts +6 -0
- package/src/core.ts +27 -3
- package/src/countries.ts +71 -0
- package/src/datetime/date-core.test.ts +217 -0
- package/src/datetime/date-core.ts +204 -0
- package/src/datetime/date-format.test.ts +121 -0
- package/src/datetime/date-format.ts +317 -0
- package/src/datetime/date-restrict.ts +60 -0
- package/src/datetime/date-selection.test.ts +129 -0
- package/src/datetime/date-selection.ts +137 -0
- package/src/datetime/mask.test.ts +36 -0
- package/src/datetime/mask.ts +84 -0
- package/src/editing.test.ts +22 -0
- package/src/editing.ts +7 -1
- package/src/editors/cell-editors.ts +7 -1
- package/src/export-data-api.test.ts +126 -0
- package/src/export-format.test.ts +107 -0
- package/src/export-format.ts +598 -0
- package/src/hyperformula-adapter.test.ts +35 -1
- package/src/hyperformula-adapter.ts +18 -6
- package/src/index.ts +71 -0
- package/src/list-option.ts +15 -0
- package/src/menus.ts +21 -2
- package/src/popover.ts +79 -0
- package/src/row-resize.test.ts +25 -0
- package/src/row-resize.ts +8 -5
- package/src/selection.test.ts +36 -4
- package/src/selection.ts +30 -9
- package/src/server-data-source.test.ts +201 -0
- package/src/server-data-source.ts +148 -0
- package/src/sparkline.test.ts +9 -0
- package/src/sparkline.ts +9 -4
- package/src/svgrid-wrapper.types.ts +28 -1
- package/src/svgrid.behavior.test.ts +27 -0
- package/src/svgrid.new-features.wrapper.test.ts +27 -1
- package/src/ui-buttons.test.ts +105 -0
- package/src/ui-composite.test.ts +89 -0
- package/src/ui-inputs.test.ts +92 -0
- package/src/ui-range.test.ts +61 -0
- package/src/ui-selection.test.ts +106 -0
package/src/index.ts
CHANGED
|
@@ -1,4 +1,27 @@
|
|
|
1
1
|
export { formatNumericWithConfig, resolveDatePattern } from './cell-formatting'
|
|
2
|
+
export {
|
|
3
|
+
coerceExportDate,
|
|
4
|
+
formatValueForExport,
|
|
5
|
+
projectGridRows,
|
|
6
|
+
toExcelNumFmt,
|
|
7
|
+
valueForExcel,
|
|
8
|
+
serializeDelimited,
|
|
9
|
+
serializeJson,
|
|
10
|
+
serializeMarkdown,
|
|
11
|
+
serializeHtml,
|
|
12
|
+
serializeXml,
|
|
13
|
+
downloadTextFile,
|
|
14
|
+
downloadBlobFile,
|
|
15
|
+
copyTextToClipboard,
|
|
16
|
+
type GridExportOptions,
|
|
17
|
+
type GridExportScope,
|
|
18
|
+
type GridExportColumn,
|
|
19
|
+
type GridClipboardOptions,
|
|
20
|
+
type GridClipboardFormat,
|
|
21
|
+
type SerializeProgress,
|
|
22
|
+
type SerializeOptions,
|
|
23
|
+
type CsvOptions,
|
|
24
|
+
} from './export-format'
|
|
2
25
|
|
|
3
26
|
export {
|
|
4
27
|
columnFilteringFeature,
|
|
@@ -46,6 +69,54 @@ export { createGrid, createSvGrid, type SvelteGrid } from './createGrid.svelte'
|
|
|
46
69
|
export { createGridState, createSvGridState } from './createGridState.svelte'
|
|
47
70
|
export { subscribeGrid, subscribeSvGrid } from './subscribe'
|
|
48
71
|
export { default as SvGrid } from './SvGrid.svelte'
|
|
72
|
+
export { default as SvGridDropdown } from './SvGridDropdown.svelte'
|
|
73
|
+
export { default as SvCalendar, type CalendarValue, type CalendarPreset, type CalendarAnimation } from './SvCalendar.svelte'
|
|
74
|
+
export { default as SvTimePicker, type TimeValue } from './SvTimePicker.svelte'
|
|
75
|
+
export { default as SvDateTimePicker, type DateTimeValue } from './SvDateTimePicker.svelte'
|
|
76
|
+
// UI kit - Group B (buttons & toggles)
|
|
77
|
+
export { default as SvButton } from './SvButton.svelte'
|
|
78
|
+
export { default as SvRepeatButton } from './SvRepeatButton.svelte'
|
|
79
|
+
export { default as SvToggleButton } from './SvToggleButton.svelte'
|
|
80
|
+
export { default as SvSwitchButton } from './SvSwitchButton.svelte'
|
|
81
|
+
export { default as SvCheckBox } from './SvCheckBox.svelte'
|
|
82
|
+
export { default as SvRadioGroup, type RadioOption } from './SvRadioGroup.svelte'
|
|
83
|
+
export { default as SvRating } from './SvRating.svelte'
|
|
84
|
+
// UI kit - Group C (text inputs)
|
|
85
|
+
export { default as SvNumberInput } from './SvNumberInput.svelte'
|
|
86
|
+
export { default as SvPasswordInput } from './SvPasswordInput.svelte'
|
|
87
|
+
export { default as SvMaskedInput } from './SvMaskedInput.svelte'
|
|
88
|
+
export { default as SvPhoneInput } from './SvPhoneInput.svelte'
|
|
89
|
+
export { default as SvColorInput } from './SvColorInput.svelte'
|
|
90
|
+
export { applyMask, unmask, isMaskComplete } from './datetime/mask'
|
|
91
|
+
export { COUNTRIES, COUNTRY_BY_CODE, flagEmoji, type Country } from './countries'
|
|
92
|
+
// UI kit - Group D (selection overlays)
|
|
93
|
+
export { default as SvListBox } from './SvListBox.svelte'
|
|
94
|
+
export { default as SvDropDownList } from './SvDropDownList.svelte'
|
|
95
|
+
export { default as SvComboBox } from './SvComboBox.svelte'
|
|
96
|
+
export { default as SvAutoComplete } from './SvAutoComplete.svelte'
|
|
97
|
+
export { default as SvTagsInput } from './SvTagsInput.svelte'
|
|
98
|
+
export { default as SvCountryInput } from './SvCountryInput.svelte'
|
|
99
|
+
export { filterOptions, type ListOption } from './list-option'
|
|
100
|
+
// UI kit - Group E (range & feedback)
|
|
101
|
+
export { default as SvSlider } from './SvSlider.svelte'
|
|
102
|
+
export { default as SvGauge } from './SvGauge.svelte'
|
|
103
|
+
// UI kit - Group F (composite / layout)
|
|
104
|
+
export { default as SvTabs, type TabItem } from './SvTabs.svelte'
|
|
105
|
+
export { default as SvTree, type TreeNode as SvTreeNode } from './SvTree.svelte'
|
|
106
|
+
export { default as SvForm, type FormField, type FormFieldType } from './SvForm.svelte'
|
|
107
|
+
export { anchoredRect, portalToBody, PANEL_THEME_VARS, type AnchoredRect } from './popover'
|
|
108
|
+
export {
|
|
109
|
+
toDate, startOfDay, addDays, addMonths, addYears, clampDate, isSameDay,
|
|
110
|
+
monthMatrix, isoWeek, decadeRange, withTime, snapMinute, type DateLike,
|
|
111
|
+
} from './datetime/date-core'
|
|
112
|
+
export { formatDate, parseDate, tokenizeMask } from './datetime/date-format'
|
|
113
|
+
export {
|
|
114
|
+
selectDate, rangeDays, isSelected, isMultiSelectMode, emptySelection,
|
|
115
|
+
type SelectionMode, type SelectionState,
|
|
116
|
+
} from './datetime/date-selection'
|
|
117
|
+
export {
|
|
118
|
+
isDisabledDay, isOutOfRange, isRestricted, isImportant, type RestrictOptions,
|
|
119
|
+
} from './datetime/date-restrict'
|
|
49
120
|
export { default as FlexRender } from './FlexRender.svelte'
|
|
50
121
|
export { renderComponent, renderSnippet } from './render-component'
|
|
51
122
|
export { default as SvGridChart } from './SvGridChart.svelte'
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Shared option shape for the SvGrid UI-kit selection controls. */
|
|
2
|
+
export type ListOption = {
|
|
3
|
+
value: string | number
|
|
4
|
+
label: string
|
|
5
|
+
disabled?: boolean
|
|
6
|
+
/** Optional group heading this option belongs under. */
|
|
7
|
+
group?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Case-insensitive substring filter over option labels. */
|
|
11
|
+
export function filterOptions(options: ReadonlyArray<ListOption>, query: string): ListOption[] {
|
|
12
|
+
const q = query.trim().toLowerCase()
|
|
13
|
+
if (!q) return [...options]
|
|
14
|
+
return options.filter((o) => o.label.toLowerCase().includes(q))
|
|
15
|
+
}
|
package/src/menus.ts
CHANGED
|
@@ -368,23 +368,42 @@ export function createMenus<
|
|
|
368
368
|
}
|
|
369
369
|
}
|
|
370
370
|
|
|
371
|
+
// External pagination is controlled: emit the requested page/size and let
|
|
372
|
+
// the consumer fetch + update props. Never mutate local pagination state.
|
|
373
|
+
function emitExternalPagination(pageIndex: number, pageSize: number): boolean {
|
|
374
|
+
if (ctx.props.externalPagination !== true) return false;
|
|
375
|
+
ctx.props.onPaginationChange?.({ pageIndex: Math.max(0, pageIndex), pageSize });
|
|
376
|
+
ctx.scrollContainer?.scrollTo?.({ top: 0 });
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
|
|
371
380
|
function changePage(delta: number) {
|
|
381
|
+
if (
|
|
382
|
+
emitExternalPagination(
|
|
383
|
+
(ctx.props.pageIndex ?? 0) + delta,
|
|
384
|
+
ctx.props.pageSize ?? 10,
|
|
385
|
+
)
|
|
386
|
+
)
|
|
387
|
+
return;
|
|
372
388
|
ctx.grid.setPagination((prev: any) => ({
|
|
373
389
|
...prev,
|
|
374
390
|
pageIndex: Math.max((prev?.pageIndex ?? 0) + delta, 0),
|
|
375
391
|
}));
|
|
376
|
-
ctx.scrollContainer?.scrollTo({ top: 0 });
|
|
392
|
+
ctx.scrollContainer?.scrollTo?.({ top: 0 });
|
|
377
393
|
}
|
|
378
394
|
|
|
379
395
|
function goToPage(pageIndex: number) {
|
|
396
|
+
if (emitExternalPagination(pageIndex, ctx.props.pageSize ?? 10)) return;
|
|
380
397
|
ctx.grid.setPagination((prev: any) => ({
|
|
381
398
|
...prev,
|
|
382
399
|
pageIndex: Math.max(0, pageIndex),
|
|
383
400
|
}));
|
|
384
|
-
ctx.scrollContainer?.scrollTo({ top: 0 });
|
|
401
|
+
ctx.scrollContainer?.scrollTo?.({ top: 0 });
|
|
385
402
|
}
|
|
386
403
|
|
|
387
404
|
function setPageSize(pageSize: number) {
|
|
405
|
+
// External mode: changing page size resets to the first page.
|
|
406
|
+
if (emitExternalPagination(0, pageSize)) return;
|
|
388
407
|
ctx.grid.setPagination((prev: any) => {
|
|
389
408
|
const oldSize = prev?.pageSize ?? 10;
|
|
390
409
|
const oldIndex = prev?.pageIndex ?? 0;
|
package/src/popover.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* popover - shared helpers for panels that must escape the grid's
|
|
3
|
+
* `overflow:hidden` scroll container by portalling to <body>: the theme-var
|
|
4
|
+
* snapshot + portal action, and the fixed-position anchor/flip math. Rune-free
|
|
5
|
+
* and DOM-only so it is reused verbatim by SvGridDropdown, SvDateTimePicker and
|
|
6
|
+
* any future popover. The per-component `$effect` wiring (scroll/resize
|
|
7
|
+
* reposition, outside-click close) stays in the component since it needs the
|
|
8
|
+
* component's own open state.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Theme tokens a portalled panel must carry with it. `--sg-*` custom properties
|
|
13
|
+
* are scoped to whatever wrapper the grid sits inside (e.g. a per-preset theme
|
|
14
|
+
* class); moving the panel to <body> leaves that scope, so we snapshot the
|
|
15
|
+
* resolved values and pin them inline. Keep this list in sync with the tokens
|
|
16
|
+
* any popover panel actually consumes.
|
|
17
|
+
*/
|
|
18
|
+
export const PANEL_THEME_VARS = [
|
|
19
|
+
'--sg-accent', '--sg-bg', '--sg-fg', '--sg-border', '--sg-muted',
|
|
20
|
+
'--sg-header-bg', '--sg-row-hover-bg', '--sg-selection-bg',
|
|
21
|
+
'--sg-input-bg', '--sg-input-border', '--sg-selection-fg',
|
|
22
|
+
] as const
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Svelte action: snapshot the resolved theme tokens from the node's current
|
|
26
|
+
* (in-scope) position, then detach it and append to <body> so it can never be
|
|
27
|
+
* clipped by an ancestor's overflow. Removes itself on destroy.
|
|
28
|
+
*/
|
|
29
|
+
export function portalToBody(node: HTMLElement, vars: ReadonlyArray<string> = PANEL_THEME_VARS) {
|
|
30
|
+
const cs = getComputedStyle(node)
|
|
31
|
+
for (const v of vars) {
|
|
32
|
+
const val = cs.getPropertyValue(v).trim()
|
|
33
|
+
if (val) node.style.setProperty(v, val)
|
|
34
|
+
}
|
|
35
|
+
document.body.appendChild(node)
|
|
36
|
+
return {
|
|
37
|
+
destroy() {
|
|
38
|
+
if (node.parentNode === document.body) document.body.removeChild(node)
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type AnchoredRect = { top: number; left: number; width: number; openUpward: boolean }
|
|
44
|
+
|
|
45
|
+
export type AnchorOptions = {
|
|
46
|
+
/** Estimated panel height, used to decide whether to flip upward. */
|
|
47
|
+
estimatedHeight: number
|
|
48
|
+
/** Gap between trigger and panel. Default 2px. */
|
|
49
|
+
gap?: number
|
|
50
|
+
/** Force the panel at least this wide (else it matches the trigger). */
|
|
51
|
+
minWidth?: number
|
|
52
|
+
/** Keep the panel within the viewport horizontally. Default true. */
|
|
53
|
+
clampHorizontal?: boolean
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Compute a `position: fixed` rect anchored to `triggerRect`, flipping above the
|
|
58
|
+
* trigger when there isn't room below and there's more room above. Mirrors the
|
|
59
|
+
* behavior SvGridDropdown shipped with, generalized with min-width + horizontal
|
|
60
|
+
* clamping for wider panels (date/time popovers).
|
|
61
|
+
*/
|
|
62
|
+
export function anchoredRect(triggerRect: DOMRect, opts: AnchorOptions): AnchoredRect {
|
|
63
|
+
const gap = opts.gap ?? 2
|
|
64
|
+
const spaceBelow = window.innerHeight - triggerRect.bottom
|
|
65
|
+
const spaceAbove = triggerRect.top
|
|
66
|
+
const openUpward = spaceBelow < opts.estimatedHeight && spaceAbove > spaceBelow
|
|
67
|
+
const width = Math.max(triggerRect.width, opts.minWidth ?? 0)
|
|
68
|
+
let left = triggerRect.left
|
|
69
|
+
if (opts.clampHorizontal !== false) {
|
|
70
|
+
const maxLeft = window.innerWidth - width - 4
|
|
71
|
+
left = Math.max(4, Math.min(left, maxLeft))
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
top: openUpward ? triggerRect.top - opts.estimatedHeight - gap : triggerRect.bottom + gap,
|
|
75
|
+
left,
|
|
76
|
+
width,
|
|
77
|
+
openUpward,
|
|
78
|
+
}
|
|
79
|
+
}
|
package/src/row-resize.test.ts
CHANGED
|
@@ -131,6 +131,31 @@ describe('rowResize - strip injection', () => {
|
|
|
131
131
|
action.destroy()
|
|
132
132
|
})
|
|
133
133
|
|
|
134
|
+
it('also injects a strip into the built-in row-number gutter (showRowNumbers)', () => {
|
|
135
|
+
// The native gutter cell is `.sv-grid-cell.sv-grid-row-number-cell`, NOT
|
|
136
|
+
// `.sv-row-gutter`; the action must recognise it so `showRowNumbers` grids
|
|
137
|
+
// are row-resizable without a custom gutter column.
|
|
138
|
+
const host = document.createElement('div')
|
|
139
|
+
const table = document.createElement('table')
|
|
140
|
+
const tbody = document.createElement('tbody')
|
|
141
|
+
const tr = document.createElement('tr')
|
|
142
|
+
tr.className = 'sv-grid-row'
|
|
143
|
+
const gutter = document.createElement('td')
|
|
144
|
+
gutter.className = 'sv-grid-cell sv-grid-row-number-cell'
|
|
145
|
+
const marker = document.createElement('span')
|
|
146
|
+
marker.setAttribute('data-svgrid-row', '0')
|
|
147
|
+
gutter.appendChild(marker)
|
|
148
|
+
tr.appendChild(gutter)
|
|
149
|
+
tbody.appendChild(tr)
|
|
150
|
+
table.appendChild(tbody)
|
|
151
|
+
host.appendChild(table)
|
|
152
|
+
document.body.appendChild(host)
|
|
153
|
+
track(host)
|
|
154
|
+
const action = rowResize(host, { onResize: vi.fn() })
|
|
155
|
+
expect(tr.querySelector(`.${STRIP_CLASS}`)).not.toBeNull()
|
|
156
|
+
action.destroy()
|
|
157
|
+
})
|
|
158
|
+
|
|
134
159
|
it('does not override an already-positioned gutter cell', () => {
|
|
135
160
|
const { host, rows } = buildGrid([{}])
|
|
136
161
|
track(host)
|
package/src/row-resize.ts
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* reads per-row heights from a reactive store.
|
|
5
5
|
*
|
|
6
6
|
* The action is OFF until a consumer attaches it AND a row-header
|
|
7
|
-
* gutter cell exists in each row. A row-header cell is
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* `cellClass: 'sv-row-gutter'` on
|
|
7
|
+
* gutter cell exists in each row. A row-header cell is either the
|
|
8
|
+
* grid's built-in row-number gutter (enable `showRowNumbers`) or any
|
|
9
|
+
* TD tagged with the `sv-row-gutter` class (set
|
|
10
|
+
* `cellClass: 'sv-row-gutter'` on a custom gutter column).
|
|
11
11
|
*
|
|
12
12
|
* <script>
|
|
13
13
|
* let heights = $state<Record<number, number>>({})
|
|
@@ -116,8 +116,11 @@ export function rowResize(node: HTMLElement, opts: RowResizeOptions) {
|
|
|
116
116
|
for (const tr of rows) {
|
|
117
117
|
if (tr.classList.contains('sv-grid-header-row')) continue
|
|
118
118
|
if (tr.classList.contains('sv-grid-row-spacer')) continue
|
|
119
|
+
// A row-header gutter cell: either a user column tagged
|
|
120
|
+
// `sv-row-gutter`, or the grid's built-in row-number gutter
|
|
121
|
+
// (`showRowNumbers`), whose cell is `.sv-grid-row-number-cell`.
|
|
119
122
|
const gutter = tr.querySelector<HTMLTableCellElement>(
|
|
120
|
-
'.sv-grid-cell.sv-row-gutter',
|
|
123
|
+
'.sv-grid-cell.sv-row-gutter, .sv-grid-cell.sv-grid-row-number-cell',
|
|
121
124
|
)
|
|
122
125
|
if (!gutter) continue
|
|
123
126
|
if (gutter.querySelector(`:scope > .${STRIP_CLASS}`)) continue
|
package/src/selection.test.ts
CHANGED
|
@@ -310,22 +310,26 @@ describe('createSelection - onCellPointerDown', () => {
|
|
|
310
310
|
expect(ctx.grid.setActiveCell).not.toHaveBeenCalled()
|
|
311
311
|
})
|
|
312
312
|
|
|
313
|
-
it('
|
|
313
|
+
it('starts a drag on checkbox columns too (by editorType)', () => {
|
|
314
|
+
// A range drag must be able to START on a checkbox cell; the plain-click
|
|
315
|
+
// toggle still runs via onCellClick (same-cell click).
|
|
314
316
|
const ctx = baseCtx({
|
|
315
317
|
allColumns: [makeColumn('c0', { editorType: 'checkbox' })],
|
|
316
318
|
})
|
|
317
319
|
const sel = createSelection(ctx)
|
|
318
320
|
sel.onCellPointerDown(0, 0, { button: 0, shiftKey: false } as any)
|
|
319
|
-
expect(ctx.grid.setActiveCell).
|
|
321
|
+
expect(ctx.grid.setActiveCell).toHaveBeenCalled()
|
|
322
|
+
expect(ctx.isDraggingSelection).toBe(true)
|
|
320
323
|
})
|
|
321
324
|
|
|
322
|
-
it('
|
|
325
|
+
it('starts a drag on boolean-valued cells too', () => {
|
|
323
326
|
const ctx = baseCtx({
|
|
324
327
|
getCellDisplayValue: vi.fn(() => true),
|
|
325
328
|
})
|
|
326
329
|
const sel = createSelection(ctx)
|
|
327
330
|
sel.onCellPointerDown(0, 0, { button: 0, shiftKey: false } as any)
|
|
328
|
-
expect(ctx.grid.setActiveCell).
|
|
331
|
+
expect(ctx.grid.setActiveCell).toHaveBeenCalled()
|
|
332
|
+
expect(ctx.isDraggingSelection).toBe(true)
|
|
329
333
|
})
|
|
330
334
|
|
|
331
335
|
it('plain click sets active + selection and starts a drag', () => {
|
|
@@ -720,3 +724,31 @@ describe('createSelection - scrollActiveCellIntoView', () => {
|
|
|
720
724
|
expect(container.scrollLeft).toBe(320)
|
|
721
725
|
})
|
|
722
726
|
})
|
|
727
|
+
|
|
728
|
+
describe('createSelection - fillMarqueeEdges (Excel fill marquee)', () => {
|
|
729
|
+
// Union rectangle spans rows 1..4, cols 0..2 (source cols 0..1 extended to col 2,
|
|
730
|
+
// source rows 1..2 extended to row 4).
|
|
731
|
+
const fillDrag = {
|
|
732
|
+
sourceMinRow: 1, sourceMaxRow: 2, sourceMinCol: 0, sourceMaxCol: 1,
|
|
733
|
+
targetRow: 4, targetCol: 2,
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
it('returns null when there is no fill drag', () => {
|
|
737
|
+
const sel = createSelection(makeCtx())
|
|
738
|
+
expect(sel.fillMarqueeEdges(1, 0)).toBeNull()
|
|
739
|
+
})
|
|
740
|
+
|
|
741
|
+
it('flags the outer edges of the whole target rectangle', () => {
|
|
742
|
+
const sel = createSelection(makeCtx({ fillDrag }))
|
|
743
|
+
expect(sel.fillMarqueeEdges(1, 0)).toEqual({ top: true, bottom: false, left: true, right: false })
|
|
744
|
+
expect(sel.fillMarqueeEdges(4, 2)).toEqual({ top: false, bottom: true, left: false, right: true })
|
|
745
|
+
expect(sel.fillMarqueeEdges(1, 1)).toEqual({ top: true, bottom: false, left: false, right: false })
|
|
746
|
+
})
|
|
747
|
+
|
|
748
|
+
it('returns null for interior cells and cells outside the rectangle', () => {
|
|
749
|
+
const sel = createSelection(makeCtx({ fillDrag }))
|
|
750
|
+
expect(sel.fillMarqueeEdges(2, 1)).toBeNull() // interior
|
|
751
|
+
expect(sel.fillMarqueeEdges(0, 0)).toBeNull() // above
|
|
752
|
+
expect(sel.fillMarqueeEdges(5, 2)).toBeNull() // below
|
|
753
|
+
})
|
|
754
|
+
})
|
package/src/selection.ts
CHANGED
|
@@ -396,6 +396,30 @@ export function createSelection<
|
|
|
396
396
|
return !inSource;
|
|
397
397
|
}
|
|
398
398
|
|
|
399
|
+
/**
|
|
400
|
+
* Which edges of the fill-drag rectangle a cell sits on, so the render can
|
|
401
|
+
* paint a single dashed marquee around the WHOLE target range (source +
|
|
402
|
+
* extension) - the Excel fill-handle look - instead of a thick per-cell
|
|
403
|
+
* border. Returns null when the cell isn't on the rectangle's boundary.
|
|
404
|
+
*/
|
|
405
|
+
function fillMarqueeEdges(rowIndex: number, colIndex: number) {
|
|
406
|
+
const d = ctx.fillDrag;
|
|
407
|
+
if (!d) return null;
|
|
408
|
+
const minR = Math.min(d.sourceMinRow, d.targetRow);
|
|
409
|
+
const maxR = Math.max(d.sourceMaxRow, d.targetRow);
|
|
410
|
+
const minC = Math.min(d.sourceMinCol, d.targetCol);
|
|
411
|
+
const maxC = Math.max(d.sourceMaxCol, d.targetCol);
|
|
412
|
+
if (rowIndex < minR || rowIndex > maxR || colIndex < minC || colIndex > maxC) {
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
const top = rowIndex === minR;
|
|
416
|
+
const bottom = rowIndex === maxR;
|
|
417
|
+
const left = colIndex === minC;
|
|
418
|
+
const right = colIndex === maxC;
|
|
419
|
+
if (!top && !bottom && !left && !right) return null;
|
|
420
|
+
return { top, bottom, left, right };
|
|
421
|
+
}
|
|
422
|
+
|
|
399
423
|
/** Look up a column by id without depending on `buildApi`'s private
|
|
400
424
|
* closure (those helpers don't exist at this scope). */
|
|
401
425
|
function findColumnById(columnId: string) {
|
|
@@ -411,15 +435,11 @@ export function createSelection<
|
|
|
411
435
|
const row = ctx.allRows[rowIndex];
|
|
412
436
|
const column = ctx.allColumns[colIndex];
|
|
413
437
|
if (!row || !column || isGroupRow(row)) return;
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
const isCheckboxColumn =
|
|
420
|
-
column.columnDef.editorType === "checkbox" ||
|
|
421
|
-
typeof cellValue === "boolean";
|
|
422
|
-
if (isCheckboxColumn) return; // let onCellClick toggle the checkbox
|
|
438
|
+
// NOTE: we no longer bail for checkbox columns. Bailing here meant a
|
|
439
|
+
// range drag could never START on a checkbox cell. A plain click still
|
|
440
|
+
// toggles the checkbox via onCellClick (a same-cell click), while a
|
|
441
|
+
// cross-cell drag fires its `click` on a common ancestor - not the cell -
|
|
442
|
+
// so it selects a range without toggling.
|
|
423
443
|
|
|
424
444
|
const active = ctx.grid.getState().activeCell;
|
|
425
445
|
ctx.activeAtPointerDown = active
|
|
@@ -566,6 +586,7 @@ export function createSelection<
|
|
|
566
586
|
extendSelection,
|
|
567
587
|
isCellInSelectedRange,
|
|
568
588
|
getCellRangeEdges,
|
|
589
|
+
fillMarqueeEdges,
|
|
569
590
|
getSelectionRects,
|
|
570
591
|
isInFillPreview,
|
|
571
592
|
findColumnById,
|
|
@@ -36,6 +36,17 @@ describe('createServerDataSource', () => {
|
|
|
36
36
|
expect(states.some((s) => s.loading)).toBe(true)
|
|
37
37
|
})
|
|
38
38
|
|
|
39
|
+
it('dispose() during an in-flight fetch clears the loading state (#83)', async () => {
|
|
40
|
+
const src = makeSource(250, 20) // 20ms delay keeps the fetch in-flight
|
|
41
|
+
const ctl = createServerDataSource(src, { pageSize: 50, onChange: () => {} })
|
|
42
|
+
ctl.refresh()
|
|
43
|
+
expect(ctl.getState().loading).toBe(true)
|
|
44
|
+
ctl.dispose()
|
|
45
|
+
expect(ctl.getState().loading).toBe(false)
|
|
46
|
+
await new Promise((r) => setTimeout(r, 30)) // let the fetch settle
|
|
47
|
+
expect(ctl.getState().loading).toBe(false) // stays cleared, not stuck
|
|
48
|
+
})
|
|
49
|
+
|
|
39
50
|
it('setPage fetches the right slice', async () => {
|
|
40
51
|
const src = makeSource(250)
|
|
41
52
|
const ctl = createServerDataSource(src, { pageSize: 50, onChange: () => {} })
|
|
@@ -86,3 +97,193 @@ describe('createServerDataSource', () => {
|
|
|
86
97
|
expect(ctl.getState().rows).toHaveLength(0)
|
|
87
98
|
})
|
|
88
99
|
})
|
|
100
|
+
|
|
101
|
+
describe('createServerDataSource mutations', () => {
|
|
102
|
+
type URow = { id: number; name: string }
|
|
103
|
+
|
|
104
|
+
function writable() {
|
|
105
|
+
let store: URow[] = [
|
|
106
|
+
{ id: 0, name: 'a' },
|
|
107
|
+
{ id: 1, name: 'b' },
|
|
108
|
+
{ id: 2, name: 'c' },
|
|
109
|
+
]
|
|
110
|
+
return {
|
|
111
|
+
getRows: vi.fn(async (req: { startRow: number; endRow: number }) => ({
|
|
112
|
+
rows: store.slice(req.startRow, req.endRow),
|
|
113
|
+
rowCount: store.length,
|
|
114
|
+
})),
|
|
115
|
+
createRow: vi.fn(async (input: Partial<URow>) => {
|
|
116
|
+
const row: URow = { id: store.length, name: input.name ?? '' }
|
|
117
|
+
store = [...store, row]
|
|
118
|
+
return row
|
|
119
|
+
}),
|
|
120
|
+
updateRow: vi.fn(async (id: string, patch: Partial<URow>) => {
|
|
121
|
+
store = store.map((r) => (String(r.id) === id ? { ...r, ...patch } : r))
|
|
122
|
+
return store.find((r) => String(r.id) === id)!
|
|
123
|
+
}),
|
|
124
|
+
deleteRow: vi.fn(async (id: string) => {
|
|
125
|
+
store = store.filter((r) => String(r.id) !== id)
|
|
126
|
+
}),
|
|
127
|
+
} as unknown as ServerDataSource<URow> & {
|
|
128
|
+
getRows: ReturnType<typeof vi.fn>
|
|
129
|
+
createRow: ReturnType<typeof vi.fn>
|
|
130
|
+
updateRow: ReturnType<typeof vi.fn>
|
|
131
|
+
deleteRow: ReturnType<typeof vi.fn>
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
it('createRow writes through and refreshes the current page', async () => {
|
|
136
|
+
const src = writable()
|
|
137
|
+
const ctl = createServerDataSource(src, { pageSize: 50, onChange: () => {} })
|
|
138
|
+
ctl.refresh()
|
|
139
|
+
await flush()
|
|
140
|
+
const created = await ctl.createRow({ name: 'z' })
|
|
141
|
+
expect(created).toEqual({ id: 3, name: 'z' })
|
|
142
|
+
expect(src.createRow).toHaveBeenCalledWith({ name: 'z' })
|
|
143
|
+
// the follow-up refresh re-fetched, so total + rows reflect the write
|
|
144
|
+
expect(ctl.getState().total).toBe(4)
|
|
145
|
+
expect(ctl.getState().rows.some((r) => r.name === 'z')).toBe(true)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('toggles `saving` around a mutation', async () => {
|
|
149
|
+
const src = writable()
|
|
150
|
+
const states: Array<{ saving: boolean }> = []
|
|
151
|
+
const ctl = createServerDataSource(src, { pageSize: 50, onChange: (s) => states.push(s) })
|
|
152
|
+
const p = ctl.createRow({ name: 'x' })
|
|
153
|
+
expect(ctl.getState().saving).toBe(true) // synchronous emit before the await
|
|
154
|
+
await p
|
|
155
|
+
expect(ctl.getState().saving).toBe(false)
|
|
156
|
+
expect(states.some((s) => s.saving)).toBe(true)
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('updateRow and deleteRow call through and refresh', async () => {
|
|
160
|
+
const src = writable()
|
|
161
|
+
const ctl = createServerDataSource(src, { pageSize: 50, onChange: () => {} })
|
|
162
|
+
ctl.refresh()
|
|
163
|
+
await flush()
|
|
164
|
+
const updated = await ctl.updateRow('1', { name: 'B!' })
|
|
165
|
+
expect(updated).toEqual({ id: 1, name: 'B!' })
|
|
166
|
+
expect(src.updateRow).toHaveBeenCalledWith('1', { name: 'B!' })
|
|
167
|
+
expect(ctl.getState().rows.find((r) => r.id === 1)?.name).toBe('B!')
|
|
168
|
+
|
|
169
|
+
await ctl.deleteRow('0')
|
|
170
|
+
expect(src.deleteRow).toHaveBeenCalledWith('0')
|
|
171
|
+
expect(ctl.getState().total).toBe(2)
|
|
172
|
+
expect(ctl.getState().rows.some((r) => r.id === 0)).toBe(false)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('rejects when the source has no matching writer', async () => {
|
|
176
|
+
const readonly: ServerDataSource<URow> = {
|
|
177
|
+
getRows: async () => ({ rows: [], rowCount: 0 }),
|
|
178
|
+
}
|
|
179
|
+
const ctl = createServerDataSource(readonly, { onChange: () => {} })
|
|
180
|
+
await expect(ctl.createRow({ name: 'x' })).rejects.toThrow(/does not implement createRow/)
|
|
181
|
+
await expect(ctl.updateRow('1', {})).rejects.toThrow(/does not implement updateRow/)
|
|
182
|
+
await expect(ctl.deleteRow('1')).rejects.toThrow(/does not implement deleteRow/)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('rejects mutations after dispose, without calling the source', async () => {
|
|
186
|
+
const src = writable()
|
|
187
|
+
const ctl = createServerDataSource(src, { onChange: () => {} })
|
|
188
|
+
ctl.dispose()
|
|
189
|
+
await expect(ctl.deleteRow('0')).rejects.toThrow(/disposed/)
|
|
190
|
+
expect(src.deleteRow).not.toHaveBeenCalled()
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('leaves read state untouched when a mutation fails', async () => {
|
|
194
|
+
const src = writable()
|
|
195
|
+
src.createRow.mockRejectedValueOnce(new Error('boom'))
|
|
196
|
+
const ctl = createServerDataSource(src, { pageSize: 50, onChange: () => {} })
|
|
197
|
+
ctl.refresh()
|
|
198
|
+
await flush()
|
|
199
|
+
await expect(ctl.createRow({ name: 'x' })).rejects.toThrow('boom')
|
|
200
|
+
expect(ctl.getState().saving).toBe(false)
|
|
201
|
+
expect(ctl.getState().total).toBe(3) // unchanged; no refresh on failure
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
describe('createServerDataSource optimistic mutations', () => {
|
|
206
|
+
type URow = { id: number; name: string }
|
|
207
|
+
const opts = (onChange: (s: any) => void = () => {}) => ({
|
|
208
|
+
pageSize: 50,
|
|
209
|
+
optimistic: true,
|
|
210
|
+
getRowId: (r: URow) => String(r.id),
|
|
211
|
+
onChange,
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
function writable() {
|
|
215
|
+
let store: URow[] = [
|
|
216
|
+
{ id: 0, name: 'a' },
|
|
217
|
+
{ id: 1, name: 'b' },
|
|
218
|
+
{ id: 2, name: 'c' },
|
|
219
|
+
]
|
|
220
|
+
return {
|
|
221
|
+
getRows: vi.fn(async (req: { startRow: number; endRow: number }) => ({
|
|
222
|
+
rows: store.slice(req.startRow, req.endRow),
|
|
223
|
+
rowCount: store.length,
|
|
224
|
+
})),
|
|
225
|
+
updateRow: vi.fn(async (id: string, patch: Partial<URow>) => {
|
|
226
|
+
store = store.map((r) => (String(r.id) === id ? { ...r, ...patch } : r))
|
|
227
|
+
return store.find((r) => String(r.id) === id)!
|
|
228
|
+
}),
|
|
229
|
+
deleteRow: vi.fn(async (id: string) => {
|
|
230
|
+
store = store.filter((r) => String(r.id) !== id)
|
|
231
|
+
}),
|
|
232
|
+
} as any
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
it('applies an update to local rows immediately, before the server resolves', async () => {
|
|
236
|
+
const src = writable()
|
|
237
|
+
src.updateRow.mockImplementationOnce(
|
|
238
|
+
(id: string, patch: any) => new Promise((r) => setTimeout(() => r({ id: Number(id), ...patch }), 30)),
|
|
239
|
+
)
|
|
240
|
+
const ctl = createServerDataSource(src, opts())
|
|
241
|
+
ctl.refresh()
|
|
242
|
+
await flush()
|
|
243
|
+
const p = ctl.updateRow('1', { name: 'B!' })
|
|
244
|
+
// synchronously reflected, no refetch
|
|
245
|
+
expect(ctl.getState().rows.find((r) => r.id === 1)?.name).toBe('B!')
|
|
246
|
+
expect(ctl.getState().saving).toBe(true)
|
|
247
|
+
await p
|
|
248
|
+
expect(ctl.getState().saving).toBe(false)
|
|
249
|
+
// getRows only called once (the initial refresh) - no refetch for the update
|
|
250
|
+
expect(src.getRows).toHaveBeenCalledTimes(1)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('rolls an update back on error', async () => {
|
|
254
|
+
const src = writable()
|
|
255
|
+
src.updateRow.mockRejectedValueOnce(new Error('nope'))
|
|
256
|
+
const ctl = createServerDataSource(src, opts())
|
|
257
|
+
ctl.refresh()
|
|
258
|
+
await flush()
|
|
259
|
+
await expect(ctl.updateRow('1', { name: 'B!' })).rejects.toThrow('nope')
|
|
260
|
+
expect(ctl.getState().rows.find((r) => r.id === 1)?.name).toBe('b') // restored
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('removes a row immediately on delete and decrements total, restoring on error', async () => {
|
|
264
|
+
const src = writable()
|
|
265
|
+
const ctl = createServerDataSource(src, opts())
|
|
266
|
+
ctl.refresh()
|
|
267
|
+
await flush()
|
|
268
|
+
|
|
269
|
+
const p = ctl.deleteRow('0')
|
|
270
|
+
expect(ctl.getState().rows.some((r) => r.id === 0)).toBe(false)
|
|
271
|
+
expect(ctl.getState().total).toBe(2)
|
|
272
|
+
await p
|
|
273
|
+
|
|
274
|
+
src.deleteRow.mockRejectedValueOnce(new Error('locked'))
|
|
275
|
+
await expect(ctl.deleteRow('1')).rejects.toThrow('locked')
|
|
276
|
+
expect(ctl.getState().rows.some((r) => r.id === 1)).toBe(true) // restored
|
|
277
|
+
expect(ctl.getState().total).toBe(2)
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('falls back to a refetch when the row is not on the current page', async () => {
|
|
281
|
+
const src = writable()
|
|
282
|
+
const ctl = createServerDataSource(src, opts())
|
|
283
|
+
ctl.refresh()
|
|
284
|
+
await flush()
|
|
285
|
+
await ctl.updateRow('999', { name: 'ghost' }) // not present locally
|
|
286
|
+
// fell back to mutate() which refreshes: getRows called again
|
|
287
|
+
expect(src.getRows).toHaveBeenCalledTimes(2)
|
|
288
|
+
})
|
|
289
|
+
})
|