@svgrid/enterprise 3.1.8 → 3.1.10
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 +1 -0
- package/dist/SvAlertRuleEditor.svelte +16 -5
- package/dist/SvGridAlerts.svelte +8 -0
- package/dist/SvGridAlerts.svelte.d.ts +4 -0
- package/dist/SvGridBoard.svelte +69 -10
- package/dist/SvGridBulkEditDrawer.svelte +13 -5
- package/dist/SvGridGallery.svelte +286 -0
- package/dist/SvGridGallery.svelte.d.ts +30 -0
- package/dist/SvGridScheduler.svelte +33 -11
- package/dist/SvGridSelectionBar.svelte +47 -0
- package/dist/advanced-filter/expr-columns-from-grid.d.ts +1 -0
- package/dist/advanced-filter/expr-columns-from-grid.js +11 -7
- package/dist/advanced-filter-enable.d.ts +6 -0
- package/dist/advanced-filter-enable.js +8 -0
- package/dist/alerts/alert-engine.d.ts +7 -0
- package/dist/alerts/alert-engine.js +5 -2
- package/dist/alerts/condition-editor.d.ts +26 -0
- package/dist/alerts/condition-editor.js +17 -0
- package/dist/base/index.d.ts +20 -0
- package/dist/base/index.js +20 -0
- package/dist/bulk-edit.d.ts +11 -0
- package/dist/bulk-edit.js +18 -1
- package/dist/cdn/svgrid-enterprise.svelte-external.js +11197 -10639
- package/dist/designer/assets/GridFooter-Cu8_yBlQ.js +2 -0
- package/dist/designer/assets/GridMenus-BUjkkAU2.js +6 -0
- package/dist/designer/assets/PinSplitter-BgTpgh1d.css +1 -0
- package/dist/designer/assets/PinSplitter-C5AwA1xV.js +1 -0
- package/dist/designer/assets/SvGrid.helpers-CIrmIo9U.js +1 -0
- package/dist/designer/assets/SvGridCellEditor-CJxThKL_.js +4 -0
- package/dist/designer/assets/SvListBox-CS7oUAiX.js +2 -0
- package/dist/designer/assets/conditional-formatting-MDWbF8_Y.js +1 -0
- package/dist/designer/assets/grid-extension.svelte-BZ7AqyJI.js +1 -0
- package/dist/designer/assets/index-BmvG99kR.css +1 -0
- package/dist/designer/assets/{index-Obk8sI77.js → index-CJodU3tU.js} +307 -307
- package/dist/designer/assets/sparkline-C-NcnB2i.js +1 -0
- package/dist/designer/assets/src-C_U1NKOY.js +58 -0
- package/dist/designer/assets/sv-grid-scrollbar-B9N6Wuj0.js +109 -0
- package/dist/designer/assets/validate-H5u2HEQh.js +1 -0
- package/dist/designer/index.html +11 -7
- package/dist/expressions/compile.js +10 -0
- package/dist/expressions/evaluate.js +18 -0
- package/dist/expressions/expression-types.d.ts +10 -0
- package/dist/expressions/kinds.d.ts +28 -0
- package/dist/expressions/kinds.js +19 -0
- package/dist/expressions/parse.js +9 -1
- package/dist/fields/AttachmentCell.svelte +242 -0
- package/dist/fields/AttachmentCell.svelte.d.ts +5 -0
- package/dist/fields/AttachmentDialog.svelte +357 -0
- package/dist/fields/AttachmentDialog.svelte.d.ts +13 -0
- package/dist/fields/AttachmentEditor.svelte +41 -0
- package/dist/fields/AttachmentEditor.svelte.d.ts +11 -0
- package/dist/fields/AttachmentPreview.svelte +72 -0
- package/dist/fields/AttachmentPreview.svelte.d.ts +8 -0
- package/dist/fields/BarcodeCell.svelte +20 -0
- package/dist/fields/BarcodeCell.svelte.d.ts +5 -0
- package/dist/fields/BaseAutomations.svelte +558 -0
- package/dist/fields/BaseAutomations.svelte.d.ts +18 -0
- package/dist/fields/BaseChartView.svelte +47 -0
- package/dist/fields/BaseChartView.svelte.d.ts +8 -0
- package/dist/fields/BaseTableView.svelte +185 -0
- package/dist/fields/BaseTableView.svelte.d.ts +37 -0
- package/dist/fields/BaseTabs.svelte +308 -0
- package/dist/fields/BaseTabs.svelte.d.ts +21 -0
- package/dist/fields/ButtonCell.svelte +47 -0
- package/dist/fields/ButtonCell.svelte.d.ts +20 -0
- package/dist/fields/CheckboxCell.svelte +46 -0
- package/dist/fields/CheckboxCell.svelte.d.ts +14 -0
- package/dist/fields/DeleteFieldConfirm.svelte +80 -0
- package/dist/fields/DeleteFieldConfirm.svelte.d.ts +8 -0
- package/dist/fields/DeleteRecordConfirm.svelte +48 -0
- package/dist/fields/DeleteRecordConfirm.svelte.d.ts +9 -0
- package/dist/fields/FieldCell.svelte +80 -0
- package/dist/fields/FieldCell.svelte.d.ts +9 -0
- package/dist/fields/FieldEditor.svelte +321 -0
- package/dist/fields/FieldEditor.svelte.d.ts +10 -0
- package/dist/fields/FieldInput.svelte +188 -0
- package/dist/fields/FieldInput.svelte.d.ts +29 -0
- package/dist/fields/FieldsHeaderEnd.svelte +146 -0
- package/dist/fields/FieldsHeaderEnd.svelte.d.ts +6 -0
- package/dist/fields/FootRowAdder.svelte +8 -0
- package/dist/fields/FootRowAdder.svelte.d.ts +6 -0
- package/dist/fields/GroupHeader.svelte +127 -0
- package/dist/fields/GroupHeader.svelte.d.ts +24 -0
- package/dist/fields/LinkCell.svelte +46 -0
- package/dist/fields/LinkCell.svelte.d.ts +9 -0
- package/dist/fields/LinkPickerEditor.svelte +172 -0
- package/dist/fields/LinkPickerEditor.svelte.d.ts +9 -0
- package/dist/fields/LinkPreview.svelte +60 -0
- package/dist/fields/LinkPreview.svelte.d.ts +7 -0
- package/dist/fields/LinkedRecordView.svelte +94 -0
- package/dist/fields/LinkedRecordView.svelte.d.ts +9 -0
- package/dist/fields/LinkedRecordsCell.svelte +59 -0
- package/dist/fields/LinkedRecordsCell.svelte.d.ts +11 -0
- package/dist/fields/LongTextEditor.svelte +157 -0
- package/dist/fields/LongTextEditor.svelte.d.ts +11 -0
- package/dist/fields/NumberEditor.svelte +309 -0
- package/dist/fields/NumberEditor.svelte.d.ts +12 -0
- package/dist/fields/OptionsEditor.svelte +117 -0
- package/dist/fields/OptionsEditor.svelte.d.ts +11 -0
- package/dist/fields/PercentCell.svelte +21 -0
- package/dist/fields/PercentCell.svelte.d.ts +8 -0
- package/dist/fields/PropertyInput.svelte +204 -0
- package/dist/fields/PropertyInput.svelte.d.ts +11 -0
- package/dist/fields/RatingCell.svelte +75 -0
- package/dist/fields/RatingCell.svelte.d.ts +16 -0
- package/dist/fields/RatingEditor.svelte +82 -0
- package/dist/fields/RatingEditor.svelte.d.ts +9 -0
- package/dist/fields/RecordView.svelte +310 -0
- package/dist/fields/RecordView.svelte.d.ts +11 -0
- package/dist/fields/RichTextCell.svelte +30 -0
- package/dist/fields/RichTextCell.svelte.d.ts +5 -0
- package/dist/fields/RowAdder.svelte +93 -0
- package/dist/fields/RowAdder.svelte.d.ts +7 -0
- package/dist/fields/RowHeader.svelte +99 -0
- package/dist/fields/RowHeader.svelte.d.ts +11 -0
- package/dist/fields/SelectCell.svelte +35 -0
- package/dist/fields/SelectCell.svelte.d.ts +15 -0
- package/dist/fields/SelectEditor.svelte +350 -0
- package/dist/fields/SelectEditor.svelte.d.ts +17 -0
- package/dist/fields/SummaryCell.svelte +160 -0
- package/dist/fields/SummaryCell.svelte.d.ts +11 -0
- package/dist/fields/SvBase.svelte +545 -0
- package/dist/fields/SvBase.svelte.d.ts +30 -0
- package/dist/fields/SvBaseForm.svelte +295 -0
- package/dist/fields/SvBaseForm.svelte.d.ts +22 -0
- package/dist/fields/SvBaseViewBar.svelte +644 -0
- package/dist/fields/SvBaseViewBar.svelte.d.ts +26 -0
- package/dist/fields/UserCell.svelte +39 -0
- package/dist/fields/UserCell.svelte.d.ts +9 -0
- package/dist/fields/UserFace.svelte +30 -0
- package/dist/fields/UserFace.svelte.d.ts +12 -0
- package/dist/fields/activity.svelte.d.ts +50 -0
- package/dist/fields/activity.svelte.js +110 -0
- package/dist/fields/attachments.d.ts +95 -0
- package/dist/fields/attachments.js +145 -0
- package/dist/fields/automation-outbound.d.ts +46 -0
- package/dist/fields/automation-outbound.js +61 -0
- package/dist/fields/automation-server.d.ts +53 -0
- package/dist/fields/automation-server.js +333 -0
- package/dist/fields/automations.svelte.d.ts +208 -0
- package/dist/fields/automations.svelte.js +437 -0
- package/dist/fields/bar-request.svelte.d.ts +16 -0
- package/dist/fields/bar-request.svelte.js +15 -0
- package/dist/fields/base-alerts.d.ts +24 -0
- package/dist/fields/base-alerts.js +64 -0
- package/dist/fields/base-connect.d.ts +82 -0
- package/dist/fields/base-connect.js +271 -0
- package/dist/fields/base-schema.d.ts +104 -0
- package/dist/fields/base-schema.js +266 -0
- package/dist/fields/base-sql.d.ts +62 -0
- package/dist/fields/base-sql.js +251 -0
- package/dist/fields/base-sync.d.ts +142 -0
- package/dist/fields/base-sync.js +499 -0
- package/dist/fields/base-tables.d.ts +89 -0
- package/dist/fields/base-tables.js +349 -0
- package/dist/fields/builtin-types.d.ts +13 -0
- package/dist/fields/builtin-types.js +773 -0
- package/dist/fields/button.d.ts +2 -0
- package/dist/fields/button.js +17 -0
- package/dist/fields/chart-view.d.ts +35 -0
- package/dist/fields/chart-view.js +154 -0
- package/dist/fields/column-order.d.ts +6 -0
- package/dist/fields/column-order.js +12 -0
- package/dist/fields/convert.d.ts +16 -0
- package/dist/fields/convert.js +176 -0
- package/dist/fields/copy-text.d.ts +13 -0
- package/dist/fields/copy-text.js +45 -0
- package/dist/fields/editor-request.svelte.d.ts +71 -0
- package/dist/fields/editor-request.svelte.js +52 -0
- package/dist/fields/field-ops.d.ts +72 -0
- package/dist/fields/field-ops.js +334 -0
- package/dist/fields/filter-model.d.ts +84 -0
- package/dist/fields/filter-model.js +446 -0
- package/dist/fields/format.d.ts +109 -0
- package/dist/fields/format.js +288 -0
- package/dist/fields/formula.d.ts +50 -0
- package/dist/fields/formula.js +530 -0
- package/dist/fields/grid-access.d.ts +60 -0
- package/dist/fields/grid-access.js +120 -0
- package/dist/fields/index.d.ts +34 -0
- package/dist/fields/index.js +312 -0
- package/dist/fields/linked-fields.d.ts +8 -0
- package/dist/fields/linked-fields.js +40 -0
- package/dist/fields/links.d.ts +10 -0
- package/dist/fields/links.js +46 -0
- package/dist/fields/name-list.d.ts +16 -0
- package/dist/fields/name-list.js +49 -0
- package/dist/fields/palette.d.ts +47 -0
- package/dist/fields/palette.js +69 -0
- package/dist/fields/record-delete.svelte.d.ts +12 -0
- package/dist/fields/record-delete.svelte.js +5 -0
- package/dist/fields/record-menu.d.ts +9 -0
- package/dist/fields/record-menu.js +78 -0
- package/dist/fields/registry.svelte.d.ts +176 -0
- package/dist/fields/registry.svelte.js +51 -0
- package/dist/fields/rich-text.d.ts +23 -0
- package/dist/fields/rich-text.js +48 -0
- package/dist/fields/row-color-choice.svelte.d.ts +31 -0
- package/dist/fields/row-color-choice.svelte.js +25 -0
- package/dist/fields/row-colors.d.ts +36 -0
- package/dist/fields/row-colors.js +90 -0
- package/dist/fields/row-height.d.ts +28 -0
- package/dist/fields/row-height.js +50 -0
- package/dist/fields/sources.svelte.d.ts +80 -0
- package/dist/fields/sources.svelte.js +131 -0
- package/dist/fields/summaries.d.ts +18 -0
- package/dist/fields/summaries.js +138 -0
- package/dist/fields/view-model.d.ts +129 -0
- package/dist/fields/view-model.js +492 -0
- package/dist/fields/viewbar/ChartPanel.svelte +61 -0
- package/dist/fields/viewbar/ChartPanel.svelte.d.ts +9 -0
- package/dist/fields/viewbar/ColorPanel.svelte +112 -0
- package/dist/fields/viewbar/ColorPanel.svelte.d.ts +9 -0
- package/dist/fields/viewbar/ConditionValue.svelte +153 -0
- package/dist/fields/viewbar/ConditionValue.svelte.d.ts +15 -0
- package/dist/fields/viewbar/FieldIcon.svelte +18 -0
- package/dist/fields/viewbar/FieldIcon.svelte.d.ts +9 -0
- package/dist/fields/viewbar/FilterPanel.svelte +192 -0
- package/dist/fields/viewbar/FilterPanel.svelte.d.ts +13 -0
- package/dist/fields/viewbar/GanttPanel.svelte +60 -0
- package/dist/fields/viewbar/GanttPanel.svelte.d.ts +10 -0
- package/dist/fields/viewbar/HideFieldsPanel.svelte +104 -0
- package/dist/fields/viewbar/HideFieldsPanel.svelte.d.ts +7 -0
- package/dist/fields/viewbar/SortPanel.svelte +85 -0
- package/dist/fields/viewbar/SortPanel.svelte.d.ts +8 -0
- package/dist/fields/viewbar/ViewsMenu.svelte +250 -0
- package/dist/fields/viewbar/ViewsMenu.svelte.d.ts +15 -0
- package/dist/fields/viewbar/panel-focus.d.ts +3 -0
- package/dist/fields/viewbar/panel-focus.js +22 -0
- package/dist/fields/viewbar/sort-state.d.ts +20 -0
- package/dist/fields/viewbar/sort-state.js +24 -0
- package/dist/gallery.d.ts +7 -0
- package/dist/gallery.js +36 -0
- package/dist/gantt/SvGridGantt.svelte +88 -16
- package/dist/gantt/gantt-config.d.ts +28 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -0
- package/dist/install.js +3 -0
- package/dist/license-core.d.ts +3 -3
- package/dist/license.js +1 -0
- package/dist/node/studio.js +105 -4
- package/dist/scheduler-config.d.ts +11 -0
- package/dist/sources/introspect-supabase.js +4 -1
- package/dist/sources/rest.js +6 -2
- package/dist/studio/ui-components.generated.js +93 -1
- package/dist/upgrade-prompt.d.ts +1 -1
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/dist/wc/GridFooter-f5K0aLe_.js +145 -0
- package/dist/wc/GridMenus-FtSN_8t9.js +1369 -0
- package/dist/wc/PinSplitter-D-qtDhSM.js +132 -0
- package/dist/wc/{SvGrid.helpers-zBkDTryR.js → SvGrid.helpers-B2GEKrxS.js} +84 -36
- package/dist/wc/{SvGridCellEditor-CzUKEbkI.js → SvGridCellEditor-B_cLJrsp.js} +20 -9
- package/dist/wc/conditional-formatting-BXis_oP1.js +189 -0
- package/dist/wc/grid-extension.svelte-Bufq4B4C.js +20 -0
- package/dist/wc/sv-sheet-element.js +7556 -7617
- package/dist/wc/svelte-virtualizer.svelte-ehwOupl-.js +360 -0
- package/dist/wc/{validate-43C03DLP.js → validate-BaMO8UPE.js} +1 -1
- package/package.json +14 -3
- package/dist/designer/assets/GridMenus-CvcASlKv.js +0 -8
- package/dist/designer/assets/SvGrid.helpers-DE5mmORW.js +0 -1
- package/dist/designer/assets/SvGridCellEditor-C7NWcnNC.js +0 -4
- package/dist/designer/assets/SvListBox-BlmmzYcU.js +0 -110
- package/dist/designer/assets/index-KmSEY7Lb.css +0 -1
- package/dist/designer/assets/src-DqHCqori.js +0 -58
- package/dist/designer/assets/validate-B71dE1Cs.js +0 -1
- package/dist/wc/GridMenus-fWbBUiRH.js +0 -1088
- package/dist/wc/svelte-virtualizer.svelte-Dntdh8Ms.js +0 -581
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SvBaseViewBar - the bar over a fields table: the view switcher (saved
|
|
4
|
+
* views of any type), then Hide fields, Filter, Group, Sort, Color and Row
|
|
5
|
+
* height, the record count and search. Mounted above the grid with the
|
|
6
|
+
* grid's api; every setting goes through the grid's own state, so a view
|
|
7
|
+
* is what `api.getState()` says plus its row height and colors.
|
|
8
|
+
*
|
|
9
|
+
* ```svelte
|
|
10
|
+
* <SvBaseViewBar {api} bind:views bind:activeView />
|
|
11
|
+
* <SvGrid {data} {columns} onApiReady={(a) => (api = a)} />
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
import { panelFocus } from './viewbar/panel-focus'
|
|
15
|
+
import { tick, untrack } from 'svelte'
|
|
16
|
+
import { SvPopover, hasGanttView } from '@svgrid/grid'
|
|
17
|
+
import { conditionCount, isGroup, isComplete, type FieldFilter, type LinkNames } from './filter-model'
|
|
18
|
+
import { fieldColumns, gridOf, groupLeaves, leafFilter, primaryIdOf } from './grid-access'
|
|
19
|
+
import { getLinkedTable, linkedRows } from './sources.svelte'
|
|
20
|
+
import { PLAIN_STATE, applyColor, applyGanttSettings, applyView, boardConfig, captureView, galleryConfig, colorSettingOf, newViewId, viewTypeInfo, type ColorSetting, type FieldView, type FieldViewType } from './view-model'
|
|
21
|
+
import { groupRules, userSorts } from './viewbar/sort-state'
|
|
22
|
+
import { recordHeightOf, syncGroupRowHeight } from './row-height'
|
|
23
|
+
import HideFieldsPanel from './viewbar/HideFieldsPanel.svelte'
|
|
24
|
+
import FilterPanel from './viewbar/FilterPanel.svelte'
|
|
25
|
+
import SortPanel from './viewbar/SortPanel.svelte'
|
|
26
|
+
import ColorPanel from './viewbar/ColorPanel.svelte'
|
|
27
|
+
import ChartPanel from './viewbar/ChartPanel.svelte'
|
|
28
|
+
import GanttPanel from './viewbar/GanttPanel.svelte'
|
|
29
|
+
import type { ChartSetting } from './chart-view'
|
|
30
|
+
import ViewsMenu from './viewbar/ViewsMenu.svelte'
|
|
31
|
+
import RecordView from './RecordView.svelte'
|
|
32
|
+
import { closeRecord, getRecordRequest } from './editor-request.svelte'
|
|
33
|
+
import DeleteRecordConfirm from './DeleteRecordConfirm.svelte'
|
|
34
|
+
import { askDeleteRecord, getDeleteRecordRequest } from './record-delete.svelte'
|
|
35
|
+
import { clearBarRequest, getBarRequest, registerBar, type BarPanel } from './bar-request.svelte'
|
|
36
|
+
|
|
37
|
+
type Props = {
|
|
38
|
+
/** The grid's api (`onApiReady`). */
|
|
39
|
+
api: any
|
|
40
|
+
/** The saved views (bindable). Default: one grid view. */
|
|
41
|
+
views?: FieldView[]
|
|
42
|
+
/** The id of the view shown (bindable). */
|
|
43
|
+
activeView?: string
|
|
44
|
+
/** The views changed: one was created, renamed, deleted, or the shown one's settings changed. */
|
|
45
|
+
onViewsChange?: (views: FieldView[]) => void
|
|
46
|
+
/** The view types "Create" offers. Default: grid, Kanban, calendar, and Gantt when it is enabled. */
|
|
47
|
+
viewTypes?: ReadonlyArray<FieldViewType>
|
|
48
|
+
/** The row heights the row height menu offers. */
|
|
49
|
+
rowHeights?: ReadonlyArray<{ label: string; height: number }>
|
|
50
|
+
/** The Automations button: how many rules, and what its buttons open. */
|
|
51
|
+
automations?: { count: number; onManage: () => void } | null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let {
|
|
55
|
+
api,
|
|
56
|
+
views = $bindable<FieldView[]>([{ id: 'grid', name: 'Grid view', type: 'grid' }]),
|
|
57
|
+
activeView = $bindable<string>(''),
|
|
58
|
+
onViewsChange,
|
|
59
|
+
viewTypes,
|
|
60
|
+
automations = null,
|
|
61
|
+
rowHeights = [
|
|
62
|
+
{ label: 'Short', height: 34 },
|
|
63
|
+
{ label: 'Medium', height: 56 },
|
|
64
|
+
{ label: 'Tall', height: 88 },
|
|
65
|
+
{ label: 'Extra tall', height: 128 },
|
|
66
|
+
],
|
|
67
|
+
}: Props = $props()
|
|
68
|
+
|
|
69
|
+
const ctrl = $derived(api ? gridOf(api) : undefined)
|
|
70
|
+
const active = $derived(views.find((v) => v.id === activeView) ?? views[0])
|
|
71
|
+
const types = $derived(viewTypes ?? (['grid', 'kanban', 'calendar', ...(hasGanttView() ? ['gantt'] : [])] as FieldViewType[]))
|
|
72
|
+
|
|
73
|
+
const linkNames: LinkNames = (table, value) => {
|
|
74
|
+
const t = getLinkedTable(table)
|
|
75
|
+
return t ? linkedRows(t, value).map((r) => String(r[t.primaryField] ?? '')) : []
|
|
76
|
+
}
|
|
77
|
+
const primaryField = $derived.by(() => {
|
|
78
|
+
if (!ctrl) return undefined
|
|
79
|
+
const id = primaryIdOf(ctrl)
|
|
80
|
+
return fieldColumns(ctrl).find((c) => c.id === id)?.field
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// ---- Views ----------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
// The view the grid shows, as it was when applied or last written here. A
|
|
86
|
+
// list or an active id set from outside that differs from it is applied;
|
|
87
|
+
// the autosave never writes into a view that has not been applied yet.
|
|
88
|
+
let appliedId: string | null = null
|
|
89
|
+
let appliedKey = ''
|
|
90
|
+
let applying = false
|
|
91
|
+
let showSeq = 0
|
|
92
|
+
const keyOf = (v: FieldView) => JSON.stringify(v)
|
|
93
|
+
|
|
94
|
+
function setViews(next: FieldView[]) {
|
|
95
|
+
views = next
|
|
96
|
+
const shown = next.find((v) => v.id === appliedId)
|
|
97
|
+
if (shown) appliedKey = keyOf(shown)
|
|
98
|
+
onViewsChange?.(next)
|
|
99
|
+
}
|
|
100
|
+
/** The renderers a view type needs, loaded when a view of that type is first shown. */
|
|
101
|
+
async function ready(t: FieldViewType) {
|
|
102
|
+
if (t === 'kanban') (await import('../board')).enableBoardView()
|
|
103
|
+
else if (t === 'calendar') (await import('../scheduler')).enableSchedulerView()
|
|
104
|
+
else if (t === 'gallery') (await import('../gallery')).enableGalleryView()
|
|
105
|
+
}
|
|
106
|
+
async function show(view: FieldView) {
|
|
107
|
+
const seq = (showSeq += 1)
|
|
108
|
+
appliedId = view.id
|
|
109
|
+
appliedKey = keyOf(view)
|
|
110
|
+
applying = true
|
|
111
|
+
await ready(view.type)
|
|
112
|
+
// A later show won while this one loaded its renderer.
|
|
113
|
+
if (seq !== showSeq) return
|
|
114
|
+
applying = false
|
|
115
|
+
if (!ctrl) return
|
|
116
|
+
applyView(ctrl, api, view, { primaryField, linkNames })
|
|
117
|
+
colorSetting = view.color ?? colorSettingOf(ctrl)
|
|
118
|
+
}
|
|
119
|
+
/** The shown view as the grid now has it (its condition colors are the bar's). */
|
|
120
|
+
// Plain data, not the bar's state proxy: the app stores views and a copy is cloned.
|
|
121
|
+
const capture = (v: FieldView) => captureView(ctrl, api, { ...v, color: $state.snapshot(colorSetting) as ColorSetting })
|
|
122
|
+
function switchTo(id: string) {
|
|
123
|
+
if (!ctrl || !active || id === active.id) return
|
|
124
|
+
const next = views.map((v) => (v.id === active.id ? capture(v) : v))
|
|
125
|
+
const target = next.find((v) => v.id === id)
|
|
126
|
+
if (!target) return
|
|
127
|
+
setViews(next)
|
|
128
|
+
activeView = id
|
|
129
|
+
void show(target)
|
|
130
|
+
viewsOpen = false
|
|
131
|
+
}
|
|
132
|
+
function create(partial: Omit<FieldView, 'id'>) {
|
|
133
|
+
if (!ctrl || !active) return
|
|
134
|
+
// A new view starts plain: no filter, sort or groups, every field shown.
|
|
135
|
+
// A calendar or Gantt is colored by the first single select (a status), so
|
|
136
|
+
// its events and bars say something; any other view starts uncolored.
|
|
137
|
+
const select = columns.find((c) => c.fieldType === 'singleSelect')
|
|
138
|
+
const timed = partial.type === 'calendar' || partial.type === 'gantt'
|
|
139
|
+
const view: FieldView = {
|
|
140
|
+
...partial,
|
|
141
|
+
id: newViewId(),
|
|
142
|
+
state: { ...PLAIN_STATE },
|
|
143
|
+
color: timed && select ? { mode: 'field', field: select.field } : { mode: 'none' },
|
|
144
|
+
}
|
|
145
|
+
const next = [...views.map((v) => (v.id === active.id ? capture(v) : v)), view]
|
|
146
|
+
setViews(next)
|
|
147
|
+
activeView = view.id
|
|
148
|
+
void show(view)
|
|
149
|
+
viewsOpen = false
|
|
150
|
+
}
|
|
151
|
+
function duplicate(id: string) {
|
|
152
|
+
if (!ctrl) return
|
|
153
|
+
const src = id === active?.id ? capture(active) : views.find((v) => v.id === id)
|
|
154
|
+
if (!src) return
|
|
155
|
+
const copy: FieldView = { ...(JSON.parse(JSON.stringify(src)) as FieldView), id: newViewId(), name: `${src.name} copy` }
|
|
156
|
+
const at = views.findIndex((v) => v.id === id)
|
|
157
|
+
const next = [...views]
|
|
158
|
+
next.splice(at + 1, 0, copy)
|
|
159
|
+
setViews(next)
|
|
160
|
+
}
|
|
161
|
+
function remove(id: string) {
|
|
162
|
+
if (views.length < 2) return
|
|
163
|
+
const next = views.filter((v) => v.id !== id)
|
|
164
|
+
setViews(next)
|
|
165
|
+
if (id === active?.id) {
|
|
166
|
+
activeView = next[0]!.id
|
|
167
|
+
void show(next[0]!)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const rename = (id: string, name: string) => setViews(views.map((v) => (v.id === id ? { ...v, name } : v)))
|
|
171
|
+
/** The shown Gantt's fields changed: kept in the view, and drawn at once. */
|
|
172
|
+
function setGantt(next: Pick<FieldView, 'dateField' | 'endField' | 'progressField' | 'dependencyField'>) {
|
|
173
|
+
if (!ctrl || !active) return
|
|
174
|
+
const updated: FieldView = { ...active, ...next }
|
|
175
|
+
setViews(views.map((v) => (v.id === active.id ? updated : v)))
|
|
176
|
+
applyGanttSettings(ctrl, api, updated, primaryField)
|
|
177
|
+
}
|
|
178
|
+
// A chart view declared without a setting: the panel opens with no field picked.
|
|
179
|
+
const NO_CHART: ChartSetting = { kind: 'bar', x: '' }
|
|
180
|
+
/** The shown chart's setting changed: kept in the view at once. */
|
|
181
|
+
const setChart = (chart: ChartSetting) => setViews(views.map((v) => (v.id === active?.id ? { ...v, chart } : v)))
|
|
182
|
+
|
|
183
|
+
// The shown view: apply what it saved (a view stored by the app), first when
|
|
184
|
+
// the grid starts, then whenever the app swaps the list or the active id and
|
|
185
|
+
// the shown view is not the one applied.
|
|
186
|
+
$effect(() => {
|
|
187
|
+
if (!ctrl || !active) return
|
|
188
|
+
const view = active
|
|
189
|
+
const key = keyOf(view)
|
|
190
|
+
untrack(() => {
|
|
191
|
+
if (!activeView) activeView = view.id
|
|
192
|
+
if (view.id === appliedId && key === appliedKey) return
|
|
193
|
+
// A plain grid view first: the grid keeps the state its own props gave it.
|
|
194
|
+
if (appliedId === null && !(view.state || view.type !== 'grid' || view.color || view.rowHeight != null)) {
|
|
195
|
+
appliedId = view.id
|
|
196
|
+
appliedKey = key
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
void show(view)
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
// Keep the shown view up to date as it changes (as a table app saves a
|
|
204
|
+
// view's settings), a moment after the last change.
|
|
205
|
+
$effect(() => {
|
|
206
|
+
if (!ctrl || !active) return
|
|
207
|
+
// Sorting and grouping live in the grid's store; its version says they changed.
|
|
208
|
+
const snapshot = JSON.stringify([ctrl.gridStateVersion, api.getState(), recordHeightOf(api.getOption('rowHeight')), effectiveColor])
|
|
209
|
+
const view = active
|
|
210
|
+
const timer = setTimeout(() => {
|
|
211
|
+
untrack(() => {
|
|
212
|
+
if (applying || view.id !== appliedId || keyOf(view) !== appliedKey) return
|
|
213
|
+
const kept = capture(view)
|
|
214
|
+
const before = JSON.stringify([view.state, view.rowHeight, view.color])
|
|
215
|
+
const after = JSON.stringify([kept.state, kept.rowHeight, kept.color])
|
|
216
|
+
if (before !== after) setViews(views.map((v) => (v.id === view.id ? kept : v)))
|
|
217
|
+
})
|
|
218
|
+
}, 400)
|
|
219
|
+
void snapshot
|
|
220
|
+
return () => clearTimeout(timer)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
// ---- The buttons' state -----------------------------------------------------
|
|
224
|
+
|
|
225
|
+
let viewsOpen = $state(false)
|
|
226
|
+
// The panel open, one at a time; the column menu can ask for one ("Filter by this field").
|
|
227
|
+
let panel = $state<BarPanel | null>(null)
|
|
228
|
+
function setPanel(p: BarPanel, open: boolean) {
|
|
229
|
+
if (open) panel = p
|
|
230
|
+
else if (panel === p) panel = null
|
|
231
|
+
}
|
|
232
|
+
$effect(() => {
|
|
233
|
+
if (ctrl) return registerBar(ctrl)
|
|
234
|
+
})
|
|
235
|
+
$effect(() => {
|
|
236
|
+
const r = getBarRequest()
|
|
237
|
+
if (!r || r.ctrl !== ctrl) return
|
|
238
|
+
untrack(() => {
|
|
239
|
+
panel = r.panel
|
|
240
|
+
clearBarRequest()
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
let colorSetting = $state<ColorSetting>({ mode: 'none' })
|
|
244
|
+
$effect(() => {
|
|
245
|
+
if (ctrl) untrack(() => (colorSetting = active?.color ?? colorSettingOf(ctrl)))
|
|
246
|
+
})
|
|
247
|
+
function setColor(s: ColorSetting) {
|
|
248
|
+
colorSetting = s
|
|
249
|
+
if (ctrl) applyColor(ctrl, s, linkNames)
|
|
250
|
+
// A calendar's events and a Gantt's bars take the view's colors.
|
|
251
|
+
if (ctrl && active && (active.type === 'calendar' || active.type === 'gantt')) {
|
|
252
|
+
const updated: FieldView = { ...active, color: $state.snapshot(s) as ColorSetting }
|
|
253
|
+
setViews(views.map((v) => (v.id === active.id ? updated : v)))
|
|
254
|
+
applyGanttSettings(ctrl, api, updated, primaryField)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const columns = $derived(ctrl ? fieldColumns(ctrl) : [])
|
|
259
|
+
const hiddenCount = $derived(columns.filter((c) => !c.visible).length)
|
|
260
|
+
const filter = $derived.by<FieldFilter | null>(() => {
|
|
261
|
+
const f = ctrl?.advancedFilter as FieldFilter | null | undefined
|
|
262
|
+
return f?.kind === 'fieldFilter' ? f : null
|
|
263
|
+
})
|
|
264
|
+
const filterCount = $derived(conditionCount(filter))
|
|
265
|
+
const filterLabel = $derived.by(() => {
|
|
266
|
+
if (!filter || !filterCount) return 'Filter'
|
|
267
|
+
const ids = new Set<string>()
|
|
268
|
+
for (const c of filter.conditions) for (const x of isGroup(c) ? c.conditions : [c]) if (isComplete(x)) ids.add(x.column)
|
|
269
|
+
if (ids.size === 1) return `Filtered by ${columns.find((c) => ids.has(c.id))?.header ?? '1 field'}`
|
|
270
|
+
return `Filtered by ${ids.size} fields`
|
|
271
|
+
})
|
|
272
|
+
const groupCount = $derived(ctrl ? groupRules(ctrl).length : 0)
|
|
273
|
+
const sortCount = $derived(ctrl ? userSorts(ctrl).length : 0)
|
|
274
|
+
// What the rows show now: a column menu can color them by a field too.
|
|
275
|
+
const effectiveColor = $derived(ctrl ? colorSettingOf(ctrl, colorSetting) : colorSetting)
|
|
276
|
+
const colorLabel = $derived(
|
|
277
|
+
effectiveColor.mode === 'field'
|
|
278
|
+
? `Colored by ${columns.find((c) => c.field === (effectiveColor as { field: string }).field)?.header ?? 'field'}`
|
|
279
|
+
: effectiveColor.mode === 'conditions' && effectiveColor.rules.length
|
|
280
|
+
? 'Colored by conditions'
|
|
281
|
+
: 'Color',
|
|
282
|
+
)
|
|
283
|
+
const rowHeight = $derived(recordHeightOf(api?.getOption?.('rowHeight')) ?? 0)
|
|
284
|
+
// Grouped, the group rows are drawn taller than the records: room for the field's name over its value.
|
|
285
|
+
$effect(() => {
|
|
286
|
+
const grouped = groupCount > 0
|
|
287
|
+
void rowHeight
|
|
288
|
+
untrack(() => syncGroupRowHeight(ctrl, api, grouped))
|
|
289
|
+
})
|
|
290
|
+
// Grouped, a collapsed group hides its rows but they are still records: count the top groups' sizes.
|
|
291
|
+
const records = $derived.by(() => {
|
|
292
|
+
if (!ctrl) return 0
|
|
293
|
+
const tops = ((ctrl.allRows ?? []) as Array<{ depth?: number; leafCount?: number; getCanExpand?: () => boolean; __treeRow?: boolean }>).filter(
|
|
294
|
+
(r) => (r.depth ?? 0) === 0 && !r.__treeRow && typeof r.getCanExpand === 'function' && r.getCanExpand() && r.leafCount != null,
|
|
295
|
+
)
|
|
296
|
+
if (!groupCount || !tops.length) return api.getDisplayedRows().length
|
|
297
|
+
// The grid groups before it filters: a group's size still holds the filtered-out records.
|
|
298
|
+
const keep = leafFilter(ctrl)
|
|
299
|
+
return keep ? tops.reduce((n, r) => n + groupLeaves(r, keep).length, 0) : tops.reduce((n, r) => n + (r.leafCount ?? 0), 0)
|
|
300
|
+
})
|
|
301
|
+
const plural = (n: number, one: string, many: string) => `${n} ${n === 1 ? one : many}`
|
|
302
|
+
|
|
303
|
+
// Search: in a grid view the grid's find bar, which marks every match; a
|
|
304
|
+
// board, calendar or gallery has no cells to mark, so there the bar shows a
|
|
305
|
+
// box that narrows the records (the grid's quick filter).
|
|
306
|
+
let searching = $state(false)
|
|
307
|
+
const quickSearch = $derived(active?.type !== 'grid')
|
|
308
|
+
/** The find bar or the search box, with its box focused (the find bar only autofocuses on page load). */
|
|
309
|
+
async function openSearch() {
|
|
310
|
+
if (quickSearch) {
|
|
311
|
+
searching = true
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
ctrl.findOpen = true
|
|
315
|
+
await tick()
|
|
316
|
+
const root = (ctrl.scrollContainer as HTMLElement | null)?.closest('.sv-grid-root')
|
|
317
|
+
root?.querySelector<HTMLInputElement>('.sv-grid-find-input')?.focus()
|
|
318
|
+
}
|
|
319
|
+
function focusBox(node: HTMLInputElement) {
|
|
320
|
+
node.focus()
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// A gallery's cards show the fields the grid shows, in its order: build its
|
|
324
|
+
// config again when a field is hidden or moved (only when that changed, as
|
|
325
|
+
// setting it changes the grid's options, which this reads).
|
|
326
|
+
let galleryKey = ''
|
|
327
|
+
$effect(() => {
|
|
328
|
+
if (!ctrl || active?.type !== 'gallery') {
|
|
329
|
+
galleryKey = ''
|
|
330
|
+
return
|
|
331
|
+
}
|
|
332
|
+
const view = active
|
|
333
|
+
const cfg = galleryConfig(ctrl, api, view)
|
|
334
|
+
const key = JSON.stringify([cfg.coverField, cfg.titleField, cfg.cardFields])
|
|
335
|
+
if (key === galleryKey) return
|
|
336
|
+
galleryKey = key
|
|
337
|
+
untrack(() => api.setOption('gallery', cfg))
|
|
338
|
+
})
|
|
339
|
+
// The same for a board's cards.
|
|
340
|
+
let boardKey = ''
|
|
341
|
+
$effect(() => {
|
|
342
|
+
if (!ctrl || active?.type !== 'kanban') {
|
|
343
|
+
boardKey = ''
|
|
344
|
+
return
|
|
345
|
+
}
|
|
346
|
+
const cfg = boardConfig(ctrl, api, active)
|
|
347
|
+
if (!cfg) return
|
|
348
|
+
const key = JSON.stringify([cfg.groupBy, cfg.titleField, cfg.cardFields])
|
|
349
|
+
if (key === boardKey) return
|
|
350
|
+
boardKey = key
|
|
351
|
+
untrack(() => api.setOption('board', cfg))
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
// A record opened while the grid is a board, a calendar or a gallery: the table's header,
|
|
355
|
+
// which shows records otherwise, is not drawn then, so the bar shows it.
|
|
356
|
+
const record = $derived.by(() => {
|
|
357
|
+
const r = getRecordRequest()
|
|
358
|
+
const t = active?.type
|
|
359
|
+
return r && ctrl && r.ctrl === ctrl && (t === 'kanban' || t === 'calendar' || t === 'gantt' || t === 'gallery') ? r : null
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
// "Delete record?" from a card's menu, for the same reason.
|
|
363
|
+
const deleting = $derived.by(() => {
|
|
364
|
+
const r = getDeleteRecordRequest()
|
|
365
|
+
const t = active?.type
|
|
366
|
+
return r && ctrl && r.ctrl === ctrl && (t === 'kanban' || t === 'calendar' || t === 'gantt' || t === 'gallery') ? r : null
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
// Which buttons a view type has.
|
|
370
|
+
const has = $derived.by(() => {
|
|
371
|
+
const t = active?.type ?? 'grid'
|
|
372
|
+
if (t === 'form') return new Set<string>()
|
|
373
|
+
if (t === 'grid') return new Set(['hide', 'filter', 'group', 'sort', 'color', 'height'])
|
|
374
|
+
// A chart reads the filtered records; how it groups them is its own setting.
|
|
375
|
+
if (t === 'chart') return new Set(['filter', 'chart'])
|
|
376
|
+
if (t === 'gantt') return new Set(['hide', 'filter', 'sort', 'color', 'gantt'])
|
|
377
|
+
if (t === 'calendar') return new Set(['hide', 'filter', 'sort', 'color'])
|
|
378
|
+
return new Set(['hide', 'filter', 'sort'])
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
const ICON = {
|
|
382
|
+
hide: 'M3 3l18 18M10.6 10.6a2 2 0 0 0 2.8 2.8M9.9 5.1A9.8 9.8 0 0 1 12 5c5 0 9 5 10 7a13.4 13.4 0 0 1-3 3.9M6.6 6.6A13.5 13.5 0 0 0 2 12c1 2 5 7 10 7a9.6 9.6 0 0 0 4.4-1.1',
|
|
383
|
+
filter: 'M4 5h16l-6 7v6l-4 2v-8z',
|
|
384
|
+
group: 'M4 5h16M4 10h10M4 15h16M4 20h10',
|
|
385
|
+
sort: 'M7 4v16M3 16l4 4 4-4M17 20V4M13 8l4-4 4 4',
|
|
386
|
+
color: 'M12 3l6 7a6.5 6.5 0 1 1-12 0l6-7z',
|
|
387
|
+
chart: 'M4 20h16M6 20V11M11 20V5M16 20v-7',
|
|
388
|
+
gantt: 'M4 5h9M7 10h11M10 15h8M4 20h16',
|
|
389
|
+
height: 'M4 6h16M4 12h16M4 18h16M20 3v18',
|
|
390
|
+
search: 'M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14zM20 20l-4-4',
|
|
391
|
+
chevron: 'M6 9l6 6 6-6',
|
|
392
|
+
bolt: 'M13 3L5 14h6l-1 7 8-11h-6z',
|
|
393
|
+
}
|
|
394
|
+
</script>
|
|
395
|
+
|
|
396
|
+
{#snippet glyph(path: string, size = 15)}
|
|
397
|
+
<svg viewBox="0 0 24 24" width={size} height={size} aria-hidden="true"><path d={path} fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
|
398
|
+
{/snippet}
|
|
399
|
+
|
|
400
|
+
{#if ctrl && active}
|
|
401
|
+
<div class="sv-fvb" role="toolbar" aria-label="View settings">
|
|
402
|
+
<SvPopover bind:open={viewsOpen} arrow={false} placement="bottom-start" ariaLabel="Views">
|
|
403
|
+
{#snippet anchor()}
|
|
404
|
+
<button type="button" class="sv-fvb-btn sv-fvb-view" aria-haspopup="dialog" aria-expanded={viewsOpen}>
|
|
405
|
+
{@render glyph(viewTypeInfo(active.type).icon)}
|
|
406
|
+
<span class="sv-fvb-view-name">{active.name}</span>
|
|
407
|
+
{@render glyph(ICON.chevron, 13)}
|
|
408
|
+
</button>
|
|
409
|
+
{/snippet}
|
|
410
|
+
<ViewsMenu
|
|
411
|
+
{ctrl}
|
|
412
|
+
{views}
|
|
413
|
+
activeId={active.id}
|
|
414
|
+
{types}
|
|
415
|
+
onSwitch={switchTo}
|
|
416
|
+
onCreate={create}
|
|
417
|
+
onRename={rename}
|
|
418
|
+
onDuplicate={duplicate}
|
|
419
|
+
onDelete={remove}
|
|
420
|
+
/>
|
|
421
|
+
</SvPopover>
|
|
422
|
+
<span class="sv-fvb-sep" aria-hidden="true"></span>
|
|
423
|
+
|
|
424
|
+
{#if has.has('hide')}
|
|
425
|
+
<SvPopover open={panel === 'hide'} onOpenChange={(o) => setPanel('hide', o)} arrow={false} placement="bottom-start" ariaLabel="Hide fields">
|
|
426
|
+
{#snippet anchor()}
|
|
427
|
+
<button type="button" class="sv-fvb-btn" class:is-set={hiddenCount > 0}>
|
|
428
|
+
{@render glyph(ICON.hide)}<span>{hiddenCount ? plural(hiddenCount, 'hidden field', 'hidden fields') : 'Hide fields'}</span>
|
|
429
|
+
</button>
|
|
430
|
+
{/snippet}
|
|
431
|
+
<HideFieldsPanel {ctrl} {api} />
|
|
432
|
+
</SvPopover>
|
|
433
|
+
{/if}
|
|
434
|
+
{#if has.has('filter')}
|
|
435
|
+
<SvPopover open={panel === 'filter'} onOpenChange={(o) => setPanel('filter', o)} arrow={false} placement="bottom-start" ariaLabel="Filter">
|
|
436
|
+
{#snippet anchor()}
|
|
437
|
+
<button type="button" class="sv-fvb-btn" class:is-set={filterCount > 0}>{@render glyph(ICON.filter)}<span>{filterLabel}</span></button>
|
|
438
|
+
{/snippet}
|
|
439
|
+
<FilterPanel {ctrl} {api} />
|
|
440
|
+
</SvPopover>
|
|
441
|
+
{/if}
|
|
442
|
+
{#if has.has('group')}
|
|
443
|
+
<SvPopover open={panel === 'group'} onOpenChange={(o) => setPanel('group', o)} arrow={false} placement="bottom-start" ariaLabel="Group">
|
|
444
|
+
{#snippet anchor()}
|
|
445
|
+
<button type="button" class="sv-fvb-btn" class:is-set={groupCount > 0}>
|
|
446
|
+
{@render glyph(ICON.group)}<span>{groupCount ? `Grouped by ${plural(groupCount, 'field', 'fields')}` : 'Group'}</span>
|
|
447
|
+
</button>
|
|
448
|
+
{/snippet}
|
|
449
|
+
<SortPanel {ctrl} {api} group />
|
|
450
|
+
</SvPopover>
|
|
451
|
+
{/if}
|
|
452
|
+
{#if has.has('sort')}
|
|
453
|
+
<SvPopover open={panel === 'sort'} onOpenChange={(o) => setPanel('sort', o)} arrow={false} placement="bottom-start" ariaLabel="Sort">
|
|
454
|
+
{#snippet anchor()}
|
|
455
|
+
<button type="button" class="sv-fvb-btn" class:is-set={sortCount > 0}>
|
|
456
|
+
{@render glyph(ICON.sort)}<span>{sortCount ? `Sorted by ${plural(sortCount, 'field', 'fields')}` : 'Sort'}</span>
|
|
457
|
+
</button>
|
|
458
|
+
{/snippet}
|
|
459
|
+
<SortPanel {ctrl} {api} />
|
|
460
|
+
</SvPopover>
|
|
461
|
+
{/if}
|
|
462
|
+
{#if has.has('color')}
|
|
463
|
+
<SvPopover open={panel === 'color'} onOpenChange={(o) => setPanel('color', o)} arrow={false} placement="bottom-start" ariaLabel="Color">
|
|
464
|
+
{#snippet anchor()}
|
|
465
|
+
<button type="button" class="sv-fvb-btn" class:is-set={effectiveColor.mode !== 'none'}>{@render glyph(ICON.color)}<span>{colorLabel}</span></button>
|
|
466
|
+
{/snippet}
|
|
467
|
+
<ColorPanel {ctrl} setting={effectiveColor} onChange={setColor} />
|
|
468
|
+
</SvPopover>
|
|
469
|
+
{/if}
|
|
470
|
+
{#if has.has('chart')}
|
|
471
|
+
{@const chart = active.chart ?? NO_CHART}
|
|
472
|
+
<SvPopover open={panel === 'chart'} onOpenChange={(o) => setPanel('chart', o)} arrow={false} placement="bottom-start" ariaLabel="Customize chart">
|
|
473
|
+
{#snippet anchor()}
|
|
474
|
+
<button type="button" class="sv-fvb-btn">{@render glyph(ICON.chart)}<span>Customize chart</span></button>
|
|
475
|
+
{/snippet}
|
|
476
|
+
<ChartPanel {ctrl} setting={chart} onChange={setChart} />
|
|
477
|
+
</SvPopover>
|
|
478
|
+
{/if}
|
|
479
|
+
{#if has.has('gantt') && active}
|
|
480
|
+
{@const settings = active}
|
|
481
|
+
<SvPopover open={panel === 'gantt'} onOpenChange={(o) => setPanel('gantt', o)} arrow={false} placement="bottom-start" ariaLabel="Customize timeline">
|
|
482
|
+
{#snippet anchor()}
|
|
483
|
+
<button type="button" class="sv-fvb-btn">{@render glyph(ICON.gantt)}<span>Customize timeline</span></button>
|
|
484
|
+
{/snippet}
|
|
485
|
+
<GanttPanel {ctrl} view={settings} onChange={setGantt} />
|
|
486
|
+
</SvPopover>
|
|
487
|
+
{/if}
|
|
488
|
+
{#if has.has('height')}
|
|
489
|
+
<SvPopover arrow={false} placement="bottom-start" ariaLabel="Row height">
|
|
490
|
+
{#snippet anchor()}
|
|
491
|
+
<button type="button" class="sv-fvb-btn sv-fvb-icon-only" aria-label="Row height" title="Row height">{@render glyph(ICON.height)}</button>
|
|
492
|
+
{/snippet}
|
|
493
|
+
<div class="sv-fvb-panel sv-fvb-heights" role="menu" aria-label="Row height" use:panelFocus>
|
|
494
|
+
{#each rowHeights as h (h.height)}
|
|
495
|
+
<button
|
|
496
|
+
type="button"
|
|
497
|
+
role="menuitemradio"
|
|
498
|
+
aria-checked={rowHeight === h.height}
|
|
499
|
+
class="sv-fvb-height"
|
|
500
|
+
class:is-on={rowHeight === h.height}
|
|
501
|
+
onclick={() => api.setOption('rowHeight', h.height)}
|
|
502
|
+
>
|
|
503
|
+
<span class="sv-fvb-height-mark" style:--lines={Math.min(4, Math.max(1, Math.round(h.height / 30)))} aria-hidden="true"></span>
|
|
504
|
+
{h.label}
|
|
505
|
+
</button>
|
|
506
|
+
{/each}
|
|
507
|
+
</div>
|
|
508
|
+
</SvPopover>
|
|
509
|
+
{/if}
|
|
510
|
+
|
|
511
|
+
<span class="sv-fvb-spacer"></span>
|
|
512
|
+
{#if automations}
|
|
513
|
+
<button type="button" class="sv-fvb-btn" class:is-set={automations.count > 0} onclick={automations.onManage}>
|
|
514
|
+
{@render glyph(ICON.bolt)}<span>{automations.count ? plural(automations.count, 'automation', 'automations') : 'Automations'}</span>
|
|
515
|
+
</button>
|
|
516
|
+
{/if}
|
|
517
|
+
{#if active.type !== 'form'}
|
|
518
|
+
<span class="sv-fvb-count" aria-live="polite">{plural(records, 'record', 'records')}</span>
|
|
519
|
+
{#if quickSearch && (searching || ctrl.globalFilter)}
|
|
520
|
+
<label class="sv-fvb-search">
|
|
521
|
+
{@render glyph(ICON.search, 14)}
|
|
522
|
+
<input
|
|
523
|
+
type="search"
|
|
524
|
+
placeholder="Find a record"
|
|
525
|
+
aria-label="Find a record"
|
|
526
|
+
bind:value={ctrl.globalFilter}
|
|
527
|
+
use:focusBox
|
|
528
|
+
onblur={() => (searching = false)}
|
|
529
|
+
onkeydown={(e) => {
|
|
530
|
+
if (e.key === 'Escape') {
|
|
531
|
+
ctrl.globalFilter = ''
|
|
532
|
+
searching = false
|
|
533
|
+
}
|
|
534
|
+
}}
|
|
535
|
+
/>
|
|
536
|
+
</label>
|
|
537
|
+
{:else}
|
|
538
|
+
<button type="button" class="sv-fvb-btn sv-fvb-icon-only" aria-label="Search" title={quickSearch ? 'Search' : 'Search (Ctrl+F)'} onclick={openSearch}>
|
|
539
|
+
{@render glyph(ICON.search)}
|
|
540
|
+
</button>
|
|
541
|
+
{/if}
|
|
542
|
+
{/if}
|
|
543
|
+
</div>
|
|
544
|
+
{/if}
|
|
545
|
+
{#if record}
|
|
546
|
+
{#key record}
|
|
547
|
+
<RecordView {ctrl} rowId={record.rowId} editPrimary={record.editPrimary} onClose={closeRecord} />
|
|
548
|
+
{/key}
|
|
549
|
+
{/if}
|
|
550
|
+
{#if deleting}
|
|
551
|
+
{#key deleting}
|
|
552
|
+
<DeleteRecordConfirm {ctrl} api={deleting.api} row={deleting.row} onClose={() => askDeleteRecord(null)} />
|
|
553
|
+
{/key}
|
|
554
|
+
{/if}
|
|
555
|
+
|
|
556
|
+
<style>
|
|
557
|
+
.sv-fvb {
|
|
558
|
+
container-type: inline-size;
|
|
559
|
+
display: flex; align-items: center; gap: 2px; flex-wrap: wrap; min-height: 40px; padding: 4px 6px;
|
|
560
|
+
font-family: var(--sg-font, inherit); font-size: 13px; color: var(--sg-fg, #0f172a);
|
|
561
|
+
border-bottom: 1px solid var(--sg-border, #e2e8f0); background: var(--sg-bg, #fff);
|
|
562
|
+
}
|
|
563
|
+
.sv-fvb-btn {
|
|
564
|
+
position: relative; display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 8px; border: 0; border-radius: 6px;
|
|
565
|
+
background: none; color: inherit; font: inherit; cursor: pointer; white-space: nowrap;
|
|
566
|
+
}
|
|
567
|
+
.sv-fvb-btn:hover { background: var(--sg-row-hover-bg, #f1f5f9); }
|
|
568
|
+
.sv-fvb-btn:focus-visible { outline: 2px solid var(--sg-accent, #2563eb); outline-offset: -2px; }
|
|
569
|
+
.sv-fvb-btn.is-set { background: color-mix(in srgb, var(--sg-accent, #2563eb) 13%, transparent); }
|
|
570
|
+
.sv-fvb-view { font-weight: 600; max-width: 240px; }
|
|
571
|
+
.sv-fvb-view-name { overflow: hidden; text-overflow: ellipsis; }
|
|
572
|
+
.sv-fvb-icon-only { padding: 0 6px; }
|
|
573
|
+
.sv-fvb-sep { width: 1px; height: 18px; margin: 0 6px; background: var(--sg-border, #e2e8f0); }
|
|
574
|
+
.sv-fvb-spacer { flex: 1; }
|
|
575
|
+
/* A narrow bar keeps the icons and the counts; the labels stay for screen readers. */
|
|
576
|
+
@container (max-width: 640px) {
|
|
577
|
+
.sv-fvb-btn:not(.sv-fvb-view) > span {
|
|
578
|
+
position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
|
|
579
|
+
}
|
|
580
|
+
.sv-fvb-btn { padding: 0 6px; }
|
|
581
|
+
.sv-fvb-view { max-width: 150px; }
|
|
582
|
+
}
|
|
583
|
+
/* A phone: the count gives way, so the bar stays one line. */
|
|
584
|
+
@container (max-width: 420px) {
|
|
585
|
+
.sv-fvb-count { display: none; }
|
|
586
|
+
}
|
|
587
|
+
.sv-fvb-search {
|
|
588
|
+
display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 8px; border-radius: 6px;
|
|
589
|
+
border: 1px solid var(--sg-border, #cbd5e1); color: var(--sg-muted, #64748b);
|
|
590
|
+
}
|
|
591
|
+
.sv-fvb-search input { width: 160px; border: 0; outline: none; background: none; color: var(--sg-fg, #0f172a); font: inherit; }
|
|
592
|
+
.sv-fvb-search:focus-within { border-color: var(--sg-accent, #2563eb); }
|
|
593
|
+
.sv-fvb-count { color: var(--sg-muted, #64748b); font-size: 12px; margin-inline-end: 4px; font-variant-numeric: tabular-nums; }
|
|
594
|
+
|
|
595
|
+
/* Shared by the panels (portalled to <body>). */
|
|
596
|
+
:global(.sv-pop__panel:has(.sv-fvb-panel-wide)) { max-width: min(94vw, 760px); }
|
|
597
|
+
:global(.sv-fvb-panel) { font-size: 13px; max-width: 100%; }
|
|
598
|
+
:global(.sv-fvb-title) { margin: 0 0 6px; color: var(--sg-muted, #64748b); font-size: 12px; }
|
|
599
|
+
:global(.sv-fvb-empty) { margin: 2px 0 8px; color: var(--sg-muted, #64748b); }
|
|
600
|
+
:global(.sv-fvb-foot) { display: flex; gap: 12px; margin-top: 8px; align-items: center; }
|
|
601
|
+
:global(.sv-fvb-select), :global(.sv-fvb-input) {
|
|
602
|
+
width: 100%; min-width: 0; height: 28px; box-sizing: border-box; padding: 0 6px; font: inherit; font-size: 13px;
|
|
603
|
+
color: inherit; background: var(--sg-bg, #fff); border: 1px solid var(--sg-border, #cbd5e1); border-radius: 6px;
|
|
604
|
+
}
|
|
605
|
+
:global(.sv-fvb-select:focus-visible), :global(.sv-fvb-input:focus-visible) { outline: 2px solid var(--sg-accent, #2563eb); outline-offset: -1px; }
|
|
606
|
+
:global(.sv-fvb-text-btn) {
|
|
607
|
+
border: 0; padding: 2px 0; background: none; font: inherit; color: var(--sg-accent, #2563eb); cursor: pointer;
|
|
608
|
+
}
|
|
609
|
+
:global(.sv-fvb-text-btn:hover) { text-decoration: underline; }
|
|
610
|
+
:global(.sv-fvb-solid-btn) {
|
|
611
|
+
border: 0; border-radius: 6px; padding: 5px 12px; font: inherit; cursor: pointer;
|
|
612
|
+
background: var(--sg-accent, #2563eb); color: var(--sg-accent-fg, #fff);
|
|
613
|
+
}
|
|
614
|
+
:global(.sv-fvb-icon-btn) {
|
|
615
|
+
display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; padding: 0;
|
|
616
|
+
border: 0; border-radius: 6px; background: none; color: var(--sg-muted, #64748b); cursor: pointer;
|
|
617
|
+
}
|
|
618
|
+
:global(.sv-fvb-icon-btn:hover) { background: var(--sg-row-hover-bg, #f1f5f9); color: var(--sg-fg, #0f172a); }
|
|
619
|
+
/* A switch, not a checkbox: on is the accent with the knob at the end. */
|
|
620
|
+
:global(.sv-fvb-switch) {
|
|
621
|
+
appearance: none; flex: none; position: relative; width: 26px; height: 15px; margin: 0; border-radius: 999px; cursor: pointer;
|
|
622
|
+
background: color-mix(in srgb, var(--sg-fg, #0f172a) 22%, transparent); transition: background-color 0.12s;
|
|
623
|
+
}
|
|
624
|
+
:global(.sv-fvb-switch::before) {
|
|
625
|
+
content: ''; position: absolute; top: 2px; left: 2px; width: 11px; height: 11px; border-radius: 50%;
|
|
626
|
+
background: var(--sg-bg, #fff); transition: transform 0.12s;
|
|
627
|
+
}
|
|
628
|
+
:global(.sv-fvb-switch:checked) { background: var(--sg-accent, #2563eb); }
|
|
629
|
+
:global(.sv-fvb-switch:checked::before) { transform: translateX(11px); }
|
|
630
|
+
:global(.sv-fvb-switch:disabled) { opacity: 0.5; cursor: default; }
|
|
631
|
+
:global(.sv-fvb-switch:focus-visible) { outline: 2px solid var(--sg-accent, #2563eb); outline-offset: 2px; }
|
|
632
|
+
.sv-fvb-heights { display: grid; min-width: 150px; }
|
|
633
|
+
.sv-fvb-height {
|
|
634
|
+
display: flex; align-items: center; gap: 10px; padding: 6px 8px; border: 0; border-radius: 6px;
|
|
635
|
+
background: none; color: inherit; font: inherit; text-align: start; cursor: pointer;
|
|
636
|
+
}
|
|
637
|
+
.sv-fvb-height:hover, .sv-fvb-height.is-on { background: var(--sg-row-hover-bg, #f1f5f9); }
|
|
638
|
+
.sv-fvb-height.is-on { font-weight: 600; }
|
|
639
|
+
/* As many lines as the height holds text. */
|
|
640
|
+
.sv-fvb-height-mark {
|
|
641
|
+
width: 14px; height: 14px; border: 1.5px solid currentColor; border-radius: 3px; box-sizing: border-box;
|
|
642
|
+
background: repeating-linear-gradient(to bottom, transparent 0 2px, currentColor 2px 3px) center / 8px calc(var(--lines) * 3px) no-repeat;
|
|
643
|
+
}
|
|
644
|
+
</style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type FieldView, type FieldViewType } from './view-model.js';
|
|
2
|
+
type Props = {
|
|
3
|
+
/** The grid's api (`onApiReady`). */
|
|
4
|
+
api: any;
|
|
5
|
+
/** The saved views (bindable). Default: one grid view. */
|
|
6
|
+
views?: FieldView[];
|
|
7
|
+
/** The id of the view shown (bindable). */
|
|
8
|
+
activeView?: string;
|
|
9
|
+
/** The views changed: one was created, renamed, deleted, or the shown one's settings changed. */
|
|
10
|
+
onViewsChange?: (views: FieldView[]) => void;
|
|
11
|
+
/** The view types "Create" offers. Default: grid, Kanban, calendar, and Gantt when it is enabled. */
|
|
12
|
+
viewTypes?: ReadonlyArray<FieldViewType>;
|
|
13
|
+
/** The row heights the row height menu offers. */
|
|
14
|
+
rowHeights?: ReadonlyArray<{
|
|
15
|
+
label: string;
|
|
16
|
+
height: number;
|
|
17
|
+
}>;
|
|
18
|
+
/** The Automations button: how many rules, and what its buttons open. */
|
|
19
|
+
automations?: {
|
|
20
|
+
count: number;
|
|
21
|
+
onManage: () => void;
|
|
22
|
+
} | null;
|
|
23
|
+
};
|
|
24
|
+
declare const SvBaseViewBar: import("svelte").Component<Props, {}, "views" | "activeView">;
|
|
25
|
+
type SvBaseViewBar = ReturnType<typeof SvBaseViewBar>;
|
|
26
|
+
export default SvBaseViewBar;
|