@raclettejs/workbench 0.1.31 → 0.1.32-nightly-1

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.
Files changed (26) hide show
  1. package/.raclette/backend/raclette.config.js +515 -0
  2. package/.raclette/backend/yarn.lock +3636 -0
  3. package/.raclette/frontend/raclette.config.js +43 -0
  4. package/.raclette/frontend/yarn.lock +3388 -0
  5. package/.raclette/virtual/backend/README.md +172 -0
  6. package/.raclette/virtual/backend/raclette.config.js +515 -0
  7. package/.raclette/virtual/backend/yarn.lock +3636 -0
  8. package/.raclette/virtual/frontend/README.md +511 -0
  9. package/.raclette/virtual/frontend/raclette.config.js +43 -0
  10. package/.raclette/virtual/frontend/src/core/lib/eggs/readme.md +75 -0
  11. package/.raclette/virtual/frontend/yarn.lock +3388 -0
  12. package/CHANGELOG.md +6 -10
  13. package/i18n/de-DE.json +1 -10
  14. package/i18n/en-EU.json +1 -10
  15. package/i18n/sk-SK.json +1 -10
  16. package/package.json +5 -3
  17. package/plugins/pacifico__compositions/frontend/widgets/CompositionListWidget.vue +63 -107
  18. package/plugins/pacifico__interactionLinks/frontend/widgets/InteractionLinkListWidget.vue +65 -122
  19. package/plugins/pacifico__plugins/frontend/widgets/PluginListWidget.vue +2 -2
  20. package/plugins/pacifico__tags/frontend/widgets/TagListWidget.vue +48 -104
  21. package/plugins/pacifico__users/frontend/widgets/UserListWidget.vue +22 -16
  22. package/services/frontend/src/app/lib/jsonTools.ts +69 -3
  23. package/services/frontend/src/app/components/BaseDataTable.vue +0 -303
  24. package/services/frontend/src/app/components/FileUpload.vue +0 -98
  25. package/services/frontend/src/app/components/SelectionDialog.vue +0 -182
  26. package/services/frontend/src/app/components/SummaryDialog.vue +0 -140
@@ -8,20 +8,20 @@
8
8
  :show-delete-action="false"
9
9
  :show-actions-column="false"
10
10
  :row-click-handler="onRowClick"
11
+ :show-export="false"
11
12
  >
12
13
  </BaseDataTable>
13
14
  </template>
14
15
 
15
16
  <script setup lang="ts">
16
17
  import useWorkbenchPlugins from "@app/composables/useWorkbenchPlugins"
17
- import BaseDataTable from "@app/components/BaseDataTable.vue"
18
+ import { BaseDataTable } from "@raclettejs/core/orchestrator/components"
18
19
  import type { PluginMetadata } from "@raclettejs/core"
19
20
  import { computed } from "vue"
20
21
  import {
21
22
  usePluginApi,
22
23
  useRouteState,
23
24
  } from "@raclettejs/core/orchestrator/composables"
24
- import { DEFAULT_SLOT_NAME } from "@raclettejs/core/orchestrator/router/routeParserHelper"
25
25
 
26
26
  const { pluginMetadataFiles, hasPluginSettingsWidget } = useWorkbenchPlugins()
27
27
 
@@ -2,6 +2,7 @@
2
2
  <div>
3
3
  <BaseDataTable
4
4
  :items="parsedTags"
5
+ :exportable-items="exportableTags"
5
6
  :loading="tagsLoading"
6
7
  :headers="headers"
7
8
  create-interaction-link-id="tagCreateInteractionLink"
@@ -9,16 +10,17 @@
9
10
  :data-name="$t('workbench.tagList.dataName')"
10
11
  :data-article="$t('workbench.tagList.dataArticle')"
11
12
  @delete="deleteTag"
12
- :itemsDeleted="showDeleted"
13
+ :items-deleted="showDeleted"
14
+ :on-file-loaded="handleFileUpload"
13
15
  >
14
- <template #actions>
16
+ <template #toolbar-leading>
15
17
  <v-tooltip
16
18
  :text="showDeleted ? $t('core.hideDeleted') : $t('core.showDeleted')"
17
19
  >
18
- <template v-slot:activator="{ props }">
20
+ <template #activator="{ props: activatorProps }">
19
21
  <v-btn-toggle
20
22
  density="compact"
21
- v-bind="props"
23
+ v-bind="activatorProps"
22
24
  v-model="showDeleted"
23
25
  mandatory
24
26
  :color="showDeleted ? 'error' : 'success'"
@@ -30,26 +32,6 @@
30
32
  </v-btn-toggle>
31
33
  </template>
32
34
  </v-tooltip>
33
- <v-btn-group density="compact">
34
- <v-tooltip :text="$t('core.export')">
35
- <template v-slot:activator="{ props }">
36
- <v-btn color="info" v-bind="props" flat @click="openExportDialog">
37
- <v-icon>mdi-database-export</v-icon>
38
- </v-btn>
39
- </template>
40
- </v-tooltip>
41
- <v-tooltip :text="$t('core.import')">
42
- <template v-slot:activator="{ props }">
43
- <v-btn v-bind="props" color="warning">
44
- <FileUpload
45
- ref="uploadRef"
46
- :onFileLoaded="handleFileUpload"
47
- accept=".json"
48
- readAs="text"
49
- /></v-btn>
50
- </template>
51
- </v-tooltip>
52
- </v-btn-group>
53
35
  </template>
54
36
  <template #prepend-row-actions="{ item }">
55
37
  <v-icon
@@ -66,18 +48,8 @@
66
48
  />
67
49
  </template>
68
50
  </BaseDataTable>
69
- <!-- Export Selection Dialog -->
70
- <SelectionDialog
71
- v-model="showExportDialog"
72
- :items="exportableItems"
73
- display-attribute="title"
74
- :title="$t('core.selectItemsToExport')"
75
- :confirm-text="$t('core.export')"
76
- @confirm="handleExportConfirm"
77
- />
78
-
79
51
  <!-- Import Selection Dialog -->
80
- <SelectionDialog
52
+ <ImportSelectionDialog
81
53
  v-model="showImportDialog"
82
54
  :items="importableItems"
83
55
  display-attribute="title"
@@ -86,18 +58,8 @@
86
58
  @confirm="handleImportConfirm"
87
59
  />
88
60
 
89
- <!-- Export Summary Dialog -->
90
- <SummaryDialog
91
- v-model="showExportSummary"
92
- display-attribute="title"
93
- :items="exportedItems"
94
- :title="$t('core.exportSummary')"
95
- :message="$t('core.exportSuccessMessage')"
96
- type="success"
97
- />
98
-
99
61
  <!-- Import Summary Dialog -->
100
- <SummaryDialog
62
+ <ImportSummaryDialog
101
63
  v-model="showImportSummary"
102
64
  display-attribute="title"
103
65
  :items="importedItems"
@@ -113,30 +75,27 @@
113
75
 
114
76
  <script setup lang="ts">
115
77
  import { usePluginApi } from "@raclettejs/core/orchestrator/composables"
116
- import { computed, ref, watch, useTemplateRef } from "vue"
78
+ import { computed, ref, watch } from "vue"
117
79
  import type { Tag } from "@raclettejs/core"
118
80
  import { useI18n } from "vue-i18n"
119
- import BaseDataTable from "@app/components/BaseDataTable.vue"
81
+ import { BaseDataTable } from "@raclettejs/core/orchestrator/components"
120
82
  import { formatDistance } from "date-fns"
121
- import { downloadJson, mapJsonValues } from "@app/lib/jsonTools"
122
- import FileUpload from "@app/components/FileUpload.vue"
123
- import SelectionDialog from "@app/components/SelectionDialog.vue"
124
- import SummaryDialog from "@app/components/SummaryDialog.vue"
83
+ import {
84
+ mapJsonValues,
85
+ parseAndNormalizeWorkbenchImportJson,
86
+ resolveWorkbenchImportSelection,
87
+ } from "@app/lib/jsonTools"
88
+ import { ImportSelectionDialog } from "@raclettejs/core/orchestrator/components"
89
+ import { ImportSummaryDialog } from "@raclettejs/core/orchestrator/components"
90
+
125
91
  const showDeleted = ref(false)
126
92
  const { t } = useI18n()
127
- // Dialog states
128
- const showExportDialog = ref(false)
129
93
  const showImportDialog = ref(false)
130
- const showExportSummary = ref(false)
131
94
  const showImportSummary = ref(false)
132
95
 
133
- // Data for dialogs
134
- const exportableItems = ref<any[]>([])
135
96
  const importableItems = ref<any[]>([])
136
- const exportedItems = ref<any[]>([])
137
97
  const importedItems = ref<any[]>([])
138
- const pendingImportData = ref<any>(null)
139
- const uploadRef = useTemplateRef("uploadRef")
98
+ const pendingImportById = ref<Record<string, any>>({})
140
99
  const headers = computed<{ title: string; key: keyof Tag }[]>(() => [
141
100
  {
142
101
  title: t("workbench.tagList.tableHeaders.title"),
@@ -183,16 +142,23 @@ const { execute: executeHardDelete } = await $data.tag.deleteHard({
183
142
  const { execute: executeRestore } = await $data.tag.updateRestore({
184
143
  options: { cb: () => execute({ isDeleted: showDeleted.value }) },
185
144
  })
186
- const parsedTags = computed(() => {
187
- if (tags.value) {
188
- return Object.values(tags.value as { [key: string]: Tag }).map((tag) => ({
189
- ...tag,
190
- tags: tag?.tags?.map((t) => tags.value?.[t]) ?? [],
191
- updatedAt: formatDistance(new Date(tag.updatedAt), new Date()),
192
- }))
145
+ const tagTableRows = (relativeUpdatedAt: boolean) => {
146
+ if (!tags.value) {
147
+ return []
193
148
  }
194
- return []
195
- })
149
+
150
+ return Object.values(tags.value as { [key: string]: Tag }).map((tag) => ({
151
+ ...tag,
152
+ tags: tag?.tags?.map((t) => tags.value?.[t]) ?? [],
153
+ ...(relativeUpdatedAt
154
+ ? { updatedAt: formatDistance(new Date(tag.updatedAt), new Date()) }
155
+ : {}),
156
+ }))
157
+ }
158
+
159
+ const parsedTags = computed(() => tagTableRows(true))
160
+
161
+ const exportableTags = computed(() => tagTableRows(false))
196
162
 
197
163
  const deleteTag = async (tag: Tag) => {
198
164
  if (showDeleted.value) {
@@ -201,48 +167,26 @@ const deleteTag = async (tag: Tag) => {
201
167
  await executeDelete({ _id: tag._id })
202
168
  }
203
169
  }
204
- const openExportDialog = () => {
205
- const allTags = Object.values(tags.value || {})
206
- const exportableTags = []
207
- allTags.forEach((tag) => {
208
- if (!tag.locked) {
209
- exportableTags.push(tag)
210
- }
211
- })
212
- exportableItems.value = exportableTags
213
- showExportDialog.value = true
214
- }
215
-
216
- const handleExportConfirm = (selectedIds: string[]) => {
217
- const dataToExport = selectedIds.reduce((acc, id) => {
218
- if (tags.value?.[id]) {
219
- acc[id] = tags.value[id]
220
- }
221
- return acc
222
- }, {} as any)
223
-
224
- exportedItems.value = Object.values(dataToExport)
225
- downloadJson(dataToExport, "tags")
226
- showExportSummary.value = true
227
- }
228
-
229
170
  // Import functions
230
- const handleFileUpload = (content: string, file: File) => {
171
+ const handleFileUpload = (content: string | ArrayBuffer) => {
172
+ if (typeof content !== "string") {
173
+ return
174
+ }
231
175
  try {
232
- const parsedData = JSON.parse(content)
233
- const itemsArray = Object.values(parsedData)
234
-
235
- importableItems.value = itemsArray
236
- pendingImportData.value = parsedData
176
+ const { items, byId } = parseAndNormalizeWorkbenchImportJson(content)
177
+ importableItems.value = items
178
+ pendingImportById.value = byId
237
179
  showImportDialog.value = true
238
180
  } catch (error) {
239
- console.error("Failed to parse JSON:", error)
181
+ console.error("Failed to prepare import:", error)
240
182
  }
241
183
  }
242
184
 
243
185
  const handleImportConfirm = (selectedIds: string[]) => {
244
- const selectedItems = selectedIds.map((id) => pendingImportData.value[id])
245
- importedItems.value = selectedItems
186
+ importedItems.value = resolveWorkbenchImportSelection(
187
+ pendingImportById.value,
188
+ selectedIds,
189
+ )
246
190
  showImportSummary.value = true
247
191
  }
248
192
 
@@ -264,7 +208,7 @@ const finalizeImport = async () => {
264
208
  // e.g., call an API endpoint to save the imported compositions
265
209
 
266
210
  // Reset states
267
- pendingImportData.value = null
211
+ pendingImportById.value = {}
268
212
  importableItems.value = []
269
213
  importedItems.value = []
270
214
  }
@@ -1,6 +1,7 @@
1
1
  <template>
2
2
  <BaseDataTable
3
3
  :items="parsedUsers"
4
+ :exportable-items="exportableUsers"
4
5
  :loading="usersLoading || tagsLoading"
5
6
  :headers="headers"
6
7
  create-interaction-link-id="userCreateInteractionLink"
@@ -9,16 +10,16 @@
9
10
  :data-article="$t('workbench.userList.dataArticle')"
10
11
  :delete-validator="(user) => user._id !== $user?._id"
11
12
  @delete="deleteUser"
12
- :itemsDeleted="showDeleted"
13
+ :items-deleted="showDeleted"
13
14
  >
14
- <template #actions>
15
+ <template #toolbar-leading>
15
16
  <v-tooltip
16
17
  :text="showDeleted ? $t('core.hideDeleted') : $t('core.showDeleted')"
17
18
  >
18
- <template v-slot:activator="{ props }">
19
+ <template #activator="{ props: activatorProps }">
19
20
  <v-btn-toggle
20
21
  density="compact"
21
- v-bind="props"
22
+ v-bind="activatorProps"
22
23
  v-model="showDeleted"
23
24
  mandatory
24
25
  :color="showDeleted ? 'error' : 'success'"
@@ -54,7 +55,7 @@ import { computed, watch, ref } from "vue"
54
55
  import type { User } from "@raclettejs/core"
55
56
  import { formatDistance } from "date-fns"
56
57
  import { useI18n } from "vue-i18n"
57
- import BaseDataTable from "@app/components/BaseDataTable.vue"
58
+ import { BaseDataTable } from "@raclettejs/core/orchestrator/components"
58
59
  const showDeleted = ref(false)
59
60
 
60
61
  const props = defineProps<{
@@ -102,18 +103,23 @@ const { execute: executeHardDelete } = await $data.user.deleteHard({
102
103
  const { execute: executeRestore } = await $data.user.updateRestore({
103
104
  options: { cb: () => execute({ isDeleted: showDeleted.value }) },
104
105
  })
105
- const parsedUsers = computed(() => {
106
- if (users.value && tags.value) {
107
- return Object.values(users.value as { [key: string]: User }).map(
108
- (user) => ({
109
- ...user,
110
- tags: user.tags.map((tag) => tags.value?.[tag]),
111
- updatedAt: formatDistance(new Date(user.updatedAt), new Date()),
112
- }),
113
- )
106
+ const userTableRows = (relativeUpdatedAt: boolean) => {
107
+ if (!users.value || !tags.value) {
108
+ return []
114
109
  }
115
- return []
116
- })
110
+
111
+ return Object.values(users.value as { [key: string]: User }).map((user) => ({
112
+ ...user,
113
+ tags: user.tags.map((tag) => tags.value?.[tag]),
114
+ ...(relativeUpdatedAt
115
+ ? { updatedAt: formatDistance(new Date(user.updatedAt), new Date()) }
116
+ : {}),
117
+ }))
118
+ }
119
+
120
+ const parsedUsers = computed(() => userTableRows(true))
121
+
122
+ const exportableUsers = computed(() => userTableRows(false))
117
123
  const deleteUser = async (user: User) => {
118
124
  if (showDeleted.value) {
119
125
  await executeHardDelete({ _id: user._id })
@@ -1,10 +1,75 @@
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
+
1
66
  /**
2
67
  * Downloads a JSON object as a .json file
3
68
  * @param {Object|Array} data - The data to be downloaded as JSON
4
69
  * @param {string} filename - The name of the file (without .json extension)
5
70
  * @throws {Error} If data cannot be serialized to JSON
6
71
  */
7
- export function downloadJson(data, filename = "download") {
72
+ export const downloadJson = (data, filename = "download") => {
8
73
  try {
9
74
  // Convert data to JSON string with pretty formatting
10
75
  const jsonString = JSON.stringify(data, null, 2)
@@ -33,7 +98,8 @@ export function downloadJson(data, filename = "download") {
33
98
  URL.revokeObjectURL(url)
34
99
  } catch (error) {
35
100
  console.error("Failed to download JSON:", error)
36
- throw new Error(`Unable to download JSON file: ${error.message}`)
101
+ const message = error instanceof Error ? error.message : String(error)
102
+ throw new Error(`Unable to download JSON file: ${message}`)
37
103
  }
38
104
  }
39
105
  /**
@@ -49,7 +115,7 @@ export function downloadJson(data, filename = "download") {
49
115
  * const result = mapJsonValues(data, mapping);
50
116
  * // Result: { owner: 'user456', name: 'Project A', lastEditor: 'user789' }
51
117
  */
52
- export function mapJsonValues(data, mapping = {}) {
118
+ export const mapJsonValues = (data, mapping = {}) => {
53
119
  if (!data || typeof data !== "object") {
54
120
  return data
55
121
  }