@svgrid/grid 3.0.4 → 3.0.6
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 +32 -11
- package/dist/GridMenus.svelte +34 -4
- package/dist/SvGrid.controller.svelte.d.ts +33 -5
- package/dist/SvGrid.controller.svelte.js +397 -58
- package/dist/SvGrid.css +200 -2
- package/dist/SvGrid.svelte +350 -40
- package/dist/SvGrid.types.d.ts +433 -7
- package/dist/SvGridDropdown.svelte +20 -0
- package/dist/SvGridDropdown.svelte.d.ts +12 -0
- package/dist/SvModal.svelte +22 -0
- package/dist/SvModal.svelte.d.ts +7 -0
- package/dist/a11y/dismissable.d.ts +10 -0
- package/dist/a11y/dismissable.js +24 -2
- package/dist/build-api.js +92 -37
- package/dist/cdn/{GridMenus-BL7ZgQvU.js → GridMenus-C-NtjlrJ.js} +155 -146
- package/dist/cdn/GridMenus-TssHej3q.js +644 -0
- package/dist/cdn/{SvChartMenu-FBSMINA6.js → SvChartMenu-BFSTpvB-.js} +1 -1
- package/dist/cdn/{SvChartMenu-Bl6PBbkT.js → SvChartMenu-BZ1Iwv0y.js} +1 -1
- package/dist/cdn/{SvDateTimePicker-DQwt4UAs.js → SvDateTimePicker-D9u_cOf8.js} +1 -1
- package/dist/cdn/{SvDateTimePicker-vNU6bZ-q.js → SvDateTimePicker-s_6IUpI7.js} +1 -1
- package/dist/cdn/{SvGridCellEditor-B1p-vCK5.js → SvGridCellEditor-BlyCSp0q.js} +1 -1
- package/dist/cdn/{SvGridCellEditor-D_0q4xAu.js → SvGridCellEditor-ClRRA9lW.js} +1 -1
- package/dist/cdn/{SvGridChart-DzLkSwCH.js → SvGridChart-B4EV8U6t.js} +4 -4
- package/dist/cdn/{SvGridChart-C3EWAZaM.js → SvGridChart-DsmwTyZF.js} +4 -4
- package/dist/cdn/{SvGridChartBuilder-CfII62sZ.js → SvGridChartBuilder-cHloHobN.js} +4 -4
- package/dist/cdn/{SvGridChartBuilder-BE2T1ykB.js → SvGridChartBuilder-vOl7C0Za.js} +4 -4
- package/dist/cdn/{SvGridChartPanel-xf4sWVTo.js → SvGridChartPanel-AcqF-Ll5.js} +5 -5
- package/dist/cdn/{SvGridChartPanel-CCX5_Wgd.js → SvGridChartPanel-biMtSBPA.js} +5 -5
- package/dist/cdn/{SvGridChartView-DYwabQWj.js → SvGridChartView-Dx_GmZuL.js} +2 -2
- package/dist/cdn/{SvGridChartView-CfuXmY5I.js → SvGridChartView-I3rDHXAq.js} +2 -2
- package/dist/cdn/{SvGridDropdown-D0VdjeR8.js → SvGridDropdown-BWeM3lMe.js} +46 -46
- package/dist/cdn/{SvGridDropdown-D13MtJ2j.js → SvGridDropdown-CJ6QqTxu.js} +67 -67
- package/dist/cdn/{SvModal-CIZWCcad.js → SvModal-ClxGmpAc.js} +187 -186
- package/dist/cdn/{SvModal-BAE-pjZX.js → SvModal-DhB_VMFR.js} +148 -147
- package/dist/cdn/{cell-formatting-C2Hf5gqd.js → cell-formatting-DVVKOSI9.js} +1 -1
- package/dist/cdn/{chart-Dz7SqMXH.js → chart-DEZxwC0i.js} +1 -1
- package/dist/cdn/{chart-panel-messages-CUbf2R4i.js → chart-panel-messages-D12XcTZJ.js} +1 -1
- package/dist/cdn/{chart-panel-messages-CmNrMdsr.js → chart-panel-messages-DguS7gC5.js} +1 -1
- package/dist/cdn/{chart-summary-BJW_tg_X.js → chart-summary-CMAXBGY-.js} +1 -1
- package/dist/cdn/{chart-trend-CaN9mDEV.js → chart-trend-BkEp61Gj.js} +1 -1
- package/dist/cdn/{dismissable-DAHetSNk.js → dismissable-Brc4EZU1.js} +5 -1
- package/dist/cdn/{export-format-CUDLy2yn.js → export-format-YzdwBww2.js} +1 -1
- package/dist/cdn/{row-resize-niQCp040.js → row-resize-DtOS1KG6.js} +41 -32
- package/dist/cdn/server-block-cache-DIqb3VZn.js +241 -0
- package/dist/cdn/{src-V1uu8iE9.js → src-CgoeSM8k.js} +5339 -4753
- package/dist/cdn/{src-BKhZ6eXd.js → src-DfwxB1TH.js} +9817 -9231
- package/dist/cdn/svgrid.js +28 -27
- package/dist/cdn/svgrid.svelte-external.js +28 -27
- package/dist/cdn/validate-AZoD8BoR.js +80 -0
- package/dist/cell-formatting.js +18 -1
- package/dist/cell-render.js +25 -6
- package/dist/clipboard.d.ts +2 -2
- package/dist/clipboard.js +72 -23
- package/dist/columns.js +7 -2
- package/dist/conditional-formatting.d.ts +2 -0
- package/dist/conditional-formatting.js +2 -0
- package/dist/core.d.ts +22 -6
- package/dist/core.js +401 -38
- package/dist/editing.js +34 -0
- package/dist/gantt-view.svelte.d.ts +24 -0
- package/dist/gantt-view.svelte.js +13 -0
- package/dist/grid-icons.d.ts +2 -2
- package/dist/grid-icons.js +2 -2
- package/dist/grid-messages.d.ts +10 -0
- package/dist/grid-messages.js +9 -0
- package/dist/headless.d.ts +5 -0
- package/dist/headless.js +12 -0
- package/dist/history.d.ts +21 -0
- package/dist/history.js +34 -4
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -3
- package/dist/keyboard-handlers.js +24 -1
- package/dist/keyboard.js +4 -0
- package/dist/row-drag-touch.d.ts +1 -1
- package/dist/row-drag.js +35 -1
- package/dist/row-model.d.ts +139 -0
- package/dist/row-model.js +16 -0
- package/dist/row-resize.js +22 -1
- package/dist/selection.d.ts +2 -1
- package/dist/selection.js +31 -3
- package/dist/server-block-cache.d.ts +219 -0
- package/dist/server-block-cache.js +539 -0
- package/dist/server-data-source.d.ts +274 -8
- package/dist/server-data-source.js +229 -20
- package/dist/server.d.ts +13 -0
- package/dist/server.js +13 -0
- package/dist/sparkline.js +7 -5
- package/dist/svgrid-wrapper.types.d.ts +33 -0
- package/dist/validate.d.ts +4 -0
- package/dist/validate.js +29 -3
- package/package.json +11 -1
- package/src/GridMenus.svelte +34 -4
- package/src/SvGrid.controller.svelte.ts +397 -61
- package/src/SvGrid.css +200 -2
- package/src/SvGrid.svelte +350 -40
- package/src/SvGrid.types.ts +453 -7
- package/src/SvGridDropdown.svelte +20 -0
- package/src/SvModal.svelte +22 -0
- package/src/SvModal.test.ts +30 -1
- package/src/a11y/dismissable.test.ts +10 -1
- package/src/a11y/dismissable.ts +22 -2
- package/src/build-api.ts +96 -31
- package/src/cell-formatting.test.ts +17 -0
- package/src/cell-formatting.ts +18 -1
- package/src/cell-render.test.ts +24 -2
- package/src/cell-render.ts +24 -8
- package/src/clipboard.test.ts +107 -0
- package/src/clipboard.ts +62 -23
- package/src/columns.test.ts +4 -4
- package/src/columns.ts +7 -2
- package/src/conditional-formatting.test.ts +8 -0
- package/src/conditional-formatting.ts +3 -0
- package/src/core.rowmodel-cache.test.ts +51 -0
- package/src/core.tick-repair.test.ts +287 -0
- package/src/core.ts +407 -38
- package/src/editing.test.ts +52 -0
- package/src/editing.ts +33 -0
- package/src/gantt-stub.test.svelte +38 -0
- package/src/gantt-view.svelte.ts +35 -0
- package/src/grid-icons.ts +2 -2
- package/src/grid-messages.test.ts +7 -0
- package/src/grid-messages.ts +22 -0
- package/src/headless.ts +24 -0
- package/src/history.test.ts +49 -1
- package/src/history.ts +48 -2
- package/src/icon-seam.test.ts +2 -4
- package/src/index.ts +38 -10
- package/src/keyboard-handlers.coverage.test.ts +65 -0
- package/src/keyboard-handlers.ts +26 -1
- package/src/keyboard.ts +3 -0
- package/src/row-drag-touch.ts +2 -2
- package/src/row-drag.test.ts +51 -0
- package/src/row-drag.ts +35 -2
- package/src/row-model.ts +152 -0
- package/src/row-resize.ts +24 -1
- package/src/selection.test.ts +3 -0
- package/src/selection.ts +33 -3
- package/src/server-block-cache.test.ts +672 -0
- package/src/server-block-cache.ts +690 -0
- package/src/server-data-source.infinite.test.ts +405 -0
- package/src/server-data-source.test.ts +19 -0
- package/src/server-data-source.ts +496 -29
- package/src/server.ts +49 -0
- package/src/sparkline.test.ts +7 -0
- package/src/sparkline.ts +6 -4
- package/src/svgrid-wrapper.types.ts +32 -0
- package/src/svgrid.api.test.ts +21 -0
- package/src/svgrid.charting.test.ts +12 -4
- package/src/svgrid.context-menu.test.ts +12 -0
- package/src/svgrid.detail-rows.svelte.test.ts +187 -0
- package/src/svgrid.filter-menu-scroll.test.ts +9 -0
- package/src/svgrid.gantt-seam.test.ts +199 -0
- package/src/svgrid.live-update-paths.svelte.test.ts +184 -0
- package/src/svgrid.menu-scroll-close.test.ts +10 -0
- package/src/svgrid.row-model-prop.svelte.test.ts +352 -0
- package/src/svgrid.row-model-seam.svelte.test.ts +305 -0
- package/src/svgrid.row-pinning.test.ts +15 -1
- package/src/svgrid.selection-bar-seam.test.ts +1 -1
- package/src/svgrid.sticky-groups.svelte.test.ts +145 -0
- package/src/svgrid.upsell-license.test.ts +8 -5
- package/src/test-setup.ts +12 -0
- package/src/transaction.test.ts +58 -0
- package/src/validate.test.ts +29 -0
- package/src/validate.ts +34 -3
- package/dist/SvGroupCell.svelte +0 -141
- package/dist/SvGroupCell.svelte.d.ts +0 -49
- package/dist/SvRowGroupPanel.svelte +0 -186
- package/dist/SvRowGroupPanel.svelte.d.ts +0 -25
- package/dist/cdn/GridMenus-7kbpnnBW.js +0 -635
- package/dist/cdn/validate-_CDJzgIo.js +0 -75
- package/dist/server-group-model.d.ts +0 -98
- package/dist/server-group-model.js +0 -263
- package/src/SvGroupCell.svelte +0 -141
- package/src/SvRowGroupPanel.svelte +0 -186
- package/src/server-group-model.test.ts +0 -294
- package/src/server-group-model.ts +0 -370
package/src/server.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@svgrid/grid/server` - the server-side data primitives, with no Svelte in
|
|
3
|
+
* the import graph.
|
|
4
|
+
*
|
|
5
|
+
* Everything here is also on the package root. This entry point exists for
|
|
6
|
+
* code that must not pull `SvGrid.svelte` into its bundle or its test
|
|
7
|
+
* runner: a SvelteKit `+server.ts` shaping a `ServerRequest` into a query,
|
|
8
|
+
* a worker feeding a block cache, or `@svgrid/enterprise`'s own row model,
|
|
9
|
+
* whose unit tests run without the Svelte compiler.
|
|
10
|
+
*/
|
|
11
|
+
export {
|
|
12
|
+
createServerDataSource,
|
|
13
|
+
type ServerDataSource,
|
|
14
|
+
type ServerSelectionRule,
|
|
15
|
+
type ServerRequest,
|
|
16
|
+
type ServerResult,
|
|
17
|
+
type ServerController,
|
|
18
|
+
type ServerControllerOptions,
|
|
19
|
+
type ServerState,
|
|
20
|
+
type ServerSortModel,
|
|
21
|
+
type ServerFilterModel,
|
|
22
|
+
type ServerAggFn,
|
|
23
|
+
type ServerAggregation,
|
|
24
|
+
type ServerGroupRow,
|
|
25
|
+
type ServerLeafRow,
|
|
26
|
+
type ServerMoreRow,
|
|
27
|
+
type ServerFooterRow,
|
|
28
|
+
type ServerSkeletonRow,
|
|
29
|
+
type ServerGrandTotalRow,
|
|
30
|
+
type ServerPlaceholderRow,
|
|
31
|
+
type ServerDisplayRow,
|
|
32
|
+
type ServerDetailRow,
|
|
33
|
+
} from './server-data-source'
|
|
34
|
+
export {
|
|
35
|
+
createBlockCache,
|
|
36
|
+
createRowPlaceholder,
|
|
37
|
+
rowPlaceholderState,
|
|
38
|
+
type BlockCache,
|
|
39
|
+
type BlockCacheOptions,
|
|
40
|
+
type BlockCacheState,
|
|
41
|
+
type BlockFetchResult,
|
|
42
|
+
type BlockState,
|
|
43
|
+
} from './server-block-cache'
|
|
44
|
+
export {
|
|
45
|
+
toServerFilterColumns,
|
|
46
|
+
type GridRowModel,
|
|
47
|
+
type GridFilterState,
|
|
48
|
+
type GridRowModelSort,
|
|
49
|
+
} from './row-model'
|
package/src/sparkline.test.ts
CHANGED
|
@@ -59,6 +59,13 @@ describe('buildSparkline', () => {
|
|
|
59
59
|
expect(g.bars[0]!.h).toBeCloseTo(g.bars[1]!.h)
|
|
60
60
|
})
|
|
61
61
|
|
|
62
|
+
it('leaves a blank gap for a zero, marking only wins and losses', () => {
|
|
63
|
+
const g = buildSparkline([1, 0, -1, 0, 1], { type: 'winloss', height: 20 })!
|
|
64
|
+
// Two wins and a loss draw; the two zeros are gaps, as Excel draws them.
|
|
65
|
+
expect(g.bars).toHaveLength(3)
|
|
66
|
+
expect(g.bars.map((b) => b.negative)).toEqual([false, true, false])
|
|
67
|
+
})
|
|
68
|
+
|
|
62
69
|
it('honors a fixed min/max scale', () => {
|
|
63
70
|
const a = buildSparkline([5, 5, 5], { type: 'line', min: 0, max: 10 })!
|
|
64
71
|
// flat series on a 0..10 scale sits at the vertical middle, not the top
|
package/src/sparkline.ts
CHANGED
|
@@ -102,11 +102,13 @@ export function buildSparkline(
|
|
|
102
102
|
const barW = Math.max(1, slot - gap)
|
|
103
103
|
const mid = height / 2
|
|
104
104
|
const h = innerH / 2
|
|
105
|
-
const bars: SparklineBar[] =
|
|
105
|
+
const bars: SparklineBar[] = []
|
|
106
|
+
values.forEach((v, i) => {
|
|
106
107
|
const x = pad + i * slot + (slot - barW) / 2
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
// A win/loss sparkline marks a win up and a loss down; a zero is a
|
|
109
|
+
// blank gap, no mark at all, the way Excel draws one.
|
|
110
|
+
if (v > 0) bars.push({ x, y: mid - h, w: barW, h, negative: false })
|
|
111
|
+
else if (v < 0) bars.push({ x, y: mid, w: barW, h, negative: true })
|
|
110
112
|
})
|
|
111
113
|
return { ...base, linePath: '', areaPath: '', bars, lastPoint: null }
|
|
112
114
|
}
|
|
@@ -146,6 +146,14 @@ export type SvGridApi<
|
|
|
146
146
|
TFeatures extends TableFeatures,
|
|
147
147
|
TData extends RowData,
|
|
148
148
|
> = {
|
|
149
|
+
// ----- DOM -----
|
|
150
|
+
/**
|
|
151
|
+
* The grid's root element (`.sv-grid-root`), or null before mount and
|
|
152
|
+
* after unmount. For measuring, focusing, or reading the theme the grid
|
|
153
|
+
* is drawn with - the PDF export takes its colours from here.
|
|
154
|
+
*/
|
|
155
|
+
getElement(): HTMLElement | null
|
|
156
|
+
|
|
149
157
|
// ----- Cells -----
|
|
150
158
|
/** Read a cell value from the underlying data at `rowIndex`. */
|
|
151
159
|
getCellValue(rowIndex: number, columnId: string): unknown
|
|
@@ -430,6 +438,16 @@ export type SvGridApi<
|
|
|
430
438
|
/** Snapshot of the current data array (pre-pipeline). */
|
|
431
439
|
getData(): ReadonlyArray<TData>
|
|
432
440
|
|
|
441
|
+
/**
|
|
442
|
+
* The stable id the grid keys a row by: what the `getRowId` prop
|
|
443
|
+
* computed, or the row's position when no `getRowId` is set. Pass a
|
|
444
|
+
* display index (the space `getActiveCell`, `scrollToRow` and the cell
|
|
445
|
+
* methods use) or a row object; returns null for an unknown index or a
|
|
446
|
+
* row that is not in the data. Pair with `selectRows`, `toggleRowSelected`
|
|
447
|
+
* or an `applyTransaction` remove, all of which take these ids.
|
|
448
|
+
*/
|
|
449
|
+
getRowId(row: number | TData): string | null
|
|
450
|
+
|
|
433
451
|
/**
|
|
434
452
|
* Snapshot of every column the grid currently knows about, in visual
|
|
435
453
|
* order, with the human-readable header label. Use this when exporting
|
|
@@ -580,6 +598,20 @@ export type SvGridApi<
|
|
|
580
598
|
canRedo(): boolean
|
|
581
599
|
/** Wipe both stacks (e.g. after a server save commits the buffer). */
|
|
582
600
|
clearHistory(): void
|
|
601
|
+
/**
|
|
602
|
+
* Mark every step recorded from now on with `tag`, until the next call;
|
|
603
|
+
* `undefined` stops the marking. The grid never reads the tag. It is for a
|
|
604
|
+
* consumer that shows one of several data sets through the grid (a
|
|
605
|
+
* workbook's sheets, say) and has to know which one the next Ctrl+Z would
|
|
606
|
+
* change before it lets the grid apply it: it tags the steps with the data
|
|
607
|
+
* set they were made on and asks `peekUndo` first.
|
|
608
|
+
*/
|
|
609
|
+
setHistoryTag(tag: unknown): void
|
|
610
|
+
/** The step the next `undo()` would apply, or null when there is none. A
|
|
611
|
+
* multi-cell action carries one tag, so it reports as one. */
|
|
612
|
+
peekUndo(): { tag: unknown } | null
|
|
613
|
+
/** The step the next `redo()` would apply, or null when there is none. */
|
|
614
|
+
peekRedo(): { tag: unknown } | null
|
|
583
615
|
|
|
584
616
|
// ----- Keyboard commands -----
|
|
585
617
|
/**
|
package/src/svgrid.api.test.ts
CHANGED
|
@@ -104,6 +104,27 @@ describe('SvGridApi - getData / getDisplayedRows', () => {
|
|
|
104
104
|
}
|
|
105
105
|
})
|
|
106
106
|
|
|
107
|
+
it('getRowId resolves a display index and a row object, and returns null for the unknown', async () => {
|
|
108
|
+
const { api, destroy } = await mountGrid()
|
|
109
|
+
try {
|
|
110
|
+
// No getRowId prop: the id is the row's index as a string.
|
|
111
|
+
expect(api.getRowId(0)).toBe('0')
|
|
112
|
+
expect(api.getRowId(4)).toBe('4')
|
|
113
|
+
expect(api.getRowId(99)).toBeNull()
|
|
114
|
+
const third = api.getData()[2]!
|
|
115
|
+
expect(api.getRowId(third)).toBe('2')
|
|
116
|
+
expect(api.getRowId({ id: 999, name: 'x', team: 'x', salary: 0 })).toBeNull()
|
|
117
|
+
// After a sort the display index follows the new order.
|
|
118
|
+
api.setSort('salary', 'desc')
|
|
119
|
+
await Promise.resolve()
|
|
120
|
+
const topId = api.getRowId(0)
|
|
121
|
+
expect(topId).not.toBeNull()
|
|
122
|
+
expect(api.getData()[Number(topId)]!.salary).toBe(Math.max(...sampleRows.map((r) => r.salary)))
|
|
123
|
+
} finally {
|
|
124
|
+
destroy()
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
|
|
107
128
|
it('getDisplayedRows returns the post-pipeline visible rows', async () => {
|
|
108
129
|
const { api, destroy } = await mountGrid()
|
|
109
130
|
try {
|
|
@@ -754,7 +754,15 @@ describe('SvGrid built-in charting', () => {
|
|
|
754
754
|
})
|
|
755
755
|
|
|
756
756
|
|
|
757
|
-
|
|
757
|
+
// The builder mounts a live SvGridChart per type, thirty of them, in one
|
|
758
|
+
// synchronous flush. Alone that is well under a second; under `--coverage`
|
|
759
|
+
// (instrumented chart code, every file of the suite in parallel workers)
|
|
760
|
+
// it took 22 s on a loaded machine and the four tests that open the
|
|
761
|
+
// builder timed out at 20 s, the last of them then reading a builder a
|
|
762
|
+
// timed-out neighbour left behind. The budget is for instrumentation,
|
|
763
|
+
// not for the product: `pnpm test` runs with coverage and is the gate.
|
|
764
|
+
const BUILDER_TIMEOUT = 90_000
|
|
765
|
+
it('the builder opens from the panel with a live thumbnail per type, and a card switches the chart', { timeout: BUILDER_TIMEOUT }, async () => {
|
|
758
766
|
const { api, target, destroy } = await mountGrid({ charting: { defaultOpen: true } })
|
|
759
767
|
try {
|
|
760
768
|
await tick()
|
|
@@ -805,7 +813,7 @@ describe('SvGrid built-in charting', () => {
|
|
|
805
813
|
}
|
|
806
814
|
})
|
|
807
815
|
|
|
808
|
-
it('the Format tab writes series labels, crosshair pills, a compact rule, a stack group and a style through configureChart', { timeout:
|
|
816
|
+
it('the Format tab writes series labels, crosshair pills, a compact rule, a stack group and a style through configureChart', { timeout: BUILDER_TIMEOUT }, async () => {
|
|
809
817
|
const { api, target, destroy } = await mountGrid({ charting: { defaultOpen: true } })
|
|
810
818
|
try {
|
|
811
819
|
await tick()
|
|
@@ -848,7 +856,7 @@ describe('SvGrid built-in charting', () => {
|
|
|
848
856
|
}
|
|
849
857
|
})
|
|
850
858
|
|
|
851
|
-
it('the builder Data tab renders the panel pickers and both write the same tab', { timeout:
|
|
859
|
+
it('the builder Data tab renders the panel pickers and both write the same tab', { timeout: BUILDER_TIMEOUT }, async () => {
|
|
852
860
|
const { api, target, destroy } = await mountGrid({ charting: { defaultOpen: true } })
|
|
853
861
|
try {
|
|
854
862
|
await tick()
|
|
@@ -883,7 +891,7 @@ describe('SvGrid built-in charting', () => {
|
|
|
883
891
|
}
|
|
884
892
|
})
|
|
885
893
|
|
|
886
|
-
it('localization.text relabels the panel and the builder; unset keys stay English', { timeout:
|
|
894
|
+
it('localization.text relabels the panel and the builder; unset keys stay English', { timeout: BUILDER_TIMEOUT }, async () => {
|
|
887
895
|
const { target, destroy } = await mountGrid({
|
|
888
896
|
charting: { defaultOpen: true },
|
|
889
897
|
localization: { text: { chartPanelTitle: 'Diagramm', chartGroupBy: 'Gruppieren nach', chartTypeBar: 'Balken', chartAdd: 'Diagramm hinzufügen', chartBuilderTitle: 'Diagramm-Editor', chartBuilderTabData: 'Daten', noRows: 'Keine Zeilen' } },
|
|
@@ -137,6 +137,18 @@ describe('SvGrid context menu', () => {
|
|
|
137
137
|
destroy()
|
|
138
138
|
})
|
|
139
139
|
|
|
140
|
+
it('closes on Escape, so a keyboard user is not trapped under it', async () => {
|
|
141
|
+
const { target, destroy } = await mountGrid(true)
|
|
142
|
+
await tick()
|
|
143
|
+
await openContextMenu(target)
|
|
144
|
+
expect(target.querySelector('.sv-grid-context-menu')).not.toBeNull()
|
|
145
|
+
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
|
|
146
|
+
await vi.waitFor(() => {
|
|
147
|
+
expect(target.querySelector('.sv-grid-context-menu')).toBeNull()
|
|
148
|
+
})
|
|
149
|
+
destroy()
|
|
150
|
+
})
|
|
151
|
+
|
|
140
152
|
it('does not open when contextMenu is omitted (native menu shows)', async () => {
|
|
141
153
|
const { target, destroy } = await mountGrid(undefined)
|
|
142
154
|
await tick()
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detail rows under virtualization: `detailRowHeight` sizes a detail row for
|
|
3
|
+
* the virtualizer, so a master-detail grid no longer needs
|
|
4
|
+
* `virtualization={false}`.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, it } from 'vitest'
|
|
7
|
+
import { mount, unmount } from 'svelte'
|
|
8
|
+
import SvGrid from './SvGrid.svelte'
|
|
9
|
+
import { createCoreRowModel, tableFeatures } from './index'
|
|
10
|
+
import type { ColumnDef, SvGridApi } from './index'
|
|
11
|
+
|
|
12
|
+
type Row = { id: string; name: string; kind: 'data' | 'detail' }
|
|
13
|
+
const features = tableFeatures({})
|
|
14
|
+
const cols: ColumnDef<typeof features, Row>[] = [{ field: 'name', header: 'Name', width: 200 }]
|
|
15
|
+
const tick = () => new Promise<void>((r) => setTimeout(r))
|
|
16
|
+
|
|
17
|
+
function rows(): Row[] {
|
|
18
|
+
const out: Row[] = []
|
|
19
|
+
for (let i = 0; i < 12; i += 1) {
|
|
20
|
+
out.push({ id: `r${i}`, name: `row ${i}`, kind: 'data' })
|
|
21
|
+
if (i === 1 || i === 4) out.push({ id: `d${i}`, name: `detail of ${i}`, kind: 'detail' })
|
|
22
|
+
}
|
|
23
|
+
return out
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function mountGrid(extra: Record<string, unknown>) {
|
|
27
|
+
return new Promise<{ api: SvGridApi<typeof features, Row>; target: HTMLElement; destroy: () => void }>((res, rej) => {
|
|
28
|
+
const target = document.createElement('div')
|
|
29
|
+
document.body.appendChild(target)
|
|
30
|
+
const app = mount(SvGrid, {
|
|
31
|
+
target,
|
|
32
|
+
props: {
|
|
33
|
+
data: rows(),
|
|
34
|
+
columns: cols,
|
|
35
|
+
features,
|
|
36
|
+
_rowModels: { coreRowModel: createCoreRowModel() },
|
|
37
|
+
getRowId: (r: Row) => r.id,
|
|
38
|
+
rowHeight: 30,
|
|
39
|
+
containerHeight: 300,
|
|
40
|
+
virtualization: true,
|
|
41
|
+
isDetailRow: (r: Row) => r.kind === 'detail',
|
|
42
|
+
onApiReady(api: SvGridApi<typeof features, Row>) {
|
|
43
|
+
res({ api, target, destroy: () => { unmount(app); target.remove() } })
|
|
44
|
+
},
|
|
45
|
+
...extra,
|
|
46
|
+
} as any,
|
|
47
|
+
})
|
|
48
|
+
queueMicrotask(() => { if (!target.querySelector('[role="grid"]')) rej(new Error('no grid')) })
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('detailRowHeight', () => {
|
|
53
|
+
it('sizes detail rows for the virtualizer and leaves the others at rowHeight', async () => {
|
|
54
|
+
const { api, target, destroy } = await mountGrid({ detailRowHeight: 120 })
|
|
55
|
+
try {
|
|
56
|
+
await tick()
|
|
57
|
+
const detail = target.querySelector<HTMLElement>('tr.sv-grid-detail-row')!
|
|
58
|
+
expect(detail.classList.contains('sv-grid-detail-row-sized')).toBe(true)
|
|
59
|
+
expect(detail.style.height).toBe('120px')
|
|
60
|
+
// Index 2 is the detail under row 1; 3 is a plain row.
|
|
61
|
+
expect(api.getRowHeight(2)).toBe(120)
|
|
62
|
+
expect(api.getRowHeight(3)).toBe(30)
|
|
63
|
+
} finally {
|
|
64
|
+
destroy()
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('takes a function of the row', async () => {
|
|
69
|
+
const { api, destroy } = await mountGrid({ detailRowHeight: (r: Row) => (r.id === 'd1' ? 80 : 200) })
|
|
70
|
+
try {
|
|
71
|
+
await tick()
|
|
72
|
+
expect(api.getRowHeight(2)).toBe(80)
|
|
73
|
+
expect(api.getRowHeight(6)).toBe(200)
|
|
74
|
+
} finally {
|
|
75
|
+
destroy()
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('without one a detail row is auto height, as before', async () => {
|
|
80
|
+
const { target, destroy } = await mountGrid({ virtualization: false })
|
|
81
|
+
try {
|
|
82
|
+
await tick()
|
|
83
|
+
const detail = target.querySelector<HTMLElement>('tr.sv-grid-detail-row')!
|
|
84
|
+
expect(detail.classList.contains('sv-grid-detail-row-sized')).toBe(false)
|
|
85
|
+
expect(detail.style.height).toBe('')
|
|
86
|
+
} finally {
|
|
87
|
+
destroy()
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe('showDetailToggle', () => {
|
|
93
|
+
it('is a row-header column: sticky, no menu, no resize handle, a chevron per data row and none on a detail row', async () => {
|
|
94
|
+
const open = new Set<string>(['r1', 'r4'])
|
|
95
|
+
const toggled: string[] = []
|
|
96
|
+
const { target, destroy } = await mountGrid({
|
|
97
|
+
showDetailToggle: true,
|
|
98
|
+
columnResize: true,
|
|
99
|
+
columnMenu: true,
|
|
100
|
+
onDetailToggle: (r: Row) => toggled.push(r.id),
|
|
101
|
+
isDetailOpen: (r: Row) => open.has(r.id),
|
|
102
|
+
detailRowHeight: 60,
|
|
103
|
+
})
|
|
104
|
+
try {
|
|
105
|
+
await tick()
|
|
106
|
+
const head = target.querySelector<HTMLElement>('thead th.sv-grid-detail-toggle-column')!
|
|
107
|
+
expect(head).not.toBeNull()
|
|
108
|
+
expect(head.getAttribute('aria-hidden')).toBe('true')
|
|
109
|
+
expect(head.querySelector('.sv-grid-col-menu-btn, .sv-grid-column-resizer, [data-resize-handle]')).toBeNull()
|
|
110
|
+
expect(head.style.position || getComputedStyle(head).position).not.toBe('')
|
|
111
|
+
const first = target.querySelector<HTMLElement>('tbody tr.sv-grid-row:not(.sv-grid-detail-row)')!
|
|
112
|
+
const cell = first.querySelector<HTMLElement>('td.sv-grid-detail-toggle-cell')!
|
|
113
|
+
expect(cell).not.toBeNull()
|
|
114
|
+
// The gutter is not a data cell: nothing the keyboard or the selection can land on.
|
|
115
|
+
expect(cell.hasAttribute('data-svgrid-col')).toBe(false)
|
|
116
|
+
const chev = cell.querySelector<HTMLButtonElement>('button.sv-grid-detail-toggle')!
|
|
117
|
+
expect(chev.getAttribute('aria-expanded')).toBe('false')
|
|
118
|
+
expect(chev.getAttribute('aria-label')).toBe('Open details')
|
|
119
|
+
chev.click()
|
|
120
|
+
expect(toggled).toEqual(['r0'])
|
|
121
|
+
// Row 1 is open: its chevron says so; the detail row under it has a blank gutter cell.
|
|
122
|
+
const rowsAll = [...target.querySelectorAll<HTMLElement>('tbody tr.sv-grid-row')]
|
|
123
|
+
const r1 = rowsAll.find((tr) => tr.textContent?.includes('row 1'))!
|
|
124
|
+
expect(r1.querySelector('button.sv-grid-detail-toggle')!.getAttribute('aria-expanded')).toBe('true')
|
|
125
|
+
expect(r1.querySelector('button.sv-grid-detail-toggle')!.getAttribute('aria-label')).toBe('Close details')
|
|
126
|
+
const detail = target.querySelector<HTMLElement>('tr.sv-grid-detail-row')!
|
|
127
|
+
expect(detail.querySelector('button.sv-grid-detail-toggle')).toBeNull()
|
|
128
|
+
// The full-width cell spans the gutter too.
|
|
129
|
+
expect(detail.querySelector<HTMLTableCellElement>('td.sv-grid-detail-cell')!.colSpan).toBe(first.children.length)
|
|
130
|
+
} finally {
|
|
131
|
+
destroy()
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('leaves the chevron out where hasDetail says no, and sticks after the row numbers and the checkbox', async () => {
|
|
136
|
+
const { target, destroy } = await mountGrid({
|
|
137
|
+
showDetailToggle: true,
|
|
138
|
+
showRowNumbers: true,
|
|
139
|
+
showRowSelection: true,
|
|
140
|
+
onDetailToggle: () => {},
|
|
141
|
+
hasDetail: (r: Row) => r.id !== 'r0',
|
|
142
|
+
})
|
|
143
|
+
try {
|
|
144
|
+
await tick()
|
|
145
|
+
const rowsAll = [...target.querySelectorAll<HTMLElement>('tbody tr.sv-grid-row:not(.sv-grid-detail-row)')]
|
|
146
|
+
expect(rowsAll[0]!.querySelector('td.sv-grid-detail-toggle-cell button')).toBeNull()
|
|
147
|
+
expect(rowsAll[1]!.querySelector('td.sv-grid-detail-toggle-cell button')).not.toBeNull()
|
|
148
|
+
// Row number (56) + selection (44) sit before it.
|
|
149
|
+
const cell = rowsAll[1]!.querySelector<HTMLElement>('td.sv-grid-detail-toggle-cell')!
|
|
150
|
+
expect(cell.style.left).toBe('100px')
|
|
151
|
+
expect(cell.style.width).toBe('36px')
|
|
152
|
+
// The system cells come in that order, then the data.
|
|
153
|
+
const classes = [...rowsAll[1]!.children].map((td) => td.className.split(' ')[1])
|
|
154
|
+
expect(classes.slice(0, 3)).toEqual(['sv-grid-row-number-cell', 'sv-grid-selection-cell', 'sv-grid-detail-toggle-cell'])
|
|
155
|
+
} finally {
|
|
156
|
+
destroy()
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('on a server row model the seam decides: groups get no chevron, a leaf reads detailOpen and calls toggleDetail', async () => {
|
|
161
|
+
const calls: string[] = []
|
|
162
|
+
const { target, destroy } = await mountGrid({
|
|
163
|
+
showDetailToggle: true,
|
|
164
|
+
serverGroup: {
|
|
165
|
+
isGroup: (r: Row) => r.id === 'r0',
|
|
166
|
+
level: () => 0,
|
|
167
|
+
onToggle: () => {},
|
|
168
|
+
toggleDetail: (r: Row) => calls.push(r.id),
|
|
169
|
+
detailOpen: (r: Row) => r.id === 'r1',
|
|
170
|
+
hasDetail: (r: Row) => r.kind === 'data',
|
|
171
|
+
},
|
|
172
|
+
})
|
|
173
|
+
try {
|
|
174
|
+
await tick()
|
|
175
|
+
const rowsAll = [...target.querySelectorAll<HTMLElement>('tbody tr.sv-grid-row')]
|
|
176
|
+
const r0 = rowsAll.find((tr) => tr.textContent?.includes('row 0'))!
|
|
177
|
+
const r1 = rowsAll.find((tr) => tr.textContent?.includes('row 1'))!
|
|
178
|
+
expect(r0.querySelector('button.sv-grid-detail-toggle')).toBeNull()
|
|
179
|
+
const chev = r1.querySelector<HTMLButtonElement>('button.sv-grid-detail-toggle')!
|
|
180
|
+
expect(chev.getAttribute('aria-expanded')).toBe('true')
|
|
181
|
+
chev.click()
|
|
182
|
+
expect(calls).toEqual(['r1'])
|
|
183
|
+
} finally {
|
|
184
|
+
destroy()
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
})
|
|
@@ -60,6 +60,14 @@ function mountGrid() {
|
|
|
60
60
|
})
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* The close-on-scroll listener arms one animation frame after a popover
|
|
65
|
+
* mounts, so a scroll queued before the opening click cannot dismiss what
|
|
66
|
+
* that click opened. Every open helper waits that frame out before a case
|
|
67
|
+
* dispatches its scroll.
|
|
68
|
+
*/
|
|
69
|
+
const nextFrame = () => new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
|
|
70
|
+
|
|
63
71
|
async function openFilterMenu(target: HTMLElement) {
|
|
64
72
|
const btn = target.querySelector('.sv-grid-col-filter-btn') as HTMLButtonElement
|
|
65
73
|
expect(btn).not.toBeNull()
|
|
@@ -74,6 +82,7 @@ async function openFilterMenu(target: HTMLElement) {
|
|
|
74
82
|
expect(menu).not.toBeNull()
|
|
75
83
|
expect(menu!.querySelector('.sv-listbox')).not.toBeNull()
|
|
76
84
|
})
|
|
85
|
+
await nextFrame()
|
|
77
86
|
return target.querySelector('.sv-grid-filter-menu') as HTMLElement
|
|
78
87
|
}
|
|
79
88
|
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The FREE half of the Gantt view: the `gantt` prop, what the grid hands the
|
|
3
|
+
* renderer, and what shows when no Enterprise renderer is registered.
|
|
4
|
+
*
|
|
5
|
+
* The renderer itself is Pro and lives behind `registerGanttView`, so most of
|
|
6
|
+
* this suite deliberately never registers one - it asserts the upsell path.
|
|
7
|
+
* The last block registers a STUB renderer to pin the seam's contract: which
|
|
8
|
+
* props the grid passes, and that the rows arrive already filtered. The real
|
|
9
|
+
* renderer is covered by `gantt.dom.test.ts` in @svgrid/enterprise.
|
|
10
|
+
*/
|
|
11
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
12
|
+
import { mount, unmount } from 'svelte'
|
|
13
|
+
import SvGrid from './SvGrid.svelte'
|
|
14
|
+
import {
|
|
15
|
+
createCoreRowModel,
|
|
16
|
+
columnFilteringFeature,
|
|
17
|
+
hasGanttView,
|
|
18
|
+
registerGanttView,
|
|
19
|
+
rowSortingFeature,
|
|
20
|
+
tableFeatures,
|
|
21
|
+
} from './index'
|
|
22
|
+
import { getGanttView } from './gantt-view.svelte'
|
|
23
|
+
import GanttStub from './gantt-stub.test.svelte'
|
|
24
|
+
import type { ColumnDef, GanttConfig, SvGridApi } from './index'
|
|
25
|
+
|
|
26
|
+
type Task = { id: number; name: string; start: string; end: string; parentId: number | null }
|
|
27
|
+
const features = tableFeatures({ rowSortingFeature, columnFilteringFeature })
|
|
28
|
+
const rows: Task[] = [
|
|
29
|
+
{ id: 1, name: 'Discovery', start: '2026-09-01', end: '2026-09-10', parentId: null },
|
|
30
|
+
{ id: 2, name: 'Interviews', start: '2026-09-01', end: '2026-09-04', parentId: 1 },
|
|
31
|
+
{ id: 3, name: 'Build', start: '2026-09-10', end: '2026-09-30', parentId: null },
|
|
32
|
+
]
|
|
33
|
+
const cols: ColumnDef<typeof features, Task>[] = [
|
|
34
|
+
{ field: 'name', header: 'Task', width: 180 },
|
|
35
|
+
{ field: 'start', header: 'Start', width: 120 },
|
|
36
|
+
{ field: 'end', header: 'End', width: 120 },
|
|
37
|
+
]
|
|
38
|
+
const gantt: GanttConfig<typeof features, Task> = {
|
|
39
|
+
startField: 'start',
|
|
40
|
+
endField: 'end',
|
|
41
|
+
parentField: 'parentId',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const tick = () => new Promise<void>((r) => setTimeout(r))
|
|
45
|
+
const UPSELL = '.sv-grid-gantt-upsell'
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
// The seam is module-level state; a stub left registered would make every
|
|
49
|
+
// later "no renderer" assertion pass for the wrong reason.
|
|
50
|
+
registerGanttView(null as never)
|
|
51
|
+
document.body.innerHTML = ''
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
function mountGrid(extra: Record<string, unknown> = {}) {
|
|
55
|
+
return new Promise<{ api: SvGridApi<typeof features, Task>; target: HTMLElement; destroy: () => void }>(
|
|
56
|
+
(res, rej) => {
|
|
57
|
+
const target = document.createElement('div')
|
|
58
|
+
document.body.appendChild(target)
|
|
59
|
+
const app = mount(SvGrid, {
|
|
60
|
+
target,
|
|
61
|
+
props: {
|
|
62
|
+
data: rows,
|
|
63
|
+
columns: cols,
|
|
64
|
+
features,
|
|
65
|
+
_rowModels: { coreRowModel: createCoreRowModel() },
|
|
66
|
+
getRowId: (r: Task) => String(r.id),
|
|
67
|
+
containerHeight: 260,
|
|
68
|
+
virtualization: false,
|
|
69
|
+
gantt,
|
|
70
|
+
onApiReady(api: SvGridApi<typeof features, Task>) {
|
|
71
|
+
res({ api, target, destroy: () => { unmount(app); target.remove() } })
|
|
72
|
+
},
|
|
73
|
+
...extra,
|
|
74
|
+
} as any,
|
|
75
|
+
})
|
|
76
|
+
queueMicrotask(() => {
|
|
77
|
+
if (!target.querySelector('.sv-grid-gantt-root')) rej(new Error('no gantt root'))
|
|
78
|
+
})
|
|
79
|
+
},
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
describe('gantt - without the Enterprise renderer', () => {
|
|
84
|
+
it('reports that no renderer is registered', () => {
|
|
85
|
+
// If this is ever true here, the suite is testing the Pro path by accident
|
|
86
|
+
// and every assertion below is meaningless.
|
|
87
|
+
expect(hasGanttView()).toBe(false)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('renders the upsell note in place of the table', async () => {
|
|
91
|
+
const { target, destroy } = await mountGrid()
|
|
92
|
+
await tick()
|
|
93
|
+
|
|
94
|
+
const note = target.querySelector(UPSELL)!
|
|
95
|
+
expect(note).not.toBeNull()
|
|
96
|
+
expect(note.getAttribute('role')).toBe('note')
|
|
97
|
+
expect(note.textContent).toContain('@svgrid/enterprise')
|
|
98
|
+
expect(note.textContent).toContain('enableGanttView()')
|
|
99
|
+
// The table is replaced, not decorated - this is a view of the grid.
|
|
100
|
+
expect(target.querySelector('[role="grid"]')).toBeNull()
|
|
101
|
+
destroy()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('says a license is needed, and links to it', async () => {
|
|
105
|
+
// Naming the package is what unblocks a developer trying it; saying a key
|
|
106
|
+
// is needed is what they have to know before shipping. Enterprise is
|
|
107
|
+
// runs without a key, so the wording must not imply the feature is inert without
|
|
108
|
+
// one - it says "watermark", which is what actually happens.
|
|
109
|
+
const { target, destroy } = await mountGrid()
|
|
110
|
+
await tick()
|
|
111
|
+
|
|
112
|
+
const license = target.querySelector('.sv-grid-upsell-license')!
|
|
113
|
+
expect(license).not.toBeNull()
|
|
114
|
+
expect(license.textContent).toContain('license key')
|
|
115
|
+
expect(license.textContent).toContain('watermark')
|
|
116
|
+
|
|
117
|
+
const link = license.querySelector('a')!
|
|
118
|
+
expect(link.getAttribute('href')).toBe('https://svgrid.com/pricing/')
|
|
119
|
+
expect(link.getAttribute('target')).toBe('_blank')
|
|
120
|
+
expect(link.getAttribute('rel')).toContain('noopener')
|
|
121
|
+
destroy()
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('shows the search box, and hides it on `searchable: false`', async () => {
|
|
125
|
+
const withBox = await mountGrid()
|
|
126
|
+
await tick()
|
|
127
|
+
const input = withBox.target.querySelector<HTMLInputElement>('.sv-grid-board-search input')!
|
|
128
|
+
expect(input).not.toBeNull()
|
|
129
|
+
expect(input.placeholder).toBe('Search tasks...')
|
|
130
|
+
withBox.destroy()
|
|
131
|
+
|
|
132
|
+
const without = await mountGrid({ gantt: { ...gantt, searchable: false } })
|
|
133
|
+
await tick()
|
|
134
|
+
expect(without.target.querySelector('.sv-grid-board-search')).toBeNull()
|
|
135
|
+
without.destroy()
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('is localizable, like the rest of the grid chrome', async () => {
|
|
139
|
+
const { target, destroy } = await mountGrid({
|
|
140
|
+
localization: {
|
|
141
|
+
text: { ganttUpsellTitle: 'Gantt-Ansicht', upsellLicense: 'Lizenz erforderlich.' },
|
|
142
|
+
},
|
|
143
|
+
})
|
|
144
|
+
await vi.waitFor(() => {
|
|
145
|
+
expect(target.querySelector(UPSELL)?.textContent).toContain('Gantt-Ansicht')
|
|
146
|
+
expect(target.querySelector('.sv-grid-upsell-license')?.textContent).toContain(
|
|
147
|
+
'Lizenz erforderlich.',
|
|
148
|
+
)
|
|
149
|
+
})
|
|
150
|
+
destroy()
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
describe('gantt - the renderer seam', () => {
|
|
155
|
+
// GanttStub stands in for @svgrid/enterprise's SvGridGantt: it writes every
|
|
156
|
+
// prop it receives into the DOM, so the grid's half of the contract is what
|
|
157
|
+
// those attributes say.
|
|
158
|
+
const stub = () => document.querySelector<HTMLElement>('.gantt-stub')!
|
|
159
|
+
|
|
160
|
+
it('mounts the registered renderer instead of the upsell', async () => {
|
|
161
|
+
registerGanttView(GanttStub as never)
|
|
162
|
+
expect(hasGanttView()).toBe(true)
|
|
163
|
+
expect(getGanttView()).not.toBeNull()
|
|
164
|
+
|
|
165
|
+
const { target, destroy } = await mountGrid()
|
|
166
|
+
await tick()
|
|
167
|
+
expect(target.querySelector(UPSELL)).toBeNull()
|
|
168
|
+
expect(target.querySelector('.gantt-stub')).not.toBeNull()
|
|
169
|
+
destroy()
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
it('hands it data, columns, the config and getRowId', async () => {
|
|
173
|
+
registerGanttView(GanttStub as never)
|
|
174
|
+
const { destroy } = await mountGrid()
|
|
175
|
+
await tick()
|
|
176
|
+
|
|
177
|
+
expect(stub().dataset.ids).toBe('1,2,3')
|
|
178
|
+
expect(stub().dataset.cols).toBe(String(cols.length))
|
|
179
|
+
expect(stub().dataset.start).toBe('start')
|
|
180
|
+
expect(stub().dataset.parent).toBe('parentId')
|
|
181
|
+
expect(stub().dataset.hasGetRowId).toBe('true')
|
|
182
|
+
destroy()
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it("hands it the grid's FILTERED rows, so the search box flows through", async () => {
|
|
186
|
+
// This is what makes the Gantt a view of the grid rather than a second
|
|
187
|
+
// data path: the renderer never filters, it just draws what it is given.
|
|
188
|
+
registerGanttView(GanttStub as never)
|
|
189
|
+
const { target, destroy } = await mountGrid()
|
|
190
|
+
await tick()
|
|
191
|
+
|
|
192
|
+
const search = target.querySelector<HTMLInputElement>('.sv-grid-board-search input')!
|
|
193
|
+
search.value = 'Interviews'
|
|
194
|
+
search.dispatchEvent(new Event('input', { bubbles: true }))
|
|
195
|
+
|
|
196
|
+
await vi.waitFor(() => expect(stub().dataset.ids).toBe('2'))
|
|
197
|
+
destroy()
|
|
198
|
+
})
|
|
199
|
+
})
|