@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.
- package/CHANGELOG.md +19 -0
- package/config/compositions.js +18 -14
- package/config/interactionLinks.js +30 -10
- package/i18n/de-DE.json +27 -1
- package/i18n/en-EU.json +27 -1
- package/i18n/sk-SK.json +26 -0
- package/package.json +3 -3
- package/plugins/pacifico__compositions/frontend/widgets/CompositionListWidget.vue +207 -45
- package/plugins/pacifico__interactionLinks/frontend/widgets/InteractionLinkListWidget.vue +226 -42
- package/plugins/pacifico__project/frontend/components/ProjectConfiguration.vue +107 -0
- package/plugins/pacifico__project/frontend/widgets/ProjectDetailWidget.vue +34 -0
- package/plugins/pacifico__project/raclette.plugin.ts +8 -0
- package/plugins/pacifico__tags/frontend/widgets/TagListWidget.vue +203 -34
- package/plugins/pacifico__users/frontend/widgets/UserListWidget.vue +20 -2
- package/raclette.config.js +0 -3
- package/services/frontend/src/app/components/BaseDataTable.vue +3 -0
- package/services/frontend/src/app/components/FileUpload.vue +98 -0
- package/services/frontend/src/app/components/SelectionDialog.vue +182 -0
- package/services/frontend/src/app/components/SummaryDialog.vue +140 -0
- package/services/frontend/src/app/components/dynamicForm/typedInputs/CompositionInput.vue +1 -0
- package/services/frontend/src/app/components/dynamicForm/typedInputs/TriggerInput.vue +4 -6
- package/services/frontend/src/app/lib/jsonTools.ts +81 -0
- package/yarn.lock +8 -8
|
@@ -1,53 +1,149 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
<v-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
<v-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
2
|
+
<div>
|
|
3
|
+
<BaseDataTable
|
|
4
|
+
:items="parsedInteractionLinks"
|
|
5
|
+
:loading="interactionLinksLoading || tagsLoading"
|
|
6
|
+
:headers="headers"
|
|
7
|
+
create-interaction-link-id="interactionLinkCreateInteractionLink"
|
|
8
|
+
edit-interaction-link-id="interactionLinkEditInteractionLink"
|
|
9
|
+
:data-name="$t('workbench.interactionLinkList.dataName')"
|
|
10
|
+
:data-article="$t('workbench.interactionLinkList.dataArticle')"
|
|
11
|
+
@delete="deleteInteractionLink"
|
|
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
|
+
<!-- Custom triggers column -->
|
|
55
|
+
<template #item.triggers="{ item }">
|
|
56
|
+
<span class="tw:flex tw:gap-2">
|
|
57
|
+
<span>{{ item.triggers }}</span>
|
|
58
|
+
<v-icon icon="mdi-link" />
|
|
59
|
+
</span>
|
|
60
|
+
</template>
|
|
61
|
+
<template #prepend-row-actions="{ item }">
|
|
62
|
+
<v-icon
|
|
63
|
+
v-if="showDeleted"
|
|
64
|
+
color="green"
|
|
65
|
+
class="mr-2"
|
|
66
|
+
@click="restore(item._id)"
|
|
67
|
+
:title="
|
|
68
|
+
$t('workbench.baseDataTable.restoreDataTitle', {
|
|
69
|
+
dataName: item.title,
|
|
70
|
+
})
|
|
71
|
+
"
|
|
72
|
+
icon="mdi-delete-restore"
|
|
73
|
+
/>
|
|
74
|
+
</template>
|
|
75
|
+
</BaseDataTable>
|
|
76
|
+
<!-- Export Selection Dialog -->
|
|
77
|
+
<SelectionDialog
|
|
78
|
+
v-model="showExportDialog"
|
|
79
|
+
:items="exportableItems"
|
|
80
|
+
display-attribute="description"
|
|
81
|
+
:title="$t('core.selectItemsToExport')"
|
|
82
|
+
:confirm-text="$t('core.export')"
|
|
83
|
+
@confirm="handleExportConfirm"
|
|
84
|
+
/>
|
|
85
|
+
|
|
86
|
+
<!-- Import Selection Dialog -->
|
|
87
|
+
<SelectionDialog
|
|
88
|
+
v-model="showImportDialog"
|
|
89
|
+
:items="importableItems"
|
|
90
|
+
display-attribute="description"
|
|
91
|
+
:title="$t('core.selectItemsToImport')"
|
|
92
|
+
:confirm-text="$t('core.import')"
|
|
93
|
+
@confirm="handleImportConfirm"
|
|
94
|
+
/>
|
|
95
|
+
|
|
96
|
+
<!-- Export Summary Dialog -->
|
|
97
|
+
<SummaryDialog
|
|
98
|
+
v-model="showExportSummary"
|
|
99
|
+
display-attribute="description"
|
|
100
|
+
:items="exportedItems"
|
|
101
|
+
:title="$t('core.exportSummary')"
|
|
102
|
+
:message="$t('core.exportSuccessMessage')"
|
|
103
|
+
type="success"
|
|
104
|
+
/>
|
|
105
|
+
|
|
106
|
+
<!-- Import Summary Dialog -->
|
|
107
|
+
<SummaryDialog
|
|
108
|
+
v-model="showImportSummary"
|
|
109
|
+
:items="importedItems"
|
|
110
|
+
display-attribute="description"
|
|
111
|
+
:title="$t('core.importSummary')"
|
|
112
|
+
:message="$t('core.importSuccessMessage')"
|
|
113
|
+
type="success"
|
|
114
|
+
:show-confirm="true"
|
|
115
|
+
:confirm-text="$t('core.saveImported')"
|
|
116
|
+
@confirm="finalizeImport"
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
41
119
|
</template>
|
|
42
120
|
|
|
43
121
|
<script setup lang="ts">
|
|
122
|
+
import { clone } from "ramda"
|
|
44
123
|
import BaseDataTable from "@app/components/BaseDataTable.vue"
|
|
45
124
|
import { InteractionLink } from "@raclettejs/core"
|
|
46
125
|
import { usePluginApi } from "@raclettejs/core/orchestrator/composables"
|
|
47
126
|
import { formatDistance } from "date-fns"
|
|
48
|
-
import { computed, ref, watch } from "vue"
|
|
127
|
+
import { computed, ref, watch, useTemplateRef } from "vue"
|
|
49
128
|
import { useI18n } from "vue-i18n"
|
|
129
|
+
import { downloadJson, mapJsonValues } from "@app/lib/jsonTools"
|
|
130
|
+
import FileUpload from "@app/components/FileUpload.vue"
|
|
131
|
+
import SelectionDialog from "@app/components/SelectionDialog.vue"
|
|
132
|
+
import SummaryDialog from "@app/components/SummaryDialog.vue"
|
|
50
133
|
const showDeleted = ref(false)
|
|
134
|
+
// Dialog states
|
|
135
|
+
const showExportDialog = ref(false)
|
|
136
|
+
const showImportDialog = ref(false)
|
|
137
|
+
const showExportSummary = ref(false)
|
|
138
|
+
const showImportSummary = ref(false)
|
|
139
|
+
|
|
140
|
+
// Data for dialogs
|
|
141
|
+
const exportableItems = ref<any[]>([])
|
|
142
|
+
const importableItems = ref<any[]>([])
|
|
143
|
+
const exportedItems = ref<any[]>([])
|
|
144
|
+
const importedItems = ref<any[]>([])
|
|
145
|
+
const pendingImportData = ref<any>(null)
|
|
146
|
+
const uploadRef = useTemplateRef("uploadRef")
|
|
51
147
|
|
|
52
148
|
const { t } = useI18n()
|
|
53
149
|
|
|
@@ -88,7 +184,7 @@ const props = defineProps<{
|
|
|
88
184
|
uuid: string
|
|
89
185
|
}>()
|
|
90
186
|
|
|
91
|
-
const { $data, $socket } = usePluginApi("raclette__core")
|
|
187
|
+
const { $data, $socket, $user } = usePluginApi("raclette__core")
|
|
92
188
|
|
|
93
189
|
const {
|
|
94
190
|
data: interactionLinks,
|
|
@@ -105,15 +201,22 @@ const { data: users } = $data.user.getAll({
|
|
|
105
201
|
})
|
|
106
202
|
|
|
107
203
|
const { data: compositions } = $data.composition.getAll({
|
|
204
|
+
params: { isDeleted: false },
|
|
108
205
|
options: { immediate: true },
|
|
109
206
|
})
|
|
110
207
|
|
|
208
|
+
const { execute: executeBulkCreate } = await $data.interactionLink.createBulk({
|
|
209
|
+
options: { notify: false, cb: () => execute() },
|
|
210
|
+
})
|
|
111
211
|
const { execute: executeDelete } = $data.interactionLink.delete({
|
|
112
212
|
options: { cb: () => execute({ isDeleted: showDeleted.value }) },
|
|
113
213
|
})
|
|
114
214
|
const { execute: executeHardDelete } = await $data.interactionLink.deleteHard({
|
|
115
215
|
options: { cb: () => execute({ isDeleted: showDeleted.value }) },
|
|
116
216
|
})
|
|
217
|
+
const { execute: executeRestore } = await $data.interactionLink.updateRestore({
|
|
218
|
+
options: { cb: () => execute({ isDeleted: showDeleted.value }) },
|
|
219
|
+
})
|
|
117
220
|
const parsedInteractionLinks = computed(() => {
|
|
118
221
|
if (interactionLinks.value && tags.value) {
|
|
119
222
|
return Object.values(
|
|
@@ -140,7 +243,9 @@ const parsedInteractionLinks = computed(() => {
|
|
|
140
243
|
|
|
141
244
|
return []
|
|
142
245
|
})
|
|
143
|
-
|
|
246
|
+
const restore = async (_id: string) => {
|
|
247
|
+
executeRestore({ _id })
|
|
248
|
+
}
|
|
144
249
|
const deleteInteractionLink = async (interactionLink: InteractionLink) => {
|
|
145
250
|
if (showDeleted.value) {
|
|
146
251
|
await executeHardDelete({ _id: interactionLink._id })
|
|
@@ -152,6 +257,85 @@ const deleteInteractionLink = async (interactionLink: InteractionLink) => {
|
|
|
152
257
|
$socket.on("interactionLinkCreated", () =>
|
|
153
258
|
execute({ isDeleted: showDeleted.value }),
|
|
154
259
|
)
|
|
260
|
+
const createTitleString = (link) => {
|
|
261
|
+
let linkTitleString = `${link.slotType}`
|
|
262
|
+
if (link.triggers) {
|
|
263
|
+
link.triggers.forEach((trigger) => {
|
|
264
|
+
linkTitleString += `${trigger.type} - ${trigger.settings?.title?.default} `
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
link.description = linkTitleString + link.description
|
|
268
|
+
return link
|
|
269
|
+
}
|
|
270
|
+
const createLinkDescriptions = (linkArr) => {
|
|
271
|
+
linkArr.forEach((link) => {
|
|
272
|
+
link = createTitleString(link)
|
|
273
|
+
})
|
|
274
|
+
return linkArr
|
|
275
|
+
}
|
|
276
|
+
// Export functions
|
|
277
|
+
const openExportDialog = () => {
|
|
278
|
+
exportableItems.value = createLinkDescriptions(
|
|
279
|
+
Object.values(clone(interactionLinks.value) || {}),
|
|
280
|
+
)
|
|
281
|
+
showExportDialog.value = true
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const handleExportConfirm = (selectedIds: string[]) => {
|
|
285
|
+
const _interactionLinks = clone(interactionLinks.value)
|
|
286
|
+
const dataToExport = clone(selectedIds).reduce((acc, id) => {
|
|
287
|
+
if (_interactionLinks?.[id]) {
|
|
288
|
+
acc[id] = createTitleString(_interactionLinks[id])
|
|
289
|
+
}
|
|
290
|
+
return acc
|
|
291
|
+
}, {} as any)
|
|
292
|
+
exportedItems.value = Object.values(dataToExport)
|
|
293
|
+
downloadJson(dataToExport, "interactionLinks")
|
|
294
|
+
showExportSummary.value = true
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Import functions
|
|
298
|
+
const handleFileUpload = (content: string, file: File) => {
|
|
299
|
+
try {
|
|
300
|
+
const parsedData = JSON.parse(content)
|
|
301
|
+
const itemsArray = Object.values(parsedData)
|
|
302
|
+
|
|
303
|
+
importableItems.value = itemsArray
|
|
304
|
+
pendingImportData.value = parsedData
|
|
305
|
+
showImportDialog.value = true
|
|
306
|
+
} catch (error) {
|
|
307
|
+
console.error("Failed to parse JSON:", error)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const handleImportConfirm = (selectedIds: string[]) => {
|
|
312
|
+
const selectedItems = selectedIds.map((id) => pendingImportData.value[id])
|
|
313
|
+
importedItems.value = selectedItems
|
|
314
|
+
showImportSummary.value = true
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const finalizeImport = async () => {
|
|
318
|
+
const mapping = {
|
|
319
|
+
owner: $user.value._id,
|
|
320
|
+
lastEditor: $user.value._id,
|
|
321
|
+
createdBy: $user.value._id,
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const dataToImport = importedItems.value.reduce((acc, item) => {
|
|
325
|
+
const mappedItem = mapJsonValues(item, mapping)
|
|
326
|
+
acc[item._id] = createTitleString(mappedItem)
|
|
327
|
+
return acc
|
|
328
|
+
}, {} as any)
|
|
329
|
+
|
|
330
|
+
await executeBulkCreate(Object.values(dataToImport))
|
|
331
|
+
// TODO: Add your actual import logic here
|
|
332
|
+
// e.g., call an API endpoint to save the imported compositions
|
|
333
|
+
|
|
334
|
+
// Reset states
|
|
335
|
+
pendingImportData.value = null
|
|
336
|
+
importableItems.value = []
|
|
337
|
+
importedItems.value = []
|
|
338
|
+
}
|
|
155
339
|
watch(
|
|
156
340
|
showDeleted,
|
|
157
341
|
() => {
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<StepNavigator
|
|
3
|
+
:steps
|
|
4
|
+
:is-saving="disabled"
|
|
5
|
+
:show-save-button="isEditing"
|
|
6
|
+
@save="$emit('save')"
|
|
7
|
+
@save-and-finish="$emit('save-and-finish')"
|
|
8
|
+
>
|
|
9
|
+
<!-- @vue-generic {ProjectUpdate} -->
|
|
10
|
+
<DynamicForm
|
|
11
|
+
ref="dynamicFormRef"
|
|
12
|
+
v-model="project"
|
|
13
|
+
:data="projectInputFields"
|
|
14
|
+
:disabled
|
|
15
|
+
:title="$t('workbench.project.formtitle')"
|
|
16
|
+
/>
|
|
17
|
+
<DynamicForm
|
|
18
|
+
ref="dynamicFormRef"
|
|
19
|
+
v-model="project.config"
|
|
20
|
+
:data="configInputFields"
|
|
21
|
+
:disabled
|
|
22
|
+
/>
|
|
23
|
+
</StepNavigator>
|
|
24
|
+
</template>
|
|
25
|
+
|
|
26
|
+
<script setup lang="ts">
|
|
27
|
+
import type { ProjectUpdate } from "@raclettejs/core"
|
|
28
|
+
import DynamicForm from "@app/components/dynamicForm/DynamicForm.vue"
|
|
29
|
+
import type { DynamicFormItem } from "@app/components/dynamicForm/DynamicFormTypes"
|
|
30
|
+
import { useI18n } from "vue-i18n"
|
|
31
|
+
import { computed, useTemplateRef } from "vue"
|
|
32
|
+
import { Step } from "@app/components/stepNavigator/StepNavigatorTypes"
|
|
33
|
+
import StepNavigator from "@app/components/stepNavigator/StepNavigator.vue"
|
|
34
|
+
|
|
35
|
+
defineProps<{ disabled?: boolean; isEditing?: boolean }>()
|
|
36
|
+
|
|
37
|
+
defineEmits<{
|
|
38
|
+
(e: "save"): void
|
|
39
|
+
(e: "save-and-finish"): void
|
|
40
|
+
}>()
|
|
41
|
+
|
|
42
|
+
const project = defineModel<ProjectUpdate>({ required: true })
|
|
43
|
+
|
|
44
|
+
const dynamicFormRef = useTemplateRef("dynamicFormRef")
|
|
45
|
+
|
|
46
|
+
const { t } = useI18n()
|
|
47
|
+
|
|
48
|
+
const projectInputFields = computed<DynamicFormItem<ProjectUpdate>[]>(() => [
|
|
49
|
+
{ divider: t("workbench.project.dividers.general") },
|
|
50
|
+
{
|
|
51
|
+
title: t("workbench.project.title.title"),
|
|
52
|
+
description: t("workbench.project.title.description"),
|
|
53
|
+
inputType: "textarea",
|
|
54
|
+
field: "title",
|
|
55
|
+
prependIcon: "text",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
title: t("workbench.project.description.title"),
|
|
59
|
+
description: t("workbench.project.description.description"),
|
|
60
|
+
inputType: "textarea",
|
|
61
|
+
field: "description",
|
|
62
|
+
prependIcon: "text",
|
|
63
|
+
},
|
|
64
|
+
/* {
|
|
65
|
+
title: t("workbench.interactionLinkConfiguration.tags.title"),
|
|
66
|
+
description: t("workbench.project.tags.description"),
|
|
67
|
+
inputType: "tags",
|
|
68
|
+
field: "tags",
|
|
69
|
+
prependIcon: "tag-multiple",
|
|
70
|
+
},*/
|
|
71
|
+
])
|
|
72
|
+
const configInputFields = computed<DynamicFormItem<ProjectUpdate["config"]>[]>(
|
|
73
|
+
() => [
|
|
74
|
+
{ divider: t("workbench.project.dividers.config") },
|
|
75
|
+
{
|
|
76
|
+
title: t("workbench.project.railPageNavigation.title"),
|
|
77
|
+
description: t("workbench.project.railPageNavigation.description"),
|
|
78
|
+
inputType: "switch",
|
|
79
|
+
field: "railPageNavigation",
|
|
80
|
+
},
|
|
81
|
+
/*{
|
|
82
|
+
title: t("workbench.project.tags.title"),
|
|
83
|
+
description: t("workbench.project.tags.description"),
|
|
84
|
+
inputType: "tags",
|
|
85
|
+
field: "tags",
|
|
86
|
+
prependIcon: "tag-multiple",
|
|
87
|
+
},*/
|
|
88
|
+
],
|
|
89
|
+
)
|
|
90
|
+
const validateForm = (): boolean => {
|
|
91
|
+
const validateAllFields = dynamicFormRef.value?.validateAllFields
|
|
92
|
+
|
|
93
|
+
if (validateAllFields) {
|
|
94
|
+
const isValid = !!validateAllFields()
|
|
95
|
+
|
|
96
|
+
return isValid
|
|
97
|
+
}
|
|
98
|
+
return true
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const steps = computed<Step[]>(() => [
|
|
102
|
+
{
|
|
103
|
+
id: "config",
|
|
104
|
+
validator: validateForm,
|
|
105
|
+
},
|
|
106
|
+
])
|
|
107
|
+
</script>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<ProjectConfiguration
|
|
3
|
+
v-if="editableProject"
|
|
4
|
+
v-model="editableProject"
|
|
5
|
+
:disabled="isLoading"
|
|
6
|
+
is-editing
|
|
7
|
+
@save="onSubmit"
|
|
8
|
+
/>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<script setup lang="ts">
|
|
12
|
+
import { clone } from "ramda"
|
|
13
|
+
import type { Project } from "@raclettejs/core"
|
|
14
|
+
import { usePluginApi } from "@raclettejs/core/orchestrator/composables"
|
|
15
|
+
import { onMounted, ref, toRaw } from "vue"
|
|
16
|
+
import ProjectConfiguration from "../components/ProjectConfiguration.vue"
|
|
17
|
+
|
|
18
|
+
const props = defineProps<{
|
|
19
|
+
uuid: string
|
|
20
|
+
}>()
|
|
21
|
+
|
|
22
|
+
const { $data } = usePluginApi("raclette__core")
|
|
23
|
+
const { $store } = usePluginApi()
|
|
24
|
+
const project = $store.get(["project"])
|
|
25
|
+
|
|
26
|
+
const { execute: executeUpdate, isLoading } = $data.project.update()
|
|
27
|
+
|
|
28
|
+
const editableProject = ref<Project>()
|
|
29
|
+
|
|
30
|
+
const onSubmit = async () => await executeUpdate(toRaw(editableProject.value))
|
|
31
|
+
onMounted(() => {
|
|
32
|
+
editableProject.value = clone({ config: {}, ...project.value })
|
|
33
|
+
})
|
|
34
|
+
</script>
|