@raclettejs/workbench 0.1.36 → 0.1.38
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/CHANGELOG.md +27 -1
- package/i18n/en-EU.json +0 -1
- package/package.json +3 -3
- package/plugins/pacifico__compositions/frontend/widgets/CompositionListWidget.vue +50 -78
- package/plugins/pacifico__interactionLinks/frontend/widgets/InteractionLinkListWidget.vue +86 -91
- package/plugins/pacifico__tags/frontend/widgets/TagListWidget.vue +52 -72
- package/plugins/pacifico__users/frontend/widgets/UserListWidget.vue +41 -13
- package/plugins/raclette__cache/frontend/widgets/CacheListWidget.vue +0 -1
- package/services/frontend/src/app/composables/useWorkbenchListImport.ts +74 -0
- package/services/frontend/src/app/lib/workbenchListTransfer.ts +89 -0
- package/services/frontend/src/app/lib/jsonTools.ts +0 -147
package/CHANGELOG.md
CHANGED
|
@@ -7,8 +7,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
-
## [0.1.
|
|
10
|
+
## [0.1.38] - 2026-06-29 <a href="https://gitlab.com/raclettejs/core-dev/-/compare/v0.1.38...v0.1.38" target="_blank" rel="noopener"><b>Overview of all changes</b></a>
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- refactor: improved store reducer performance, enhanced baseDataTable filters
|
|
16
|
+
- Updated dependencies
|
|
17
|
+
- @raclettejs/core@0.1.38
|
|
18
|
+
|
|
19
|
+
## [0.1.38] - 2026-06-29 <a href="https://gitlab.com/raclettejs/core-dev/-/compare/v0.1.37...v0.1.38" target="_blank" rel="noopener"><b>Overview of all changes</b></a>
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- workbench lists now have deletion confirm dialog
|
|
11
24
|
|
|
25
|
+
## [0.1.37] - 2026-06-25 <a href="https://gitlab.com/raclettejs/core-dev/-/compare/v0.1.36...v0.1.37" target="_blank" rel="noopener"><b>Overview of all changes</b></a>
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- fix: json importer and exporter fixed for workbench datatypes. Export selection reset after filter reset
|
|
30
|
+
- Updated dependencies
|
|
31
|
+
- @raclettejs/core@0.1.37
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- Importer can now properly import compositions and interactionLinks
|
|
36
|
+
|
|
37
|
+
## [0.1.36] - 2026-06-22 <a href="https://gitlab.com/raclettejs/core-dev/-/compare/v0.1.35...v0.1.36" target="_blank" rel="noopener"><b>Overview of all changes</b></a>
|
|
12
38
|
|
|
13
39
|
### Changed
|
|
14
40
|
|
package/i18n/en-EU.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raclettejs/workbench",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.38",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "racletteJS Workbench - used to configure your application, for dev and prod",
|
|
6
6
|
"repository": "https://gitlab.com/raclettejs/workbench",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
".": "./index.ts"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@raclettejs/core": "0.1.
|
|
36
|
+
"@raclettejs/core": "0.1.38",
|
|
37
37
|
"@vueuse/integrations": "14.2.1",
|
|
38
38
|
"fuse.js": "7.1.0",
|
|
39
39
|
"three": "0.183.2",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"@emnapi/core": "1.9.2",
|
|
44
44
|
"@emnapi/runtime": "1.9.2",
|
|
45
45
|
"@eslint/js": "9.35.0",
|
|
46
|
-
"@raclettejs/types": "0.1.
|
|
46
|
+
"@raclettejs/types": "0.1.38",
|
|
47
47
|
"@sinclair/typebox": "0.34.48",
|
|
48
48
|
"@types/ramda": "0.31.1",
|
|
49
49
|
"@vueuse/core": "14.2.1",
|
|
@@ -3,13 +3,36 @@
|
|
|
3
3
|
<BaseDataTable
|
|
4
4
|
:items="parsedCompositions"
|
|
5
5
|
:exportable-items="exportableCompositions"
|
|
6
|
+
export-row-shape="full-records"
|
|
7
|
+
:export-show-data-scope-switch="false"
|
|
6
8
|
:loading="compositionsLoading || tagsLoading"
|
|
7
9
|
:headers="headers"
|
|
8
10
|
:row-click-handler="handleRowClick"
|
|
9
11
|
:data-name="$t('workbench.compositionList.dataName')"
|
|
10
12
|
:show-filters="true"
|
|
11
13
|
:items-deleted="showDeleted"
|
|
14
|
+
:toolbar-indicators="
|
|
15
|
+
showDeleted
|
|
16
|
+
? [
|
|
17
|
+
{
|
|
18
|
+
id: 'deleted',
|
|
19
|
+
label: $t('core.showDeleted'),
|
|
20
|
+
color: 'error',
|
|
21
|
+
icon: 'mdi-delete-outline',
|
|
22
|
+
},
|
|
23
|
+
]
|
|
24
|
+
: []
|
|
25
|
+
"
|
|
12
26
|
:on-file-loaded="handleFileUpload"
|
|
27
|
+
:confirm-delete="true"
|
|
28
|
+
:get-delete-confirm-message="
|
|
29
|
+
(item) =>
|
|
30
|
+
$t('core.baseDataTable.confirmDeleteMessage', { name: item.title })
|
|
31
|
+
"
|
|
32
|
+
:get-hard-delete-confirm-message="
|
|
33
|
+
(item) =>
|
|
34
|
+
$t('core.baseDataTable.confirmHardDeleteMessage', { name: item.title })
|
|
35
|
+
"
|
|
13
36
|
>
|
|
14
37
|
<template #toolbar-end>
|
|
15
38
|
<v-btn
|
|
@@ -63,12 +86,9 @@
|
|
|
63
86
|
/>
|
|
64
87
|
</template>
|
|
65
88
|
<template #row-actions="{ item }">
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
variant="text"
|
|
70
|
-
color="error"
|
|
71
|
-
@click.stop.prevent="deleteComposition(item)"
|
|
89
|
+
<BaseDataTableConfirmDeleteBtn
|
|
90
|
+
:item="item"
|
|
91
|
+
@delete="deleteComposition"
|
|
72
92
|
/>
|
|
73
93
|
</template>
|
|
74
94
|
</BaseDataTable>
|
|
@@ -107,25 +127,17 @@ import { computed, watch, ref } from "vue"
|
|
|
107
127
|
import type { Composition } from "@raclettejs/core"
|
|
108
128
|
import { formatDistance } from "date-fns"
|
|
109
129
|
import BaseDataTable from "@raclettejs/core/orchestrator/components/dataTable/BaseDataTable.vue"
|
|
130
|
+
import BaseDataTableConfirmDeleteBtn from "@raclettejs/core/orchestrator/components/dataTable/BaseDataTableConfirmDeleteBtn.vue"
|
|
110
131
|
import { useI18n } from "vue-i18n"
|
|
111
|
-
import {
|
|
112
|
-
mapJsonValues,
|
|
113
|
-
parseAndNormalizeWorkbenchImportJson,
|
|
114
|
-
resolveWorkbenchImportSelection,
|
|
115
|
-
} from "@app/lib/jsonTools"
|
|
132
|
+
import { recordsFromStore } from "@app/lib/workbenchListTransfer"
|
|
116
133
|
import { useWorkbenchTableActions } from "@app/composables/useWorkbenchTableActions"
|
|
134
|
+
import useWorkbenchListImport from "@app/composables/useWorkbenchListImport"
|
|
117
135
|
import ImportSelectionDialog from "@raclettejs/core/orchestrator/components/dataImport/ImportSelectionDialog.vue"
|
|
118
136
|
import ImportSummaryDialog from "@raclettejs/core/orchestrator/components/dataImport/ImportSummaryDialog.vue"
|
|
119
137
|
|
|
120
138
|
const { t } = useI18n()
|
|
121
139
|
const { goToCreate, goToEditById } = useWorkbenchTableActions()
|
|
122
140
|
const showDeleted = ref(false)
|
|
123
|
-
const showImportDialog = ref(false)
|
|
124
|
-
const showImportSummary = ref(false)
|
|
125
|
-
|
|
126
|
-
const importableItems = ref<any[]>([])
|
|
127
|
-
const importedItems = ref<any[]>([])
|
|
128
|
-
const pendingImportById = ref<Record<string, any>>({})
|
|
129
141
|
|
|
130
142
|
const headers = computed<
|
|
131
143
|
{ title: string; key: keyof Composition | "actions" }[]
|
|
@@ -160,7 +172,7 @@ const props = defineProps<{
|
|
|
160
172
|
uuid: string
|
|
161
173
|
}>()
|
|
162
174
|
|
|
163
|
-
const { $data, $socket
|
|
175
|
+
const { $data, $socket } = usePluginApi("raclette__core")
|
|
164
176
|
const { compositionSlots } = useRouteState()
|
|
165
177
|
|
|
166
178
|
const COMPOSITION_LIST_PATH = "composition-list"
|
|
@@ -194,7 +206,20 @@ const { execute: executeRestore } = await $data.composition.updateRestore({
|
|
|
194
206
|
const { execute: executeHardDelete } = await $data.composition.deleteHard({
|
|
195
207
|
options: { cb: () => execute({ isDeleted: showDeleted.value }) },
|
|
196
208
|
})
|
|
197
|
-
|
|
209
|
+
|
|
210
|
+
const {
|
|
211
|
+
showImportDialog,
|
|
212
|
+
showImportSummary,
|
|
213
|
+
importableItems,
|
|
214
|
+
importedItems,
|
|
215
|
+
handleFileUpload,
|
|
216
|
+
handleImportConfirm,
|
|
217
|
+
finalizeImport,
|
|
218
|
+
} = useWorkbenchListImport({
|
|
219
|
+
onBulkCreate: (items) => executeBulkCreate(items),
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
const compositionTableRows = () => {
|
|
198
223
|
if (!compositions.value || !tags.value) {
|
|
199
224
|
return []
|
|
200
225
|
}
|
|
@@ -209,24 +234,16 @@ const compositionTableRows = (relativeDates: boolean) => {
|
|
|
209
234
|
status: composition.status, // TODO: parse status
|
|
210
235
|
statusIcon: getStatusIcon(composition.status),
|
|
211
236
|
statusColor: getStatusColor(composition.status),
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
createdAt: formatDistance(
|
|
215
|
-
new Date(composition.createdAt),
|
|
216
|
-
new Date(),
|
|
217
|
-
),
|
|
218
|
-
updatedAt: formatDistance(
|
|
219
|
-
new Date(composition.updatedAt),
|
|
220
|
-
new Date(),
|
|
221
|
-
),
|
|
222
|
-
}
|
|
223
|
-
: {}),
|
|
237
|
+
createdAt: formatDistance(new Date(composition.createdAt), new Date()),
|
|
238
|
+
updatedAt: formatDistance(new Date(composition.updatedAt), new Date()),
|
|
224
239
|
}))
|
|
225
240
|
}
|
|
226
241
|
|
|
227
|
-
const parsedCompositions = computed(() => compositionTableRows(
|
|
242
|
+
const parsedCompositions = computed(() => compositionTableRows())
|
|
228
243
|
|
|
229
|
-
const exportableCompositions = computed(() =>
|
|
244
|
+
const exportableCompositions = computed(() =>
|
|
245
|
+
recordsFromStore(compositions.value as Record<string, Composition> | undefined),
|
|
246
|
+
)
|
|
230
247
|
|
|
231
248
|
// TODO: replace with composition status type
|
|
232
249
|
const getStatusIcon = (status: string) => {
|
|
@@ -250,51 +267,6 @@ const getStatusColor = (status: string) => {
|
|
|
250
267
|
return "#B66218"
|
|
251
268
|
}
|
|
252
269
|
}
|
|
253
|
-
// Import functions
|
|
254
|
-
const handleFileUpload = (content: string | ArrayBuffer) => {
|
|
255
|
-
if (typeof content !== "string") {
|
|
256
|
-
return
|
|
257
|
-
}
|
|
258
|
-
try {
|
|
259
|
-
const { items, byId } = parseAndNormalizeWorkbenchImportJson(content)
|
|
260
|
-
importableItems.value = items
|
|
261
|
-
pendingImportById.value = byId
|
|
262
|
-
showImportDialog.value = true
|
|
263
|
-
} catch (error) {
|
|
264
|
-
console.error("Failed to prepare import:", error)
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const handleImportConfirm = (selectedIds: string[]) => {
|
|
269
|
-
importedItems.value = resolveWorkbenchImportSelection(
|
|
270
|
-
pendingImportById.value,
|
|
271
|
-
selectedIds,
|
|
272
|
-
)
|
|
273
|
-
showImportSummary.value = true
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const finalizeImport = async () => {
|
|
277
|
-
const mapping = {
|
|
278
|
-
owner: $user.value._id,
|
|
279
|
-
lastEditor: $user.value._id,
|
|
280
|
-
createdBy: $user.value._id,
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const dataToImport = importedItems.value.reduce((acc, item) => {
|
|
284
|
-
const mappedItem = mapJsonValues(item, mapping)
|
|
285
|
-
acc[item._id] = mappedItem
|
|
286
|
-
return acc
|
|
287
|
-
}, {} as any)
|
|
288
|
-
|
|
289
|
-
await executeBulkCreate(Object.values(dataToImport))
|
|
290
|
-
// TODO: Add your actual import logic here
|
|
291
|
-
// e.g., call an API endpoint to save the imported compositions
|
|
292
|
-
|
|
293
|
-
// Reset states
|
|
294
|
-
pendingImportById.value = {}
|
|
295
|
-
importableItems.value = []
|
|
296
|
-
importedItems.value = []
|
|
297
|
-
}
|
|
298
270
|
const deleteComposition = async (composition: Composition) => {
|
|
299
271
|
if (showDeleted.value) {
|
|
300
272
|
await executeHardDelete({ _id: composition._id })
|
|
@@ -3,13 +3,40 @@
|
|
|
3
3
|
<BaseDataTable
|
|
4
4
|
:items="parsedInteractionLinks"
|
|
5
5
|
:exportable-items="exportableInteractionLinks"
|
|
6
|
+
export-row-shape="full-records"
|
|
7
|
+
:export-show-data-scope-switch="false"
|
|
6
8
|
:loading="interactionLinksLoading || tagsLoading"
|
|
7
9
|
:headers="headers"
|
|
8
10
|
:row-click-handler="handleRowClick"
|
|
9
11
|
:data-name="$t('workbench.interactionLinkList.dataName')"
|
|
10
12
|
:show-filters="true"
|
|
11
13
|
:items-deleted="showDeleted"
|
|
14
|
+
:toolbar-indicators="
|
|
15
|
+
showDeleted
|
|
16
|
+
? [
|
|
17
|
+
{
|
|
18
|
+
id: 'deleted',
|
|
19
|
+
label: $t('core.showDeleted'),
|
|
20
|
+
color: 'error',
|
|
21
|
+
icon: 'mdi-delete-outline',
|
|
22
|
+
},
|
|
23
|
+
]
|
|
24
|
+
: []
|
|
25
|
+
"
|
|
12
26
|
:on-file-loaded="handleFileUpload"
|
|
27
|
+
:confirm-delete="true"
|
|
28
|
+
:get-delete-confirm-message="
|
|
29
|
+
(item) =>
|
|
30
|
+
$t('core.baseDataTable.confirmDeleteMessage', {
|
|
31
|
+
name: item.composition || item._id,
|
|
32
|
+
})
|
|
33
|
+
"
|
|
34
|
+
:get-hard-delete-confirm-message="
|
|
35
|
+
(item) =>
|
|
36
|
+
$t('core.baseDataTable.confirmHardDeleteMessage', {
|
|
37
|
+
name: item.composition || item._id,
|
|
38
|
+
})
|
|
39
|
+
"
|
|
13
40
|
>
|
|
14
41
|
<template #toolbar-end>
|
|
15
42
|
<v-btn
|
|
@@ -58,12 +85,9 @@
|
|
|
58
85
|
/>
|
|
59
86
|
</template>
|
|
60
87
|
<template #row-actions="{ item }">
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
variant="text"
|
|
65
|
-
color="error"
|
|
66
|
-
@click.stop.prevent="deleteInteractionLink(item)"
|
|
88
|
+
<BaseDataTableConfirmDeleteBtn
|
|
89
|
+
:item="item"
|
|
90
|
+
@delete="deleteInteractionLink"
|
|
67
91
|
/>
|
|
68
92
|
</template>
|
|
69
93
|
</BaseDataTable>
|
|
@@ -94,26 +118,21 @@
|
|
|
94
118
|
|
|
95
119
|
<script setup lang="ts">
|
|
96
120
|
import BaseDataTable from "@raclettejs/core/orchestrator/components/dataTable/BaseDataTable.vue"
|
|
121
|
+
import BaseDataTableConfirmDeleteBtn from "@raclettejs/core/orchestrator/components/dataTable/BaseDataTableConfirmDeleteBtn.vue"
|
|
97
122
|
import { InteractionLink } from "@raclettejs/core"
|
|
98
|
-
import {
|
|
123
|
+
import {
|
|
124
|
+
usePluginApi,
|
|
125
|
+
} from "@raclettejs/core/orchestrator/composables"
|
|
99
126
|
import { formatDistance } from "date-fns"
|
|
100
127
|
import { computed, ref, watch } from "vue"
|
|
101
128
|
import { useI18n } from "vue-i18n"
|
|
102
|
-
import {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
resolveWorkbenchImportSelection,
|
|
106
|
-
} from "@app/lib/jsonTools"
|
|
129
|
+
import { recordsFromStore } from "@app/lib/workbenchListTransfer"
|
|
130
|
+
import { useWorkbenchTableActions } from "@app/composables/useWorkbenchTableActions"
|
|
131
|
+
import useWorkbenchListImport from "@app/composables/useWorkbenchListImport"
|
|
107
132
|
import ImportSelectionDialog from "@raclettejs/core/orchestrator/components/dataImport/ImportSelectionDialog.vue"
|
|
108
133
|
import ImportSummaryDialog from "@raclettejs/core/orchestrator/components/dataImport/ImportSummaryDialog.vue"
|
|
109
|
-
import { useWorkbenchTableActions } from "@app/composables/useWorkbenchTableActions"
|
|
110
|
-
const showDeleted = ref(false)
|
|
111
|
-
const showImportDialog = ref(false)
|
|
112
|
-
const showImportSummary = ref(false)
|
|
113
134
|
|
|
114
|
-
const
|
|
115
|
-
const importedItems = ref<any[]>([])
|
|
116
|
-
const pendingImportById = ref<Record<string, any>>({})
|
|
135
|
+
const showDeleted = ref(false)
|
|
117
136
|
|
|
118
137
|
const { t } = useI18n()
|
|
119
138
|
const { goToCreate, goToEditById } = useWorkbenchTableActions()
|
|
@@ -155,7 +174,7 @@ const props = defineProps<{
|
|
|
155
174
|
uuid: string
|
|
156
175
|
}>()
|
|
157
176
|
|
|
158
|
-
const { $data, $socket
|
|
177
|
+
const { $data, $socket } = usePluginApi("raclette__core")
|
|
159
178
|
|
|
160
179
|
const {
|
|
161
180
|
data: interactionLinks,
|
|
@@ -188,7 +207,41 @@ const { execute: executeHardDelete } = await $data.interactionLink.deleteHard({
|
|
|
188
207
|
const { execute: executeRestore } = await $data.interactionLink.updateRestore({
|
|
189
208
|
options: { cb: () => execute({ isDeleted: showDeleted.value }) },
|
|
190
209
|
})
|
|
191
|
-
|
|
210
|
+
|
|
211
|
+
const enrichInteractionLinkDescription = (
|
|
212
|
+
link: Record<string, unknown>,
|
|
213
|
+
): Record<string, unknown> => {
|
|
214
|
+
const enriched = { ...link }
|
|
215
|
+
let linkTitleString = `${enriched.slotType ?? ""}`
|
|
216
|
+
const triggers = enriched.triggers
|
|
217
|
+
if (Array.isArray(triggers)) {
|
|
218
|
+
for (const trigger of triggers) {
|
|
219
|
+
if (trigger && typeof trigger === "object") {
|
|
220
|
+
const settings = (trigger as { settings?: { title?: { default?: string } } })
|
|
221
|
+
.settings
|
|
222
|
+
const type = (trigger as { type?: string }).type ?? ""
|
|
223
|
+
linkTitleString += `${type} - ${settings?.title?.default ?? ""} `
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
enriched.description = linkTitleString + (enriched.description ?? "")
|
|
228
|
+
return enriched
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const {
|
|
232
|
+
showImportDialog,
|
|
233
|
+
showImportSummary,
|
|
234
|
+
importableItems,
|
|
235
|
+
importedItems,
|
|
236
|
+
handleFileUpload,
|
|
237
|
+
handleImportConfirm,
|
|
238
|
+
finalizeImport,
|
|
239
|
+
} = useWorkbenchListImport({
|
|
240
|
+
prepareRecord: enrichInteractionLinkDescription,
|
|
241
|
+
onBulkCreate: (items) => executeBulkCreate(items),
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
const interactionLinkTableRows = () => {
|
|
192
245
|
if (!interactionLinks.value || !tags.value) {
|
|
193
246
|
return []
|
|
194
247
|
}
|
|
@@ -202,25 +255,23 @@ const interactionLinkTableRows = (relativeDates: boolean) => {
|
|
|
202
255
|
lastEditor: users.value?.[interactionLink.author]?.email,
|
|
203
256
|
author: users.value?.[interactionLink.author]?.email,
|
|
204
257
|
triggers: interactionLink.triggers.length,
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
new Date(),
|
|
214
|
-
),
|
|
215
|
-
}
|
|
216
|
-
: {}),
|
|
258
|
+
createdAt: formatDistance(
|
|
259
|
+
new Date(interactionLink.createdAt),
|
|
260
|
+
new Date(),
|
|
261
|
+
),
|
|
262
|
+
updatedAt: formatDistance(
|
|
263
|
+
new Date(interactionLink.updatedAt),
|
|
264
|
+
new Date(),
|
|
265
|
+
),
|
|
217
266
|
}))
|
|
218
267
|
}
|
|
219
268
|
|
|
220
|
-
const parsedInteractionLinks = computed(() => interactionLinkTableRows(
|
|
269
|
+
const parsedInteractionLinks = computed(() => interactionLinkTableRows())
|
|
221
270
|
|
|
222
271
|
const exportableInteractionLinks = computed(() =>
|
|
223
|
-
|
|
272
|
+
recordsFromStore(
|
|
273
|
+
interactionLinks.value as Record<string, InteractionLink> | undefined,
|
|
274
|
+
),
|
|
224
275
|
)
|
|
225
276
|
const restore = async (_id: string) => {
|
|
226
277
|
executeRestore({ _id })
|
|
@@ -240,62 +291,6 @@ const deleteInteractionLink = async (interactionLink: InteractionLink) => {
|
|
|
240
291
|
$socket.on("interactionLinkCreated", () =>
|
|
241
292
|
execute({ isDeleted: showDeleted.value }),
|
|
242
293
|
)
|
|
243
|
-
const createTitleString = (link) => {
|
|
244
|
-
let linkTitleString = `${link.slotType}`
|
|
245
|
-
if (link.triggers) {
|
|
246
|
-
link.triggers.forEach((trigger) => {
|
|
247
|
-
linkTitleString += `${trigger.type} - ${trigger.settings?.title?.default} `
|
|
248
|
-
})
|
|
249
|
-
}
|
|
250
|
-
link.description = linkTitleString + link.description
|
|
251
|
-
return link
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Import functions
|
|
255
|
-
const handleFileUpload = (content: string | ArrayBuffer) => {
|
|
256
|
-
if (typeof content !== "string") {
|
|
257
|
-
return
|
|
258
|
-
}
|
|
259
|
-
try {
|
|
260
|
-
const { items, byId } = parseAndNormalizeWorkbenchImportJson(content)
|
|
261
|
-
importableItems.value = items
|
|
262
|
-
pendingImportById.value = byId
|
|
263
|
-
showImportDialog.value = true
|
|
264
|
-
} catch (error) {
|
|
265
|
-
console.error("Failed to prepare import:", error)
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const handleImportConfirm = (selectedIds: string[]) => {
|
|
270
|
-
importedItems.value = resolveWorkbenchImportSelection(
|
|
271
|
-
pendingImportById.value,
|
|
272
|
-
selectedIds,
|
|
273
|
-
)
|
|
274
|
-
showImportSummary.value = true
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const finalizeImport = async () => {
|
|
278
|
-
const mapping = {
|
|
279
|
-
owner: $user.value._id,
|
|
280
|
-
lastEditor: $user.value._id,
|
|
281
|
-
createdBy: $user.value._id,
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const dataToImport = importedItems.value.reduce((acc, item) => {
|
|
285
|
-
const mappedItem = mapJsonValues(item, mapping)
|
|
286
|
-
acc[item._id] = createTitleString(mappedItem)
|
|
287
|
-
return acc
|
|
288
|
-
}, {} as any)
|
|
289
|
-
|
|
290
|
-
await executeBulkCreate(Object.values(dataToImport))
|
|
291
|
-
// TODO: Add your actual import logic here
|
|
292
|
-
// e.g., call an API endpoint to save the imported compositions
|
|
293
|
-
|
|
294
|
-
// Reset states
|
|
295
|
-
pendingImportById.value = {}
|
|
296
|
-
importableItems.value = []
|
|
297
|
-
importedItems.value = []
|
|
298
|
-
}
|
|
299
294
|
watch(
|
|
300
295
|
showDeleted,
|
|
301
296
|
() => {
|
|
@@ -3,13 +3,36 @@
|
|
|
3
3
|
<BaseDataTable
|
|
4
4
|
:items="parsedTags"
|
|
5
5
|
:exportable-items="exportableTags"
|
|
6
|
+
export-row-shape="full-records"
|
|
7
|
+
:export-show-data-scope-switch="false"
|
|
6
8
|
:loading="tagsLoading"
|
|
7
9
|
:headers="headers"
|
|
8
10
|
:row-click-handler="handleRowClick"
|
|
9
11
|
:data-name="$t('workbench.tagList.dataName')"
|
|
10
12
|
:show-filters="true"
|
|
11
13
|
:items-deleted="showDeleted"
|
|
14
|
+
:toolbar-indicators="
|
|
15
|
+
showDeleted
|
|
16
|
+
? [
|
|
17
|
+
{
|
|
18
|
+
id: 'deleted',
|
|
19
|
+
label: $t('core.showDeleted'),
|
|
20
|
+
color: 'error',
|
|
21
|
+
icon: 'mdi-delete-outline',
|
|
22
|
+
},
|
|
23
|
+
]
|
|
24
|
+
: []
|
|
25
|
+
"
|
|
12
26
|
:on-file-loaded="handleFileUpload"
|
|
27
|
+
:confirm-delete="true"
|
|
28
|
+
:get-delete-confirm-message="
|
|
29
|
+
(item) =>
|
|
30
|
+
$t('core.baseDataTable.confirmDeleteMessage', { name: item.title })
|
|
31
|
+
"
|
|
32
|
+
:get-hard-delete-confirm-message="
|
|
33
|
+
(item) =>
|
|
34
|
+
$t('core.baseDataTable.confirmHardDeleteMessage', { name: item.title })
|
|
35
|
+
"
|
|
13
36
|
>
|
|
14
37
|
<template #toolbar-end>
|
|
15
38
|
<v-btn
|
|
@@ -51,13 +74,7 @@
|
|
|
51
74
|
/>
|
|
52
75
|
</template>
|
|
53
76
|
<template #row-actions="{ item }">
|
|
54
|
-
<
|
|
55
|
-
icon="mdi-delete"
|
|
56
|
-
size="small"
|
|
57
|
-
variant="text"
|
|
58
|
-
color="error"
|
|
59
|
-
@click.stop.prevent="deleteTag(item)"
|
|
60
|
-
/>
|
|
77
|
+
<BaseDataTableConfirmDeleteBtn :item="item" @delete="deleteTag" />
|
|
61
78
|
</template>
|
|
62
79
|
</BaseDataTable>
|
|
63
80
|
<!-- Import Selection Dialog -->
|
|
@@ -86,29 +103,24 @@
|
|
|
86
103
|
</template>
|
|
87
104
|
|
|
88
105
|
<script setup lang="ts">
|
|
89
|
-
import {
|
|
106
|
+
import {
|
|
107
|
+
usePluginApi,
|
|
108
|
+
} from "@raclettejs/core/orchestrator/composables"
|
|
90
109
|
import { computed, ref, watch } from "vue"
|
|
91
110
|
import type { Tag } from "@raclettejs/core"
|
|
92
111
|
import { useI18n } from "vue-i18n"
|
|
93
112
|
import BaseDataTable from "@raclettejs/core/orchestrator/components/dataTable/BaseDataTable.vue"
|
|
113
|
+
import BaseDataTableConfirmDeleteBtn from "@raclettejs/core/orchestrator/components/dataTable/BaseDataTableConfirmDeleteBtn.vue"
|
|
94
114
|
import { formatDistance } from "date-fns"
|
|
95
|
-
import {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
resolveWorkbenchImportSelection,
|
|
99
|
-
} from "@app/lib/jsonTools"
|
|
115
|
+
import { recordsFromStore } from "@app/lib/workbenchListTransfer"
|
|
116
|
+
import { useWorkbenchTableActions } from "@app/composables/useWorkbenchTableActions"
|
|
117
|
+
import useWorkbenchListImport from "@app/composables/useWorkbenchListImport"
|
|
100
118
|
import ImportSelectionDialog from "@raclettejs/core/orchestrator/components/dataImport/ImportSelectionDialog.vue"
|
|
101
119
|
import ImportSummaryDialog from "@raclettejs/core/orchestrator/components/dataImport/ImportSummaryDialog.vue"
|
|
102
|
-
|
|
120
|
+
|
|
103
121
|
const showDeleted = ref(false)
|
|
104
122
|
const { t } = useI18n()
|
|
105
123
|
const { goToCreate, goToEditById } = useWorkbenchTableActions()
|
|
106
|
-
const showImportDialog = ref(false)
|
|
107
|
-
const showImportSummary = ref(false)
|
|
108
|
-
|
|
109
|
-
const importableItems = ref<any[]>([])
|
|
110
|
-
const importedItems = ref<any[]>([])
|
|
111
|
-
const pendingImportById = ref<Record<string, any>>({})
|
|
112
124
|
const headers = computed<{ title: string; key: keyof Tag }[]>(() => [
|
|
113
125
|
{
|
|
114
126
|
title: t("workbench.tagList.tableHeaders.title"),
|
|
@@ -132,7 +144,7 @@ const props = defineProps<{
|
|
|
132
144
|
uuid: string
|
|
133
145
|
}>()
|
|
134
146
|
|
|
135
|
-
const { $data, $socket
|
|
147
|
+
const { $data, $socket } = usePluginApi("raclette__core")
|
|
136
148
|
|
|
137
149
|
const {
|
|
138
150
|
data: tags,
|
|
@@ -155,7 +167,20 @@ const { execute: executeHardDelete } = await $data.tag.deleteHard({
|
|
|
155
167
|
const { execute: executeRestore } = await $data.tag.updateRestore({
|
|
156
168
|
options: { cb: () => execute({ isDeleted: showDeleted.value }) },
|
|
157
169
|
})
|
|
158
|
-
|
|
170
|
+
|
|
171
|
+
const {
|
|
172
|
+
showImportDialog,
|
|
173
|
+
showImportSummary,
|
|
174
|
+
importableItems,
|
|
175
|
+
importedItems,
|
|
176
|
+
handleFileUpload,
|
|
177
|
+
handleImportConfirm,
|
|
178
|
+
finalizeImport,
|
|
179
|
+
} = useWorkbenchListImport({
|
|
180
|
+
onBulkCreate: (items) => executeBulkCreate(items),
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
const tagTableRows = () => {
|
|
159
184
|
if (!tags.value) {
|
|
160
185
|
return []
|
|
161
186
|
}
|
|
@@ -163,15 +188,15 @@ const tagTableRows = (relativeUpdatedAt: boolean) => {
|
|
|
163
188
|
return Object.values(tags.value as { [key: string]: Tag }).map((tag) => ({
|
|
164
189
|
...tag,
|
|
165
190
|
tags: tag?.tags?.map((t) => tags.value?.[t]) ?? [],
|
|
166
|
-
|
|
167
|
-
? { updatedAt: formatDistance(new Date(tag.updatedAt), new Date()) }
|
|
168
|
-
: {}),
|
|
191
|
+
updatedAt: formatDistance(new Date(tag.updatedAt), new Date()),
|
|
169
192
|
}))
|
|
170
193
|
}
|
|
171
194
|
|
|
172
|
-
const parsedTags = computed(() => tagTableRows(
|
|
195
|
+
const parsedTags = computed(() => tagTableRows())
|
|
173
196
|
|
|
174
|
-
const exportableTags = computed(() =>
|
|
197
|
+
const exportableTags = computed(() =>
|
|
198
|
+
recordsFromStore(tags.value as Record<string, Tag> | undefined),
|
|
199
|
+
)
|
|
175
200
|
|
|
176
201
|
const deleteTag = async (tag: Tag) => {
|
|
177
202
|
if (showDeleted.value) {
|
|
@@ -180,51 +205,6 @@ const deleteTag = async (tag: Tag) => {
|
|
|
180
205
|
await executeDelete({ _id: tag._id })
|
|
181
206
|
}
|
|
182
207
|
}
|
|
183
|
-
// Import functions
|
|
184
|
-
const handleFileUpload = (content: string | ArrayBuffer) => {
|
|
185
|
-
if (typeof content !== "string") {
|
|
186
|
-
return
|
|
187
|
-
}
|
|
188
|
-
try {
|
|
189
|
-
const { items, byId } = parseAndNormalizeWorkbenchImportJson(content)
|
|
190
|
-
importableItems.value = items
|
|
191
|
-
pendingImportById.value = byId
|
|
192
|
-
showImportDialog.value = true
|
|
193
|
-
} catch (error) {
|
|
194
|
-
console.error("Failed to prepare import:", error)
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const handleImportConfirm = (selectedIds: string[]) => {
|
|
199
|
-
importedItems.value = resolveWorkbenchImportSelection(
|
|
200
|
-
pendingImportById.value,
|
|
201
|
-
selectedIds,
|
|
202
|
-
)
|
|
203
|
-
showImportSummary.value = true
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const finalizeImport = async () => {
|
|
207
|
-
const mapping = {
|
|
208
|
-
owner: $user.value._id,
|
|
209
|
-
lastEditor: $user.value._id,
|
|
210
|
-
createdBy: $user.value._id,
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const dataToImport = importedItems.value.reduce((acc, item) => {
|
|
214
|
-
const mappedItem = mapJsonValues(item, mapping)
|
|
215
|
-
acc[item._id] = mappedItem
|
|
216
|
-
return acc
|
|
217
|
-
}, {} as any)
|
|
218
|
-
|
|
219
|
-
await executeBulkCreate(Object.values(dataToImport))
|
|
220
|
-
// TODO: Add your actual import logic here
|
|
221
|
-
// e.g., call an API endpoint to save the imported compositions
|
|
222
|
-
|
|
223
|
-
// Reset states
|
|
224
|
-
pendingImportById.value = {}
|
|
225
|
-
importableItems.value = []
|
|
226
|
-
importedItems.value = []
|
|
227
|
-
}
|
|
228
208
|
const restore = async (_id: string) => {
|
|
229
209
|
executeRestore({ _id })
|
|
230
210
|
}
|
|
@@ -3,12 +3,39 @@
|
|
|
3
3
|
<BaseDataTable
|
|
4
4
|
:items="parsedUsers"
|
|
5
5
|
:exportable-items="exportableUsers"
|
|
6
|
+
export-row-shape="full-records"
|
|
7
|
+
:export-show-data-scope-switch="false"
|
|
6
8
|
:loading="usersLoading || tagsLoading"
|
|
7
9
|
:headers="headers"
|
|
8
10
|
:row-click-handler="handleRowClick"
|
|
9
11
|
:data-name="$t('workbench.userList.dataName')"
|
|
10
12
|
:show-filters="true"
|
|
11
13
|
:items-deleted="showDeleted"
|
|
14
|
+
:toolbar-indicators="
|
|
15
|
+
showDeleted
|
|
16
|
+
? [
|
|
17
|
+
{
|
|
18
|
+
id: 'deleted',
|
|
19
|
+
label: $t('core.showDeleted'),
|
|
20
|
+
color: 'error',
|
|
21
|
+
icon: 'mdi-delete-outline',
|
|
22
|
+
},
|
|
23
|
+
]
|
|
24
|
+
: []
|
|
25
|
+
"
|
|
26
|
+
:confirm-delete="true"
|
|
27
|
+
:get-delete-confirm-message="
|
|
28
|
+
(item) =>
|
|
29
|
+
$t('core.baseDataTable.confirmDeleteMessage', {
|
|
30
|
+
name: item.email || item._id,
|
|
31
|
+
})
|
|
32
|
+
"
|
|
33
|
+
:get-hard-delete-confirm-message="
|
|
34
|
+
(item) =>
|
|
35
|
+
$t('core.baseDataTable.confirmHardDeleteMessage', {
|
|
36
|
+
name: item.email || item._id,
|
|
37
|
+
})
|
|
38
|
+
"
|
|
12
39
|
>
|
|
13
40
|
<template #toolbar-end>
|
|
14
41
|
<v-btn
|
|
@@ -54,13 +81,10 @@
|
|
|
54
81
|
/>
|
|
55
82
|
</template>
|
|
56
83
|
<template #row-actions="{ item }">
|
|
57
|
-
<
|
|
84
|
+
<BaseDataTableConfirmDeleteBtn
|
|
58
85
|
v-if="item._id !== $user?._id"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
variant="text"
|
|
62
|
-
color="error"
|
|
63
|
-
@click.stop.prevent="deleteUser(item)"
|
|
86
|
+
:item="item"
|
|
87
|
+
@delete="deleteUser"
|
|
64
88
|
/>
|
|
65
89
|
</template>
|
|
66
90
|
</BaseDataTable>
|
|
@@ -68,12 +92,16 @@
|
|
|
68
92
|
</template>
|
|
69
93
|
|
|
70
94
|
<script setup lang="ts">
|
|
71
|
-
import {
|
|
95
|
+
import {
|
|
96
|
+
usePluginApi,
|
|
97
|
+
} from "@raclettejs/core/orchestrator/composables"
|
|
72
98
|
import { computed, watch, ref } from "vue"
|
|
73
99
|
import type { User } from "@raclettejs/core"
|
|
74
100
|
import { formatDistance } from "date-fns"
|
|
75
101
|
import { useI18n } from "vue-i18n"
|
|
76
102
|
import BaseDataTable from "@raclettejs/core/orchestrator/components/dataTable/BaseDataTable.vue"
|
|
103
|
+
import BaseDataTableConfirmDeleteBtn from "@raclettejs/core/orchestrator/components/dataTable/BaseDataTableConfirmDeleteBtn.vue"
|
|
104
|
+
import { recordsFromStore } from "@app/lib/workbenchListTransfer"
|
|
77
105
|
import { useWorkbenchTableActions } from "@app/composables/useWorkbenchTableActions"
|
|
78
106
|
const showDeleted = ref(false)
|
|
79
107
|
|
|
@@ -123,7 +151,7 @@ const { execute: executeHardDelete } = await $data.user.deleteHard({
|
|
|
123
151
|
const { execute: executeRestore } = await $data.user.updateRestore({
|
|
124
152
|
options: { cb: () => execute({ isDeleted: showDeleted.value }) },
|
|
125
153
|
})
|
|
126
|
-
const userTableRows = (
|
|
154
|
+
const userTableRows = () => {
|
|
127
155
|
if (!users.value || !tags.value) {
|
|
128
156
|
return []
|
|
129
157
|
}
|
|
@@ -131,15 +159,15 @@ const userTableRows = (relativeUpdatedAt: boolean) => {
|
|
|
131
159
|
return Object.values(users.value as { [key: string]: User }).map((user) => ({
|
|
132
160
|
...user,
|
|
133
161
|
tags: user.tags.map((tag) => tags.value?.[tag]),
|
|
134
|
-
|
|
135
|
-
? { updatedAt: formatDistance(new Date(user.updatedAt), new Date()) }
|
|
136
|
-
: {}),
|
|
162
|
+
updatedAt: formatDistance(new Date(user.updatedAt), new Date()),
|
|
137
163
|
}))
|
|
138
164
|
}
|
|
139
165
|
|
|
140
|
-
const parsedUsers = computed(() => userTableRows(
|
|
166
|
+
const parsedUsers = computed(() => userTableRows())
|
|
141
167
|
|
|
142
|
-
const exportableUsers = computed(() =>
|
|
168
|
+
const exportableUsers = computed(() =>
|
|
169
|
+
recordsFromStore(users.value as Record<string, User> | undefined),
|
|
170
|
+
)
|
|
143
171
|
const deleteUser = async (user: User) => {
|
|
144
172
|
if (showDeleted.value) {
|
|
145
173
|
await executeHardDelete({ _id: user._id })
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
:data-name="$t('workbench.cacheList.dataName')"
|
|
18
18
|
:show-filters="true"
|
|
19
19
|
:show-actions-column="true"
|
|
20
|
-
:actions-header-title="$t('workbench.cacheList.tableActions')"
|
|
21
20
|
:group-by="listAllKeys ? 'keyGroup' : undefined"
|
|
22
21
|
item-value="_id"
|
|
23
22
|
:row-click-handler="openDetail"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { ref } from "vue"
|
|
2
|
+
import {
|
|
3
|
+
indexRecordsById,
|
|
4
|
+
parseWorkbenchListImport,
|
|
5
|
+
resolveSelectedRecords,
|
|
6
|
+
toBulkCreateBody,
|
|
7
|
+
type WorkbenchListRecord,
|
|
8
|
+
} from "@app/lib/workbenchListTransfer"
|
|
9
|
+
|
|
10
|
+
export interface UseWorkbenchListImportOptions {
|
|
11
|
+
onBulkCreate: (items: WorkbenchListRecord[]) => Promise<void>
|
|
12
|
+
/** Optional per-datatype tweak after server-field omission (e.g. interaction-link description). */
|
|
13
|
+
prepareRecord?: (record: WorkbenchListRecord) => WorkbenchListRecord
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* File upload → selection dialog → summary dialog → `createBulk`.
|
|
18
|
+
*/
|
|
19
|
+
export const useWorkbenchListImport = (
|
|
20
|
+
options: UseWorkbenchListImportOptions,
|
|
21
|
+
) => {
|
|
22
|
+
const showImportDialog = ref(false)
|
|
23
|
+
const showImportSummary = ref(false)
|
|
24
|
+
const importableItems = ref<WorkbenchListRecord[]>([])
|
|
25
|
+
const importedItems = ref<WorkbenchListRecord[]>([])
|
|
26
|
+
const pendingImportById = ref<Record<string, WorkbenchListRecord>>({})
|
|
27
|
+
|
|
28
|
+
const resetImportState = () => {
|
|
29
|
+
pendingImportById.value = {}
|
|
30
|
+
importableItems.value = []
|
|
31
|
+
importedItems.value = []
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const handleFileUpload = (content: string | ArrayBuffer) => {
|
|
35
|
+
if (typeof content !== "string") {
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
const records = parseWorkbenchListImport(content)
|
|
40
|
+
importableItems.value = records
|
|
41
|
+
pendingImportById.value = indexRecordsById(records)
|
|
42
|
+
showImportDialog.value = true
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error("Failed to prepare import:", error)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const handleImportConfirm = (selectedIds: string[]) => {
|
|
49
|
+
importedItems.value = resolveSelectedRecords(
|
|
50
|
+
pendingImportById.value,
|
|
51
|
+
selectedIds,
|
|
52
|
+
)
|
|
53
|
+
showImportSummary.value = true
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const finalizeImport = async () => {
|
|
57
|
+
await options.onBulkCreate(
|
|
58
|
+
toBulkCreateBody(importedItems.value, options.prepareRecord),
|
|
59
|
+
)
|
|
60
|
+
resetImportState()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
showImportDialog,
|
|
65
|
+
showImportSummary,
|
|
66
|
+
importableItems,
|
|
67
|
+
importedItems,
|
|
68
|
+
handleFileUpload,
|
|
69
|
+
handleImportConfirm,
|
|
70
|
+
finalizeImport,
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default useWorkbenchListImport
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workbench list JSON transfer format (export and import).
|
|
3
|
+
*
|
|
4
|
+
* ```json
|
|
5
|
+
* [
|
|
6
|
+
* { "_id": "uuid", "title": "…", … }
|
|
7
|
+
* ]
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* - Export: raw store records via `recordsFromStore`, matched to table selection in
|
|
11
|
+
* `BaseDataTable` (`export-row-shape="full-records"`).
|
|
12
|
+
* - Import: same array shape; server-owned fields are omitted before `createBulk`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export type WorkbenchListRecord = Record<string, unknown>
|
|
16
|
+
|
|
17
|
+
/** Fields present on store documents but not accepted by bulk-create APIs. */
|
|
18
|
+
const BULK_CREATE_OMIT_KEYS = new Set([
|
|
19
|
+
"createdAt",
|
|
20
|
+
"updatedAt",
|
|
21
|
+
"isDeleted",
|
|
22
|
+
"author",
|
|
23
|
+
"lastEditor",
|
|
24
|
+
"owner",
|
|
25
|
+
"createdBy",
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
const isRecord = (value: unknown): value is WorkbenchListRecord =>
|
|
29
|
+
value != null && typeof value === "object" && !Array.isArray(value)
|
|
30
|
+
|
|
31
|
+
/** Store map (`{ [_id]: T }`) → array for `exportable-items`. */
|
|
32
|
+
export const recordsFromStore = <T extends WorkbenchListRecord>(
|
|
33
|
+
store: Record<string, T> | null | undefined,
|
|
34
|
+
): T[] => Object.values(store ?? {}) as T[]
|
|
35
|
+
|
|
36
|
+
/** Parse an exported workbench list JSON file. */
|
|
37
|
+
export const parseWorkbenchListImport = (
|
|
38
|
+
content: string,
|
|
39
|
+
): WorkbenchListRecord[] => {
|
|
40
|
+
let parsed: unknown
|
|
41
|
+
try {
|
|
42
|
+
parsed = JSON.parse(content)
|
|
43
|
+
} catch {
|
|
44
|
+
throw new Error("Invalid JSON file.")
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!Array.isArray(parsed)) {
|
|
48
|
+
throw new Error("Import file must be a JSON array of records.")
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const records = parsed.filter(isRecord)
|
|
52
|
+
if (!records.length) {
|
|
53
|
+
throw new Error("Import file must contain at least one record.")
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const record of records) {
|
|
57
|
+
if (record._id == null || record._id === "") {
|
|
58
|
+
throw new Error("Every record must have an _id.")
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return records
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const indexRecordsById = (
|
|
66
|
+
records: WorkbenchListRecord[],
|
|
67
|
+
): Record<string, WorkbenchListRecord> =>
|
|
68
|
+
Object.fromEntries(records.map((record) => [String(record._id), record]))
|
|
69
|
+
|
|
70
|
+
export const resolveSelectedRecords = (
|
|
71
|
+
byId: Record<string, WorkbenchListRecord>,
|
|
72
|
+
selectedIds: string[],
|
|
73
|
+
): WorkbenchListRecord[] =>
|
|
74
|
+
selectedIds
|
|
75
|
+
.map((id) => byId[id])
|
|
76
|
+
.filter((record): record is WorkbenchListRecord => record != null)
|
|
77
|
+
|
|
78
|
+
/** Drop server-owned fields before POSTing to `createBulk`. */
|
|
79
|
+
export const toBulkCreateBody = (
|
|
80
|
+
records: WorkbenchListRecord[],
|
|
81
|
+
prepare?: (record: WorkbenchListRecord) => WorkbenchListRecord,
|
|
82
|
+
): WorkbenchListRecord[] =>
|
|
83
|
+
records.map((record) => {
|
|
84
|
+
const body: WorkbenchListRecord = { ...record }
|
|
85
|
+
for (const key of BULK_CREATE_OMIT_KEYS) {
|
|
86
|
+
delete body[key]
|
|
87
|
+
}
|
|
88
|
+
return prepare ? prepare(body) : body
|
|
89
|
+
})
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Parsed workbench list import: rows for {@link SelectionDialog} and a map by `_id` for confirm.
|
|
3
|
-
*/
|
|
4
|
-
export interface WorkbenchImportNormalization {
|
|
5
|
-
items: Record<string, unknown>[]
|
|
6
|
-
byId: Record<string, Record<string, unknown>>
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Parses JSON from a table export (array of records with `_id`) into dialog state.
|
|
11
|
-
* Rejects non-arrays so imports match the current export format only.
|
|
12
|
-
*/
|
|
13
|
-
export const parseAndNormalizeWorkbenchImportJson = (
|
|
14
|
-
content: string,
|
|
15
|
-
): WorkbenchImportNormalization => {
|
|
16
|
-
let parsed: unknown
|
|
17
|
-
try {
|
|
18
|
-
parsed = JSON.parse(content)
|
|
19
|
-
} catch {
|
|
20
|
-
throw new Error("Invalid JSON file.")
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (!Array.isArray(parsed)) {
|
|
24
|
-
throw new Error(
|
|
25
|
-
"Imported JSON must be an array of records (export from the data table).",
|
|
26
|
-
)
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const byId: Record<string, Record<string, unknown>> = {}
|
|
30
|
-
const items: Record<string, unknown>[] = []
|
|
31
|
-
|
|
32
|
-
for (const entry of parsed) {
|
|
33
|
-
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
34
|
-
continue
|
|
35
|
-
}
|
|
36
|
-
const row = entry as Record<string, unknown>
|
|
37
|
-
const id = row._id
|
|
38
|
-
if (id === undefined || id === null || id === "") {
|
|
39
|
-
continue
|
|
40
|
-
}
|
|
41
|
-
const sid = String(id)
|
|
42
|
-
byId[sid] = row
|
|
43
|
-
items.push(row)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (!items.length) {
|
|
47
|
-
throw new Error(
|
|
48
|
-
"No importable records found. Each entry must be an object with an _id.",
|
|
49
|
-
)
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return { items, byId }
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Resolve selected `_id`s using the `byId` map from {@link parseAndNormalizeWorkbenchImportJson}. */
|
|
56
|
-
export const resolveWorkbenchImportSelection = <
|
|
57
|
-
T extends Record<string, unknown>,
|
|
58
|
-
>(
|
|
59
|
-
byId: Record<string, T>,
|
|
60
|
-
selectedIds: string[],
|
|
61
|
-
): T[] =>
|
|
62
|
-
selectedIds
|
|
63
|
-
.map((id) => byId[id])
|
|
64
|
-
.filter((item): item is T => item != null)
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Downloads a JSON object as a .json file
|
|
68
|
-
* @param {Object|Array} data - The data to be downloaded as JSON
|
|
69
|
-
* @param {string} filename - The name of the file (without .json extension)
|
|
70
|
-
* @throws {Error} If data cannot be serialized to JSON
|
|
71
|
-
*/
|
|
72
|
-
export const downloadJson = (data, filename = "download") => {
|
|
73
|
-
try {
|
|
74
|
-
// Convert data to JSON string with pretty formatting
|
|
75
|
-
const jsonString = JSON.stringify(data, null, 2)
|
|
76
|
-
|
|
77
|
-
// Create blob from JSON string
|
|
78
|
-
const blob = new Blob([jsonString], { type: "application/json" })
|
|
79
|
-
|
|
80
|
-
// Create download link
|
|
81
|
-
const url = URL.createObjectURL(blob)
|
|
82
|
-
const link = document.createElement("a")
|
|
83
|
-
|
|
84
|
-
// Ensure filename has .json extension
|
|
85
|
-
const normalizedFilename = filename.endsWith(".json")
|
|
86
|
-
? filename
|
|
87
|
-
: `${filename}.json`
|
|
88
|
-
|
|
89
|
-
link.href = url
|
|
90
|
-
link.download = normalizedFilename
|
|
91
|
-
|
|
92
|
-
// Trigger download
|
|
93
|
-
document.body.appendChild(link)
|
|
94
|
-
link.click()
|
|
95
|
-
|
|
96
|
-
// Cleanup
|
|
97
|
-
document.body.removeChild(link)
|
|
98
|
-
URL.revokeObjectURL(url)
|
|
99
|
-
} catch (error) {
|
|
100
|
-
console.error("Failed to download JSON:", error)
|
|
101
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
102
|
-
throw new Error(`Unable to download JSON file: ${message}`)
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Recursively maps values in a JSON object based on a mapping configuration
|
|
107
|
-
* @param {Object|Array} data - The source JSON data to transform
|
|
108
|
-
* @param {Object} mapping - Mapping object where keys are attribute names and values are replacement values
|
|
109
|
-
* @returns {Object|Array} New object with mapped values
|
|
110
|
-
* @throws {Error} If data cannot be processed
|
|
111
|
-
*
|
|
112
|
-
* @example
|
|
113
|
-
* const data = { owner: 'user123', name: 'Project A', lastEditor: 'user123' };
|
|
114
|
-
* const mapping = { owner: 'user456', lastEditor: 'user789' };
|
|
115
|
-
* const result = mapJsonValues(data, mapping);
|
|
116
|
-
* // Result: { owner: 'user456', name: 'Project A', lastEditor: 'user789' }
|
|
117
|
-
*/
|
|
118
|
-
export const mapJsonValues = (data, mapping = {}) => {
|
|
119
|
-
if (!data || typeof data !== "object") {
|
|
120
|
-
return data
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Handle arrays
|
|
124
|
-
if (Array.isArray(data)) {
|
|
125
|
-
return data.map((item) => mapJsonValues(item, mapping))
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Handle objects
|
|
129
|
-
const result = {}
|
|
130
|
-
|
|
131
|
-
for (const [key, value] of Object.entries(data)) {
|
|
132
|
-
// If this key exists in mapping, use the mapped value
|
|
133
|
-
if (key in mapping) {
|
|
134
|
-
result[key] = mapping[key]
|
|
135
|
-
}
|
|
136
|
-
// If value is an object or array, recursively process it
|
|
137
|
-
else if (value && typeof value === "object") {
|
|
138
|
-
result[key] = mapJsonValues(value, mapping)
|
|
139
|
-
}
|
|
140
|
-
// Otherwise keep the original value
|
|
141
|
-
else {
|
|
142
|
-
result[key] = value
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return result
|
|
147
|
-
}
|