@brftech/filex-core 0.42.1 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -2
- package/dist/ArchiveViewer-BqU761Wk.js +149 -0
- package/dist/ArchiveViewer-BqU761Wk.js.map +1 -0
- package/dist/CsvViewer-ByHVK5ga.js +172 -0
- package/dist/CsvViewer-ByHVK5ga.js.map +1 -0
- package/dist/{DrawioViewer-BuhZOAx6.js → DrawioViewer-Bgrj8-zG.js} +45 -44
- package/dist/DrawioViewer-Bgrj8-zG.js.map +1 -0
- package/dist/{EpubViewer-DS3mb3k8.js → EpubViewer-DJwUKlHW.js} +50 -50
- package/dist/EpubViewer-DJwUKlHW.js.map +1 -0
- package/dist/IpynbViewer-Bz0eJ4NS.js +184 -0
- package/dist/IpynbViewer-Bz0eJ4NS.js.map +1 -0
- package/dist/MermaidViewer-CEDTD_pp.js +136 -0
- package/dist/MermaidViewer-CEDTD_pp.js.map +1 -0
- package/dist/PsdViewer-CZcxUpZO.js +121 -0
- package/dist/PsdViewer-CZcxUpZO.js.map +1 -0
- package/dist/TiffViewer-BwI62CbQ.js +141 -0
- package/dist/TiffViewer-BwI62CbQ.js.map +1 -0
- package/dist/{Viewer3D-CIrOn63I.js → Viewer3D-DoeZcuMm.js} +16 -16
- package/dist/Viewer3D-DoeZcuMm.js.map +1 -0
- package/dist/filex-core.js +475 -219
- package/dist/filex-core.umd.cjs +165 -108
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/fonts/caveat-latin-ext.woff2 +0 -0
- package/dist/fonts/caveat-latin.woff2 +0 -0
- package/dist/fonts/dancing-script-latin-ext.woff2 +0 -0
- package/dist/fonts/dancing-script-latin.woff2 +0 -0
- package/dist/fonts/homemade-apple-latin.woff2 +0 -0
- package/dist/fonts/source-serif-4-latin-ext.woff2 +0 -0
- package/dist/fonts/source-serif-4-latin.woff2 +0 -0
- package/dist/index-DuWnGuv5.js +35901 -0
- package/dist/index-DuWnGuv5.js.map +1 -0
- package/dist/index.d.ts +5241 -553
- package/dist/pdf-CaPYsOUo.js +21302 -0
- package/dist/pdf-CaPYsOUo.js.map +1 -0
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +1207 -343
- package/src/assets/fonts/LICENSE-Apache-2.0.txt +202 -0
- package/src/assets/fonts/LICENSE-OFL-1.1.txt +93 -0
- package/src/assets/fonts/README.md +58 -0
- package/src/assets/fonts/caveat-latin-ext.woff2 +0 -0
- package/src/assets/fonts/caveat-latin.woff2 +0 -0
- package/src/assets/fonts/dancing-script-latin-ext.woff2 +0 -0
- package/src/assets/fonts/dancing-script-latin.woff2 +0 -0
- package/src/assets/fonts/homemade-apple-latin.woff2 +0 -0
- package/src/assets/fonts/source-serif-4-latin-ext.woff2 +0 -0
- package/src/assets/fonts/source-serif-4-latin.woff2 +0 -0
- package/src/components/AdvancedSearch.vue +49 -0
- package/src/components/Breadcrumb.vue +5 -5
- package/src/components/ChoiceButtons.vue +152 -0
- package/src/components/CommandPalette.vue +5 -5
- package/src/components/ConnectionGuideView.vue +1 -1
- package/src/components/ConnectionNotice.vue +59 -0
- package/src/components/ConnectionsPanel.vue +30 -568
- package/src/components/ContextMenu.vue +31 -5
- package/src/components/DataTable.vue +1331 -0
- package/src/components/FilePane.vue +3 -0
- package/src/components/FilterBar.vue +12 -5
- package/src/components/GalleryView.vue +50 -2
- package/src/components/GridView.vue +51 -3
- package/src/components/HomeView.vue +18 -6
- package/src/components/InspectorPanel.vue +134 -17
- package/src/components/ListView.vue +366 -900
- package/src/components/LogoMark.vue +57 -0
- package/src/components/NFSExportsPanel.vue +76 -28
- package/src/components/OnboardingTour.vue +19 -7
- package/src/components/OperationsCenter.vue +77 -1
- package/src/components/PendingOpsTray.vue +59 -5
- package/src/components/PresenceBar.vue +7 -7
- package/src/components/ProductVersion.vue +31 -0
- package/src/components/QuickLook.vue +10 -14
- package/src/components/RecentlyOpened.vue +47 -17
- package/src/components/RecoveryKeyModal.vue +1 -1
- package/src/components/RowActions.vue +134 -0
- package/src/components/S3KeysPanel.vue +98 -56
- package/src/components/SSHKeysPanel.vue +79 -26
- package/src/components/SideNav.vue +260 -96
- package/src/components/StarButton.vue +1 -0
- package/src/components/StorageFields.vue +74 -6
- package/src/components/StorageTags.vue +62 -0
- package/src/components/TabBar.vue +1 -1
- package/src/components/TagKindIcon.vue +56 -0
- package/src/components/TagPicker.vue +226 -40
- package/src/components/ThemePalette.vue +4 -4
- package/src/components/TimeZonePicker.vue +29 -10
- package/src/components/TokensPanel.vue +82 -27
- package/src/components/Toolbar.vue +28 -6
- package/src/components/plugin/PluginConfirmModal.vue +54 -0
- package/src/components/plugin/PluginInspectorSection.vue +149 -0
- package/src/components/plugin/PluginPageView.vue +339 -0
- package/src/components/plugin/PluginViewModal.vue +159 -0
- package/src/components/plugin/SurfaceConversation.vue +76 -0
- package/src/components/plugin/SurfaceFooterButtons.vue +73 -0
- package/src/components/plugin/SurfaceRenderer.vue +299 -0
- package/src/components/plugin/SurfaceSections.vue +91 -0
- package/src/components/plugin/nodes/PdfFieldEditor.vue +509 -0
- package/src/components/plugin/nodes/SurfaceFileChooser.vue +94 -0
- package/src/components/plugin/nodes/SurfaceForm.vue +82 -0
- package/src/components/plugin/nodes/SurfaceList.vue +127 -0
- package/src/components/plugin/nodes/SurfacePdfFields.vue +1285 -0
- package/src/components/plugin/nodes/SurfacePeoplePicker.vue +213 -0
- package/src/components/plugin/nodes/SurfacePinInput.vue +57 -0
- package/src/components/plugin/nodes/SurfacePreview.vue +119 -0
- package/src/components/plugin/nodes/SurfaceProgress.vue +48 -0
- package/src/components/plugin/nodes/SurfaceSignaturePad.vue +472 -0
- package/src/components/plugin/nodes/SurfaceSteps.vue +53 -0
- package/src/components/public/PublicLanguagePicker.vue +58 -0
- package/src/components/public/PublicLinkPage.vue +286 -0
- package/src/components/public/PublicLinkPreview.vue +85 -0
- package/src/components/public/PublicPinGate.vue +108 -0
- package/src/components/public/PublicRequestBody.vue +204 -0
- package/src/components/public/PublicShareBody.vue +188 -0
- package/src/components/public/PublicShell.vue +332 -0
- package/src/composables/useConnections.ts +8 -2
- package/src/composables/useFileApi.ts +183 -44
- package/src/composables/useLocale.ts +277 -41
- package/src/composables/useNFSExports.ts +8 -10
- package/src/composables/useOperations.ts +44 -3
- package/src/composables/usePendingOps.ts +59 -8
- package/src/composables/usePluginActions.ts +121 -0
- package/src/composables/usePluginSurface.ts +223 -0
- package/src/composables/usePublicBranding.ts +172 -0
- package/src/composables/usePublicLink.ts +573 -0
- package/src/composables/usePublicPage.ts +61 -0
- package/src/composables/useS3Keys.ts +17 -12
- package/src/composables/useSSHKeys.ts +8 -10
- package/src/composables/useTokens.ts +8 -12
- package/src/composables/useUploadChunked.ts +18 -8
- package/src/composables/useViewerFetch.ts +6 -3
- package/src/index.ts +404 -8
- package/src/lib/actionIcons.ts +46 -1
- package/src/lib/advSearch.ts +14 -7
- package/src/lib/appLock.ts +130 -0
- package/src/lib/connection.ts +110 -0
- package/src/lib/destinationTree.ts +19 -3
- package/src/lib/direction.ts +353 -0
- package/src/lib/dragGhost.ts +6 -0
- package/src/lib/dropLimits.ts +69 -0
- package/src/lib/errorWords.ts +394 -0
- package/src/lib/externalReach.ts +5 -1
- package/src/lib/fileFilters.ts +7 -2
- package/src/lib/fileIcons.ts +4 -0
- package/src/lib/internalPaths.ts +77 -0
- package/src/lib/listInput.ts +64 -0
- package/src/lib/listing.ts +58 -23
- package/src/lib/modalStack.ts +26 -0
- package/src/lib/nodeRow.ts +121 -0
- package/src/lib/pdfFieldRules.ts +126 -0
- package/src/lib/pdfFields.ts +589 -0
- package/src/lib/pdfFieldsGeom.ts +203 -0
- package/src/lib/pdfjsLoader.ts +51 -0
- package/src/lib/personName.ts +47 -0
- package/src/lib/pluginApplies.ts +123 -0
- package/src/lib/pluginLabel.ts +72 -0
- package/src/lib/pluginMenu.ts +253 -0
- package/src/lib/pluginPage.ts +58 -0
- package/src/lib/plural.ts +181 -0
- package/src/lib/popupLayer.ts +103 -0
- package/src/lib/prefs.ts +563 -0
- package/src/lib/productVersion.ts +35 -0
- package/src/lib/publicLayout.ts +39 -0
- package/src/lib/serviceGate.ts +104 -0
- package/src/lib/shareTtl.ts +25 -11
- package/src/lib/signFonts.ts +85 -0
- package/src/lib/signaturePad.ts +111 -0
- package/src/lib/sortOrder.ts +100 -38
- package/src/lib/star.ts +7 -1
- package/src/lib/surfaceCell.ts +41 -0
- package/src/lib/surfaceConditions.ts +156 -0
- package/src/lib/surfaceOpen.ts +106 -0
- package/src/lib/surfaceSteps.ts +68 -0
- package/src/lib/surfaceValues.ts +191 -0
- package/src/lib/symlink.ts +150 -0
- package/src/lib/tableColumns.ts +493 -0
- package/src/lib/tableEnv.ts +34 -0
- package/src/lib/tablePrefs.ts +84 -0
- package/src/lib/tags.ts +121 -12
- package/src/lib/themes.ts +286 -18
- package/src/lib/tour.ts +152 -0
- package/src/lib/trashEmpty.ts +111 -0
- package/src/lib/trashTimeLeft.ts +23 -0
- package/src/lib/uiLocales.ts +460 -0
- package/src/lib/viewPrefs.ts +696 -660
- package/src/lib/viewPrefsHttp.ts +78 -0
- package/src/locales/en.ts +522 -119
- package/src/locales/resolve.ts +17 -8
- package/src/locales/tr.ts +539 -155
- package/src/modals/ConvertModal.vue +62 -11
- package/src/modals/DestinationPickerModal.vue +59 -16
- package/src/modals/Modal.vue +113 -23
- package/src/modals/NewDocumentModal.vue +16 -6
- package/src/modals/NewFolderModal.vue +8 -0
- package/src/modals/PermissionsModal.vue +272 -163
- package/src/modals/PreviewModal.vue +119 -20
- package/src/modals/RenameModal.vue +37 -1
- package/src/styles/base.css +2895 -241
- package/src/styles/sign-fonts.css +118 -0
- package/src/styles/variables.css +60 -0
- package/src/types/Connections.ts +17 -0
- package/src/types/ExplorerConfig.ts +117 -1
- package/src/types/FileNode.ts +58 -0
- package/src/types/Plugins.ts +494 -0
- package/src/types/Public.ts +212 -0
- package/src/viewers/ArchiveViewer.vue +93 -48
- package/src/viewers/CsvViewer.vue +99 -53
- package/src/viewers/DrawioViewer.vue +35 -9
- package/src/viewers/EpubViewer.vue +7 -8
- package/src/viewers/IpynbViewer.vue +3 -1
- package/src/viewers/MermaidViewer.vue +4 -5
- package/src/viewers/PdfViewer.vue +8 -17
- package/src/viewers/PsdViewer.vue +2 -4
- package/src/viewers/TiffViewer.vue +4 -5
- package/src/viewers/Viewer3D.vue +1 -1
- package/LICENSE +0 -21
- package/dist/ArchiveViewer-CLNevfPE.js +0 -124
- package/dist/ArchiveViewer-CLNevfPE.js.map +0 -1
- package/dist/CsvViewer-K8XA3z7N.js +0 -141
- package/dist/CsvViewer-K8XA3z7N.js.map +0 -1
- package/dist/DrawioViewer-BuhZOAx6.js.map +0 -1
- package/dist/EpubViewer-DS3mb3k8.js.map +0 -1
- package/dist/IpynbViewer-BTk7p4Dn.js +0 -184
- package/dist/IpynbViewer-BTk7p4Dn.js.map +0 -1
- package/dist/MermaidViewer-DqucH_io.js +0 -137
- package/dist/MermaidViewer-DqucH_io.js.map +0 -1
- package/dist/PsdViewer-CwiVENmq.js +0 -122
- package/dist/PsdViewer-CwiVENmq.js.map +0 -1
- package/dist/TiffViewer-CpewSVmQ.js +0 -142
- package/dist/TiffViewer-CpewSVmQ.js.map +0 -1
- package/dist/Viewer3D-CIrOn63I.js.map +0 -1
- package/dist/index-Dx0IbBcI.js +0 -26831
- package/dist/index-Dx0IbBcI.js.map +0 -1
- package/dist/useViewerFetch-czqbd2Lj.js +0 -25
- package/dist/useViewerFetch-czqbd2Lj.js.map +0 -1
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tableColumns — THE column model of the one table this product has.
|
|
3
|
+
*
|
|
4
|
+
* ⚠⚠ Read this before writing anything tabular. filex has exactly ONE table:
|
|
5
|
+
* the explorer's list view, extracted into `components/DataTable.vue`. Every
|
|
6
|
+
* table in the product — the explorer's own listing, every admin page, the
|
|
7
|
+
* connection panels, My shares, the notifications list and an app's `list`
|
|
8
|
+
* node — renders through that component, and every one of them therefore gets
|
|
9
|
+
* what the explorer's list can do: resize a column, sort by it, hide it, move
|
|
10
|
+
* it, and have all of that remembered.
|
|
11
|
+
*
|
|
12
|
+
* The owner, 2026-09-21, after a round that had built a SECOND table which
|
|
13
|
+
* imitated the explorer (the same pinned edges, the same Actions menu) and left
|
|
14
|
+
* everything else behind: "Admin tabloları hâlâ explore tablolarıyla AYNI
|
|
15
|
+
* KODDA DEĞİL. LÜTFEN AYNI KODA ALALIM. Örnek veriyorum: tablo sütunları
|
|
16
|
+
* düzenlenebilir değil, büyütme küçültme yok, sıralama yok." An imitation gets
|
|
17
|
+
* the parts somebody remembered to copy. The same code gets all of it, and
|
|
18
|
+
* keeps getting whatever is added next.
|
|
19
|
+
*
|
|
20
|
+
* This file is the half of that table that has no DOM: which columns exist,
|
|
21
|
+
* how wide each one is, which ones are hidden, in what order they stand, and
|
|
22
|
+
* how wide the whole row therefore is. It used to live inside `lib/viewPrefs`
|
|
23
|
+
* as the explorer's private column state, typed to the explorer's seven
|
|
24
|
+
* columns; it is written here once, generic over the column ids, and the
|
|
25
|
+
* explorer is one caller of it among many.
|
|
26
|
+
*
|
|
27
|
+
* ⚠ It holds NO state of its own. A store is handed a BACKING — get the
|
|
28
|
+
* state, replace the state — and the backing decides where that lives: the
|
|
29
|
+
* explorer's lives in the per-folder view document (`lib/viewPrefs`), every
|
|
30
|
+
* other table's in the same per-person document under its own table id
|
|
31
|
+
* (`lib/tablePrefs`). That split is the reason the arithmetic can be shared
|
|
32
|
+
* without the storage being shared.
|
|
33
|
+
*/
|
|
34
|
+
import { computed, shallowRef } from 'vue';
|
|
35
|
+
|
|
36
|
+
/** One column the table knows how to draw. */
|
|
37
|
+
export interface TableColumnSpec<Id extends string = string> {
|
|
38
|
+
id: Id;
|
|
39
|
+
/** Shipped width in px. */
|
|
40
|
+
width: number;
|
|
41
|
+
min: number;
|
|
42
|
+
max: number;
|
|
43
|
+
/** May the person hide it from the column menu? Also what makes it MOVABLE:
|
|
44
|
+
* a column the person cannot hide is pinned where the table put it (the
|
|
45
|
+
* explorer's Name first, its star last). */
|
|
46
|
+
hideable: boolean;
|
|
47
|
+
/** May the person drag its edge? */
|
|
48
|
+
resizable: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* What is remembered about one table's columns. Short keys on purpose — the
|
|
53
|
+
* explorer keeps one of these per folder and the whole document is
|
|
54
|
+
* re-serialised on every save.
|
|
55
|
+
*/
|
|
56
|
+
export interface ColsState<Id extends string = string> {
|
|
57
|
+
/** Widths the person set, px. A column absent here is at its shipped width. */
|
|
58
|
+
w: Partial<Record<Id, number>>;
|
|
59
|
+
/** Columns the person switched off. */
|
|
60
|
+
hidden: Id[];
|
|
61
|
+
/** The person's order. Absent = the order the columns were declared in. */
|
|
62
|
+
o?: Id[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function emptyCols<Id extends string = string>(): ColsState<Id> {
|
|
66
|
+
return { w: {}, hidden: [] };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Read a stored column state against the columns that exist TODAY.
|
|
71
|
+
*
|
|
72
|
+
* ⚠ Clamped and filtered on the way IN, not only on the way out: a width
|
|
73
|
+
* stored by an older build (or hand-edited) outside today's bounds would draw
|
|
74
|
+
* a column nobody can reach the edge of, and a hidden id that no longer names
|
|
75
|
+
* a column would sit in the document forever. Nothing in the UI could explain
|
|
76
|
+
* either, so neither is allowed through.
|
|
77
|
+
*/
|
|
78
|
+
export function readCols<Id extends string>(
|
|
79
|
+
raw: unknown,
|
|
80
|
+
specs: readonly TableColumnSpec<Id>[],
|
|
81
|
+
): ColsState<Id> {
|
|
82
|
+
const fallback = emptyCols<Id>();
|
|
83
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return fallback;
|
|
84
|
+
const specOf = (id: unknown) => specs.find((c) => c.id === id);
|
|
85
|
+
const p = raw as { w?: unknown; hidden?: unknown; o?: unknown };
|
|
86
|
+
const w: Partial<Record<Id, number>> = {};
|
|
87
|
+
if (p.w && typeof p.w === 'object') {
|
|
88
|
+
for (const [k, v] of Object.entries(p.w as Record<string, unknown>)) {
|
|
89
|
+
const spec = specOf(k);
|
|
90
|
+
if (!spec || typeof v !== 'number' || !Number.isFinite(v)) continue;
|
|
91
|
+
w[spec.id] = Math.min(spec.max, Math.max(spec.min, Math.round(v)));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const hidden = Array.isArray(p.hidden)
|
|
95
|
+
? [...new Set(p.hidden.filter((x) => specOf(x)?.hideable) as Id[])]
|
|
96
|
+
: [];
|
|
97
|
+
const o = Array.isArray(p.o) ? ([...new Set(p.o.filter((x) => !!specOf(x)))] as Id[]) : undefined;
|
|
98
|
+
return o && o.length ? { w, hidden, o } : { w, hidden };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* THE REORDER ARITHMETIC, on its own and with no state attached.
|
|
103
|
+
*
|
|
104
|
+
* ⚠⚠ `to` is the index the item ENDS UP AT — equivalently, the insertion index
|
|
105
|
+
* in the list with the item already taken out. The two readings coincide for
|
|
106
|
+
* every `to`, and that equivalence is the whole point of writing it down:
|
|
107
|
+
* every reorder bug this function exists to close was a caller mixing the two
|
|
108
|
+
* index spaces. A caller that computes its target against the list WITH the
|
|
109
|
+
* column still in it hands over an index one too large whenever the column
|
|
110
|
+
* moves right — which is exactly how "drag one column to the right" became
|
|
111
|
+
* "two columns to the right". (Owner, 2026-09-20: "bir adım ileri çekince iki
|
|
112
|
+
* adım ilerliyor".)
|
|
113
|
+
*
|
|
114
|
+
* `from === to` is a no-op and returns a copy, so a drop on the column's own
|
|
115
|
+
* place writes nothing.
|
|
116
|
+
*/
|
|
117
|
+
export function reorderColumns<T>(list: readonly T[], from: number, to: number): T[] {
|
|
118
|
+
const next = [...list];
|
|
119
|
+
if (from < 0 || from >= next.length) return next;
|
|
120
|
+
const target = Math.max(0, Math.min(next.length - 1, to));
|
|
121
|
+
if (target === from) return next;
|
|
122
|
+
const [item] = next.splice(from, 1);
|
|
123
|
+
next.splice(target, 0, item);
|
|
124
|
+
return next;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The stretch of the order a movable column may occupy — `[lo, hi]`, both
|
|
129
|
+
* inclusive indices into `order`.
|
|
130
|
+
*
|
|
131
|
+
* ⚠ The pinned columns are not just un-draggable, they are also un-passable:
|
|
132
|
+
* the lead is the row's handle and stays first, the explorer's star is a
|
|
133
|
+
* control that lives beside the row menu and stays last. Measured 2026-09-20:
|
|
134
|
+
* dropping Type at the end of the drawn columns put it PAST the star, so the
|
|
135
|
+
* header read `… Size ★ Type ⋮` and the Type label stood over a 24px control
|
|
136
|
+
* column. The band is derived rather than hard-coded so a pinned column at
|
|
137
|
+
* either end of ANY table is honoured without another rule remembering to.
|
|
138
|
+
*/
|
|
139
|
+
export function columnDropBand<Id extends string>(
|
|
140
|
+
order: readonly Id[],
|
|
141
|
+
specs: readonly TableColumnSpec<Id>[],
|
|
142
|
+
): { lo: number; hi: number } {
|
|
143
|
+
const movable = (id: Id) => specs.find((c) => c.id === id)?.hideable === true;
|
|
144
|
+
let lo = 0;
|
|
145
|
+
while (lo < order.length && !movable(order[lo])) lo++;
|
|
146
|
+
let hi = order.length - 1;
|
|
147
|
+
while (hi >= 0 && !movable(order[hi])) hi--;
|
|
148
|
+
return { lo, hi: Math.max(lo, hi) };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Where a store reads and writes its state. */
|
|
152
|
+
export interface ColumnBacking<Id extends string = string> {
|
|
153
|
+
/** The stored state, or anything that does not parse as one (read as empty). */
|
|
154
|
+
get(): unknown;
|
|
155
|
+
/** Replace it. The store always hands over a complete, normalised state. */
|
|
156
|
+
set(next: ColsState<Id>): void;
|
|
157
|
+
/**
|
|
158
|
+
* Is there anything here the person could RESET? Optional — by default,
|
|
159
|
+
* "the state says something". The explorer's folder backing answers "does
|
|
160
|
+
* THIS folder have columns of its own", because a folder that shows the
|
|
161
|
+
* person's default columns has nothing of its own to reset, and a "Reset
|
|
162
|
+
* columns" row that does nothing when clicked reads as broken.
|
|
163
|
+
*/
|
|
164
|
+
customised?(): boolean;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** What one table looks like right now. */
|
|
168
|
+
export interface TableLayout<Id extends string = string> {
|
|
169
|
+
/** The columns drawn after the lead, in the person's order. Never shortened
|
|
170
|
+
* for width — a table wider than its pane scrolls. */
|
|
171
|
+
visible: Id[];
|
|
172
|
+
/** Every drawn track's width in px, the lead's included. */
|
|
173
|
+
widths: Record<Id, number>;
|
|
174
|
+
/** The table's own width: every track, every gap, both paddings. */
|
|
175
|
+
total: number;
|
|
176
|
+
/** True while the widths are derived from the pane rather than stored. */
|
|
177
|
+
auto: boolean;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface LayoutMetrics {
|
|
181
|
+
/** Space between cells (`--fe-gap-sm`). */
|
|
182
|
+
gap?: number;
|
|
183
|
+
/** Row padding, both sides together (`--fe-gap` × 2). */
|
|
184
|
+
padding?: number;
|
|
185
|
+
/** The tick column's track, or 0 for a table with no tick. */
|
|
186
|
+
check?: number;
|
|
187
|
+
/** The trailing actions column's track (the explorer's ⋮ is 28). */
|
|
188
|
+
menu?: number;
|
|
189
|
+
/** What the lead opens at when nobody has sized anything. */
|
|
190
|
+
leadAuto?: number;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export interface ColumnStore<Id extends string = string> {
|
|
194
|
+
readonly lead: Id;
|
|
195
|
+
specs(): readonly TableColumnSpec<Id>[];
|
|
196
|
+
spec(id: Id): TableColumnSpec<Id> | undefined;
|
|
197
|
+
/** The full order, lead first, reconciled with the columns that exist. */
|
|
198
|
+
order(): Id[];
|
|
199
|
+
width(id: Id): number;
|
|
200
|
+
setWidth(id: Id, px: number): void;
|
|
201
|
+
hidden(id: Id): boolean;
|
|
202
|
+
setHidden(id: Id, hidden: boolean): void;
|
|
203
|
+
/** Put `id` at `index` in the order — `index` is where it ENDS UP. */
|
|
204
|
+
move(id: Id, index: number): void;
|
|
205
|
+
/** Has anybody ever sized this table? (the lead's width is the bit) */
|
|
206
|
+
widthsAreAuto(): boolean;
|
|
207
|
+
/** Commit the widths on screen for every column that has none stored. */
|
|
208
|
+
freeze(widths: Partial<Record<Id, number>>): void;
|
|
209
|
+
/** Back to the shipped widths, order and visibility. */
|
|
210
|
+
reset(): void;
|
|
211
|
+
/** Has anything about the columns been changed by hand? */
|
|
212
|
+
customised(): boolean;
|
|
213
|
+
/** The state as it resolves right now — what "Apply to all folders" copies. */
|
|
214
|
+
state(): ColsState<Id>;
|
|
215
|
+
layout(available: number, candidates: readonly Id[], metrics?: LayoutMetrics): TableLayout<Id>;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* A column store over `specs`, reading and writing through `backing`.
|
|
220
|
+
*
|
|
221
|
+
* `lead` is the column pinned first: never hidden, never moved, always
|
|
222
|
+
* resizable. It is the one that says WHICH ROW this is — the explorer's Name,
|
|
223
|
+
* an admin table's email or path — and the table freezes it on the left edge
|
|
224
|
+
* while the others scroll.
|
|
225
|
+
*
|
|
226
|
+
* ⚠ The state is read through a `computed`, so a store created inside a
|
|
227
|
+
* component re-renders it when the backing changes (the explorer's backing
|
|
228
|
+
* changes every time the folder does) and parses the stored document once per
|
|
229
|
+
* change rather than once per cell.
|
|
230
|
+
*/
|
|
231
|
+
export function createColumnStore<Id extends string>(
|
|
232
|
+
specsOf: () => readonly TableColumnSpec<Id>[],
|
|
233
|
+
backing: ColumnBacking<Id>,
|
|
234
|
+
opts: { lead?: Id } = {},
|
|
235
|
+
): ColumnStore<Id> {
|
|
236
|
+
const lead = (opts.lead ?? specsOf()[0]?.id) as Id;
|
|
237
|
+
const state = computed<ColsState<Id>>(() => readCols(backing.get(), specsOf()));
|
|
238
|
+
const specOf = (id: Id) => specsOf().find((c) => c.id === id);
|
|
239
|
+
|
|
240
|
+
function write(next: ColsState<Id>): void {
|
|
241
|
+
backing.set(next);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* THE COLUMN ORDER — the person's, reconciled with the columns that exist.
|
|
246
|
+
*
|
|
247
|
+
* ⚠⚠ Reconciled, not trusted. A stored order was written by an older build,
|
|
248
|
+
* so it can be missing a column added since and can name one removed since.
|
|
249
|
+
* Returning it as-is would make a new column INVISIBLE to everyone who had
|
|
250
|
+
* ever touched the order — a feature that ships and then does not appear for
|
|
251
|
+
* exactly the users engaged enough to have customised something.
|
|
252
|
+
*
|
|
253
|
+
* ⚠ The lead first, whatever the document says: it is pinned in the table,
|
|
254
|
+
* so an order that claimed otherwise would be a sequence nothing on screen
|
|
255
|
+
* obeys.
|
|
256
|
+
*
|
|
257
|
+
* ⚠ A column the stored list never heard of goes to the END, not to its own
|
|
258
|
+
* shipped index. Measured: inserting at the shipped index walked newcomers in
|
|
259
|
+
* FRONT of the columns the person had deliberately moved — somebody who had
|
|
260
|
+
* put Size first would open a later release to find two new columns ahead of
|
|
261
|
+
* it and their arrangement silently rewritten.
|
|
262
|
+
*/
|
|
263
|
+
function order(): Id[] {
|
|
264
|
+
const known = specsOf().map((c) => c.id);
|
|
265
|
+
const stored = state.value.o;
|
|
266
|
+
if (!stored || stored.length === 0) {
|
|
267
|
+
return [lead, ...known.filter((id) => id !== lead)];
|
|
268
|
+
}
|
|
269
|
+
const out = stored.filter((id) => known.includes(id) && id !== lead);
|
|
270
|
+
out.unshift(lead);
|
|
271
|
+
for (const id of known) if (!out.includes(id)) out.push(id);
|
|
272
|
+
return out;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function width(id: Id): number {
|
|
276
|
+
const spec = specOf(id);
|
|
277
|
+
if (!spec) return 0;
|
|
278
|
+
return state.value.w[id] ?? spec.width;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function setWidth(id: Id, px: number): void {
|
|
282
|
+
const spec = specOf(id);
|
|
283
|
+
if (!spec || !spec.resizable) return;
|
|
284
|
+
const next = Math.min(spec.max, Math.max(spec.min, Math.round(px)));
|
|
285
|
+
if (state.value.w[id] === next) return;
|
|
286
|
+
write({ ...state.value, w: { ...state.value.w, [id]: next } });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* HAS ANYBODY EVER SIZED THIS TABLE — the one bit that separates "I have
|
|
291
|
+
* never touched this" from "I deliberately made the lead narrow".
|
|
292
|
+
*
|
|
293
|
+
* ⚠⚠ Without it the two are indistinguishable and the first resize is undone
|
|
294
|
+
* by the next pane resize: an untouched table is sized to the pane it is in,
|
|
295
|
+
* so a person who drags the lead to 140 and then opens a side panel would get
|
|
296
|
+
* it recomputed back to whatever the narrower pane suggests. The bit is the
|
|
297
|
+
* LEAD's width rather than "the map is empty": documents written before the
|
|
298
|
+
* explorer's Name became a sized column carry widths but have never been
|
|
299
|
+
* asked about Name, and reading those as "configured" would open the table
|
|
300
|
+
* at a width nobody chose. The first drag writes every width at once
|
|
301
|
+
* (`freeze`), so a table somebody has actually sized can never fall back.
|
|
302
|
+
*/
|
|
303
|
+
function widthsAreAuto(): boolean {
|
|
304
|
+
return state.value.w[lead] === undefined;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* ⚠⚠ Called at the START of a resize gesture, and it has to write EVERY
|
|
309
|
+
* drawn column, not the one being dragged. Writing only the dragged column
|
|
310
|
+
* would leave the lead auto — and the auto lead takes the slack, so
|
|
311
|
+
* narrowing Size would widen Name, which is the exact behaviour the owner
|
|
312
|
+
* called wrong ("küçültme yapınca name kısmı büyüyormuş gibi davranıyor").
|
|
313
|
+
*/
|
|
314
|
+
function freeze(widths: Partial<Record<Id, number>>): void {
|
|
315
|
+
const w: Partial<Record<Id, number>> = { ...state.value.w };
|
|
316
|
+
let changed = false;
|
|
317
|
+
for (const [k, v] of Object.entries(widths) as [Id, number | undefined][]) {
|
|
318
|
+
if (typeof v !== 'number' || !Number.isFinite(v)) continue;
|
|
319
|
+
if (w[k] !== undefined) continue;
|
|
320
|
+
const spec = specOf(k);
|
|
321
|
+
if (!spec || !spec.resizable) continue;
|
|
322
|
+
w[k] = Math.min(spec.max, Math.max(spec.min, Math.round(v)));
|
|
323
|
+
changed = true;
|
|
324
|
+
}
|
|
325
|
+
if (changed) write({ ...state.value, w });
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function hidden(id: Id): boolean {
|
|
329
|
+
return state.value.hidden.includes(id);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function setHidden(id: Id, hide: boolean): void {
|
|
333
|
+
const spec = specOf(id);
|
|
334
|
+
if (!spec || !spec.hideable || id === lead) return;
|
|
335
|
+
if (hidden(id) === hide) return;
|
|
336
|
+
const set = new Set(state.value.hidden);
|
|
337
|
+
if (hide) set.add(id);
|
|
338
|
+
else set.delete(id);
|
|
339
|
+
write({ ...state.value, hidden: [...set] });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function move(id: Id, index: number): void {
|
|
343
|
+
const spec = specOf(id);
|
|
344
|
+
/* A column the person cannot hide is not theirs to move either: it is
|
|
345
|
+
pinned where the table put it (the lead first, a control column last). */
|
|
346
|
+
if (!spec || !spec.hideable || id === lead) return;
|
|
347
|
+
const cur = order();
|
|
348
|
+
const from = cur.indexOf(id);
|
|
349
|
+
if (from === -1) return;
|
|
350
|
+
const band = columnDropBand(cur, specsOf());
|
|
351
|
+
const to = Math.max(band.lo, Math.min(band.hi, index));
|
|
352
|
+
const next = reorderColumns(cur, from, to);
|
|
353
|
+
if (next.join() === cur.join()) return;
|
|
354
|
+
write({ ...state.value, o: next });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function reset(): void {
|
|
358
|
+
write(emptyCols<Id>());
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function customised(): boolean {
|
|
362
|
+
if (backing.customised) return backing.customised();
|
|
363
|
+
return (
|
|
364
|
+
Object.keys(state.value.w).length > 0 ||
|
|
365
|
+
state.value.hidden.length > 0 ||
|
|
366
|
+
(state.value.o?.length ?? 0) > 0
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* THE TABLE'S WIDTHS — one answer, used by the header and by every row.
|
|
372
|
+
*
|
|
373
|
+
* · nothing is ever dropped for want of room — `visible` is the person's
|
|
374
|
+
* choice and the caller's candidates, and width has no vote. The owner
|
|
375
|
+
* rejected the table that shed columns and said "no room": "no room
|
|
376
|
+
* demesin, kenara devam eden bir scroll getirsin";
|
|
377
|
+
* · the table's width is the SUM of its columns, so when that exceeds the
|
|
378
|
+
* pane the table scrolls sideways instead of shrinking;
|
|
379
|
+
* · with nothing stored the widths are derived from the pane, so a table
|
|
380
|
+
* nobody has configured opens sensibly at 1440 and at 390 alike: the lead
|
|
381
|
+
* asks for `leadAuto`, the others give up room toward their minimums to
|
|
382
|
+
* pay for it, and the lead takes any slack that is left. The instant one
|
|
383
|
+
* width is stored the pane stops having an opinion.
|
|
384
|
+
*
|
|
385
|
+
* ⚠ `candidates` is what the CALLER is willing to draw at all — the
|
|
386
|
+
* explorer's Location only where rows come from more than one folder, its
|
|
387
|
+
* star only where starring is offered. Passing them keeps those facts with
|
|
388
|
+
* the component that knows them.
|
|
389
|
+
*/
|
|
390
|
+
function layout(
|
|
391
|
+
available: number,
|
|
392
|
+
candidates: readonly Id[],
|
|
393
|
+
metrics: LayoutMetrics = {},
|
|
394
|
+
): TableLayout<Id> {
|
|
395
|
+
const gap = metrics.gap ?? 8;
|
|
396
|
+
const padding = metrics.padding ?? 24;
|
|
397
|
+
const check = metrics.check ?? 0;
|
|
398
|
+
const menu = metrics.menu ?? 28;
|
|
399
|
+
const leadSpec = specOf(lead);
|
|
400
|
+
|
|
401
|
+
const visible = order().filter((id) => id !== lead && candidates.includes(id) && !hidden(id));
|
|
402
|
+
|
|
403
|
+
const widths = {} as Record<Id, number>;
|
|
404
|
+
for (const id of visible) widths[id] = width(id);
|
|
405
|
+
widths[lead] = width(lead);
|
|
406
|
+
|
|
407
|
+
/* Cells: [tick] + lead + the others + the actions column, so one gap
|
|
408
|
+
fewer than that. */
|
|
409
|
+
const cells = (check > 0 ? 1 : 0) + 1 + visible.length + 1;
|
|
410
|
+
const chrome = check + menu + padding + gap * (cells - 1);
|
|
411
|
+
const auto = widthsAreAuto();
|
|
412
|
+
|
|
413
|
+
/* A container we cannot measure yet (0 on the first tick, or a test with
|
|
414
|
+
no layout) keeps the shipped widths — the observer corrects it one frame
|
|
415
|
+
later, and a table that guessed would flash. */
|
|
416
|
+
if (auto && available > 0 && leadSpec) {
|
|
417
|
+
const leadAuto = metrics.leadAuto ?? leadSpec.width;
|
|
418
|
+
const room = available - chrome;
|
|
419
|
+
let rest = visible.reduce((s, id) => s + widths[id], 0);
|
|
420
|
+
const target = room - leadAuto;
|
|
421
|
+
if (rest > target) {
|
|
422
|
+
const give = visible.reduce((s, id) => s + (widths[id] - (specOf(id)?.min ?? widths[id])), 0);
|
|
423
|
+
if (give > 0) {
|
|
424
|
+
const f = Math.min(1, (rest - target) / give);
|
|
425
|
+
for (const id of visible) {
|
|
426
|
+
const min = specOf(id)?.min ?? widths[id];
|
|
427
|
+
/* ⚠ `ceil` on the reduction, not `floor`: rounding the other way
|
|
428
|
+
leaves the row a pixel or two over the pane and hands an
|
|
429
|
+
otherwise fitting table a horizontal scrollbar on first sight —
|
|
430
|
+
measured at 960, 712 in a 711px pane. */
|
|
431
|
+
widths[id] = Math.max(min, widths[id] - Math.ceil((widths[id] - min) * f));
|
|
432
|
+
}
|
|
433
|
+
rest = visible.reduce((s, id) => s + widths[id], 0);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
/* Slack goes to the lead, and only to the lead — and where there is
|
|
437
|
+
none to give, the lead gives way too, down to its own minimum.
|
|
438
|
+
|
|
439
|
+
⚠⚠ It used to stop at `leadAuto`, and `leadAuto` is a DESKTOP
|
|
440
|
+
default (240px for a table that names nothing else). In a 265px side
|
|
441
|
+
panel that IS the pane: an app's list drew its Identity column and
|
|
442
|
+
nothing else, and the State and When columns it was sent sat under
|
|
443
|
+
the pinned Actions cell, so a signer table read as names, buttons,
|
|
444
|
+
and a blank gap between them (v0.43.0). Standing at 240 does not
|
|
445
|
+
make anything fit — the table is wider than the pane either way and
|
|
446
|
+
scrolls, which is the owner's rule — it only decides what the person
|
|
447
|
+
is shown FIRST, and one wide column plus nothing is the worse
|
|
448
|
+
answer. Nothing is ever dropped here; the lead simply stops taking
|
|
449
|
+
the whole pane. */
|
|
450
|
+
const leadFloor = Math.min(leadAuto, leadSpec.min);
|
|
451
|
+
widths[lead] = Math.round(Math.max(leadFloor, Math.min(leadSpec.max, room - rest)));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const total = chrome + widths[lead] + visible.reduce((s, id) => s + widths[id], 0);
|
|
455
|
+
return { visible, widths, total, auto };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
lead,
|
|
460
|
+
specs: specsOf,
|
|
461
|
+
spec: specOf,
|
|
462
|
+
order,
|
|
463
|
+
width,
|
|
464
|
+
setWidth,
|
|
465
|
+
hidden,
|
|
466
|
+
setHidden,
|
|
467
|
+
move,
|
|
468
|
+
widthsAreAuto,
|
|
469
|
+
freeze,
|
|
470
|
+
reset,
|
|
471
|
+
customised,
|
|
472
|
+
state: () => state.value,
|
|
473
|
+
layout,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* A backing that lives in this component's memory only — for a table on a
|
|
479
|
+
* surface with no account behind it (a public page), and for tests.
|
|
480
|
+
*/
|
|
481
|
+
export function memoryBacking<Id extends string = string>(
|
|
482
|
+
initial: ColsState<Id> = emptyCols<Id>(),
|
|
483
|
+
): ColumnBacking<Id> {
|
|
484
|
+
/* A ref, or the store's `computed` would never see a write and the table
|
|
485
|
+
would not move when a column is dragged. */
|
|
486
|
+
const box = shallowRef<ColsState<Id>>(initial);
|
|
487
|
+
return {
|
|
488
|
+
get: () => box.value,
|
|
489
|
+
set: (next) => {
|
|
490
|
+
box.value = next;
|
|
491
|
+
},
|
|
492
|
+
};
|
|
493
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tableEnv — the two things every table needs from its HOST and no view
|
|
3
|
+
* should have to pass by hand: the interface language and the light/dark
|
|
4
|
+
* mode.
|
|
5
|
+
*
|
|
6
|
+
* ⚠ Why an injection and not two props on every table. The admin app draws
|
|
7
|
+
* some thirty tables; each one needs the language (the column menu, "Resize
|
|
8
|
+
* {col}", the pager, the Actions label) and the light/dark mode (the column
|
|
9
|
+
* menu and the Actions menu teleport to <body>, outside the chrome that paints
|
|
10
|
+
* with `.dark`, so they have to be told). Threading two props through thirty
|
|
11
|
+
* call sites is thirty places for one of them to be forgotten — which is how
|
|
12
|
+
* one table ends up with an English column menu inside a Turkish page. The
|
|
13
|
+
* host provides it ONCE (web/src/App.vue); a prop on a table still wins, and
|
|
14
|
+
* with neither the table falls back to English and the browser's own mode.
|
|
15
|
+
*/
|
|
16
|
+
import { inject, provide, type InjectionKey, type Ref } from 'vue';
|
|
17
|
+
import type { LocaleCode, ThemeMode } from '../types/ExplorerConfig';
|
|
18
|
+
|
|
19
|
+
export interface TableEnv {
|
|
20
|
+
locale: Ref<LocaleCode>;
|
|
21
|
+
theme?: Ref<ThemeMode | undefined>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const TABLE_ENV: InjectionKey<TableEnv> = Symbol('fe-table-env');
|
|
25
|
+
|
|
26
|
+
/** Called once by a host, in its root component's setup. */
|
|
27
|
+
export function provideTableEnv(env: TableEnv): void {
|
|
28
|
+
provide(TABLE_ENV, env);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The host's answer, or null. Must be called during `setup()`. */
|
|
32
|
+
export function useTableEnv(): TableEnv | null {
|
|
33
|
+
return inject(TABLE_ENV, null);
|
|
34
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tablePrefs — where every table that is NOT the explorer's listing keeps its
|
|
3
|
+
* columns and its sort.
|
|
4
|
+
*
|
|
5
|
+
* One corner of the per-person view document (`lib/viewPrefs` →
|
|
6
|
+
* `viewPrefsSlot('t')`), keyed by a table id the table is given:
|
|
7
|
+
*
|
|
8
|
+
* { "t": { "admin.users": { "c": {w, hidden, o}, "s": {k, d} }, … } }
|
|
9
|
+
*
|
|
10
|
+
* ⚠⚠ On the ACCOUNT, like the explorer's folders, and for the same reason the
|
|
11
|
+
* owner gave for those ("tarayıcıya değil db'ye kaydedeceğiz"): a column
|
|
12
|
+
* dragged wider in the Users table on the office machine is there at home,
|
|
13
|
+
* and a second person signing into the same browser does not inherit it.
|
|
14
|
+
* With nobody signed in (a public page, an app token) the slot is
|
|
15
|
+
* session-only — the table still resizes and sorts, it just forgets on reload.
|
|
16
|
+
*
|
|
17
|
+
* ⚠ The id is the table's name, not its route: two tables on one page (the
|
|
18
|
+
* Replica screen draws three) need three ids, and a table that moves to
|
|
19
|
+
* another page keeps its arrangement. `web/tests/ui/tablePinnedActions.test.ts`
|
|
20
|
+
* refuses a table with no id and two tables with the same one.
|
|
21
|
+
*/
|
|
22
|
+
import { viewPrefsSlot } from './viewPrefs';
|
|
23
|
+
import type { ColsState, ColumnBacking } from './tableColumns';
|
|
24
|
+
|
|
25
|
+
/** One table's remembered state. */
|
|
26
|
+
export interface TablePrefs {
|
|
27
|
+
c?: ColsState;
|
|
28
|
+
s?: { k: string; d: 'asc' | 'desc' };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type TableMap = Record<string, TablePrefs>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The slot is created lazily and ONCE: `viewPrefsSlot` throws for a reserved
|
|
35
|
+
* name, and calling it at module load would make importing this file a side
|
|
36
|
+
* effect in every bundle that never draws a table.
|
|
37
|
+
*/
|
|
38
|
+
let slot: ReturnType<typeof viewPrefsSlot<TableMap>> | null = null;
|
|
39
|
+
function tables() {
|
|
40
|
+
if (!slot) slot = viewPrefsSlot<TableMap>('t');
|
|
41
|
+
return slot;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function read(): TableMap {
|
|
45
|
+
const v = tables().get();
|
|
46
|
+
return v && typeof v === 'object' && !Array.isArray(v) ? v : {};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function write(id: string, patch: Partial<TablePrefs>): void {
|
|
50
|
+
const all = read();
|
|
51
|
+
const cur: TablePrefs = { ...(all[id] ?? {}) };
|
|
52
|
+
for (const [k, v] of Object.entries(patch) as [keyof TablePrefs, unknown][]) {
|
|
53
|
+
if (v === undefined) delete cur[k];
|
|
54
|
+
else (cur as Record<string, unknown>)[k] = v;
|
|
55
|
+
}
|
|
56
|
+
const next = { ...all };
|
|
57
|
+
if (Object.keys(cur).length) next[id] = cur;
|
|
58
|
+
else delete next[id];
|
|
59
|
+
tables().set(next);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The backing a `ColumnStore` for table `id` reads and writes through. */
|
|
63
|
+
export function tableColumnBacking(id: string): ColumnBacking {
|
|
64
|
+
return {
|
|
65
|
+
get: () => read()[id]?.c,
|
|
66
|
+
set: (next) => {
|
|
67
|
+
const says =
|
|
68
|
+
Object.keys(next.w).length > 0 || next.hidden.length > 0 || (next.o?.length ?? 0) > 0;
|
|
69
|
+
write(id, { c: says ? next : undefined });
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The table's remembered sort, or null. */
|
|
75
|
+
export function tableSort(id: string): { key: string; dir: 'asc' | 'desc' } | null {
|
|
76
|
+
const s = read()[id]?.s;
|
|
77
|
+
if (!s || typeof s.k !== 'string' || (s.d !== 'asc' && s.d !== 'desc')) return null;
|
|
78
|
+
return { key: s.k, dir: s.d };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Remember (or with null, forget) the table's sort. */
|
|
82
|
+
export function setTableSort(id: string, sort: { key: string; dir: 'asc' | 'desc' } | null): void {
|
|
83
|
+
write(id, { s: sort ? { k: sort.key, d: sort.dir } : undefined });
|
|
84
|
+
}
|