@raclettejs/workbench 0.1.30 → 0.1.32-nightly

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 +7 -9
  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 +61 -105
  18. package/plugins/pacifico__interactionLinks/frontend/widgets/InteractionLinkListWidget.vue +64 -120
  19. package/plugins/pacifico__plugins/frontend/widgets/PluginListWidget.vue +2 -2
  20. package/plugins/pacifico__tags/frontend/widgets/TagListWidget.vue +45 -102
  21. package/plugins/pacifico__users/frontend/widgets/UserListWidget.vue +24 -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/dataTable/BaseDataTable.vue"
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,16 +48,6 @@
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
52
  <SelectionDialog
81
53
  v-model="showImportDialog"
@@ -86,16 +58,6 @@
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
62
  <SummaryDialog
101
63
  v-model="showImportSummary"
@@ -113,30 +75,26 @@
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/dataTable/BaseDataTable.vue"
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 SelectionDialog from "@raclettejs/core/orchestrator/components/dataTable/SelectionDialog.vue"
89
+ import SummaryDialog from "@raclettejs/core/orchestrator/components/dataTable/SummaryDialog.vue"
125
90
  const showDeleted = ref(false)
126
91
  const { t } = useI18n()
127
- // Dialog states
128
- const showExportDialog = ref(false)
129
92
  const showImportDialog = ref(false)
130
- const showExportSummary = ref(false)
131
93
  const showImportSummary = ref(false)
132
94
 
133
- // Data for dialogs
134
- const exportableItems = ref<any[]>([])
135
95
  const importableItems = ref<any[]>([])
136
- const exportedItems = ref<any[]>([])
137
96
  const importedItems = ref<any[]>([])
138
- const pendingImportData = ref<any>(null)
139
- const uploadRef = useTemplateRef("uploadRef")
97
+ const pendingImportById = ref<Record<string, any>>({})
140
98
  const headers = computed<{ title: string; key: keyof Tag }[]>(() => [
141
99
  {
142
100
  title: t("workbench.tagList.tableHeaders.title"),
@@ -183,16 +141,23 @@ const { execute: executeHardDelete } = await $data.tag.deleteHard({
183
141
  const { execute: executeRestore } = await $data.tag.updateRestore({
184
142
  options: { cb: () => execute({ isDeleted: showDeleted.value }) },
185
143
  })
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
- }))
144
+ const tagTableRows = (relativeUpdatedAt: boolean) => {
145
+ if (!tags.value) {
146
+ return []
193
147
  }
194
- return []
195
- })
148
+
149
+ return Object.values(tags.value as { [key: string]: Tag }).map((tag) => ({
150
+ ...tag,
151
+ tags: tag?.tags?.map((t) => tags.value?.[t]) ?? [],
152
+ ...(relativeUpdatedAt
153
+ ? { updatedAt: formatDistance(new Date(tag.updatedAt), new Date()) }
154
+ : {}),
155
+ }))
156
+ }
157
+
158
+ const parsedTags = computed(() => tagTableRows(true))
159
+
160
+ const exportableTags = computed(() => tagTableRows(false))
196
161
 
197
162
  const deleteTag = async (tag: Tag) => {
198
163
  if (showDeleted.value) {
@@ -201,48 +166,26 @@ const deleteTag = async (tag: Tag) => {
201
166
  await executeDelete({ _id: tag._id })
202
167
  }
203
168
  }
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
169
  // Import functions
230
- const handleFileUpload = (content: string, file: File) => {
170
+ const handleFileUpload = (content: string | ArrayBuffer) => {
171
+ if (typeof content !== "string") {
172
+ return
173
+ }
231
174
  try {
232
- const parsedData = JSON.parse(content)
233
- const itemsArray = Object.values(parsedData)
234
-
235
- importableItems.value = itemsArray
236
- pendingImportData.value = parsedData
175
+ const { items, byId } = parseAndNormalizeWorkbenchImportJson(content)
176
+ importableItems.value = items
177
+ pendingImportById.value = byId
237
178
  showImportDialog.value = true
238
179
  } catch (error) {
239
- console.error("Failed to parse JSON:", error)
180
+ console.error("Failed to prepare import:", error)
240
181
  }
241
182
  }
242
183
 
243
184
  const handleImportConfirm = (selectedIds: string[]) => {
244
- const selectedItems = selectedIds.map((id) => pendingImportData.value[id])
245
- importedItems.value = selectedItems
185
+ importedItems.value = resolveWorkbenchImportSelection(
186
+ pendingImportById.value,
187
+ selectedIds,
188
+ )
246
189
  showImportSummary.value = true
247
190
  }
248
191
 
@@ -264,7 +207,7 @@ const finalizeImport = async () => {
264
207
  // e.g., call an API endpoint to save the imported compositions
265
208
 
266
209
  // Reset states
267
- pendingImportData.value = null
210
+ pendingImportById.value = {}
268
211
  importableItems.value = []
269
212
  importedItems.value = []
270
213
  }
@@ -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/dataTable/BaseDataTable.vue"
58
59
  const showDeleted = ref(false)
59
60
 
60
61
  const props = defineProps<{
@@ -102,18 +103,25 @@ 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(
112
+ (user) => ({
113
+ ...user,
114
+ tags: user.tags.map((tag) => tags.value?.[tag]),
115
+ ...(relativeUpdatedAt
116
+ ? { updatedAt: formatDistance(new Date(user.updatedAt), new Date()) }
117
+ : {}),
118
+ }),
119
+ )
120
+ }
121
+
122
+ const parsedUsers = computed(() => userTableRows(true))
123
+
124
+ const exportableUsers = computed(() => userTableRows(false))
117
125
  const deleteUser = async (user: User) => {
118
126
  if (showDeleted.value) {
119
127
  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
  }