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,25 +1,25 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// (
|
|
1
|
+
// Column layout: order, widths, visibility, pinning — plus the
|
|
2
|
+
// pointer gestures for resize and reorder. Everything in ONE reactive state object
|
|
3
|
+
// (analogous to the component rule), so the component only has to render.
|
|
4
4
|
|
|
5
5
|
import { computed, reactive } from "vue"
|
|
6
|
-
import type { PinSide, ResolvedColumn, Row } from "./types"
|
|
6
|
+
import type { PinSide, ResolvedColumn, Row } from "./types.ts"
|
|
7
7
|
|
|
8
8
|
const DEFAULT_WIDTH = 160
|
|
9
9
|
const DEFAULT_MIN_WIDTH = 64
|
|
10
10
|
|
|
11
11
|
export interface LayoutColumn<R extends Row = Row> {
|
|
12
12
|
column: ResolvedColumn<R>
|
|
13
|
-
/** Index in
|
|
13
|
+
/** Index in the visible order. */
|
|
14
14
|
index: number
|
|
15
|
-
/**
|
|
15
|
+
/** Effective width in px (measured or set). */
|
|
16
16
|
width: number
|
|
17
|
-
/**
|
|
17
|
+
/** Column width for grid-template-columns. */
|
|
18
18
|
track: string
|
|
19
19
|
pinned: PinSide | false
|
|
20
|
-
/** Sticky
|
|
20
|
+
/** Sticky offset to the respective edge (px). */
|
|
21
21
|
offset: number
|
|
22
|
-
/**
|
|
22
|
+
/** Last column pinned at the start or first pinned at the end (for the divider). */
|
|
23
23
|
pinEdge: boolean
|
|
24
24
|
resizable: boolean
|
|
25
25
|
movable: boolean
|
|
@@ -45,19 +45,19 @@ function clampWidth<R extends Row>(column: ResolvedColumn<R>, width: number): nu
|
|
|
45
45
|
|
|
46
46
|
export function useColumnLayout<R extends Row = Row>(options: ColumnLayoutOptions<R>) {
|
|
47
47
|
const state = reactive({
|
|
48
|
-
/**
|
|
48
|
+
/** User-set order (ids). Empty = order of the definitions. */
|
|
49
49
|
order: [] as string[],
|
|
50
|
-
/**
|
|
50
|
+
/** Widths dragged by the user. */
|
|
51
51
|
widths: {} as Record<string, number>,
|
|
52
|
-
/**
|
|
52
|
+
/** Visibility; if an entry is missing, `hidden` from the definition applies. */
|
|
53
53
|
visibility: {} as Record<string, boolean>,
|
|
54
|
-
/** Pinning;
|
|
54
|
+
/** Pinning; if an entry is missing, `pinned` from the definition applies. */
|
|
55
55
|
pinning: {} as Record<string, PinSide | false>,
|
|
56
|
-
/**
|
|
56
|
+
/** Widths measured in the DOM — basis for the sticky offsets. */
|
|
57
57
|
measured: {} as Record<string, number>,
|
|
58
|
-
/**
|
|
58
|
+
/** Ongoing resize gesture. */
|
|
59
59
|
resizing: null as { id: string; startX: number; startWidth: number } | null,
|
|
60
|
-
/**
|
|
60
|
+
/** Ongoing reorder gesture. */
|
|
61
61
|
dragging: null as { id: string; overId: string | null; x: number; y: number } | null,
|
|
62
62
|
|
|
63
63
|
ordered: computed((): ResolvedColumn<R>[] => {
|
|
@@ -72,11 +72,11 @@ export function useColumnLayout<R extends Row = Row>(options: ColumnLayoutOption
|
|
|
72
72
|
byId.delete(id)
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
-
//
|
|
75
|
+
// New columns unknown to the saved order go at the end.
|
|
76
76
|
return [...sorted, ...byId.values()]
|
|
77
77
|
}),
|
|
78
78
|
|
|
79
|
-
/**
|
|
79
|
+
/** Visible columns in display order: pinned (start) → free → pinned (end). */
|
|
80
80
|
visibleColumns: computed((): LayoutColumn<R>[] => {
|
|
81
81
|
const visible = state.ordered.filter((column: ResolvedColumn<R>) => isVisible(column))
|
|
82
82
|
const pinnedStart = visible.filter((column: ResolvedColumn<R>) => pinOf(column) === "start")
|
|
@@ -128,15 +128,15 @@ export function useColumnLayout<R extends Row = Row>(options: ColumnLayoutOption
|
|
|
128
128
|
return state.pinning[column.id] ?? column.def.pinned ?? false
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
/**
|
|
131
|
+
/** Effective width: dragged > measured > defined > default. */
|
|
132
132
|
function widthOf(column: ResolvedColumn<R>): number {
|
|
133
133
|
const width = state.widths[column.id] ?? state.measured[column.id] ?? column.def.width ?? DEFAULT_WIDTH
|
|
134
134
|
return clampWidth(column, width)
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
function trackOf(column: ResolvedColumn<R>): string {
|
|
138
|
-
//
|
|
139
|
-
//
|
|
138
|
+
// A width dragged by the user ALWAYS wins — even over flex, otherwise
|
|
139
|
+
// a flex column couldn't be resized by hand.
|
|
140
140
|
const dragged = state.widths[column.id]
|
|
141
141
|
if (dragged !== undefined) return `${clampWidth(column, dragged)}px`
|
|
142
142
|
const fixed = column.def.width
|
|
@@ -153,7 +153,7 @@ export function useColumnLayout<R extends Row = Row>(options: ColumnLayoutOption
|
|
|
153
153
|
state.measured[id] = width
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
// ──
|
|
156
|
+
// ── Visibility ──────────────────────────────────────────────────────
|
|
157
157
|
function toggleColumn(id: string, visible?: boolean, silent = false): void {
|
|
158
158
|
const column = options.columns().find((item) => item.id === id)
|
|
159
159
|
if (!column) return
|
|
@@ -164,8 +164,8 @@ export function useColumnLayout<R extends Row = Row>(options: ColumnLayoutOption
|
|
|
164
164
|
|
|
165
165
|
// ── Pinning ─────────────────────────────────────────────────────────
|
|
166
166
|
function pinColumn(id: string, side: PinSide | false, silent = false): void {
|
|
167
|
-
//
|
|
168
|
-
//
|
|
167
|
+
// Without a fixed width, freeze the current width — otherwise the column
|
|
168
|
+
// jumps when pinned, because the sticky offset needs a px width.
|
|
169
169
|
const column = options.columns().find((item) => item.id === id)
|
|
170
170
|
if (column && side && state.widths[id] === undefined && column.def.width === undefined) {
|
|
171
171
|
state.widths[id] = widthOf(column)
|
|
@@ -179,7 +179,7 @@ export function useColumnLayout<R extends Row = Row>(options: ColumnLayoutOption
|
|
|
179
179
|
return column ? pinOf(column) : false
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
// ──
|
|
182
|
+
// ── Order ───────────────────────────────────────────────────────────
|
|
183
183
|
function currentOrder(): string[] {
|
|
184
184
|
return state.ordered.map((column: ResolvedColumn<R>) => column.id)
|
|
185
185
|
}
|
|
@@ -195,7 +195,7 @@ export function useColumnLayout<R extends Row = Row>(options: ColumnLayoutOption
|
|
|
195
195
|
options.onMove?.(order, finished)
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
/**
|
|
198
|
+
/** Move a column by `delta` positions (keyboard: Shift + ←/→). */
|
|
199
199
|
function shiftColumn(id: string, delta: number): void {
|
|
200
200
|
const visible = state.visibleColumns.map((item: LayoutColumn<R>) => item.column.id)
|
|
201
201
|
const index = visible.indexOf(id)
|
|
@@ -250,7 +250,7 @@ export function useColumnLayout<R extends Row = Row>(options: ColumnLayoutOption
|
|
|
250
250
|
if (active?.overId && active.overId !== active.id) moveColumn(active.id, active.overId, true)
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
// ──
|
|
253
|
+
// ── Reset / state ───────────────────────────────────────────────────
|
|
254
254
|
function resetColumns(): void {
|
|
255
255
|
state.order = []
|
|
256
256
|
state.widths = {}
|
|
@@ -258,7 +258,7 @@ export function useColumnLayout<R extends Row = Row>(options: ColumnLayoutOption
|
|
|
258
258
|
state.pinning = {}
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
-
/**
|
|
261
|
+
/** Remove column ids that no longer exist from the state. */
|
|
262
262
|
function pruneUnknown(): void {
|
|
263
263
|
const ids = new Set(options.columns().map((column) => column.id))
|
|
264
264
|
state.order = state.order.filter((id) => ids.has(id))
|
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// Keyboard navigation following the ARIA grid pattern: the whole grid is ONE
|
|
2
|
+
// tab stop; inside it a roving tabindex moves across the cells.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
4
|
+
// The composable only knows coordinates (row -1 = header row, 0..n-1 = body)
|
|
5
|
+
// and calls the component's callbacks for all domain logic.
|
|
6
6
|
|
|
7
7
|
import { nextTick, reactive, type Ref } from "vue"
|
|
8
8
|
|
|
9
9
|
export interface GridNavigationOptions {
|
|
10
10
|
rowCount: () => number
|
|
11
11
|
colCount: () => number
|
|
12
|
-
/**
|
|
12
|
+
/** Rows per page for Page Up/Down. */
|
|
13
13
|
pageRows?: () => number
|
|
14
|
-
/** Enter
|
|
14
|
+
/** Enter on a header cell. */
|
|
15
15
|
onHeaderActivate?: (col: number, shift: boolean) => void
|
|
16
|
-
/** Enter
|
|
16
|
+
/** Enter on a data cell. */
|
|
17
17
|
onCellActivate?: (row: number, col: number) => void
|
|
18
|
-
/**
|
|
18
|
+
/** Space on a row. */
|
|
19
19
|
onToggleSelect?: (row: number) => void
|
|
20
20
|
/** Shift + ↑/↓ */
|
|
21
21
|
onExtendSelection?: (row: number) => void
|
|
22
|
-
/**
|
|
22
|
+
/** Ctrl + A */
|
|
23
23
|
onSelectAll?: () => void
|
|
24
|
-
/**
|
|
24
|
+
/** Ctrl + C */
|
|
25
25
|
onCopy?: () => void
|
|
26
|
-
/** Shift + F10 /
|
|
26
|
+
/** Shift + F10 / context menu key */
|
|
27
27
|
onContextMenu?: (row: number, col: number) => void
|
|
28
|
-
/** Alt + ↑/↓
|
|
28
|
+
/** Alt + ↑/↓ on a data row (fixed order). */
|
|
29
29
|
onMoveRow?: (row: number, delta: number) => void
|
|
30
|
-
/** Shift + ←/→
|
|
30
|
+
/** Shift + ←/→ on a header cell. */
|
|
31
31
|
onMoveColumn?: (col: number, delta: number) => void
|
|
32
|
-
/** Alt + ←/→
|
|
32
|
+
/** Alt + ←/→ on a header cell. */
|
|
33
33
|
onResizeColumn?: (col: number, delta: number) => void
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -37,7 +37,7 @@ const RESIZE_STEP = 24
|
|
|
37
37
|
|
|
38
38
|
export function useGridNavigation(scroller: Ref<HTMLElement | undefined>, options: GridNavigationOptions) {
|
|
39
39
|
const state = reactive({
|
|
40
|
-
/** -1 =
|
|
40
|
+
/** -1 = header row. */
|
|
41
41
|
row: -1,
|
|
42
42
|
col: 0,
|
|
43
43
|
})
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Virtualization: only the visible window (plus overscan) is rendered.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// so
|
|
3
|
+
// Heights are not the same everywhere — detail panels may be arbitrarily tall.
|
|
4
|
+
// That's why a prefix-sum array over the row heights is maintained (measured
|
|
5
|
+
// heights override the default height) and binary-searched. The window
|
|
6
|
+
// is positioned via top/bottom padding, not via absolute positions —
|
|
7
|
+
// so grid columns and sticky pinning remain untouched.
|
|
8
8
|
|
|
9
9
|
import { computed, reactive, type Ref } from "vue"
|
|
10
|
-
import type { DisplayRow, Row } from "./types"
|
|
10
|
+
import type { DisplayRow, Row } from "./types.ts"
|
|
11
11
|
|
|
12
12
|
export interface VirtualRowsOptions<R extends Row = Row> {
|
|
13
13
|
rows: () => DisplayRow<R>[]
|
|
@@ -25,7 +25,7 @@ export function useVirtualRows<R extends Row = Row>(
|
|
|
25
25
|
const state = reactive({
|
|
26
26
|
scrollTop: 0,
|
|
27
27
|
viewportHeight: 0,
|
|
28
|
-
/**
|
|
28
|
+
/** Measured heights (only for rows that deviate from the default height). */
|
|
29
29
|
heights: {} as Record<string, number>,
|
|
30
30
|
|
|
31
31
|
offsets: computed((): number[] => {
|
|
@@ -74,7 +74,7 @@ export function useVirtualRows<R extends Row = Row>(
|
|
|
74
74
|
}),
|
|
75
75
|
})
|
|
76
76
|
|
|
77
|
-
/**
|
|
77
|
+
/** Largest index whose offset is <= position (binary search). */
|
|
78
78
|
function findIndex(offsets: number[], position: number): number {
|
|
79
79
|
let low = 0
|
|
80
80
|
let high = offsets.length - 1
|
|
@@ -97,7 +97,7 @@ export function useVirtualRows<R extends Row = Row>(
|
|
|
97
97
|
syncViewport()
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
/**
|
|
100
|
+
/** Store the height of a deviating row (detail panel). */
|
|
101
101
|
function measureRow(key: string, height: number): void {
|
|
102
102
|
if (height <= 0) return
|
|
103
103
|
if (Math.abs((state.heights[key] ?? 0) - height) < 1) return
|
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
import type { CellBadge, DataTableColumn, Row } from "./types"
|
|
2
|
-
import { compareValues, firstValueOf } from "./columns"
|
|
3
|
-
// @ts-expect-error – JS
|
|
1
|
+
import type { CellBadge, DataTableColumn, Row } from "./types.ts"
|
|
2
|
+
import { compareValues, firstValueOf } from "./columns.ts"
|
|
3
|
+
// @ts-expect-error – JS module without types
|
|
4
4
|
import Utils from "../../forms/bones/utils.js"
|
|
5
5
|
|
|
6
6
|
// --------------------------------------------------------------------------- //
|
|
7
|
-
// ViUR
|
|
7
|
+
// ViUR structure → DataTable columns //
|
|
8
8
|
// --------------------------------------------------------------------------- //
|
|
9
9
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// "select", "relational.…"), `multiple`, `languages`, `indexed`, `visible`
|
|
13
|
-
//
|
|
14
|
-
// `format`
|
|
10
|
+
// Besides the records, a ViUR list delivers its `structure`: per bone an
|
|
11
|
+
// object with `descr` (label), `type` ("str", "str.email", "numeric",
|
|
12
|
+
// "select", "relational.…"), `multiple`, `languages`, `indexed`, `visible` and
|
|
13
|
+
// type-specific extras (`values` for select, `precision` for numeric,
|
|
14
|
+
// `format` for relational bones).
|
|
15
15
|
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// (src/forms/bones/utils.js) —
|
|
19
|
-
//
|
|
16
|
+
// This turns them into columns: label, alignment, sortability and
|
|
17
|
+
// a `formatter`. The value → text conversion is done by `Utils.boneToString`
|
|
18
|
+
// (src/forms/bones/utils.js) — the ONE place for it, which the forms and the
|
|
19
|
+
// bones' format strings use as well.
|
|
20
20
|
|
|
21
|
-
/**
|
|
21
|
+
/** A bone of the structure, as delivered by the renderer. */
|
|
22
22
|
export interface BoneDescription {
|
|
23
23
|
descr?: string
|
|
24
24
|
type?: string
|
|
25
25
|
multiple?: boolean
|
|
26
26
|
languages?: string[] | null
|
|
27
|
-
/**
|
|
27
|
+
/** Only indexed bones can be sorted server-side. */
|
|
28
28
|
indexed?: boolean
|
|
29
29
|
visible?: boolean
|
|
30
30
|
readonly?: boolean
|
|
31
|
-
/** select:
|
|
31
|
+
/** select: value → label. */
|
|
32
32
|
values?: Record<string, string> | [string, string][]
|
|
33
|
-
/** numeric:
|
|
33
|
+
/** numeric: decimal places. */
|
|
34
34
|
precision?: number
|
|
35
|
-
/** date:
|
|
35
|
+
/** date: what the bone actually holds. */
|
|
36
36
|
date?: boolean
|
|
37
37
|
time?: boolean
|
|
38
38
|
params?: Record<string, unknown> | null
|
|
@@ -43,49 +43,49 @@ export type BoneStructure = Record<string, BoneDescription>
|
|
|
43
43
|
|
|
44
44
|
export interface ColumnsFromStructureOptions<R extends Row = Row> {
|
|
45
45
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
46
|
+
* Language of multilingual bones: a code shows that language (empty →
|
|
47
|
+
* first filled one, marked with its flag), `"*"` shows ALL filled
|
|
48
|
+
* languages with a flag prefix; if unset, the bone's first language.
|
|
49
49
|
*/
|
|
50
50
|
language?: string | null
|
|
51
|
-
/**
|
|
51
|
+
/** Comes first in `"*"` mode — usually the UI language. */
|
|
52
52
|
primaryLanguage?: string | null
|
|
53
|
-
/**
|
|
53
|
+
/** Locale for numbers and dates. */
|
|
54
54
|
locale?: string
|
|
55
|
-
/**
|
|
55
|
+
/** Only these bones, in this order (default: all visible ones). */
|
|
56
56
|
bones?: string[]
|
|
57
|
-
/**
|
|
57
|
+
/** Never show these bones (in addition to the ones hidden anyway). */
|
|
58
58
|
exclude?: string[]
|
|
59
|
-
/**
|
|
59
|
+
/** Labels for `bool` values. */
|
|
60
60
|
boolTexts?: [string, string]
|
|
61
|
-
/**
|
|
61
|
+
/** Extends/overrides the generated column per bone. */
|
|
62
62
|
overrides?: Record<string, Partial<DataTableColumn<R>>>
|
|
63
63
|
/**
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
64
|
+
* The COMPLETE dataset is in the client (everything loaded): sorting,
|
|
65
|
+
* searching and column filters then run locally on all columns — without
|
|
66
|
+
* the server. Without the flag both stay off, because on a PARTIALLY loaded
|
|
67
|
+
* slice local sorting/filtering would be a lie; there the server sorts
|
|
68
|
+
* (indexed bones only, via the table events).
|
|
69
69
|
*/
|
|
70
70
|
local?: boolean
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
/** Bones
|
|
73
|
+
/** Bones that have no place in a list. */
|
|
74
74
|
const HIDDEN_TYPES = ["password", "credential"]
|
|
75
75
|
|
|
76
76
|
// --------------------------------------------------------------------------- //
|
|
77
|
-
//
|
|
77
|
+
// Badges for plain select and bool bones //
|
|
78
78
|
// --------------------------------------------------------------------------- //
|
|
79
79
|
//
|
|
80
|
-
//
|
|
81
|
-
// bool
|
|
82
|
-
// NEGATIVE
|
|
83
|
-
// (
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
80
|
+
// Plain choice values ("select" without subtype — country & co. stay text) and
|
|
81
|
+
// bool values are rendered as colored badges: POSITIVE values green,
|
|
82
|
+
// NEGATIVE red, all other options get rainbow-graded hues
|
|
83
|
+
// (evenly across the color wheel, stable per option order). Whether a value
|
|
84
|
+
// is positive/negative is decided by the keyword list below — both option
|
|
85
|
+
// KEYS and LABELS are checked, regardless of upper/lower
|
|
86
|
+
// case ("Ja", "ja", "yes", "Aktiv", "visible", …).
|
|
87
87
|
|
|
88
|
-
/** Positive
|
|
88
|
+
/** Positive values → green. Extensible (e.g. `POSITIVE_KEYWORDS.add("live")`). */
|
|
89
89
|
export const POSITIVE_KEYWORDS = new Set([
|
|
90
90
|
"yes",
|
|
91
91
|
"ja",
|
|
@@ -109,7 +109,7 @@ export const POSITIVE_KEYWORDS = new Set([
|
|
|
109
109
|
"online",
|
|
110
110
|
])
|
|
111
111
|
|
|
112
|
-
/** Negative
|
|
112
|
+
/** Negative values → red. */
|
|
113
113
|
export const NEGATIVE_KEYWORDS = new Set([
|
|
114
114
|
"no",
|
|
115
115
|
"nein",
|
|
@@ -151,7 +151,7 @@ function keywordTone(...candidates: unknown[]): string | null {
|
|
|
151
151
|
return null
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
/** select
|
|
154
|
+
/** Normalize select `values`: [ [key, label], … ]. */
|
|
155
155
|
function selectOptions(bone: BoneDescription): [string, string][] {
|
|
156
156
|
const values = bone.values
|
|
157
157
|
if (Array.isArray(values)) return values.map(([key, label]) => [String(key), String(label)])
|
|
@@ -160,9 +160,9 @@ function selectOptions(bone: BoneDescription): [string, string][] {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
/**
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
* (
|
|
163
|
+
* Color mapping for the options of a plain select bone: keywords
|
|
164
|
+
* decide green/red, the remaining options share the color wheel
|
|
165
|
+
* (rainbow), in the order of their definition.
|
|
166
166
|
*/
|
|
167
167
|
export function selectToneMap(bone: BoneDescription): Map<string, { label: string; color: string }> {
|
|
168
168
|
const options = selectOptions(bone)
|
|
@@ -174,7 +174,7 @@ export function selectToneMap(bone: BoneDescription): Map<string, { label: strin
|
|
|
174
174
|
else neutrals.push([key, label])
|
|
175
175
|
}
|
|
176
176
|
neutrals.forEach(([key, label], index) => {
|
|
177
|
-
//
|
|
177
|
+
// Rainbow: evenly distributed hues, fixed saturation/lightness.
|
|
178
178
|
const hue = Math.round((360 / Math.max(1, neutrals.length)) * index)
|
|
179
179
|
map.set(key, { label, color: `oklch(0.62 0.16 ${hue})` })
|
|
180
180
|
})
|
|
@@ -182,8 +182,8 @@ export function selectToneMap(bone: BoneDescription): Map<string, { label: strin
|
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
/**
|
|
185
|
-
*
|
|
186
|
-
*
|
|
185
|
+
* Badge function for a bone — or null if the bone doesn't get any.
|
|
186
|
+
* Only PLAIN selects (`type === "select"`, no select.country or similar) and bools.
|
|
187
187
|
*/
|
|
188
188
|
export function badgesForBone(
|
|
189
189
|
bone: BoneDescription,
|
|
@@ -211,21 +211,21 @@ export function badgesForBone(
|
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
/**
|
|
214
|
+
/** Type family of a bone: "str.email" → "str". */
|
|
215
215
|
export function boneFamily(type: string | undefined | null): string {
|
|
216
216
|
return Utils.boneFamily(type) as string
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
/**
|
|
220
|
-
* Bone
|
|
221
|
-
*
|
|
222
|
-
*
|
|
220
|
+
* Bone value of a record as text — with languages, `multiple` and the
|
|
221
|
+
* format string of relational bones. Thin wrapper around `Utils.boneToString`
|
|
222
|
+
* so table code can stay with the DataTable types.
|
|
223
223
|
*/
|
|
224
224
|
export function boneText(value: unknown, bone: BoneDescription, options: ColumnsFromStructureOptions = {}): string {
|
|
225
225
|
return Utils.boneToString(value, bone, options) as string
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
-
/**
|
|
228
|
+
/** Local column filter matching the bone type. */
|
|
229
229
|
function localFilterFor(bone: BoneDescription, options: ColumnsFromStructureOptions): Partial<DataTableColumn> {
|
|
230
230
|
const family = boneFamily(bone.type)
|
|
231
231
|
if (bone.type === "bool") {
|
|
@@ -251,10 +251,10 @@ function localFilterFor(bone: BoneDescription, options: ColumnsFromStructureOpti
|
|
|
251
251
|
const isEmptyValue = (value: unknown): boolean => value === null || value === undefined || value === ""
|
|
252
252
|
|
|
253
253
|
/**
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
254
|
+
* Sort value of a MULTILINGUAL bone: the same language the display puts
|
|
255
|
+
* first — the requested one (or the primary one for `"*"`), and if that is
|
|
256
|
+
* empty, the first filled one. This way the column sorts by the text the
|
|
257
|
+
* cell actually shows, not blindly by the bone's first language.
|
|
258
258
|
*/
|
|
259
259
|
function languageSortValue(value: unknown, languages: string[], options: ColumnsFromStructureOptions): unknown {
|
|
260
260
|
if (!value || typeof value !== "object" || Array.isArray(value)) return value
|
|
@@ -267,18 +267,18 @@ function languageSortValue(value: unknown, languages: string[], options: Columns
|
|
|
267
267
|
return record[lang]
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
-
/**
|
|
270
|
+
/** Numbers right, everything else left — as usual in tables. */
|
|
271
271
|
function alignOf(bone: BoneDescription): "start" | "end" {
|
|
272
272
|
return boneFamily(bone.type) === "numeric" && !bone.multiple ? "end" : "start"
|
|
273
273
|
}
|
|
274
274
|
|
|
275
275
|
/**
|
|
276
|
-
*
|
|
276
|
+
* Columns from a ViUR structure.
|
|
277
277
|
*
|
|
278
|
-
*
|
|
279
|
-
* `multiple`
|
|
280
|
-
*
|
|
281
|
-
*
|
|
278
|
+
* A column only becomes sortable if the bone is `indexed` and neither
|
|
279
|
+
* `multiple` nor multilingual — the server can't order by anything else.
|
|
280
|
+
* Search and column filters stay off: for a list the server handles those,
|
|
281
|
+
* the caller wires them up via the table's events.
|
|
282
282
|
*/
|
|
283
283
|
export function columnsFromStructure<R extends Row = Row>(
|
|
284
284
|
structure: BoneStructure | null | undefined,
|
|
@@ -295,8 +295,8 @@ export function columnsFromStructure<R extends Row = Row>(
|
|
|
295
295
|
if (bone.visible === false) continue
|
|
296
296
|
if (HIDDEN_TYPES.includes(boneFamily(bone.type))) continue
|
|
297
297
|
|
|
298
|
-
// Server
|
|
299
|
-
//
|
|
298
|
+
// Server mode: only indexed bones are sortable; locally (everything
|
|
299
|
+
// loaded) every column may be sorted, searched and filtered.
|
|
300
300
|
const sortable = options.local === true || (bone.indexed === true && !bone.multiple && !bone.languages?.length)
|
|
301
301
|
const column: DataTableColumn<R> = {
|
|
302
302
|
field: name,
|
|
@@ -309,7 +309,7 @@ export function columnsFromStructure<R extends Row = Row>(
|
|
|
309
309
|
minWidth: 120,
|
|
310
310
|
flex: 1,
|
|
311
311
|
}
|
|
312
|
-
//
|
|
312
|
+
// Multilingual: sort by the DISPLAYED language (see above).
|
|
313
313
|
if (bone.languages?.length) {
|
|
314
314
|
const languages = bone.languages
|
|
315
315
|
column.comparator = (a, b) =>
|
|
@@ -320,7 +320,7 @@ export function columnsFromStructure<R extends Row = Row>(
|
|
|
320
320
|
)
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
-
//
|
|
323
|
+
// Plain selects and bools as colored badges (see badgesForBone).
|
|
324
324
|
const badge = badgesForBone(bone, options as ColumnsFromStructureOptions)
|
|
325
325
|
if (badge) column.badge = (value) => badge(value)
|
|
326
326
|
if (options.local === true) Object.assign(column, localFilterFor(bone, options as ColumnsFromStructureOptions))
|