@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 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.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>
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
@@ -252,7 +252,6 @@
252
252
  "deselectAll": "Deselect all",
253
253
  "done": "Done",
254
254
  "valueTableTitle": "Cache value",
255
- "tableActions": "Actions",
256
255
  "openValueTable": "Open value table",
257
256
  "openValueTableFor": "Open value table for {key}",
258
257
  "tableHeaders": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raclettejs/workbench",
3
- "version": "0.1.36",
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",
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.36",
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
- <v-btn
67
- icon="mdi-delete"
68
- size="small"
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, $user } = usePluginApi("raclette__core")
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
- const compositionTableRows = (relativeDates: boolean) => {
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
- ...(relativeDates
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(true))
242
+ const parsedCompositions = computed(() => compositionTableRows())
228
243
 
229
- const exportableCompositions = computed(() => compositionTableRows(false))
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
- <v-btn
62
- icon="mdi-delete"
63
- size="small"
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 { usePluginApi } from "@raclettejs/core/orchestrator/composables"
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
- mapJsonValues,
104
- parseAndNormalizeWorkbenchImportJson,
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 importableItems = ref<any[]>([])
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, $user } = usePluginApi("raclette__core")
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
- const interactionLinkTableRows = (relativeDates: boolean) => {
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
- ...(relativeDates
206
- ? {
207
- createdAt: formatDistance(
208
- new Date(interactionLink.createdAt),
209
- new Date(),
210
- ),
211
- updatedAt: formatDistance(
212
- new Date(interactionLink.updatedAt),
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(true))
269
+ const parsedInteractionLinks = computed(() => interactionLinkTableRows())
221
270
 
222
271
  const exportableInteractionLinks = computed(() =>
223
- interactionLinkTableRows(false),
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
- <v-btn
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 { usePluginApi } from "@raclettejs/core/orchestrator/composables"
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
- mapJsonValues,
97
- parseAndNormalizeWorkbenchImportJson,
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
- import { useWorkbenchTableActions } from "@app/composables/useWorkbenchTableActions"
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, $user } = usePluginApi("raclette__core")
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
- const tagTableRows = (relativeUpdatedAt: boolean) => {
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
- ...(relativeUpdatedAt
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(true))
195
+ const parsedTags = computed(() => tagTableRows())
173
196
 
174
- const exportableTags = computed(() => tagTableRows(false))
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
- <v-btn
84
+ <BaseDataTableConfirmDeleteBtn
58
85
  v-if="item._id !== $user?._id"
59
- icon="mdi-delete"
60
- size="small"
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 { usePluginApi } from "@raclettejs/core/orchestrator/composables"
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 = (relativeUpdatedAt: boolean) => {
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
- ...(relativeUpdatedAt
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(true))
166
+ const parsedUsers = computed(() => userTableRows())
141
167
 
142
- const exportableUsers = computed(() => userTableRows(false))
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
- }