@raclettejs/workbench 0.1.17 → 0.1.19

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.
@@ -1,48 +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
- </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>
34
112
  </template>
35
113
 
36
114
  <script setup lang="ts">
37
115
  import { usePluginApi } from "@raclettejs/core/orchestrator/composables"
38
- import { computed, ref, watch } from "vue"
116
+ import { computed, ref, watch, useTemplateRef } from "vue"
39
117
  import type { Tag } from "@raclettejs/core"
40
118
  import { useI18n } from "vue-i18n"
41
119
  import BaseDataTable from "@app/components/BaseDataTable.vue"
42
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"
43
125
  const showDeleted = ref(false)
44
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)
45
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")
46
140
  const headers = computed<{ title: string; key: keyof Tag }[]>(() => [
47
141
  {
48
142
  title: t("workbench.tagList.tableHeaders.title"),
@@ -66,7 +160,7 @@ const props = defineProps<{
66
160
  uuid: string
67
161
  }>()
68
162
 
69
- const { $data, $socket } = usePluginApi("raclette__core")
163
+ const { $data, $socket, $user } = usePluginApi("raclette__core")
70
164
 
71
165
  const {
72
166
  data: tags,
@@ -80,9 +174,15 @@ const {
80
174
  const { execute: executeDelete } = $data.tag.delete({
81
175
  options: { cb: () => execute({ isDeleted: showDeleted.value }) },
82
176
  })
177
+ const { execute: executeBulkCreate } = await $data.tag.createBulk({
178
+ options: { notify: false, cb: () => execute() },
179
+ })
83
180
  const { execute: executeHardDelete } = await $data.tag.deleteHard({
84
181
  options: { cb: () => execute({ isDeleted: showDeleted.value }) },
85
182
  })
183
+ const { execute: executeRestore } = await $data.tag.updateRestore({
184
+ options: { cb: () => execute({ isDeleted: showDeleted.value }) },
185
+ })
86
186
  const parsedTags = computed(() => {
87
187
  if (tags.value) {
88
188
  return Object.values(tags.value as { [key: string]: Tag }).map((tag) => ({
@@ -101,7 +201,76 @@ const deleteTag = async (tag: Tag) => {
101
201
  await executeDelete({ _id: tag._id })
102
202
  }
103
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
+ }
104
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
+ }
271
+ const restore = async (_id: string) => {
272
+ executeRestore({ _id })
273
+ }
105
274
  $socket.on("tagCreated", () => execute({ isDeleted: showDeleted.value }))
106
275
  watch(
107
276
  showDeleted,
@@ -31,6 +31,20 @@
31
31
  </template>
32
32
  </v-tooltip>
33
33
  </template>
34
+ <template #prepend-row-actions="{ item }">
35
+ <v-icon
36
+ v-if="showDeleted"
37
+ color="green"
38
+ class="mr-2"
39
+ @click.stop.prevent="restore(item._id)"
40
+ :title="
41
+ $t('workbench.baseDataTable.restoreDataTitle', {
42
+ dataName: item.title,
43
+ })
44
+ "
45
+ icon="mdi-delete-restore"
46
+ />
47
+ </template>
34
48
  </BaseDataTable>
35
49
  </template>
36
50
 
@@ -79,13 +93,15 @@ const { data: users, execute, isLoading: usersLoading } = $data.user.getAll()
79
93
  const { data: tags, isLoading: tagsLoading } = $data.tag.getAll({
80
94
  options: { immediate: true },
81
95
  })
82
-
83
96
  const { execute: executeDelete } = $data.user.delete({
84
97
  options: { cb: () => execute({ isDeleted: showDeleted.value }) },
85
98
  })
86
99
  const { execute: executeHardDelete } = await $data.user.deleteHard({
87
100
  options: { cb: () => execute({ isDeleted: showDeleted.value }) },
88
101
  })
102
+ const { execute: executeRestore } = await $data.user.updateRestore({
103
+ options: { cb: () => execute({ isDeleted: showDeleted.value }) },
104
+ })
89
105
  const parsedUsers = computed(() => {
90
106
  if (users.value && tags.value) {
91
107
  return Object.values(users.value as { [key: string]: User }).map(
@@ -105,7 +121,9 @@ const deleteUser = async (user: User) => {
105
121
  await executeDelete({ _id: user._id })
106
122
  }
107
123
  }
108
-
124
+ const restore = async (_id: string) => {
125
+ executeRestore({ _id })
126
+ }
109
127
  $socket.on("userCreated", () => execute({ isDeleted: showDeleted.value }))
110
128
  watch(
111
129
  showDeleted,
@@ -62,9 +62,6 @@ export default defineRacletteConfig({
62
62
  },
63
63
  production: {},
64
64
  },
65
- global: {
66
- authTokenExpiryDays: 1,
67
- },
68
65
  frontend: {
69
66
  meta: {
70
67
  title: "Workbench",
@@ -81,6 +81,7 @@
81
81
 
82
82
  <!-- Actions column template -->
83
83
  <template #item.actions="{ item }" v-if="showActionsColumn">
84
+ <slot name="prepend-row-actions" :item="item"> </slot>
84
85
  <slot name="row-actions" :item="item">
85
86
  <!-- Default delete action -->
86
87
  <v-icon
@@ -96,6 +97,7 @@
96
97
  icon="mdi-delete"
97
98
  />
98
99
  </slot>
100
+ <slot name="append-row-actions" :item="item"> </slot>
99
101
  </template>
100
102
 
101
103
  <!-- Loading template -->
@@ -296,5 +298,6 @@ const isDeleteDisabled = (item: T): boolean => {
296
298
  <style lang="css">
297
299
  .itemsDeleted .v-data-table__tr {
298
300
  background-color: rgb(233, 190, 190);
301
+ color: #000;
299
302
  }
300
303
  </style>
@@ -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>