viur-ui 0.0.1 → 0.0.3
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 +6 -6
- 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 +359 -351
- 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 +74 -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 +211 -121
- 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
- package/src/public/assets/icons/fontawesome-manifest.json +0 -8
- package/src/public/assets/icons/regular/file.svg +0 -1
- package/src/public/assets/icons/solid/arrows-left-right.svg +0 -1
- package/src/public/assets/icons/solid/bug.svg +0 -1
- package/src/public/assets/icons/solid/check.svg +0 -1
- package/src/public/assets/icons/solid/circle-info.svg +0 -1
- package/src/public/assets/icons/solid/compress.svg +0 -1
- package/src/public/assets/icons/solid/download.svg +0 -1
- package/src/public/assets/icons/solid/expand.svg +0 -1
- package/src/public/assets/icons/solid/grip-vertical.svg +0 -1
- package/src/public/assets/icons/solid/plus.svg +0 -1
- package/src/public/assets/icons/solid/question.svg +0 -1
- package/src/public/assets/icons/solid/triangle-exclamation.svg +0 -1
- package/src/public/assets/icons/solid/upload.svg +0 -1
- package/src/public/assets/icons/solid/xmark.svg +0 -1
|
@@ -51,20 +51,20 @@
|
|
|
51
51
|
<script setup>
|
|
52
52
|
import { useI18n } from "vue-i18n"
|
|
53
53
|
import { ref, computed, watch, inject, onMounted, onBeforeUnmount } from "vue"
|
|
54
|
-
import { DRAG_SESSION } from "./dragSession"
|
|
54
|
+
import { DRAG_SESSION } from "./dragSession.ts"
|
|
55
55
|
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
56
|
+
// Lean NAVIGATION tree: only nodes (name, icon, arrow), children are loaded
|
|
57
|
+
// via `loadChildren`. Deliberately no DataTable — navigation is not a data
|
|
58
|
+
// grid. As a drop target it registers with the browser's drag session
|
|
59
|
+
// (always "inside": into the folder that was hit).
|
|
60
60
|
|
|
61
61
|
const props = defineProps({
|
|
62
|
-
/**
|
|
62
|
+
/** Roots (e.g. the rootnodes) — { key, name, icon?, hasChildren? } */
|
|
63
63
|
roots: { type: Array, default: () => [] },
|
|
64
|
-
/**
|
|
64
|
+
/** Fetch the children of a node: (node) => Promise<BrowserNode[]> */
|
|
65
65
|
loadChildren: { type: Function, default: null },
|
|
66
66
|
label: { type: [String, null], default: null },
|
|
67
|
-
/** Container
|
|
67
|
+
/** Container id in the drag session */
|
|
68
68
|
containerId: { type: String, default: "tree" },
|
|
69
69
|
})
|
|
70
70
|
const location = defineModel({ type: [String, Number, null], default: null })
|
|
@@ -76,8 +76,8 @@ const rootEl = ref(null)
|
|
|
76
76
|
const expanded = ref(new Set())
|
|
77
77
|
const loading = ref(new Set())
|
|
78
78
|
const children = ref(new Map()) // String(key) → BrowserNode[]
|
|
79
|
-
//
|
|
80
|
-
//
|
|
79
|
+
// Locally inserted nodes for levels not loaded yet: merged in when the level
|
|
80
|
+
// is first loaded (the server may lag behind briefly).
|
|
81
81
|
const pendingInserts = ref(new Map()) // String(key) → BrowserNode[]
|
|
82
82
|
const bucketOf = (key) => children.value.get(String(key))
|
|
83
83
|
const activeKey = ref(null)
|
|
@@ -105,7 +105,7 @@ watch(
|
|
|
105
105
|
{ immediate: true }
|
|
106
106
|
)
|
|
107
107
|
|
|
108
|
-
/**
|
|
108
|
+
/** The roots (rootnodes) are always expanded — without a first click. */
|
|
109
109
|
async function expandRoots() {
|
|
110
110
|
for (const root of props.roots ?? []) {
|
|
111
111
|
if (expanded.value.has(root.key) || root.hasChildren === false) continue
|
|
@@ -128,9 +128,9 @@ async function fetchChildren(node, force = false) {
|
|
|
128
128
|
loading.value = new Set(loading.value).add(node.key)
|
|
129
129
|
try {
|
|
130
130
|
const result = (await props.loadChildren(node)) ?? []
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
131
|
+
// Merge in nodes moved here locally (via drag & drop) — the server may
|
|
132
|
+
// not know about the move yet. Whatever it already returns has thus
|
|
133
|
+
// arrived and no longer needs a marker.
|
|
134
134
|
const bucketKey = String(node.key)
|
|
135
135
|
const pending = pendingInserts.value.get(bucketKey) ?? []
|
|
136
136
|
const merged = [...result]
|
|
@@ -168,7 +168,7 @@ async function toggle(node) {
|
|
|
168
168
|
try {
|
|
169
169
|
await fetchChildren(node)
|
|
170
170
|
} catch {
|
|
171
|
-
return //
|
|
171
|
+
return // level can't be loaded → stay collapsed
|
|
172
172
|
}
|
|
173
173
|
if ((bucketOf(node.key) ?? []).length) {
|
|
174
174
|
next.add(node.key)
|
|
@@ -180,15 +180,15 @@ function open(node) {
|
|
|
180
180
|
activeKey.value = node.key
|
|
181
181
|
location.value = node.key
|
|
182
182
|
emit("navigate", node)
|
|
183
|
-
void toggle(node) //
|
|
183
|
+
void toggle(node) // expanding is part of navigating; only the arrow collapses
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
/**
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
* `node`
|
|
191
|
-
*
|
|
187
|
+
* Take a node LOCALLY out of its previous level and — if the target level is
|
|
188
|
+
* already loaded — attach it there. No request: a target level that isn't
|
|
189
|
+
* loaded yet fetches it fresh from the server on first expand anyway.
|
|
190
|
+
* `node` may be a full BrowserNode (for nodes the tree itself doesn't know
|
|
191
|
+
* yet, e.g. folder rows from the list) or just a key.
|
|
192
192
|
*/
|
|
193
193
|
function moveNode(node, targetKey) {
|
|
194
194
|
const key = typeof node === "object" && node !== null ? node.key : node
|
|
@@ -201,15 +201,15 @@ function moveNode(node, targetKey) {
|
|
|
201
201
|
next.set(bucketKey, [...(next.get(bucketKey) ?? []), entry])
|
|
202
202
|
children.value = next
|
|
203
203
|
} else {
|
|
204
|
-
//
|
|
205
|
-
//
|
|
204
|
+
// Target level never loaded: queue it — on first load the node is
|
|
205
|
+
// merged in (see fetchChildren). No request needed.
|
|
206
206
|
const next = new Map(pendingInserts.value)
|
|
207
207
|
next.set(bucketKey, [...(next.get(bucketKey) ?? []), entry])
|
|
208
208
|
pendingInserts.value = next
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
/**
|
|
212
|
+
/** Remove a node locally from all loaded levels; returns it. */
|
|
213
213
|
function removeNode(key) {
|
|
214
214
|
let removed = null
|
|
215
215
|
const next = new Map(children.value)
|
|
@@ -233,7 +233,7 @@ function removeNode(key) {
|
|
|
233
233
|
return removed
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
-
/**
|
|
236
|
+
/** Discard a node's level and re-fetch it (e.g. after a move). */
|
|
237
237
|
function reloadChildren(key) {
|
|
238
238
|
const entry = visibleEntries.value.find((candidate) => String(candidate.node.key) === String(key))
|
|
239
239
|
if (!entry) return Promise.resolve()
|
|
@@ -247,7 +247,7 @@ function reset() {
|
|
|
247
247
|
void expandRoots()
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
// ──
|
|
250
|
+
// ── Keyboard (WAI-ARIA tree) ──────────────────────────────────────────────────
|
|
251
251
|
function onKeydown(event) {
|
|
252
252
|
const entries = visibleEntries.value
|
|
253
253
|
const index = entries.findIndex((entry) => entry.node.key === activeKey.value)
|
|
@@ -292,12 +292,12 @@ function onKeydown(event) {
|
|
|
292
292
|
event.preventDefault()
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
// ── Drag
|
|
296
|
-
//
|
|
297
|
-
//
|
|
295
|
+
// ── Drag source: drag nodes into the list (or onto other nodes) ───────────────
|
|
296
|
+
// Only past a small movement threshold does a click become a drag — clicks
|
|
297
|
+
// for navigating stay clicks.
|
|
298
298
|
function onPointerDown(node, event) {
|
|
299
299
|
if (!session || event.button !== 0) return
|
|
300
|
-
//
|
|
300
|
+
// Roots (rootnodes) can't be moved.
|
|
301
301
|
if (props.roots.some((root) => String(root.key) === String(node.key))) return
|
|
302
302
|
const startX = event.clientX
|
|
303
303
|
const startY = event.clientY
|
|
@@ -322,7 +322,7 @@ function onPointerDown(node, event) {
|
|
|
322
322
|
window.addEventListener("pointerup", up)
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
-
// ── Drop
|
|
325
|
+
// ── Drop target of the drag session ───────────────────────────────────────────
|
|
326
326
|
let unregister = null
|
|
327
327
|
let hoverTimer = null
|
|
328
328
|
let hoverKey = null
|
|
@@ -336,7 +336,7 @@ onMounted(() => {
|
|
|
336
336
|
const row = hit.closest?.("[data-tree-key]")
|
|
337
337
|
if (!row) return null
|
|
338
338
|
const key = row.dataset.treeKey
|
|
339
|
-
//
|
|
339
|
+
// Hovering over a collapsed folder expands it.
|
|
340
340
|
if (hoverKey !== key) {
|
|
341
341
|
hoverKey = key
|
|
342
342
|
clearTimeout(hoverTimer)
|
|
@@ -1,57 +1,57 @@
|
|
|
1
1
|
import type { DefineComponent } from "vue"
|
|
2
|
-
import type { RowKey } from "../DataTable/types"
|
|
3
|
-
import type { BrowserMovePayload, BrowserUploadPayload } from "./types"
|
|
2
|
+
import type { RowKey } from "../DataTable/types.ts"
|
|
3
|
+
import type { BrowserMovePayload, BrowserUploadPayload } from "./types.ts"
|
|
4
4
|
|
|
5
|
-
/** Props
|
|
5
|
+
/** Props of ViurBrowser.vue (see defineProps there). */
|
|
6
6
|
export interface ViurBrowserProps {
|
|
7
|
-
/**
|
|
7
|
+
/** Module name or dotted path ("file", "shop.category"). */
|
|
8
8
|
module: string
|
|
9
|
-
/** Renderer
|
|
9
|
+
/** Renderer for the requests; default: VITE_DEFAULT_RENDERER. */
|
|
10
10
|
renderer?: string | null
|
|
11
|
-
/**
|
|
11
|
+
/** Fixed root node; without it, `/listRootNodes` is queried. */
|
|
12
12
|
rootNode?: string | Record<string, unknown> | null
|
|
13
|
-
/**
|
|
13
|
+
/** Columns; if omitted, derived from the leaf structure. */
|
|
14
14
|
columns?: unknown[] | null
|
|
15
|
-
/**
|
|
15
|
+
/** Only these bones as columns (for derived columns). */
|
|
16
16
|
bones?: string[] | null
|
|
17
|
-
/**
|
|
17
|
+
/** Additional parameters for the list requests. */
|
|
18
18
|
params?: Record<string, unknown>
|
|
19
|
-
/**
|
|
19
|
+
/** Field holding the fixed order (default "sortindex"). */
|
|
20
20
|
orderField?: string
|
|
21
|
-
/**
|
|
21
|
+
/** Allow uploading files into the level (file modules). */
|
|
22
22
|
uploads?: boolean
|
|
23
|
-
/**
|
|
23
|
+
/** Passed through to the DataTable. */
|
|
24
24
|
tableProps?: Record<string, unknown>
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
/**
|
|
27
|
+
/** Events as `on*` props so that `@open` & co. are typed in the template. */
|
|
28
28
|
export interface ViurBrowserEmitProps<R = Record<string, unknown>> {
|
|
29
|
-
/**
|
|
29
|
+
/** Double-click on a LEAF (folders navigate into themselves instead). */
|
|
30
30
|
onOpen?: (payload: { row: R; key: RowKey }) => void
|
|
31
|
-
/**
|
|
31
|
+
/** Move saved. */
|
|
32
32
|
onMoved?: (payload: { keys: RowKey[]; target: RowKey | null; parentChanged?: boolean }) => void
|
|
33
33
|
onMoveFailed?: (payload: { keys: RowKey[]; target: RowKey | null; error: unknown }) => void
|
|
34
|
-
/** Uploads
|
|
34
|
+
/** Uploads finished (only with `uploads`). */
|
|
35
35
|
onUploaded?: (payload: { files: unknown[]; target: RowKey | null }) => void
|
|
36
36
|
onUploadFailed?: (payload: { file: File; error: unknown }) => void
|
|
37
|
-
/** v-model:location —
|
|
37
|
+
/** v-model:location — current location (key of the node, null = root). */
|
|
38
38
|
onUpdateLocation?: (value: RowKey | null) => void
|
|
39
39
|
/** v-model:selectedKeys */
|
|
40
40
|
onUpdateSelectedKeys?: (value: RowKey[]) => void
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
/**
|
|
43
|
+
/** What ViurBrowser exposes via defineExpose. */
|
|
44
44
|
export interface ViurBrowserApi<R = Record<string, unknown>> {
|
|
45
|
-
/**
|
|
45
|
+
/** The generic browser underneath. */
|
|
46
46
|
browser: unknown
|
|
47
|
-
/**
|
|
47
|
+
/** Contents of the current location (nodes AND leaves, each with `skelType`). */
|
|
48
48
|
tree: {
|
|
49
49
|
data: { value: R[] }
|
|
50
50
|
structure: { value: { node?: Record<string, unknown>; leaf?: Record<string, unknown> } }
|
|
51
51
|
isFetching: { value: boolean }
|
|
52
52
|
[key: string]: unknown
|
|
53
53
|
}
|
|
54
|
-
/**
|
|
54
|
+
/** Re-fetch the location from the server. */
|
|
55
55
|
refresh: () => void
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -37,28 +37,29 @@
|
|
|
37
37
|
|
|
38
38
|
<script setup>
|
|
39
39
|
import { ref, computed, watch } from "vue"
|
|
40
|
+
import { useI18n } from "vue-i18n"
|
|
40
41
|
import Browser from "./Browser.vue"
|
|
41
42
|
import { useTreeFetch, useListFetch, useListRootNodesFetch, useMoveFetch, uploadFile } from "../../request/index.js"
|
|
42
|
-
import { columnsFromStructure } from "../DataTable/viurColumns"
|
|
43
|
+
import { columnsFromStructure } from "../DataTable/viurColumns.ts"
|
|
43
44
|
|
|
44
|
-
//
|
|
45
|
-
// Rootnodes
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
45
|
+
// Thin ViUR wrapper around the generic browser: pass in `module`, done.
|
|
46
|
+
// Rootnodes via /listRootNodes, contents of the location via useTreeFetch
|
|
47
|
+
// (nodes + leaves), tree levels via list(skelType=node), moving via /move,
|
|
48
|
+
// uploads via uploadFile. Saving happens per row; the browser moves the view
|
|
49
|
+
// locally — only the location itself is reloaded, after uploads.
|
|
49
50
|
|
|
50
51
|
const props = defineProps({
|
|
51
52
|
module: { type: String, required: true },
|
|
52
53
|
renderer: { type: String, default: null },
|
|
53
|
-
/**
|
|
54
|
+
/** Fixed rootnode; without it, /listRootNodes is queried. */
|
|
54
55
|
rootNode: { type: [String, Object], default: null },
|
|
55
|
-
/**
|
|
56
|
+
/** Columns; if omitted, derived from the viewLeafSkel structure. */
|
|
56
57
|
columns: { type: Array, default: null },
|
|
57
|
-
/**
|
|
58
|
+
/** Only these bones as columns (for derived columns). */
|
|
58
59
|
bones: { type: Array, default: null },
|
|
59
60
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
61
|
+
* Display language of multilingual bones ("de", "en" or "*" for all, with the
|
|
62
|
+
* primary one first) — determines display AND local sorting of the columns.
|
|
62
63
|
*/
|
|
63
64
|
language: { type: String, default: null },
|
|
64
65
|
primaryLanguage: { type: String, default: null },
|
|
@@ -68,6 +69,7 @@ const props = defineProps({
|
|
|
68
69
|
tableProps: { type: Object, default: () => ({}) },
|
|
69
70
|
})
|
|
70
71
|
const emit = defineEmits(["open", "moved", "move-failed", "uploaded", "upload-failed"])
|
|
72
|
+
const { t } = useI18n()
|
|
71
73
|
|
|
72
74
|
const browserEl = ref(null)
|
|
73
75
|
const location = defineModel("location", { type: [String, Number, null], default: null })
|
|
@@ -75,24 +77,24 @@ const selectedKeys = defineModel("selectedKeys", { type: Array, default: () => [
|
|
|
75
77
|
|
|
76
78
|
const ctx = computed(() => (props.renderer ? { renderer: props.renderer } : {}))
|
|
77
79
|
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
80
|
+
// Page size of the cursor bar = ViUR parameter `limit` (core ≥ 3.9; the
|
|
81
|
+
// former `amount` no longer exists there). It applies from the FIRST fetch:
|
|
82
|
+
// a pageSize given via tableProps (or a `limit` in the params) already
|
|
83
|
+
// determines the initial request; otherwise 30 (ViUR default). The actual
|
|
84
|
+
// fetch size is always what's displayed.
|
|
83
85
|
const limit = ref(Math.min(99, Number(props.params?.limit ?? props.tableProps?.pageSize ?? 30) || 30))
|
|
84
86
|
const baseParams = () => ({ ...props.params, orderby: props.orderField, limit: limit.value })
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
87
|
+
// Every fetch after setup ALWAYS uses the current location (rootnode or
|
|
88
|
+
// navigated folder) as parententry — otherwise the server lists across the
|
|
89
|
+
// whole tree.
|
|
88
90
|
const currentParams = () => ({
|
|
89
91
|
...baseParams(),
|
|
90
92
|
...(location.value != null ? { parententry: location.value } : {}),
|
|
91
93
|
})
|
|
92
94
|
const mergedTableProps = computed(() => ({
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
// `tableProps.withSearch` (
|
|
95
|
+
// No search in the browser: navigation happens via tree and breadcrumb,
|
|
96
|
+
// searching in the module's list view. Anyone who wants it anyway sets
|
|
97
|
+
// `tableProps.withSearch` (locally only useful once everything is loaded:
|
|
96
98
|
// `allLoaded`).
|
|
97
99
|
...props.tableProps,
|
|
98
100
|
pageSize: limit.value,
|
|
@@ -110,7 +112,7 @@ const rootNodeList = computed(() => {
|
|
|
110
112
|
return Array.isArray(list) ? list.map((entry) => ({ key: entry.key, name: entry.name ?? entry.key })) : []
|
|
111
113
|
})
|
|
112
114
|
|
|
113
|
-
// ──
|
|
115
|
+
// ── Contents of the current location (nodes + leaves) ─────────────────────────
|
|
114
116
|
const tree = useTreeFetch(props.module, {
|
|
115
117
|
params: baseParams(),
|
|
116
118
|
parententry: location.value ?? undefined,
|
|
@@ -120,9 +122,9 @@ const tree = useTreeFetch(props.module, {
|
|
|
120
122
|
const rows = computed(() => tree.data.value)
|
|
121
123
|
const rowKind = (row) => (row?.skelType === "node" ? "node" : "leaf")
|
|
122
124
|
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
125
|
+
// Without an explicit location, the ROOTNODE is the location: as soon as it is
|
|
126
|
+
// known, it becomes the default `parententry` for everything — list fetch,
|
|
127
|
+
// drops onto the empty area, reorder and uploads. Never fetch without parententry.
|
|
126
128
|
watch(
|
|
127
129
|
rootNodeList,
|
|
128
130
|
(roots) => {
|
|
@@ -139,8 +141,8 @@ watch(
|
|
|
139
141
|
{ immediate: true }
|
|
140
142
|
)
|
|
141
143
|
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
+
// Everything loaded (tree.complete)? Then sorting, search and column
|
|
145
|
+
// filters run only LOCALLY on the complete dataset.
|
|
144
146
|
const allLoaded = computed(() => tree.complete.value)
|
|
145
147
|
|
|
146
148
|
const effectiveColumns = computed(() => {
|
|
@@ -151,15 +153,16 @@ const effectiveColumns = computed(() => {
|
|
|
151
153
|
...(props.language ? { language: props.language } : {}),
|
|
152
154
|
...(props.primaryLanguage ? { primaryLanguage: props.primaryLanguage } : {}),
|
|
153
155
|
local: allLoaded.value,
|
|
156
|
+
boolTexts: [t("bones.bool.true"), t("bones.bool.false")],
|
|
154
157
|
})
|
|
155
|
-
return derived.length ? derived : [{ id: "name", label: "
|
|
158
|
+
return derived.length ? derived : [{ id: "name", label: t("browser.name") }]
|
|
156
159
|
})
|
|
157
160
|
|
|
158
161
|
function onNavigate() {
|
|
159
162
|
selectedKeys.value = []
|
|
160
163
|
}
|
|
161
164
|
|
|
162
|
-
// ── Cursor
|
|
165
|
+
// ── Cursor loading: 1 / 5 / 10 pages or everything ───────────────────────────
|
|
163
166
|
async function onLoadMore({ pages }) {
|
|
164
167
|
if (pages === "all") {
|
|
165
168
|
await tree.nextAll().catch(() => null)
|
|
@@ -171,14 +174,14 @@ async function onLoadMore({ pages }) {
|
|
|
171
174
|
}
|
|
172
175
|
}
|
|
173
176
|
|
|
174
|
-
/**
|
|
177
|
+
/** New page size: re-fetch the current location with the changed `limit`. */
|
|
175
178
|
function onPageSizeChange({ pageSize }) {
|
|
176
179
|
if (!pageSize || pageSize === limit.value) return
|
|
177
180
|
limit.value = Math.min(99, pageSize)
|
|
178
181
|
void tree.filter(currentParams()).catch(() => null)
|
|
179
182
|
}
|
|
180
183
|
|
|
181
|
-
// ──
|
|
184
|
+
// ── Moving (drop on tree/folder) & reordering ────────────────────────────────
|
|
182
185
|
async function saveMove(row, parentNode, sortindex, kind = rowKind(row)) {
|
|
183
186
|
const data = {
|
|
184
187
|
skelType: kind,
|
|
@@ -191,7 +194,7 @@ async function saveMove(row, parentNode, sortindex, kind = rowKind(row)) {
|
|
|
191
194
|
await result.execute()
|
|
192
195
|
return result.data.value
|
|
193
196
|
} catch (error) {
|
|
194
|
-
//
|
|
197
|
+
// The caller handles errors (onMove/onReorder catch and report).
|
|
195
198
|
throw error
|
|
196
199
|
}
|
|
197
200
|
}
|
|
@@ -199,17 +202,17 @@ async function saveMove(row, parentNode, sortindex, kind = rowKind(row)) {
|
|
|
199
202
|
async function onMove({ rows: movedRows, target, keys, source }) {
|
|
200
203
|
try {
|
|
201
204
|
for (const row of movedRows) {
|
|
202
|
-
//
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
+
// From the TREE come BrowserNodes ({ key, name, data }) — the skel
|
|
206
|
+
// lives in `data`, and a tree entry is always a node. From the
|
|
207
|
+
// LIST come skels with skelType.
|
|
205
208
|
const raw = row?.data ?? row
|
|
206
209
|
await saveMove(raw, target, undefined, source === "tree" ? "node" : rowKind(raw))
|
|
207
210
|
}
|
|
208
211
|
emit("moved", { keys, target })
|
|
209
|
-
//
|
|
212
|
+
// View (list AND tree) has already been moved locally — no reload.
|
|
210
213
|
} catch (error) {
|
|
211
214
|
emit("move-failed", { keys, target, error })
|
|
212
|
-
//
|
|
215
|
+
// The local state (list AND tree) is then wrong — fetch it fresh once.
|
|
213
216
|
void refresh().catch(() => null)
|
|
214
217
|
}
|
|
215
218
|
}
|
|
@@ -236,15 +239,15 @@ async function onUpload({ files, target }) {
|
|
|
236
239
|
}
|
|
237
240
|
if (results.length) {
|
|
238
241
|
emit("uploaded", { files: results, target })
|
|
239
|
-
//
|
|
242
|
+
// New entries only come from the server — fetch the current location once.
|
|
240
243
|
void tree.execute().catch(() => null)
|
|
241
244
|
}
|
|
242
245
|
}
|
|
243
246
|
|
|
244
|
-
// ──
|
|
247
|
+
// ── Tree levels (nodes only) ──────────────────────────────────────────────────
|
|
245
248
|
async function loadNodeChildren(node) {
|
|
246
249
|
const list = useListFetch(props.module, {
|
|
247
|
-
//
|
|
250
|
+
// Tree levels are navigation: as complete as possible (ViUR maximum 99).
|
|
248
251
|
params: { skelType: "node", parententry: node.key, orderby: props.orderField, limit: 99 },
|
|
249
252
|
...(props.renderer ? { renderer: props.renderer } : {}),
|
|
250
253
|
immediate: false,
|
|
@@ -258,9 +261,9 @@ async function loadNodeChildren(node) {
|
|
|
258
261
|
}
|
|
259
262
|
|
|
260
263
|
/**
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
+
* Reload everything: the list of the current location AND the navigation tree.
|
|
265
|
+
* The tree discards its loaded levels and expands the rootnodes again
|
|
266
|
+
* (fresh from the server); open sublevels reload when expanded.
|
|
264
267
|
*/
|
|
265
268
|
function refresh() {
|
|
266
269
|
browserEl.value?.tree?.reset?.()
|
|
@@ -1,49 +1,49 @@
|
|
|
1
1
|
import { reactive } from "vue"
|
|
2
2
|
import type { InjectionKey } from "vue"
|
|
3
|
-
import type { RowKey } from "../DataTable/types"
|
|
3
|
+
import type { RowKey } from "../DataTable/types.ts"
|
|
4
4
|
|
|
5
5
|
// --------------------------------------------------------------------------- //
|
|
6
|
-
// Drag
|
|
6
|
+
// Drag session — shared dragging between panels (list ↔ tree) //
|
|
7
7
|
// --------------------------------------------------------------------------- //
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
9
|
+
// The DataTable drags rows with pointer events (no HTML5 D&D). So that a row
|
|
10
|
+
// can also be dropped OUTSIDE the table (onto a folder in the navigation tree
|
|
11
|
+
// or a folder row of the list), the browser provides this session via
|
|
12
|
+
// provide():
|
|
13
13
|
//
|
|
14
|
-
// •
|
|
15
|
-
// •
|
|
16
|
-
//
|
|
17
|
-
// null
|
|
18
|
-
// •
|
|
19
|
-
//
|
|
20
|
-
//
|
|
14
|
+
// • The SOURCE (DataTable) reports start/move/end of the drag.
|
|
15
|
+
// • Every PANEL registers itself as a container with a `targetFor` function
|
|
16
|
+
// that translates a DOM element under the pointer into a drop target (or
|
|
17
|
+
// null if nothing can be dropped there).
|
|
18
|
+
// • On release over a target, `drop()` hands payload + target to the
|
|
19
|
+
// holder of the session (browser) — the source then skips its internal
|
|
20
|
+
// move.
|
|
21
21
|
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
22
|
+
// The session is deliberately DOM-based (elementFromPoint instead of measuring
|
|
23
|
+
// target rectangles up front): panels scroll and virtualize independently.
|
|
24
24
|
|
|
25
25
|
export interface DragPayload<R = Record<string, unknown>> {
|
|
26
26
|
keys: RowKey[]
|
|
27
27
|
rows: R[]
|
|
28
|
-
/** Container
|
|
28
|
+
/** Container id of the source (e.g. "list"). */
|
|
29
29
|
source: string
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export interface DropTarget {
|
|
33
|
-
/** Container
|
|
33
|
+
/** Container id of the panel the target lies in. */
|
|
34
34
|
container: string
|
|
35
|
-
/**
|
|
35
|
+
/** Key of the target node; null = root level of the container. */
|
|
36
36
|
key: RowKey | null
|
|
37
37
|
mode: "inside" | "before" | "after"
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export interface DragContainer {
|
|
41
41
|
id: string
|
|
42
|
-
/**
|
|
42
|
+
/** Root element of the panel — only hits inside it count. */
|
|
43
43
|
el: () => HTMLElement | null
|
|
44
44
|
/**
|
|
45
|
-
* Element
|
|
46
|
-
*
|
|
45
|
+
* Element under the pointer → drop target (or null). `y`/`x` allow
|
|
46
|
+
* zone decisions (before/inside/after) based on the hit position.
|
|
47
47
|
*/
|
|
48
48
|
targetFor: (hit: Element, x: number, y: number) => DropTarget | null
|
|
49
49
|
}
|
|
@@ -56,9 +56,9 @@ export interface DragSession<R = Record<string, unknown>> {
|
|
|
56
56
|
}
|
|
57
57
|
register(container: DragContainer): () => void
|
|
58
58
|
start(payload: DragPayload<R>): void
|
|
59
|
-
/** Hit
|
|
59
|
+
/** Hit test at (x, y); `hint` bypasses elementFromPoint (tests). */
|
|
60
60
|
update(x: number, y: number, hint?: Element | null): DropTarget | null
|
|
61
|
-
/**
|
|
61
|
+
/** Target redeemed: returns payload+target and ends the session. */
|
|
62
62
|
drop(): { payload: DragPayload<R>; target: DropTarget } | null
|
|
63
63
|
cancel(): void
|
|
64
64
|
}
|
|
@@ -69,9 +69,9 @@ export function createDragSession<R = Record<string, unknown>>(
|
|
|
69
69
|
onDrop?: (payload: DragPayload<R>, target: DropTarget) => void
|
|
70
70
|
): DragSession<R> {
|
|
71
71
|
const containers = new Map<string, DragContainer>()
|
|
72
|
-
// `reactive`
|
|
73
|
-
//
|
|
74
|
-
//
|
|
72
|
+
// `reactive` unwraps generic types (R becomes UnwrapRefSimple<R>), so the
|
|
73
|
+
// state no longer matches DragSession<R>. The payload is pure data
|
|
74
|
+
// pass-through — hence the shape is pinned down here.
|
|
75
75
|
const state = reactive({
|
|
76
76
|
active: false,
|
|
77
77
|
payload: null as DragPayload<R> | null,
|
|
@@ -124,7 +124,7 @@ export function createDragSession<R = Record<string, unknown>>(
|
|
|
124
124
|
return { state, register, start, update, drop, cancel }
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
/**
|
|
127
|
+
/** Zone helper: position within the element → before / inside / after. */
|
|
128
128
|
export function dropModeFor(hitRect: DOMRect, y: number, insideOnly = false): "inside" | "before" | "after" {
|
|
129
129
|
if (insideOnly) return "inside"
|
|
130
130
|
const offset = (y - hitRect.top) / Math.max(1, hitRect.height)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Public surface of the browser widget.
|
|
2
2
|
export { default as Browser } from "./Browser.vue"
|
|
3
3
|
export { default as ViurBrowser } from "./ViurBrowser.vue"
|
|
4
4
|
export { default as TreeNav } from "./TreeNav.vue"
|
|
@@ -10,5 +10,5 @@ export {
|
|
|
10
10
|
type DragPayload,
|
|
11
11
|
type DragContainer,
|
|
12
12
|
type DropTarget,
|
|
13
|
-
} from "./dragSession"
|
|
14
|
-
export type { BrowserNode, BrowserMovePayload, BrowserUploadPayload } from "./types"
|
|
13
|
+
} from "./dragSession.ts"
|
|
14
|
+
export type { BrowserNode, BrowserMovePayload, BrowserUploadPayload } from "./types.ts"
|