@raclettejs/workbench 0.1.36 → 0.1.37
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 +15 -1
- package/package.json +3 -3
- package/plugins/pacifico__compositions/frontend/widgets/CompositionListWidget.vue +25 -72
- package/plugins/pacifico__interactionLinks/frontend/widgets/InteractionLinkListWidget.vue +54 -84
- package/plugins/pacifico__tags/frontend/widgets/TagListWidget.vue +26 -64
- package/plugins/pacifico__users/frontend/widgets/UserListWidget.vue +9 -6
- 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,22 @@ 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.37] - 2026-06-25 <a href="https://gitlab.com/raclettejs/core-dev/-/compare/v0.1.37...v0.1.37" target="_blank" rel="noopener"><b>Overview of all changes</b></a>
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- fix: json importer and exporter fixed for workbench datatypes. Export selection reset after filter reset
|
|
16
|
+
- Updated dependencies
|
|
17
|
+
- @raclettejs/core@0.1.37
|
|
11
18
|
|
|
19
|
+
## [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>
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Importer can now properly import compositions and interactionLinks
|
|
24
|
+
|
|
25
|
+
## [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
26
|
|
|
13
27
|
### Changed
|
|
14
28
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raclettejs/workbench",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.37",
|
|
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.37",
|
|
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.37",
|
|
47
47
|
"@sinclair/typebox": "0.34.48",
|
|
48
48
|
"@types/ramda": "0.31.1",
|
|
49
49
|
"@vueuse/core": "14.2.1",
|
|
@@ -3,6 +3,8 @@
|
|
|
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"
|
|
@@ -108,24 +110,15 @@ import type { Composition } from "@raclettejs/core"
|
|
|
108
110
|
import { formatDistance } from "date-fns"
|
|
109
111
|
import BaseDataTable from "@raclettejs/core/orchestrator/components/dataTable/BaseDataTable.vue"
|
|
110
112
|
import { useI18n } from "vue-i18n"
|
|
111
|
-
import {
|
|
112
|
-
mapJsonValues,
|
|
113
|
-
parseAndNormalizeWorkbenchImportJson,
|
|
114
|
-
resolveWorkbenchImportSelection,
|
|
115
|
-
} from "@app/lib/jsonTools"
|
|
113
|
+
import { recordsFromStore } from "@app/lib/workbenchListTransfer"
|
|
116
114
|
import { useWorkbenchTableActions } from "@app/composables/useWorkbenchTableActions"
|
|
115
|
+
import useWorkbenchListImport from "@app/composables/useWorkbenchListImport"
|
|
117
116
|
import ImportSelectionDialog from "@raclettejs/core/orchestrator/components/dataImport/ImportSelectionDialog.vue"
|
|
118
117
|
import ImportSummaryDialog from "@raclettejs/core/orchestrator/components/dataImport/ImportSummaryDialog.vue"
|
|
119
118
|
|
|
120
119
|
const { t } = useI18n()
|
|
121
120
|
const { goToCreate, goToEditById } = useWorkbenchTableActions()
|
|
122
121
|
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
122
|
|
|
130
123
|
const headers = computed<
|
|
131
124
|
{ title: string; key: keyof Composition | "actions" }[]
|
|
@@ -160,7 +153,7 @@ const props = defineProps<{
|
|
|
160
153
|
uuid: string
|
|
161
154
|
}>()
|
|
162
155
|
|
|
163
|
-
const { $data, $socket
|
|
156
|
+
const { $data, $socket } = usePluginApi("raclette__core")
|
|
164
157
|
const { compositionSlots } = useRouteState()
|
|
165
158
|
|
|
166
159
|
const COMPOSITION_LIST_PATH = "composition-list"
|
|
@@ -194,7 +187,20 @@ const { execute: executeRestore } = await $data.composition.updateRestore({
|
|
|
194
187
|
const { execute: executeHardDelete } = await $data.composition.deleteHard({
|
|
195
188
|
options: { cb: () => execute({ isDeleted: showDeleted.value }) },
|
|
196
189
|
})
|
|
197
|
-
|
|
190
|
+
|
|
191
|
+
const {
|
|
192
|
+
showImportDialog,
|
|
193
|
+
showImportSummary,
|
|
194
|
+
importableItems,
|
|
195
|
+
importedItems,
|
|
196
|
+
handleFileUpload,
|
|
197
|
+
handleImportConfirm,
|
|
198
|
+
finalizeImport,
|
|
199
|
+
} = useWorkbenchListImport({
|
|
200
|
+
onBulkCreate: (items) => executeBulkCreate(items),
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
const compositionTableRows = () => {
|
|
198
204
|
if (!compositions.value || !tags.value) {
|
|
199
205
|
return []
|
|
200
206
|
}
|
|
@@ -209,24 +215,16 @@ const compositionTableRows = (relativeDates: boolean) => {
|
|
|
209
215
|
status: composition.status, // TODO: parse status
|
|
210
216
|
statusIcon: getStatusIcon(composition.status),
|
|
211
217
|
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
|
-
: {}),
|
|
218
|
+
createdAt: formatDistance(new Date(composition.createdAt), new Date()),
|
|
219
|
+
updatedAt: formatDistance(new Date(composition.updatedAt), new Date()),
|
|
224
220
|
}))
|
|
225
221
|
}
|
|
226
222
|
|
|
227
|
-
const parsedCompositions = computed(() => compositionTableRows(
|
|
223
|
+
const parsedCompositions = computed(() => compositionTableRows())
|
|
228
224
|
|
|
229
|
-
const exportableCompositions = computed(() =>
|
|
225
|
+
const exportableCompositions = computed(() =>
|
|
226
|
+
recordsFromStore(compositions.value as Record<string, Composition> | undefined),
|
|
227
|
+
)
|
|
230
228
|
|
|
231
229
|
// TODO: replace with composition status type
|
|
232
230
|
const getStatusIcon = (status: string) => {
|
|
@@ -250,51 +248,6 @@ const getStatusColor = (status: string) => {
|
|
|
250
248
|
return "#B66218"
|
|
251
249
|
}
|
|
252
250
|
}
|
|
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
251
|
const deleteComposition = async (composition: Composition) => {
|
|
299
252
|
if (showDeleted.value) {
|
|
300
253
|
await executeHardDelete({ _id: composition._id })
|
|
@@ -3,6 +3,8 @@
|
|
|
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"
|
|
@@ -99,21 +101,13 @@ import { usePluginApi } from "@raclettejs/core/orchestrator/composables"
|
|
|
99
101
|
import { formatDistance } from "date-fns"
|
|
100
102
|
import { computed, ref, watch } from "vue"
|
|
101
103
|
import { useI18n } from "vue-i18n"
|
|
102
|
-
import {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
resolveWorkbenchImportSelection,
|
|
106
|
-
} from "@app/lib/jsonTools"
|
|
104
|
+
import { recordsFromStore } from "@app/lib/workbenchListTransfer"
|
|
105
|
+
import { useWorkbenchTableActions } from "@app/composables/useWorkbenchTableActions"
|
|
106
|
+
import useWorkbenchListImport from "@app/composables/useWorkbenchListImport"
|
|
107
107
|
import ImportSelectionDialog from "@raclettejs/core/orchestrator/components/dataImport/ImportSelectionDialog.vue"
|
|
108
108
|
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
109
|
|
|
114
|
-
const
|
|
115
|
-
const importedItems = ref<any[]>([])
|
|
116
|
-
const pendingImportById = ref<Record<string, any>>({})
|
|
110
|
+
const showDeleted = ref(false)
|
|
117
111
|
|
|
118
112
|
const { t } = useI18n()
|
|
119
113
|
const { goToCreate, goToEditById } = useWorkbenchTableActions()
|
|
@@ -155,7 +149,7 @@ const props = defineProps<{
|
|
|
155
149
|
uuid: string
|
|
156
150
|
}>()
|
|
157
151
|
|
|
158
|
-
const { $data, $socket
|
|
152
|
+
const { $data, $socket } = usePluginApi("raclette__core")
|
|
159
153
|
|
|
160
154
|
const {
|
|
161
155
|
data: interactionLinks,
|
|
@@ -188,7 +182,41 @@ const { execute: executeHardDelete } = await $data.interactionLink.deleteHard({
|
|
|
188
182
|
const { execute: executeRestore } = await $data.interactionLink.updateRestore({
|
|
189
183
|
options: { cb: () => execute({ isDeleted: showDeleted.value }) },
|
|
190
184
|
})
|
|
191
|
-
|
|
185
|
+
|
|
186
|
+
const enrichInteractionLinkDescription = (
|
|
187
|
+
link: Record<string, unknown>,
|
|
188
|
+
): Record<string, unknown> => {
|
|
189
|
+
const enriched = { ...link }
|
|
190
|
+
let linkTitleString = `${enriched.slotType ?? ""}`
|
|
191
|
+
const triggers = enriched.triggers
|
|
192
|
+
if (Array.isArray(triggers)) {
|
|
193
|
+
for (const trigger of triggers) {
|
|
194
|
+
if (trigger && typeof trigger === "object") {
|
|
195
|
+
const settings = (trigger as { settings?: { title?: { default?: string } } })
|
|
196
|
+
.settings
|
|
197
|
+
const type = (trigger as { type?: string }).type ?? ""
|
|
198
|
+
linkTitleString += `${type} - ${settings?.title?.default ?? ""} `
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
enriched.description = linkTitleString + (enriched.description ?? "")
|
|
203
|
+
return enriched
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const {
|
|
207
|
+
showImportDialog,
|
|
208
|
+
showImportSummary,
|
|
209
|
+
importableItems,
|
|
210
|
+
importedItems,
|
|
211
|
+
handleFileUpload,
|
|
212
|
+
handleImportConfirm,
|
|
213
|
+
finalizeImport,
|
|
214
|
+
} = useWorkbenchListImport({
|
|
215
|
+
prepareRecord: enrichInteractionLinkDescription,
|
|
216
|
+
onBulkCreate: (items) => executeBulkCreate(items),
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
const interactionLinkTableRows = () => {
|
|
192
220
|
if (!interactionLinks.value || !tags.value) {
|
|
193
221
|
return []
|
|
194
222
|
}
|
|
@@ -202,25 +230,23 @@ const interactionLinkTableRows = (relativeDates: boolean) => {
|
|
|
202
230
|
lastEditor: users.value?.[interactionLink.author]?.email,
|
|
203
231
|
author: users.value?.[interactionLink.author]?.email,
|
|
204
232
|
triggers: interactionLink.triggers.length,
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
new Date(),
|
|
214
|
-
),
|
|
215
|
-
}
|
|
216
|
-
: {}),
|
|
233
|
+
createdAt: formatDistance(
|
|
234
|
+
new Date(interactionLink.createdAt),
|
|
235
|
+
new Date(),
|
|
236
|
+
),
|
|
237
|
+
updatedAt: formatDistance(
|
|
238
|
+
new Date(interactionLink.updatedAt),
|
|
239
|
+
new Date(),
|
|
240
|
+
),
|
|
217
241
|
}))
|
|
218
242
|
}
|
|
219
243
|
|
|
220
|
-
const parsedInteractionLinks = computed(() => interactionLinkTableRows(
|
|
244
|
+
const parsedInteractionLinks = computed(() => interactionLinkTableRows())
|
|
221
245
|
|
|
222
246
|
const exportableInteractionLinks = computed(() =>
|
|
223
|
-
|
|
247
|
+
recordsFromStore(
|
|
248
|
+
interactionLinks.value as Record<string, InteractionLink> | undefined,
|
|
249
|
+
),
|
|
224
250
|
)
|
|
225
251
|
const restore = async (_id: string) => {
|
|
226
252
|
executeRestore({ _id })
|
|
@@ -240,62 +266,6 @@ const deleteInteractionLink = async (interactionLink: InteractionLink) => {
|
|
|
240
266
|
$socket.on("interactionLinkCreated", () =>
|
|
241
267
|
execute({ isDeleted: showDeleted.value }),
|
|
242
268
|
)
|
|
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
269
|
watch(
|
|
300
270
|
showDeleted,
|
|
301
271
|
() => {
|
|
@@ -3,6 +3,8 @@
|
|
|
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"
|
|
@@ -92,23 +94,15 @@ import type { Tag } from "@raclettejs/core"
|
|
|
92
94
|
import { useI18n } from "vue-i18n"
|
|
93
95
|
import BaseDataTable from "@raclettejs/core/orchestrator/components/dataTable/BaseDataTable.vue"
|
|
94
96
|
import { formatDistance } from "date-fns"
|
|
95
|
-
import {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
resolveWorkbenchImportSelection,
|
|
99
|
-
} from "@app/lib/jsonTools"
|
|
97
|
+
import { recordsFromStore } from "@app/lib/workbenchListTransfer"
|
|
98
|
+
import { useWorkbenchTableActions } from "@app/composables/useWorkbenchTableActions"
|
|
99
|
+
import useWorkbenchListImport from "@app/composables/useWorkbenchListImport"
|
|
100
100
|
import ImportSelectionDialog from "@raclettejs/core/orchestrator/components/dataImport/ImportSelectionDialog.vue"
|
|
101
101
|
import ImportSummaryDialog from "@raclettejs/core/orchestrator/components/dataImport/ImportSummaryDialog.vue"
|
|
102
|
-
|
|
102
|
+
|
|
103
103
|
const showDeleted = ref(false)
|
|
104
104
|
const { t } = useI18n()
|
|
105
105
|
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
106
|
const headers = computed<{ title: string; key: keyof Tag }[]>(() => [
|
|
113
107
|
{
|
|
114
108
|
title: t("workbench.tagList.tableHeaders.title"),
|
|
@@ -132,7 +126,7 @@ const props = defineProps<{
|
|
|
132
126
|
uuid: string
|
|
133
127
|
}>()
|
|
134
128
|
|
|
135
|
-
const { $data, $socket
|
|
129
|
+
const { $data, $socket } = usePluginApi("raclette__core")
|
|
136
130
|
|
|
137
131
|
const {
|
|
138
132
|
data: tags,
|
|
@@ -155,7 +149,20 @@ const { execute: executeHardDelete } = await $data.tag.deleteHard({
|
|
|
155
149
|
const { execute: executeRestore } = await $data.tag.updateRestore({
|
|
156
150
|
options: { cb: () => execute({ isDeleted: showDeleted.value }) },
|
|
157
151
|
})
|
|
158
|
-
|
|
152
|
+
|
|
153
|
+
const {
|
|
154
|
+
showImportDialog,
|
|
155
|
+
showImportSummary,
|
|
156
|
+
importableItems,
|
|
157
|
+
importedItems,
|
|
158
|
+
handleFileUpload,
|
|
159
|
+
handleImportConfirm,
|
|
160
|
+
finalizeImport,
|
|
161
|
+
} = useWorkbenchListImport({
|
|
162
|
+
onBulkCreate: (items) => executeBulkCreate(items),
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
const tagTableRows = () => {
|
|
159
166
|
if (!tags.value) {
|
|
160
167
|
return []
|
|
161
168
|
}
|
|
@@ -163,15 +170,15 @@ const tagTableRows = (relativeUpdatedAt: boolean) => {
|
|
|
163
170
|
return Object.values(tags.value as { [key: string]: Tag }).map((tag) => ({
|
|
164
171
|
...tag,
|
|
165
172
|
tags: tag?.tags?.map((t) => tags.value?.[t]) ?? [],
|
|
166
|
-
|
|
167
|
-
? { updatedAt: formatDistance(new Date(tag.updatedAt), new Date()) }
|
|
168
|
-
: {}),
|
|
173
|
+
updatedAt: formatDistance(new Date(tag.updatedAt), new Date()),
|
|
169
174
|
}))
|
|
170
175
|
}
|
|
171
176
|
|
|
172
|
-
const parsedTags = computed(() => tagTableRows(
|
|
177
|
+
const parsedTags = computed(() => tagTableRows())
|
|
173
178
|
|
|
174
|
-
const exportableTags = computed(() =>
|
|
179
|
+
const exportableTags = computed(() =>
|
|
180
|
+
recordsFromStore(tags.value as Record<string, Tag> | undefined),
|
|
181
|
+
)
|
|
175
182
|
|
|
176
183
|
const deleteTag = async (tag: Tag) => {
|
|
177
184
|
if (showDeleted.value) {
|
|
@@ -180,51 +187,6 @@ const deleteTag = async (tag: Tag) => {
|
|
|
180
187
|
await executeDelete({ _id: tag._id })
|
|
181
188
|
}
|
|
182
189
|
}
|
|
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
190
|
const restore = async (_id: string) => {
|
|
229
191
|
executeRestore({ _id })
|
|
230
192
|
}
|
|
@@ -3,6 +3,8 @@
|
|
|
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"
|
|
@@ -74,6 +76,7 @@ import type { User } from "@raclettejs/core"
|
|
|
74
76
|
import { formatDistance } from "date-fns"
|
|
75
77
|
import { useI18n } from "vue-i18n"
|
|
76
78
|
import BaseDataTable from "@raclettejs/core/orchestrator/components/dataTable/BaseDataTable.vue"
|
|
79
|
+
import { recordsFromStore } from "@app/lib/workbenchListTransfer"
|
|
77
80
|
import { useWorkbenchTableActions } from "@app/composables/useWorkbenchTableActions"
|
|
78
81
|
const showDeleted = ref(false)
|
|
79
82
|
|
|
@@ -123,7 +126,7 @@ const { execute: executeHardDelete } = await $data.user.deleteHard({
|
|
|
123
126
|
const { execute: executeRestore } = await $data.user.updateRestore({
|
|
124
127
|
options: { cb: () => execute({ isDeleted: showDeleted.value }) },
|
|
125
128
|
})
|
|
126
|
-
const userTableRows = (
|
|
129
|
+
const userTableRows = () => {
|
|
127
130
|
if (!users.value || !tags.value) {
|
|
128
131
|
return []
|
|
129
132
|
}
|
|
@@ -131,15 +134,15 @@ const userTableRows = (relativeUpdatedAt: boolean) => {
|
|
|
131
134
|
return Object.values(users.value as { [key: string]: User }).map((user) => ({
|
|
132
135
|
...user,
|
|
133
136
|
tags: user.tags.map((tag) => tags.value?.[tag]),
|
|
134
|
-
|
|
135
|
-
? { updatedAt: formatDistance(new Date(user.updatedAt), new Date()) }
|
|
136
|
-
: {}),
|
|
137
|
+
updatedAt: formatDistance(new Date(user.updatedAt), new Date()),
|
|
137
138
|
}))
|
|
138
139
|
}
|
|
139
140
|
|
|
140
|
-
const parsedUsers = computed(() => userTableRows(
|
|
141
|
+
const parsedUsers = computed(() => userTableRows())
|
|
141
142
|
|
|
142
|
-
const exportableUsers = computed(() =>
|
|
143
|
+
const exportableUsers = computed(() =>
|
|
144
|
+
recordsFromStore(users.value as Record<string, User> | undefined),
|
|
145
|
+
)
|
|
143
146
|
const deleteUser = async (user: User) => {
|
|
144
147
|
if (showDeleted.value) {
|
|
145
148
|
await executeHardDelete({ _id: user._id })
|
|
@@ -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
|
-
}
|