viur-ui 0.0.1 → 0.0.2
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 +67 -41
- package/index.d.ts +18 -9
- package/index.js +6 -3
- package/package.json +3 -5
- package/src/components/Browser/Browser.vue +58 -58
- package/src/components/Browser/TreeNav.vue +32 -32
- package/src/components/Browser/ViurBrowser.d.vue.ts +21 -21
- package/src/components/Browser/ViurBrowser.vue +47 -44
- package/src/components/Browser/dragSession.ts +27 -27
- package/src/components/Browser/index.ts +3 -3
- package/src/components/Browser/types.ts +9 -9
- package/src/components/DataTable/ColumnFilterPanel.vue +7 -7
- package/src/components/DataTable/DataTable.vue +346 -346
- package/src/components/DataTable/ViurDataTable.d.vue.ts +26 -26
- package/src/components/DataTable/ViurDataTable.vue +51 -50
- package/src/components/DataTable/columns.ts +30 -30
- package/src/components/DataTable/index.ts +12 -12
- package/src/components/DataTable/rows.ts +71 -72
- package/src/components/DataTable/types.ts +55 -55
- package/src/components/DataTable/useColumnLayout.ts +28 -28
- package/src/components/DataTable/useGridNavigation.ts +15 -15
- package/src/components/DataTable/useVirtualRows.ts +10 -10
- package/src/components/DataTable/viurColumns.ts +69 -69
- package/src/forms/ViForm.d.vue.ts +32 -32
- package/src/forms/ViForm.vue +17 -16
- package/src/forms/ViFormCategory.vue +3 -1
- package/src/forms/bones/Bone.vue +125 -108
- package/src/forms/bones/BoneActions.vue +21 -21
- package/src/forms/bones/BoneLabel.vue +1 -1
- package/src/forms/bones/BoneMultipleWrapper.vue +3 -3
- package/src/forms/bones/BoneNestedWrapper.vue +3 -3
- package/src/forms/bones/BoneSingleEntry.vue +2 -2
- package/src/forms/bones/FilterBone.vue +3 -3
- package/src/forms/bones/actions/ActionDialog.vue +2 -2
- package/src/forms/bones/actions/Debug.vue +6 -6
- package/src/forms/bones/actions/Delete.vue +2 -2
- package/src/forms/bones/actions/DescribeImage.vue +4 -3
- package/src/forms/bones/actions/Download.vue +4 -4
- package/src/forms/bones/actions/Placeholder.vue +2 -2
- package/src/forms/bones/actions/index.js +72 -74
- package/src/forms/bones/bone.d.ts +19 -19
- package/src/forms/bones/uploadAdapter.js +3 -3
- package/src/forms/bones/utils.js +40 -41
- package/src/forms/bones/view/base_item.vue +1 -1
- package/src/forms/bones/view/boneLogic.js +1 -1
- package/src/forms/bones/widgets/booleanBone.vue +19 -19
- package/src/forms/bones/widgets/booleanBoneChoose.vue +2 -2
- package/src/forms/bones/widgets/booleanBoneSelect.vue +3 -3
- package/src/forms/bones/widgets/codeBone.vue +22 -22
- package/src/forms/bones/widgets/colorBone.vue +18 -18
- package/src/forms/bones/widgets/dateBone.vue +22 -22
- package/src/forms/bones/widgets/emailBone.vue +20 -20
- package/src/forms/bones/widgets/fileBone.vue +32 -31
- package/src/forms/bones/widgets/keyBone.vue +21 -21
- package/src/forms/bones/widgets/numericBone.vue +21 -21
- package/src/forms/bones/widgets/passwordBone.vue +34 -31
- package/src/forms/bones/widgets/phoneBone.vue +20 -20
- package/src/forms/bones/widgets/rawBone.vue +19 -19
- package/src/forms/bones/widgets/recordBone.vue +16 -16
- package/src/forms/bones/widgets/relationalBone.vue +26 -26
- package/src/forms/bones/widgets/selectBone.vue +23 -23
- package/src/forms/bones/widgets/selectBoneChoose.vue +2 -2
- package/src/forms/bones/widgets/spatialBone.vue +21 -21
- package/src/forms/bones/widgets/stringBone.vue +27 -27
- package/src/forms/bones/widgets/textBone.vue +28 -26
- package/src/forms/install.d.ts +1 -1
- package/src/forms/install.js +7 -0
- package/src/forms/layouts/LayloutList2Columns.vue +1 -1
- package/src/forms/layouts/LayoutCategory.vue +1 -1
- package/src/forms/layouts/LayoutList.vue +1 -1
- package/src/forms/utils.js +22 -22
- package/src/request/configStore.js +4 -4
- package/src/request/envelope.js +6 -6
- package/src/request/request.js +22 -22
- package/src/translations/de.js +31 -0
- package/src/translations/en.js +31 -0
- package/src/user/LoginScreen.d.vue.ts +24 -24
- package/src/user/LoginScreen.vue +6 -7
- package/src/user/store.d.ts +16 -16
- package/src/vite/safeHtml.js +73 -0
- package/src/vite/sanitizeVHtml.d.ts +4 -0
- package/src/vite/sanitizeVHtml.js +15 -0
- package/src/webawesomeCompat.css +0 -40
|
@@ -1,69 +1,69 @@
|
|
|
1
1
|
import type { DefineComponent, Ref } from "vue"
|
|
2
|
-
import type { Row, RowKey } from "./types"
|
|
2
|
+
import type { Row, RowKey } from "./types.ts"
|
|
3
3
|
|
|
4
|
-
/** Props
|
|
4
|
+
/** Props of ViurDataTable.vue (see defineProps there). */
|
|
5
5
|
export interface ViurDataTableProps {
|
|
6
|
-
/**
|
|
6
|
+
/** Module name or dot path ("user", "shop.order"). */
|
|
7
7
|
module: string
|
|
8
|
-
/** Renderer
|
|
8
|
+
/** Renderer for the requests; default: VITE_DEFAULT_RENDERER. */
|
|
9
9
|
renderer?: string | null
|
|
10
|
-
/**
|
|
10
|
+
/** Subtype of grouped lists (handler `list.grouped`). */
|
|
11
11
|
group?: string | null
|
|
12
|
-
/**
|
|
12
|
+
/** Additional request parameters — spread into every fetch. */
|
|
13
13
|
params?: Record<string, unknown>
|
|
14
|
-
/**
|
|
14
|
+
/** Columns; if not given, derived from the viewSkel structure. */
|
|
15
15
|
columns?: unknown[] | null
|
|
16
|
-
/**
|
|
16
|
+
/** Only these bones as columns (for derived columns). */
|
|
17
17
|
bones?: string[] | null
|
|
18
18
|
label?: string | null
|
|
19
19
|
selectable?: false | "single" | "multiple"
|
|
20
|
-
/**
|
|
20
|
+
/** Entries per load (ViUR `limit`). */
|
|
21
21
|
pageSize?: number
|
|
22
|
-
/**
|
|
22
|
+
/** Upper limit for local mode (everything loaded → client pipeline). */
|
|
23
23
|
localModeLimit?: number
|
|
24
|
-
/** X-VIUR-BONELIST
|
|
24
|
+
/** Send X-VIUR-BONELIST when fields are deselected (server needs a "*" subskel). */
|
|
25
25
|
bonelist?: boolean
|
|
26
|
-
/** localStorage
|
|
26
|
+
/** localStorage key for field visibility; null = no persistence. */
|
|
27
27
|
storageKey?: string | null
|
|
28
28
|
language?: string | null
|
|
29
|
-
/**
|
|
29
|
+
/** Comes first with language="*" (usually the UI language). */
|
|
30
30
|
primaryLanguage?: string | null
|
|
31
31
|
locale?: string | null
|
|
32
|
-
/**
|
|
32
|
+
/** Column width draggable (default true — unlike the bare DataTable). */
|
|
33
33
|
resizable?: boolean
|
|
34
|
-
/**
|
|
34
|
+
/** Columns reorderable via drag (default true). */
|
|
35
35
|
reorderable?: boolean
|
|
36
|
-
/**
|
|
36
|
+
/** Columns pinnable (default true). */
|
|
37
37
|
pinnable?: boolean
|
|
38
|
-
/**
|
|
38
|
+
/** Passed through to the DataTable. */
|
|
39
39
|
tableProps?: Record<string, unknown>
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
export interface ViurDataTableEmitProps<R extends Row = Row> {
|
|
43
|
-
/**
|
|
43
|
+
/** Double-click on a row. */
|
|
44
44
|
onOpen?: (payload: { row: R; key: RowKey }) => void
|
|
45
45
|
/** v-model:selectedKeys */
|
|
46
46
|
"onUpdate:selectedKeys"?: (value: RowKey[]) => void
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
/**
|
|
49
|
+
/** What ViurDataTable exposes via defineExpose. */
|
|
50
50
|
export interface ViurDataTableApi<R extends Row = Row> {
|
|
51
|
-
/**
|
|
51
|
+
/** The ListRequest (useListFetch): data, structure, cursor, execute, … */
|
|
52
52
|
list: Record<string, unknown>
|
|
53
|
-
/** Template
|
|
53
|
+
/** Template ref to the DataTable (DataTableApi). */
|
|
54
54
|
table: Ref<unknown>
|
|
55
|
-
/**
|
|
55
|
+
/** Load from the start — with the current props.params. */
|
|
56
56
|
refresh: () => Promise<unknown>
|
|
57
|
-
/**
|
|
57
|
+
/** Search: server-side (ViUR `search`) or local, if everything is loaded. */
|
|
58
58
|
search: (term: string) => void
|
|
59
|
-
/**
|
|
59
|
+
/** Field visibility (e.g. for a "Fields" dialog). */
|
|
60
60
|
columns: {
|
|
61
61
|
all: () => { id: string; label: string; hidden: boolean }[]
|
|
62
62
|
apply: (visibleIds: string[]) => void
|
|
63
63
|
}
|
|
64
|
-
/**
|
|
64
|
+
/** Label of an entry (first text value of its columns). */
|
|
65
65
|
entryTitle: (key: RowKey) => string
|
|
66
|
-
/**
|
|
66
|
+
/** Everything loaded → sorting/filtering/searching run locally. */
|
|
67
67
|
allLoaded: Ref<boolean>
|
|
68
68
|
rows: Ref<R[]>
|
|
69
69
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class="vi-vdt">
|
|
3
|
-
<!--
|
|
4
|
-
|
|
3
|
+
<!-- Structure didn't arrive (missing permission, unexpected response): columns
|
|
4
|
+
are then derived from the data — the notice stays visible. -->
|
|
5
5
|
<wa-callout
|
|
6
6
|
v-if="notice"
|
|
7
7
|
class="vi-vdt-notice"
|
|
@@ -53,61 +53,61 @@ import { computed, ref, watch } from "vue"
|
|
|
53
53
|
import { useListFetch } from "../../request/index.js"
|
|
54
54
|
import { useI18n } from "vue-i18n"
|
|
55
55
|
import DataTable from "./DataTable.vue"
|
|
56
|
-
import { columnsFromStructure } from "./viurColumns"
|
|
56
|
+
import { columnsFromStructure } from "./viurColumns.ts"
|
|
57
57
|
|
|
58
|
-
// ViurDataTable:
|
|
59
|
-
// (
|
|
60
|
-
//
|
|
61
|
-
// (
|
|
62
|
-
//
|
|
63
|
-
// X-VIUR-BONELIST (
|
|
58
|
+
// ViurDataTable: thin ViUR wrapper around the DataTable for LIST modules
|
|
59
|
+
// (counterpart to ViurBrowser for tree modules): pass `module`, done. Contains the complete
|
|
60
|
+
// list mechanics — server sorting/search, cursor-based loading, local mode
|
|
61
|
+
// (everything loaded → sorting/filtering/searching client-side), columns from the
|
|
62
|
+
// structure, field visibility incl. optional persistence and optional
|
|
63
|
+
// X-VIUR-BONELIST (server serializes only the visible fields).
|
|
64
64
|
|
|
65
65
|
const props = defineProps({
|
|
66
66
|
module: { type: String, required: true },
|
|
67
67
|
renderer: { type: String, default: null },
|
|
68
|
-
/**
|
|
68
|
+
/** Subtype of grouped lists (handler `list.grouped`). */
|
|
69
69
|
group: { type: String, default: null },
|
|
70
|
-
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
70
|
+
/** Additional request parameters — spread into EVERY fetch (e.g. the
|
|
71
|
+
* server context as @ parameters). Changes take effect from the next
|
|
72
|
+
* fetch; `refresh()` reloads immediately. */
|
|
73
73
|
params: { type: Object, default: () => ({}) },
|
|
74
|
-
/**
|
|
74
|
+
/** Columns; if not given, derived from the viewSkel structure. */
|
|
75
75
|
columns: { type: Array, default: null },
|
|
76
|
-
/**
|
|
76
|
+
/** Only these bones as columns (for derived columns). */
|
|
77
77
|
bones: { type: Array, default: null },
|
|
78
|
-
/**
|
|
78
|
+
/** Accessible label; default: module name. */
|
|
79
79
|
label: { type: String, default: null },
|
|
80
80
|
selectable: { type: [String, Boolean], default: "multiple" },
|
|
81
|
-
/**
|
|
81
|
+
/** Entries per load (ViUR `limit`). */
|
|
82
82
|
pageSize: { type: Number, default: 50 },
|
|
83
83
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
84
|
+
* Upper limit for local mode: above it, sorting/filtering/searching stay
|
|
85
|
+
* with the server (the first local search sends every row through the
|
|
86
|
+
* bone formatter — at five digits that freezes the main thread).
|
|
87
87
|
*/
|
|
88
88
|
localModeLimit: { type: Number, default: 5_000 },
|
|
89
|
-
/** X-VIUR-BONELIST
|
|
90
|
-
*
|
|
91
|
-
*
|
|
89
|
+
/** Send X-VIUR-BONELIST when fields are deselected. ONLY enable if
|
|
90
|
+
* the module supports it server-side ("*" subskel) — otherwise the
|
|
91
|
+
* core responds with 400. */
|
|
92
92
|
bonelist: { type: Boolean, default: false },
|
|
93
|
-
/** localStorage
|
|
94
|
-
*
|
|
95
|
-
*
|
|
93
|
+
/** localStorage key for field visibility (the HIDDEN list is
|
|
94
|
+
* stored: new server fields appear automatically); null = no
|
|
95
|
+
* persistence. */
|
|
96
96
|
storageKey: { type: String, default: null },
|
|
97
|
-
/**
|
|
97
|
+
/** Language/locale for bone rendering in the columns. */
|
|
98
98
|
language: { type: String, default: null },
|
|
99
|
-
/**
|
|
99
|
+
/** Comes first with language="*" (usually the UI language). */
|
|
100
100
|
primaryLanguage: { type: String, default: null },
|
|
101
101
|
locale: { type: String, default: null },
|
|
102
|
-
/**
|
|
103
|
-
*
|
|
104
|
-
* ViurDataTable
|
|
102
|
+
/** Drag columns wider/narrower via a handle in the header. Unlike the
|
|
103
|
+
* bare DataTable (opt-in there), ON by default here — the
|
|
104
|
+
* ViurDataTable is the convenience widget. */
|
|
105
105
|
resizable: { type: Boolean, default: true },
|
|
106
|
-
/**
|
|
106
|
+
/** Reorder columns via drag. */
|
|
107
107
|
reorderable: { type: Boolean, default: true },
|
|
108
|
-
/**
|
|
108
|
+
/** Pin columns left/right. */
|
|
109
109
|
pinnable: { type: Boolean, default: true },
|
|
110
|
-
/**
|
|
110
|
+
/** Passed through to the DataTable (overrides individual props). */
|
|
111
111
|
tableProps: { type: Object, default: () => ({}) },
|
|
112
112
|
})
|
|
113
113
|
|
|
@@ -119,12 +119,12 @@ const selectedKeys = defineModel("selectedKeys", { type: Array, default: () => [
|
|
|
119
119
|
|
|
120
120
|
const sort = ref([])
|
|
121
121
|
const limit = ref(Math.min(99, Number(props.params?.limit ?? props.pageSize) || 50))
|
|
122
|
-
/**
|
|
122
|
+
/** Server-side search term (ViUR `search`); empty = off. */
|
|
123
123
|
const serverSearch = ref("")
|
|
124
|
-
/** Client
|
|
124
|
+
/** Client search/filters — apply ONLY in the fully loaded state. */
|
|
125
125
|
const clientSearch = ref("")
|
|
126
126
|
const clientFilters = ref([])
|
|
127
|
-
/**
|
|
127
|
+
/** Hidden columns (field names). */
|
|
128
128
|
const hiddenFields = ref(loadHiddenFields())
|
|
129
129
|
|
|
130
130
|
const list = useListFetch(props.module, {
|
|
@@ -137,7 +137,7 @@ const list = useListFetch(props.module, {
|
|
|
137
137
|
|
|
138
138
|
const rows = computed(() => list.data.value)
|
|
139
139
|
|
|
140
|
-
/**
|
|
140
|
+
/** Everything loaded and small enough → local mode (the table's client pipeline). */
|
|
141
141
|
const allLoaded = computed(
|
|
142
142
|
() =>
|
|
143
143
|
rows.value.length > 0 &&
|
|
@@ -155,6 +155,7 @@ const effectiveColumns = computed(() => {
|
|
|
155
155
|
...(props.locale ? { locale: props.locale } : {}),
|
|
156
156
|
exclude: ["key"],
|
|
157
157
|
local: allLoaded.value,
|
|
158
|
+
boolTexts: [t("bones.bool.true"), t("bones.bool.false")],
|
|
158
159
|
})
|
|
159
160
|
if (derived.length) {
|
|
160
161
|
return derived.map((column) => ({
|
|
@@ -171,7 +172,7 @@ const notice = computed(() =>
|
|
|
171
172
|
: ""
|
|
172
173
|
)
|
|
173
174
|
|
|
174
|
-
/**
|
|
175
|
+
/** Fallback columns: the fields of the first record, unformatted. */
|
|
175
176
|
function fallbackColumns(row) {
|
|
176
177
|
if (!row) return []
|
|
177
178
|
return Object.keys(row)
|
|
@@ -179,7 +180,7 @@ function fallbackColumns(row) {
|
|
|
179
180
|
.map((field) => ({ field, label: field, sortable: false, minWidth: 120, flex: 1 }))
|
|
180
181
|
}
|
|
181
182
|
|
|
182
|
-
/**
|
|
183
|
+
/** Fetch parameters: props.params (context etc.) + search + sorting + limit. */
|
|
183
184
|
function requestParams(sortEntries = sort.value) {
|
|
184
185
|
const params = { ...props.params, limit: limit.value }
|
|
185
186
|
if (serverSearch.value) params.search = serverSearch.value
|
|
@@ -210,9 +211,9 @@ async function onLoadMore({ pages }) {
|
|
|
210
211
|
await list.next(pages).catch(() => null)
|
|
211
212
|
}
|
|
212
213
|
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
214
|
+
// Back in server mode (reload, new cursor, server search): reset local
|
|
215
|
+
// refinements and remove sortings the server can't handle
|
|
216
|
+
// (only indexed, single-valued bones).
|
|
216
217
|
watch(allLoaded, (local) => {
|
|
217
218
|
if (local) return
|
|
218
219
|
clientSearch.value = ""
|
|
@@ -223,7 +224,7 @@ watch(allLoaded, (local) => {
|
|
|
223
224
|
})
|
|
224
225
|
})
|
|
225
226
|
|
|
226
|
-
// ──
|
|
227
|
+
// ── Field visibility (+ optional persistence, + optional bonelist) ──────────
|
|
227
228
|
function loadHiddenFields() {
|
|
228
229
|
if (!props.storageKey) return []
|
|
229
230
|
try {
|
|
@@ -244,7 +245,7 @@ function bonelistHeader() {
|
|
|
244
245
|
}
|
|
245
246
|
|
|
246
247
|
const columnsApi = {
|
|
247
|
-
/**
|
|
248
|
+
/** All showable/hideable columns with their current state. */
|
|
248
249
|
all: () =>
|
|
249
250
|
effectiveColumns.value
|
|
250
251
|
.filter((column) => column.hideable !== false)
|
|
@@ -253,7 +254,7 @@ const columnsApi = {
|
|
|
253
254
|
label: String(column.label ?? column.field ?? column.id),
|
|
254
255
|
hidden: column.hidden === true,
|
|
255
256
|
})),
|
|
256
|
-
/**
|
|
257
|
+
/** Set visible columns (persisted; with bonelist a reload follows). */
|
|
257
258
|
apply: (visibleIds) => {
|
|
258
259
|
const visible = new Set(visibleIds)
|
|
259
260
|
hiddenFields.value = effectiveColumns.value
|
|
@@ -263,14 +264,14 @@ const columnsApi = {
|
|
|
263
264
|
try {
|
|
264
265
|
localStorage.setItem(props.storageKey, JSON.stringify(hiddenFields.value))
|
|
265
266
|
} catch {
|
|
266
|
-
// localStorage
|
|
267
|
+
// localStorage may be missing (private mode) — then just not persistent.
|
|
267
268
|
}
|
|
268
269
|
}
|
|
269
270
|
if (props.bonelist) void list.filter(requestParams()).catch(() => null)
|
|
270
271
|
},
|
|
271
272
|
}
|
|
272
273
|
|
|
273
|
-
/**
|
|
274
|
+
/** Server-side search — or local, if everything is loaded. */
|
|
274
275
|
function search(term) {
|
|
275
276
|
const trimmed = String(term ?? "").trim()
|
|
276
277
|
if (allLoaded.value) {
|
|
@@ -281,12 +282,12 @@ function search(term) {
|
|
|
281
282
|
}
|
|
282
283
|
}
|
|
283
284
|
|
|
284
|
-
/**
|
|
285
|
+
/** Load from the start — with the CURRENT props.params (context etc.). */
|
|
285
286
|
function refresh() {
|
|
286
287
|
return list.filter(requestParams())
|
|
287
288
|
}
|
|
288
289
|
|
|
289
|
-
/**
|
|
290
|
+
/** Label of an entry: first text value of its columns, otherwise the key. */
|
|
290
291
|
function entryTitle(key) {
|
|
291
292
|
const row = rows.value.find((entry) => String(entry.key) === String(key))
|
|
292
293
|
if (!row) return String(key)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// Pure column/cell logic: resolving definitions, comparison, filters,
|
|
2
|
+
// facets, aggregation. Deliberately free of Vue — so the whole
|
|
3
|
+
// data path is testable without a DOM.
|
|
4
4
|
|
|
5
5
|
import type {
|
|
6
6
|
AggregationKind,
|
|
@@ -13,9 +13,9 @@ import type {
|
|
|
13
13
|
Row,
|
|
14
14
|
SortEntry,
|
|
15
15
|
SortMode,
|
|
16
|
-
} from "./types"
|
|
16
|
+
} from "./types.ts"
|
|
17
17
|
|
|
18
|
-
/**
|
|
18
|
+
/** Read a value via a dot path ("user.name"). */
|
|
19
19
|
export function getPath(row: Row, path: string): unknown {
|
|
20
20
|
if (!path.includes(".")) return row[path]
|
|
21
21
|
let current: unknown = row
|
|
@@ -75,7 +75,7 @@ export function findColumn<R extends Row>(columns: ResolvedColumn<R>[], id: stri
|
|
|
75
75
|
return columns.find((column) => column.id === id)
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
// ──
|
|
78
|
+
// ── Sorting ───────────────────────────────────────────────────────────
|
|
79
79
|
|
|
80
80
|
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" })
|
|
81
81
|
const collatorCase = new Intl.Collator(undefined, { numeric: true })
|
|
@@ -92,11 +92,11 @@ export function isEmptyValue(value: unknown): boolean {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
95
|
+
* Reduce composite cell values to the sort atom: for arrays the FIRST entry
|
|
96
|
+
* counts, for plain objects the FIRST value — recursively, until a primitive
|
|
97
|
+
* (or Date) remains. This way `multiple` bones, multilingual values
|
|
98
|
+
* ({ de: "…" }) and relational bones ({ dest: { … } }) sort by their
|
|
99
|
+
* respective first value instead of by "[object Object]".
|
|
100
100
|
*/
|
|
101
101
|
export function firstValueOf(value: unknown, depth = 0): unknown {
|
|
102
102
|
if (depth > 10 || value === null || value === undefined) return value
|
|
@@ -108,7 +108,7 @@ export function firstValueOf(value: unknown, depth = 0): unknown {
|
|
|
108
108
|
return value
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
/**
|
|
111
|
+
/** Compare two cell values in one of the built-in modes (ascending). */
|
|
112
112
|
export function compareValues(a: unknown, b: unknown, mode: SortMode = "auto"): number {
|
|
113
113
|
switch (mode) {
|
|
114
114
|
case "basic":
|
|
@@ -135,7 +135,7 @@ export function compareValues(a: unknown, b: unknown, mode: SortMode = "auto"):
|
|
|
135
135
|
return collatorCase.compare(defaultText(a), defaultText(b))
|
|
136
136
|
case "auto":
|
|
137
137
|
default: {
|
|
138
|
-
// Arrays/
|
|
138
|
+
// Arrays/objects: sort by the first value (see firstValueOf).
|
|
139
139
|
if (a !== null && typeof a === "object" && !(a instanceof Date)) a = firstValueOf(a)
|
|
140
140
|
if (b !== null && typeof b === "object" && !(b instanceof Date)) b = firstValueOf(b)
|
|
141
141
|
if (typeof a === "number" && typeof b === "number") return a - b
|
|
@@ -147,8 +147,8 @@ export function compareValues(a: unknown, b: unknown, mode: SortMode = "auto"):
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
/**
|
|
150
|
-
*
|
|
151
|
-
*
|
|
150
|
+
* Stably sorts a row list by multiple columns. Empty values end up at the
|
|
151
|
+
* front or back according to the column's `sortUndefined` (default: compared normally).
|
|
152
152
|
*/
|
|
153
153
|
export function sortRows<R extends Row>(rows: R[], sort: SortEntry[], columns: ResolvedColumn<R>[]): R[] {
|
|
154
154
|
if (!sort.length) return rows
|
|
@@ -157,7 +157,7 @@ export function sortRows<R extends Row>(rows: R[], sort: SortEntry[], columns: R
|
|
|
157
157
|
.filter((item): item is { entry: SortEntry; column: ResolvedColumn<R> } => Boolean(item.column))
|
|
158
158
|
if (!entries.length) return rows
|
|
159
159
|
|
|
160
|
-
//
|
|
160
|
+
// Carry the index along → stable sorting regardless of the engine.
|
|
161
161
|
return rows
|
|
162
162
|
.map((row, index) => ({ row, index }))
|
|
163
163
|
.sort((left, right) => {
|
|
@@ -181,9 +181,9 @@ export function sortRows<R extends Row>(rows: R[], sort: SortEntry[], columns: R
|
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
/**
|
|
184
|
-
*
|
|
185
|
-
* asc → desc →
|
|
186
|
-
*
|
|
184
|
+
* Next sort state when clicking a column header.
|
|
185
|
+
* asc → desc → off (or cycling endlessly with `withoutSortRemoval`),
|
|
186
|
+
* multi-column with Shift (limited by `maxMultiSort`, 0 = unlimited).
|
|
187
187
|
*/
|
|
188
188
|
export function nextSortState(
|
|
189
189
|
current: SortEntry[],
|
|
@@ -211,7 +211,7 @@ export function nextSortState(
|
|
|
211
211
|
return max > 0 ? combined.slice(-max) : combined
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
// ── Filter &
|
|
214
|
+
// ── Filter & search ───────────────────────────────────────────────────
|
|
215
215
|
|
|
216
216
|
function dayStart(value: unknown): number {
|
|
217
217
|
const time = toTime(value)
|
|
@@ -220,7 +220,7 @@ function dayStart(value: unknown): number {
|
|
|
220
220
|
return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime()
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
-
/**
|
|
223
|
+
/** Is a filter value "empty" (then the column doesn't filter)? */
|
|
224
224
|
export function isEmptyFilter(value: unknown): boolean {
|
|
225
225
|
if (value === null || value === undefined || value === "") return true
|
|
226
226
|
if (Array.isArray(value)) return value.every((item) => isEmptyFilter(item))
|
|
@@ -286,11 +286,11 @@ export function rowMatchesFilters<R extends Row>(
|
|
|
286
286
|
return true
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
-
//
|
|
290
|
-
// (
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
289
|
+
// Search text cache per (column, row): `column.text` runs through the formatter
|
|
290
|
+
// (for ViUR columns: language resolution, multiple joins, format strings) — without
|
|
291
|
+
// a cache, searching a large, fully loaded dataset freezes the
|
|
292
|
+
// browser (rows × columns × keystroke). WeakMaps: the cache lives as
|
|
293
|
+
// long as the column or row and cleans up after itself.
|
|
294
294
|
const searchTextCache = new WeakMap<object, WeakMap<object, string>>()
|
|
295
295
|
|
|
296
296
|
function searchTextOf<R extends Row>(column: ResolvedColumn<R>, row: R): string {
|
|
@@ -323,9 +323,9 @@ export function rowMatchesSearch<R extends Row>(
|
|
|
323
323
|
}
|
|
324
324
|
|
|
325
325
|
/**
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
*
|
|
326
|
+
* Facets of a column: distinct values with hit count and numeric
|
|
327
|
+
* range — computed over the rows that pass all OTHER filters (and the search).
|
|
328
|
+
* That's exactly what a set filter needs so it doesn't empty itself.
|
|
329
329
|
*/
|
|
330
330
|
export function computeFacets<R extends Row>(rows: R[], column: ResolvedColumn<R>): ColumnFacets {
|
|
331
331
|
const counts = new Map<string, number>()
|
|
@@ -354,7 +354,7 @@ export function computeFacets<R extends Row>(rows: R[], column: ResolvedColumn<R
|
|
|
354
354
|
return { values, min, max }
|
|
355
355
|
}
|
|
356
356
|
|
|
357
|
-
// ── Aggregation (
|
|
357
|
+
// ── Aggregation (group rows) ──────────────────────────────────────────
|
|
358
358
|
|
|
359
359
|
function numbers(values: unknown[]): number[] {
|
|
360
360
|
return values.map((value) => Number(value)).filter((value) => !Number.isNaN(value))
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Public surface of the DataTable widget.
|
|
2
2
|
export { default as DataTable } from "./DataTable.vue"
|
|
3
3
|
export { default as ColumnFilterPanel } from "./ColumnFilterPanel.vue"
|
|
4
|
-
export * from "./types"
|
|
4
|
+
export * from "./types.ts"
|
|
5
5
|
export {
|
|
6
6
|
aggregate,
|
|
7
7
|
aggregateColumn,
|
|
@@ -19,7 +19,7 @@ export {
|
|
|
19
19
|
rowMatchesFilters,
|
|
20
20
|
rowMatchesSearch,
|
|
21
21
|
sortRows,
|
|
22
|
-
} from "./columns"
|
|
22
|
+
} from "./columns.ts"
|
|
23
23
|
export {
|
|
24
24
|
buildRows,
|
|
25
25
|
collectKeys,
|
|
@@ -37,10 +37,10 @@ export {
|
|
|
37
37
|
type NodeLocation,
|
|
38
38
|
type OrderValue,
|
|
39
39
|
type SelectionState,
|
|
40
|
-
} from "./rows"
|
|
41
|
-
export { useColumnLayout, type ColumnLayout, type LayoutColumn } from "./useColumnLayout"
|
|
42
|
-
export { useGridNavigation } from "./useGridNavigation"
|
|
43
|
-
export { useVirtualRows } from "./useVirtualRows"
|
|
40
|
+
} from "./rows.ts"
|
|
41
|
+
export { useColumnLayout, type ColumnLayout, type LayoutColumn } from "./useColumnLayout.ts"
|
|
42
|
+
export { useGridNavigation } from "./useGridNavigation.ts"
|
|
43
|
+
export { useVirtualRows } from "./useVirtualRows.ts"
|
|
44
44
|
export {
|
|
45
45
|
badgesForBone,
|
|
46
46
|
boneFamily,
|
|
@@ -52,9 +52,9 @@ export {
|
|
|
52
52
|
type BoneDescription,
|
|
53
53
|
type BoneStructure,
|
|
54
54
|
type ColumnsFromStructureOptions,
|
|
55
|
-
} from "./viurColumns"
|
|
56
|
-
// ViurDataTable
|
|
57
|
-
// ViurDataTable.d.vue.ts (
|
|
58
|
-
//
|
|
59
|
-
// @ts-expect-error — JS
|
|
55
|
+
} from "./viurColumns.ts"
|
|
56
|
+
// ViurDataTable is (like ViurBrowser) a JS SFC — its types live in
|
|
57
|
+
// ViurDataTable.d.vue.ts (import: ".../DataTable/ViurDataTable.d.vue", as
|
|
58
|
+
// with ViForm).
|
|
59
|
+
// @ts-expect-error — JS SFC without embedded typing
|
|
60
60
|
export { default as ViurDataTable } from "./ViurDataTable.vue"
|