@raclettejs/workbench 0.1.18 → 0.1.20

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 (35) hide show
  1. package/.raclette/backend/raclette.config.js +519 -0
  2. package/{yarn.lock → .raclette/backend/yarn.lock} +1014 -796
  3. package/.raclette/frontend/raclette.config.js +47 -0
  4. package/.raclette/frontend/yarn.lock +3428 -0
  5. package/.raclette/raclette.config.js +643 -0
  6. package/.raclette/virtual/backend/README.md +172 -0
  7. package/.raclette/virtual/backend/raclette.config.js +519 -0
  8. package/.raclette/virtual/backend/yarn.lock +3773 -0
  9. package/.raclette/virtual/frontend/README.md +511 -0
  10. package/.raclette/virtual/frontend/raclette.config.js +47 -0
  11. package/.raclette/virtual/frontend/src/core/lib/eggs/readme.md +75 -0
  12. package/.raclette/virtual/frontend/yarn.lock +3428 -0
  13. package/CHANGELOG.md +11 -0
  14. package/i18n/de-DE.json +17 -1
  15. package/i18n/en-EU.json +17 -1
  16. package/i18n/sk-SK.json +17 -1
  17. package/package.json +6 -4
  18. package/packages.json +3 -1
  19. package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/CompositionWidgetsLayout.vue +8 -3
  20. package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/widgetsLayout/WidgetContextMenu.vue +60 -0
  21. package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/widgetsLayout/WidgetIcon.vue +4 -7
  22. package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/widgetsLayout/WidgetList.vue +34 -117
  23. package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/widgetsLayout/WidgetListFlat.vue +20 -0
  24. package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/widgetsLayout/WidgetListGrouped.vue +50 -0
  25. package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/widgetsLayout/WidgetListHeader.vue +213 -0
  26. package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/widgetsLayout/WidgetListItem.vue +56 -0
  27. package/plugins/pacifico__compositions/frontend/composables/useWidgetList.ts +288 -0
  28. package/plugins/pacifico__compositions/frontend/widgets/CompositionListWidget.vue +201 -57
  29. package/plugins/pacifico__interactionLinks/frontend/widgets/InteractionLinkListWidget.vue +219 -55
  30. package/plugins/pacifico__tags/frontend/widgets/TagListWidget.vue +198 -48
  31. package/raclette.config.js +4 -1
  32. package/services/frontend/src/app/components/FileUpload.vue +98 -0
  33. package/services/frontend/src/app/components/SelectionDialog.vue +182 -0
  34. package/services/frontend/src/app/components/SummaryDialog.vue +140 -0
  35. package/services/frontend/src/app/lib/jsonTools.ts +81 -0
@@ -1,62 +1,142 @@
1
1
  <template>
2
- <BaseDataTable
3
- :items="parsedTags"
4
- :loading="tagsLoading"
5
- :headers="headers"
6
- create-interaction-link-id="tagCreateInteractionLink"
7
- edit-interaction-link-id="tagEditInteractionLink"
8
- :data-name="$t('workbench.tagList.dataName')"
9
- :data-article="$t('workbench.tagList.dataArticle')"
10
- @delete="deleteTag"
11
- :itemsDeleted="showDeleted"
12
- >
13
- <template #actions>
14
- <v-tooltip
15
- :text="showDeleted ? $t('core.hideDeleted') : $t('core.showDeleted')"
16
- >
17
- <template v-slot:activator="{ props }">
18
- <v-btn-toggle
19
- density="compact"
20
- v-bind="props"
21
- v-model="showDeleted"
22
- mandatory
23
- :color="showDeleted ? 'error' : 'success'"
24
- >
25
- <v-btn :value="true">
26
- <v-icon>mdi-table-eye-off</v-icon>
27
- </v-btn>
28
- <v-btn :value="false"><v-icon>mdi-table-eye</v-icon></v-btn>
29
- </v-btn-toggle>
30
- </template>
31
- </v-tooltip>
32
- </template>
33
- <template #prepend-row-actions="{ item }">
34
- <v-icon
35
- v-if="showDeleted"
36
- color="green"
37
- class="mr-2"
38
- @click="restore(item._id)"
39
- :title="
40
- $t('workbench.baseDataTable.restoreDataTitle', {
41
- dataName: item.title,
42
- })
43
- "
44
- icon="mdi-delete-restore"
45
- />
46
- </template>
47
- </BaseDataTable>
2
+ <div>
3
+ <BaseDataTable
4
+ :items="parsedTags"
5
+ :loading="tagsLoading"
6
+ :headers="headers"
7
+ create-interaction-link-id="tagCreateInteractionLink"
8
+ edit-interaction-link-id="tagEditInteractionLink"
9
+ :data-name="$t('workbench.tagList.dataName')"
10
+ :data-article="$t('workbench.tagList.dataArticle')"
11
+ @delete="deleteTag"
12
+ :itemsDeleted="showDeleted"
13
+ >
14
+ <template #actions>
15
+ <v-tooltip
16
+ :text="showDeleted ? $t('core.hideDeleted') : $t('core.showDeleted')"
17
+ >
18
+ <template v-slot:activator="{ props }">
19
+ <v-btn-toggle
20
+ density="compact"
21
+ v-bind="props"
22
+ v-model="showDeleted"
23
+ mandatory
24
+ :color="showDeleted ? 'error' : 'success'"
25
+ >
26
+ <v-btn :value="true">
27
+ <v-icon>mdi-table-eye-off</v-icon>
28
+ </v-btn>
29
+ <v-btn :value="false"><v-icon>mdi-table-eye</v-icon></v-btn>
30
+ </v-btn-toggle>
31
+ </template>
32
+ </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
+ </template>
54
+ <template #prepend-row-actions="{ item }">
55
+ <v-icon
56
+ v-if="showDeleted"
57
+ color="green"
58
+ class="mr-2"
59
+ @click="restore(item._id)"
60
+ :title="
61
+ $t('workbench.baseDataTable.restoreDataTitle', {
62
+ dataName: item.title,
63
+ })
64
+ "
65
+ icon="mdi-delete-restore"
66
+ />
67
+ </template>
68
+ </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
+ <!-- Import Selection Dialog -->
80
+ <SelectionDialog
81
+ v-model="showImportDialog"
82
+ :items="importableItems"
83
+ display-attribute="title"
84
+ :title="$t('core.selectItemsToImport')"
85
+ :confirm-text="$t('core.import')"
86
+ @confirm="handleImportConfirm"
87
+ />
88
+
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
+ <!-- Import Summary Dialog -->
100
+ <SummaryDialog
101
+ v-model="showImportSummary"
102
+ display-attribute="title"
103
+ :items="importedItems"
104
+ :title="$t('core.importSummary')"
105
+ :message="$t('core.importSuccessMessage')"
106
+ type="success"
107
+ :show-confirm="true"
108
+ :confirm-text="$t('core.saveImported')"
109
+ @confirm="finalizeImport"
110
+ />
111
+ </div>
48
112
  </template>
49
113
 
50
114
  <script setup lang="ts">
51
115
  import { usePluginApi } from "@raclettejs/core/orchestrator/composables"
52
- import { computed, ref, watch } from "vue"
116
+ import { computed, ref, watch, useTemplateRef } from "vue"
53
117
  import type { Tag } from "@raclettejs/core"
54
118
  import { useI18n } from "vue-i18n"
55
119
  import BaseDataTable from "@app/components/BaseDataTable.vue"
56
120
  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"
57
125
  const showDeleted = ref(false)
58
126
  const { t } = useI18n()
127
+ // Dialog states
128
+ const showExportDialog = ref(false)
129
+ const showImportDialog = ref(false)
130
+ const showExportSummary = ref(false)
131
+ const showImportSummary = ref(false)
59
132
 
133
+ // Data for dialogs
134
+ const exportableItems = ref<any[]>([])
135
+ const importableItems = ref<any[]>([])
136
+ const exportedItems = ref<any[]>([])
137
+ const importedItems = ref<any[]>([])
138
+ const pendingImportData = ref<any>(null)
139
+ const uploadRef = useTemplateRef("uploadRef")
60
140
  const headers = computed<{ title: string; key: keyof Tag }[]>(() => [
61
141
  {
62
142
  title: t("workbench.tagList.tableHeaders.title"),
@@ -80,7 +160,7 @@ const props = defineProps<{
80
160
  uuid: string
81
161
  }>()
82
162
 
83
- const { $data, $socket } = usePluginApi("raclette__core")
163
+ const { $data, $socket, $user } = usePluginApi("raclette__core")
84
164
 
85
165
  const {
86
166
  data: tags,
@@ -94,6 +174,9 @@ const {
94
174
  const { execute: executeDelete } = $data.tag.delete({
95
175
  options: { cb: () => execute({ isDeleted: showDeleted.value }) },
96
176
  })
177
+ const { execute: executeBulkCreate } = await $data.tag.createBulk({
178
+ options: { notify: false, cb: () => execute() },
179
+ })
97
180
  const { execute: executeHardDelete } = await $data.tag.deleteHard({
98
181
  options: { cb: () => execute({ isDeleted: showDeleted.value }) },
99
182
  })
@@ -118,6 +201,73 @@ const deleteTag = async (tag: Tag) => {
118
201
  await executeDelete({ _id: tag._id })
119
202
  }
120
203
  }
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
+ // Import functions
230
+ const handleFileUpload = (content: string, file: File) => {
231
+ try {
232
+ const parsedData = JSON.parse(content)
233
+ const itemsArray = Object.values(parsedData)
234
+
235
+ importableItems.value = itemsArray
236
+ pendingImportData.value = parsedData
237
+ showImportDialog.value = true
238
+ } catch (error) {
239
+ console.error("Failed to parse JSON:", error)
240
+ }
241
+ }
242
+
243
+ const handleImportConfirm = (selectedIds: string[]) => {
244
+ const selectedItems = selectedIds.map((id) => pendingImportData.value[id])
245
+ importedItems.value = selectedItems
246
+ showImportSummary.value = true
247
+ }
248
+
249
+ const finalizeImport = async () => {
250
+ const mapping = {
251
+ owner: $user.value._id,
252
+ lastEditor: $user.value._id,
253
+ createdBy: $user.value._id,
254
+ }
255
+
256
+ const dataToImport = importedItems.value.reduce((acc, item) => {
257
+ const mappedItem = mapJsonValues(item, mapping)
258
+ acc[item._id] = mappedItem
259
+ return acc
260
+ }, {} as any)
261
+
262
+ await executeBulkCreate(Object.values(dataToImport))
263
+ // TODO: Add your actual import logic here
264
+ // e.g., call an API endpoint to save the imported compositions
265
+
266
+ // Reset states
267
+ pendingImportData.value = null
268
+ importableItems.value = []
269
+ importedItems.value = []
270
+ }
121
271
  const restore = async (_id: string) => {
122
272
  executeRestore({ _id })
123
273
  }
@@ -4,7 +4,7 @@ import interactionLinks from "./config/interactionLinks.js"
4
4
 
5
5
  export default defineRacletteConfig({
6
6
  name: "raclette-workbench",
7
-
7
+ root: ".raclette",
8
8
  // Configure services
9
9
  services: {
10
10
  frontend: {
@@ -144,4 +144,7 @@ export default defineRacletteConfig({
144
144
  eslint: {
145
145
  useRecommended: true,
146
146
  },
147
+ global: {
148
+ requiresAuthentication: true
149
+ }
147
150
  })
@@ -0,0 +1,98 @@
1
+ <template>
2
+ <v-file-input
3
+ v-model="files"
4
+ :accept="accept"
5
+ ref="fileInput"
6
+ hide-input
7
+ flat
8
+ prepend-icon="mdi-database-import"
9
+ @update:model-value="handleFileChange"
10
+ >
11
+ </v-file-input>
12
+ </template>
13
+
14
+ <script setup lang="ts">
15
+ import { ref, useTemplateRef } from "vue"
16
+ const fileInput = useTemplateRef("fileInput")
17
+ const props = defineProps({
18
+ /**
19
+ * Function to call when file content is loaded.
20
+ * Receives: (content: string | ArrayBuffer, file: File)
21
+ */
22
+ onFileLoaded: {
23
+ type: Function,
24
+ required: true,
25
+ },
26
+
27
+ /**
28
+ * Optional: accept only certain file types
29
+ */
30
+ accept: {
31
+ type: String,
32
+ default: "*/*",
33
+ },
34
+
35
+ /**
36
+ * How to read the file: "text" or "arrayBuffer"
37
+ */
38
+ readAs: {
39
+ type: String,
40
+ default: "text",
41
+ validator: (v: string) => ["text", "arrayBuffer"].includes(v),
42
+ },
43
+ })
44
+
45
+ const files = ref<File[]>([])
46
+ const trigger = () => {
47
+ fileInput.value?.click()
48
+ }
49
+ const handleFileChange = (newFile: File | null) => {
50
+ console.log("File change triggered:", newFile)
51
+ console.log(files.value)
52
+
53
+ if (!newFile) {
54
+ console.log("No file selected")
55
+ return
56
+ }
57
+
58
+ const selectedFile = newFile
59
+ if (!selectedFile) {
60
+ console.log("Selected file is null")
61
+ return
62
+ }
63
+
64
+ console.log(
65
+ "Reading file:",
66
+ selectedFile.name,
67
+ selectedFile.type,
68
+ selectedFile.size,
69
+ )
70
+
71
+ const reader = new FileReader()
72
+
73
+ reader.onload = () => {
74
+ console.log(
75
+ "File loaded successfully, length:",
76
+ (reader.result as string)?.length,
77
+ )
78
+ // Pass content first, then file
79
+ props.onFileLoaded(reader.result, selectedFile)
80
+ }
81
+
82
+ reader.onerror = () => {
83
+ console.error("Error reading file:", reader.error)
84
+ }
85
+
86
+ reader.onprogress = (e) => {
87
+ console.log("Reading progress:", e.loaded, "/", e.total)
88
+ }
89
+
90
+ // Read the file from the parameter, not from refs
91
+ if (props.readAs === "arrayBuffer") {
92
+ reader.readAsArrayBuffer(selectedFile)
93
+ } else {
94
+ reader.readAsText(selectedFile)
95
+ }
96
+ }
97
+ defineExpose({ trigger })
98
+ </script>
@@ -0,0 +1,182 @@
1
+ <template>
2
+ <!-- Dialog overlay -->
3
+ <div
4
+ v-if="dialog"
5
+ class="tw:fixed tw:inset-0 tw:flex tw:items-center tw:justify-center tw:bg-black/50 tw:z-50"
6
+ >
7
+ <!-- Dialog panel -->
8
+ <div
9
+ class="tw:bg-white tw:rounded-lg tw:shadow-xl tw:max-w-xl tw:w-full tw:overflow-auto"
10
+ >
11
+ <!-- Title bar -->
12
+ <div
13
+ class="tw:flex tw:justify-between tw:items-center tw:px-4 tw:py-3 tw:border-b tw:border-gray-200"
14
+ >
15
+ <span class="tw:text-lg tw:font-semibold">{{ title }}</span>
16
+
17
+ <button
18
+ @click="close"
19
+ class="tw:text-gray-500 hover:tw:text-gray-700 tw:p-1 tw:rounded tw:hover:bg-gray-100"
20
+ >
21
+ <i class="mdi mdi-close tw:text-xl"></i>
22
+ </button>
23
+ </div>
24
+
25
+ <!-- Content -->
26
+ <div class="tw:px-4 tw:py-4">
27
+ <!-- Header row -->
28
+ <div class="tw:flex tw:justify-between tw:items-center tw:mb-4">
29
+ <!-- Select all -->
30
+ <label class="tw:flex tw:items-center tw:space-x-2 tw:cursor-pointer">
31
+ <input
32
+ type="checkbox"
33
+ v-model="selectAll"
34
+ @change="toggleSelectAll"
35
+ class="tw:w-4 tw:h-4 tw:rounded tw:border-gray-300 tw:text-primary tw:focus:ring-primary"
36
+ />
37
+ <span class="tw:text-sm tw:text-gray-700">
38
+ {{ $t("core.selectAll") }}
39
+ </span>
40
+ </label>
41
+
42
+ <!-- Chip -->
43
+ <div
44
+ class="tw:px-3 tw:py-1 tw:bg-primary tw:text-white tw:rounded-full tw:text-sm tw:font-medium"
45
+ >
46
+ {{ selectedCount }} / {{ items.length }} {{ $t("core.selected") }}
47
+ </div>
48
+ </div>
49
+
50
+ <!-- List -->
51
+ <ul class="tw:max-h-96 tw:overflow-y-auto tw:space-y-1">
52
+ <li
53
+ v-for="item in items"
54
+ :key="item._id"
55
+ @click="toggleSelection(item._id)"
56
+ class="tw:flex tw:items-center tw:space-x-3 tw:p-2 tw:rounded tw:cursor-pointer hover:tw:bg-gray-50"
57
+ >
58
+ <!-- Checkbox -->
59
+ <input
60
+ type="checkbox"
61
+ :checked="selected.includes(item._id)"
62
+ @click.stop="toggleSelection(item._id)"
63
+ class="tw:w-4 tw:h-4 tw:rounded tw:border-gray-300 tw:text-primary tw:focus:ring-primary"
64
+ />
65
+
66
+ <!-- Label -->
67
+ <span class="tw:text-sm tw:text-gray-800">
68
+ {{ getDisplayValue(item) }}
69
+ </span>
70
+ </li>
71
+ </ul>
72
+
73
+ <!-- Warning for no selection -->
74
+ <div
75
+ v-if="selectedCount === 0"
76
+ class="tw:mt-4 tw:bg-yellow-50 tw:text-yellow-700 tw:p-3 tw:rounded tw:text-sm"
77
+ >
78
+ {{ $t("core.noItemsSelected") }}
79
+ </div>
80
+ </div>
81
+
82
+ <!-- Actions -->
83
+ <div
84
+ class="tw:flex tw:justify-end tw:space-x-2 tw:px-4 tw:py-3 tw:border-t tw:border-gray-200"
85
+ >
86
+ <button
87
+ @click="close"
88
+ class="tw:px-4 tw:py-2 tw:text-sm tw:text-gray-700 tw:rounded"
89
+ >
90
+ {{ $t("core.cancel") }}
91
+ </button>
92
+
93
+ <button
94
+ v-if="selectedCount"
95
+ @click="confirm"
96
+ :disabled="selectedCount === 0"
97
+ class="tw:px-4 tw:py-2 tw:text-sm tw:rounded tw:text-gray-700 tw:bg-primary"
98
+ >
99
+ {{ confirmText }}
100
+ </button>
101
+ </div>
102
+ </div>
103
+ </div>
104
+ </template>
105
+
106
+ <script setup lang="ts">
107
+ import { ref, computed, watch } from "vue"
108
+
109
+ interface Props {
110
+ modelValue: boolean
111
+ items: any[]
112
+ displayAttribute: string
113
+ title: string
114
+ confirmText: string
115
+ }
116
+
117
+ const props = defineProps<Props>()
118
+
119
+ const emit = defineEmits<{
120
+ "update:modelValue": [value: boolean]
121
+ confirm: [selectedIds: string[]]
122
+ }>()
123
+
124
+ const dialog = computed({
125
+ get: () => props.modelValue,
126
+ set: (value) => emit("update:modelValue", value),
127
+ })
128
+
129
+ const selected = ref<string[]>([])
130
+ const selectAll = ref(false)
131
+
132
+ const selectedCount = computed(() => selected.value.length)
133
+
134
+ const getDisplayValue = (item: any) => {
135
+ return item[props.displayAttribute] || item._id
136
+ }
137
+
138
+ const toggleSelection = (id: string) => {
139
+ const index = selected.value.indexOf(id)
140
+ if (index > -1) {
141
+ selected.value.splice(index, 1)
142
+ } else {
143
+ selected.value.push(id)
144
+ }
145
+ updateSelectAllState()
146
+ }
147
+
148
+ const toggleSelectAll = (value: boolean) => {
149
+ if (value) {
150
+ selected.value = props.items.map((item) => item._id)
151
+ } else {
152
+ selected.value = []
153
+ }
154
+ }
155
+
156
+ const updateSelectAllState = () => {
157
+ selectAll.value =
158
+ selected.value.length === props.items.length && props.items.length > 0
159
+ }
160
+
161
+ const close = () => {
162
+ dialog.value = false
163
+ selected.value = []
164
+ selectAll.value = false
165
+ }
166
+
167
+ const confirm = () => {
168
+ if (selectedCount.value > 0) {
169
+ emit("confirm", selected.value)
170
+ close()
171
+ }
172
+ }
173
+
174
+ // Initialize selectAll state when items change
175
+ watch(
176
+ () => props.items,
177
+ () => {
178
+ updateSelectAllState()
179
+ },
180
+ { immediate: true },
181
+ )
182
+ </script>