@raclettejs/core 0.1.31 → 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.
- package/CHANGELOG.md +31 -4
- package/dist/cli.js +94 -94
- package/dist/cli.js.map +3 -3
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/services/backend/.yarn/install-state.gz +0 -0
- package/services/backend/dist/core/eventBus/index.js +7 -0
- package/services/backend/dist/core/pluginSystem/configGenerator/index.js +346 -0
- package/services/backend/dist/core/sockets/index.js +95 -0
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/account/events/index.js +31 -0
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/account/index.js +48 -0
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/account/routes/index.js +15 -0
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/composition/events/index.js +22 -0
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/composition/index.js +51 -0
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/composition/routes/index.js +19 -0
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/interactionLink/events/index.js +22 -0
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/interactionLink/index.js +48 -0
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/interactionLink/routes/index.js +19 -0
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/project/events/index.js +31 -0
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/project/index.js +49 -0
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/project/routes/index.js +16 -0
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/tag/events/index.js +39 -0
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/tag/index.js +50 -0
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/tag/routes/index.js +19 -0
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/user/events/index.js +31 -0
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/user/index.js +51 -0
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/user/routes/index.js +21 -0
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/workSession/events/index.js +31 -0
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/workSession/index.js +48 -0
- package/services/backend/dist/corePlugins/raclette__core/backend/datatypes/workSession/routes/index.js +15 -0
- package/services/backend/dist/corePlugins/raclette__core/backend/index.js +34 -0
- package/services/backend/dist/domains/index.js +11 -0
- package/services/backend/dist/domains/system/index.js +11 -0
- package/services/backend/dist/domains/system/routes/index.js +17 -0
- package/services/backend/dist/helpers/index.js +14 -0
- package/services/backend/dist/index.js +3 -0
- package/services/backend/dist/modules/authentication/index.js +253 -0
- package/services/backend/dist/shared/types/core/index.js +8 -0
- package/services/backend/dist/shared/types/dataTypes/index.js +6 -0
- package/services/backend/dist/shared/types/index.js +8 -0
- package/services/backend/dist/shared/types/plugins/index.js +8 -0
- package/services/backend/dist/types/index.js +12 -0
- package/services/backend/dist/utils/index.js +2 -0
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/routes/route.user.patchOwn.ts +1 -1
- package/services/backend/src/corePlugins/raclette__core/backend/datatypes/user/user.service.ts +22 -1
- package/services/backend/yarn.lock +1 -1
- package/services/frontend/.yarn/install-state.gz +0 -0
- package/services/frontend/package.json +2 -2
- package/services/frontend/src/core/lib/data/dataApi.ts +1 -2
- package/services/frontend/src/orchestrator/ProductOrchestrator.vue +8 -1
- package/services/frontend/src/orchestrator/assets/styles/layers.css +1 -4
- package/services/frontend/src/orchestrator/assets/styles/tailwindStyles.css +3 -3
- package/services/frontend/src/orchestrator/assets/styles/vuetifyStyles.scss +4 -1
- package/services/frontend/src/orchestrator/components/dataExport/DataExporter.vue +303 -0
- package/services/frontend/src/orchestrator/components/dataTable/BaseDataTable.vue +473 -0
- package/services/frontend/src/orchestrator/components/dataTable/FileUpload.vue +83 -0
- package/services/frontend/src/orchestrator/components/dataTable/SelectionDialog.vue +169 -0
- package/services/frontend/src/orchestrator/components/dataTable/SummaryDialog.vue +211 -0
- package/services/frontend/src/orchestrator/components/index.ts +2 -0
- package/services/frontend/src/orchestrator/composables/index.ts +1 -0
- package/services/frontend/src/orchestrator/composables/useExport/formats/builtins.ts +124 -0
- package/services/frontend/src/orchestrator/composables/useExport/index.ts +176 -0
- package/services/frontend/src/orchestrator/composables/useExport/types.ts +141 -0
- package/services/frontend/src/orchestrator/i18n/de-DE.json +26 -1
- package/services/frontend/src/orchestrator/i18n/en-EU.json +26 -1
- package/services/frontend/src/orchestrator/i18n/sk.json +26 -1
- package/services/frontend/src/orchestrator/i18n/tl-TL.json +14 -0
- package/services/frontend/src/orchestrator/setup/vuetify.ts +0 -1
- package/services/frontend/yarn.lock +4854 -0
- package/types/index.ts +1 -1
- package/package/LICENSE.md +0 -699
- package/package/README.md +0 -49
- package/package/dev/README.md +0 -4
- package/package/index.js +0 -11
- package/package/raclette.default.config.yaml +0 -60
- package/package/services/backend/README.md +0 -172
- package/package/services/backend/yarn.lock +0 -5320
- package/package/services/frontend/README.md +0 -511
- package/package/services/frontend/src/core/lib/eggs/readme.md +0 -75
- package/package/services/frontend/yarn.lock +0 -0
- package/package/src/README.md +0 -404
- package/package/templates/README.md +0 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-dialog v-model="dialog" max-width="760" scrollable>
|
|
3
|
+
<v-card>
|
|
4
|
+
<v-card-title class="tw:flex tw:justify-between tw:items-center tw:gap-2">
|
|
5
|
+
<span class="tw:text-lg tw:font-semibold">{{ title }}</span>
|
|
6
|
+
<v-btn icon="mdi-close" size="small" variant="text" @click="close" />
|
|
7
|
+
</v-card-title>
|
|
8
|
+
|
|
9
|
+
<v-divider />
|
|
10
|
+
|
|
11
|
+
<v-card-text class="tw:py-4">
|
|
12
|
+
<div class="tw:flex tw:justify-between tw:items-center tw:mb-4 tw:gap-2">
|
|
13
|
+
<v-checkbox
|
|
14
|
+
:model-value="selectAll"
|
|
15
|
+
@update:model-value="toggleSelectAll"
|
|
16
|
+
:label="$t('core.selectAll')"
|
|
17
|
+
hide-details
|
|
18
|
+
density="compact"
|
|
19
|
+
/>
|
|
20
|
+
|
|
21
|
+
<v-chip color="primary" variant="flat" size="small">
|
|
22
|
+
{{ selectedCount }} / {{ items.length }} {{ $t("core.selected") }}
|
|
23
|
+
</v-chip>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<v-list class="tw:max-h-96 tw:overflow-y-auto tw:border tw:rounded-md tw:py-0">
|
|
27
|
+
<v-list-item
|
|
28
|
+
v-for="item in items"
|
|
29
|
+
:key="item._id"
|
|
30
|
+
@click="toggleSelection(String(item._id))"
|
|
31
|
+
class="tw:cursor-pointer"
|
|
32
|
+
>
|
|
33
|
+
<template #prepend>
|
|
34
|
+
<v-checkbox-btn
|
|
35
|
+
:model-value="selected.includes(String(item._id))"
|
|
36
|
+
@update:model-value="(checked) => setSelection(String(item._id), checked)"
|
|
37
|
+
@click.stop
|
|
38
|
+
/>
|
|
39
|
+
</template>
|
|
40
|
+
|
|
41
|
+
<v-list-item-title class="tw:text-sm">
|
|
42
|
+
{{ getDisplayValue(item) }}
|
|
43
|
+
</v-list-item-title>
|
|
44
|
+
</v-list-item>
|
|
45
|
+
</v-list>
|
|
46
|
+
|
|
47
|
+
<v-alert
|
|
48
|
+
v-if="selectedCount === 0"
|
|
49
|
+
type="warning"
|
|
50
|
+
variant="tonal"
|
|
51
|
+
density="compact"
|
|
52
|
+
class="tw:mt-4"
|
|
53
|
+
>
|
|
54
|
+
{{ $t("core.noItemsSelected") }}
|
|
55
|
+
</v-alert>
|
|
56
|
+
</v-card-text>
|
|
57
|
+
|
|
58
|
+
<v-divider />
|
|
59
|
+
|
|
60
|
+
<v-card-actions class="tw:px-4 tw:py-3">
|
|
61
|
+
<v-spacer />
|
|
62
|
+
<v-btn variant="text" @click="close">
|
|
63
|
+
{{ $t("core.cancel") }}
|
|
64
|
+
</v-btn>
|
|
65
|
+
|
|
66
|
+
<v-btn
|
|
67
|
+
v-if="selectedCount"
|
|
68
|
+
@click="confirm"
|
|
69
|
+
:disabled="selectedCount === 0"
|
|
70
|
+
color="primary"
|
|
71
|
+
variant="flat"
|
|
72
|
+
>
|
|
73
|
+
{{ confirmText }}
|
|
74
|
+
</v-btn>
|
|
75
|
+
</v-card-actions>
|
|
76
|
+
</v-card>
|
|
77
|
+
</v-dialog>
|
|
78
|
+
</template>
|
|
79
|
+
|
|
80
|
+
<script setup lang="ts">
|
|
81
|
+
import { ref, computed, watch } from "vue"
|
|
82
|
+
|
|
83
|
+
interface Props {
|
|
84
|
+
modelValue: boolean
|
|
85
|
+
items: any[]
|
|
86
|
+
displayAttribute: string
|
|
87
|
+
title: string
|
|
88
|
+
confirmText: string
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const props = defineProps<Props>()
|
|
92
|
+
|
|
93
|
+
const emit = defineEmits<{
|
|
94
|
+
"update:modelValue": [value: boolean]
|
|
95
|
+
confirm: [selectedIds: string[]]
|
|
96
|
+
}>()
|
|
97
|
+
|
|
98
|
+
const dialog = computed({
|
|
99
|
+
get: () => props.modelValue,
|
|
100
|
+
set: (value) => emit("update:modelValue", value),
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const selected = ref<string[]>([])
|
|
104
|
+
const selectAll = ref(false)
|
|
105
|
+
|
|
106
|
+
const selectedCount = computed(() => selected.value.length)
|
|
107
|
+
|
|
108
|
+
const getDisplayValue = (item: any) => {
|
|
109
|
+
return item[props.displayAttribute] || item._id
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const toggleSelection = (id: string) => {
|
|
113
|
+
const index = selected.value.indexOf(id)
|
|
114
|
+
if (index > -1) {
|
|
115
|
+
selected.value.splice(index, 1)
|
|
116
|
+
} else {
|
|
117
|
+
selected.value.push(id)
|
|
118
|
+
}
|
|
119
|
+
updateSelectAllState()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const setSelection = (id: string, checked: boolean | null) => {
|
|
123
|
+
const isChecked = Boolean(checked)
|
|
124
|
+
const exists = selected.value.includes(id)
|
|
125
|
+
if (isChecked && !exists) {
|
|
126
|
+
selected.value.push(id)
|
|
127
|
+
} else if (!isChecked && exists) {
|
|
128
|
+
selected.value = selected.value.filter((selectedId) => selectedId !== id)
|
|
129
|
+
}
|
|
130
|
+
updateSelectAllState()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const toggleSelectAll = (value: boolean | null) => {
|
|
134
|
+
const enabled = Boolean(value)
|
|
135
|
+
if (enabled) {
|
|
136
|
+
selected.value = props.items.map((item) => String(item._id))
|
|
137
|
+
} else {
|
|
138
|
+
selected.value = []
|
|
139
|
+
}
|
|
140
|
+
selectAll.value = enabled
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const updateSelectAllState = () => {
|
|
144
|
+
selectAll.value =
|
|
145
|
+
selected.value.length === props.items.length && props.items.length > 0
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const close = () => {
|
|
149
|
+
dialog.value = false
|
|
150
|
+
selected.value = []
|
|
151
|
+
selectAll.value = false
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const confirm = () => {
|
|
155
|
+
if (selectedCount.value > 0) {
|
|
156
|
+
emit("confirm", selected.value)
|
|
157
|
+
close()
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Initialize selectAll state when items change
|
|
162
|
+
watch(
|
|
163
|
+
() => props.items,
|
|
164
|
+
() => {
|
|
165
|
+
updateSelectAllState()
|
|
166
|
+
},
|
|
167
|
+
{ immediate: true },
|
|
168
|
+
)
|
|
169
|
+
</script>
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-dialog v-model="dialog" max-width="800" scrollable>
|
|
3
|
+
<v-card class="tw:overflow-hidden">
|
|
4
|
+
<v-card-title class="tw:flex tw:items-center tw:justify-between tw:gap-2">
|
|
5
|
+
<span class="tw:text-lg tw:font-semibold">{{ title }}</span>
|
|
6
|
+
<v-btn icon="mdi-close" size="small" variant="text" @click="close" />
|
|
7
|
+
</v-card-title>
|
|
8
|
+
|
|
9
|
+
<v-divider />
|
|
10
|
+
|
|
11
|
+
<v-card-text class="tw:py-4">
|
|
12
|
+
<v-alert :type="type" variant="tonal" class="tw:mb-4">
|
|
13
|
+
<div class="tw:text-base tw:font-medium tw:mb-2">
|
|
14
|
+
{{ message }}
|
|
15
|
+
</div>
|
|
16
|
+
<div class="tw:text-sm">
|
|
17
|
+
{{ $t("core.itemsCount") }}:
|
|
18
|
+
<strong>{{ itemCount }}</strong>
|
|
19
|
+
</div>
|
|
20
|
+
</v-alert>
|
|
21
|
+
|
|
22
|
+
<div v-if="showItems && renderedItems.length > 0">
|
|
23
|
+
<div class="tw:text-xs tw:uppercase tw:font-semibold tw:opacity-60 tw:mb-2">
|
|
24
|
+
{{ $t("core.selectedItems") }}
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<v-list density="compact" class="tw:border tw:rounded-md tw:py-0">
|
|
28
|
+
<v-list-item
|
|
29
|
+
v-for="item in renderedItems"
|
|
30
|
+
:key="item.key"
|
|
31
|
+
class="tw:py-2"
|
|
32
|
+
>
|
|
33
|
+
<template #title>
|
|
34
|
+
<span class="tw:text-sm tw:font-medium">{{ item.title }}</span>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<template #subtitle>
|
|
38
|
+
<pre
|
|
39
|
+
v-if="item.kind === 'pre'"
|
|
40
|
+
class="tw:mt-1 tw:text-xs tw:font-mono tw:p-2 tw:max-h-44 tw:overflow-auto tw:rounded tw:bg-black/5 dark:tw:bg-white/5 tw:whitespace-pre-wrap tw:break-all"
|
|
41
|
+
>{{ item.text }}</pre>
|
|
42
|
+
<div v-else class="tw:mt-1 tw:text-sm tw:opacity-80 tw:break-all">
|
|
43
|
+
{{ item.text }}
|
|
44
|
+
</div>
|
|
45
|
+
</template>
|
|
46
|
+
</v-list-item>
|
|
47
|
+
</v-list>
|
|
48
|
+
</div>
|
|
49
|
+
</v-card-text>
|
|
50
|
+
|
|
51
|
+
<v-divider />
|
|
52
|
+
|
|
53
|
+
<v-card-actions class="tw:px-4 tw:py-3">
|
|
54
|
+
<v-spacer />
|
|
55
|
+
<v-btn variant="text" @click="close">
|
|
56
|
+
{{ cancelText || $t("core.close") }}
|
|
57
|
+
</v-btn>
|
|
58
|
+
<v-btn
|
|
59
|
+
v-if="showConfirm"
|
|
60
|
+
:color="type === 'warning' ? 'warning' : type === 'error' ? 'error' : 'primary'"
|
|
61
|
+
variant="flat"
|
|
62
|
+
@click="confirm"
|
|
63
|
+
>
|
|
64
|
+
{{ confirmText }}
|
|
65
|
+
</v-btn>
|
|
66
|
+
</v-card-actions>
|
|
67
|
+
</v-card>
|
|
68
|
+
</v-dialog>
|
|
69
|
+
</template>
|
|
70
|
+
|
|
71
|
+
<script setup lang="ts">
|
|
72
|
+
import { computed } from "vue"
|
|
73
|
+
import { useI18n } from "vue-i18n"
|
|
74
|
+
|
|
75
|
+
interface Props {
|
|
76
|
+
modelValue: boolean
|
|
77
|
+
items: any[]
|
|
78
|
+
title: string
|
|
79
|
+
message: string
|
|
80
|
+
type?: "info" | "success" | "warning" | "error"
|
|
81
|
+
showItems?: boolean
|
|
82
|
+
showConfirm?: boolean
|
|
83
|
+
displayAttribute: string
|
|
84
|
+
confirmText?: string
|
|
85
|
+
cancelText?: string
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
89
|
+
type: "info",
|
|
90
|
+
showItems: true,
|
|
91
|
+
showConfirm: false,
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
const emit = defineEmits<{
|
|
95
|
+
"update:modelValue": [value: boolean]
|
|
96
|
+
confirm: []
|
|
97
|
+
}>()
|
|
98
|
+
const { t } = useI18n()
|
|
99
|
+
|
|
100
|
+
const dialog = computed({
|
|
101
|
+
get: () => props.modelValue,
|
|
102
|
+
set: (value) => emit("update:modelValue", value),
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const itemCount = computed(() => props.items.length)
|
|
106
|
+
|
|
107
|
+
const close = () => {
|
|
108
|
+
dialog.value = false
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
type SummaryRenderKind = "text" | "pre"
|
|
112
|
+
|
|
113
|
+
interface SummaryRenderItem {
|
|
114
|
+
key: string
|
|
115
|
+
title: string
|
|
116
|
+
kind: SummaryRenderKind
|
|
117
|
+
text: string
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface SummaryFormatter {
|
|
121
|
+
kind: SummaryRenderKind
|
|
122
|
+
canHandle: (value: unknown) => boolean
|
|
123
|
+
format: (value: unknown) => string
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const formatAsText = (value: unknown): string => {
|
|
127
|
+
if (value === null || value === undefined) return "-"
|
|
128
|
+
return String(value)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const objectJsonFormatter: SummaryFormatter = {
|
|
132
|
+
kind: "pre",
|
|
133
|
+
canHandle: (value) =>
|
|
134
|
+
value !== null && typeof value === "object" && !Array.isArray(value),
|
|
135
|
+
format: (value) => JSON.stringify(value, null, 2),
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const arrayJsonFormatter: SummaryFormatter = {
|
|
139
|
+
kind: "pre",
|
|
140
|
+
canHandle: (value) => Array.isArray(value),
|
|
141
|
+
format: (value) => JSON.stringify(value, null, 2),
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const primitiveFormatter: SummaryFormatter = {
|
|
145
|
+
kind: "text",
|
|
146
|
+
canHandle: () => true,
|
|
147
|
+
format: (value) => formatAsText(value),
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// TODO(summary-formatters): Add XML formatter when import/export surfaces XML payloads.
|
|
151
|
+
// TODO(summary-formatters): Add CSV formatter with row/column-aware preview rendering.
|
|
152
|
+
// TODO(summary-formatters): Add image formatter for URL/blob metadata and thumbnail previews.
|
|
153
|
+
const summaryFormatters: SummaryFormatter[] = [
|
|
154
|
+
objectJsonFormatter,
|
|
155
|
+
arrayJsonFormatter,
|
|
156
|
+
primitiveFormatter,
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
const resolveFormatter = (value: unknown): SummaryFormatter => {
|
|
160
|
+
return (
|
|
161
|
+
summaryFormatters.find((formatter) => formatter.canHandle(value)) ??
|
|
162
|
+
primitiveFormatter
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const formatDisplayTitle = (item: unknown, index: number): string => {
|
|
167
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
168
|
+
return `${t("core.selectedItems")} #${index + 1}`
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const record = item as Record<string, unknown>
|
|
172
|
+
const attributeValue = record[props.displayAttribute]
|
|
173
|
+
if (attributeValue !== undefined && attributeValue !== null) {
|
|
174
|
+
return formatAsText(attributeValue)
|
|
175
|
+
}
|
|
176
|
+
if (record._id !== undefined && record._id !== null) {
|
|
177
|
+
return formatAsText(record._id)
|
|
178
|
+
}
|
|
179
|
+
return `${t("core.selectedItems")} #${index + 1}`
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const getDisplayPayload = (item: unknown): unknown => {
|
|
183
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
184
|
+
return item
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const record = item as Record<string, unknown>
|
|
188
|
+
const payload = { ...record }
|
|
189
|
+
delete payload[props.displayAttribute]
|
|
190
|
+
return payload
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const renderedItems = computed<SummaryRenderItem[]>(() =>
|
|
194
|
+
props.items.map((item, index) => {
|
|
195
|
+
const payload = getDisplayPayload(item)
|
|
196
|
+
const formatter = resolveFormatter(payload)
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
key: `${index}-${formatDisplayTitle(item, index)}`,
|
|
200
|
+
title: formatDisplayTitle(item, index),
|
|
201
|
+
kind: formatter.kind,
|
|
202
|
+
text: formatter.format(payload),
|
|
203
|
+
}
|
|
204
|
+
}),
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
const confirm = () => {
|
|
208
|
+
emit("confirm")
|
|
209
|
+
close()
|
|
210
|
+
}
|
|
211
|
+
</script>
|
|
@@ -14,9 +14,11 @@ import UserChip from "./chip/UserChip.vue"
|
|
|
14
14
|
import UserMenu from "./menu/UserMenu.vue"
|
|
15
15
|
import UserNotifications from "./menu/UserNotifications.vue"
|
|
16
16
|
import WidgetsLayoutLoader from "./composition/WidgetsLayoutLoader.vue"
|
|
17
|
+
import DataExporter from "./dataExport/DataExporter.vue"
|
|
17
18
|
|
|
18
19
|
export {
|
|
19
20
|
CompositionOverlay,
|
|
21
|
+
DataExporter,
|
|
20
22
|
DefaultModal,
|
|
21
23
|
DraggableElement,
|
|
22
24
|
DropzoneElement,
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { ExportFormat } from "../types"
|
|
2
|
+
|
|
3
|
+
// ─── Optional peer dep: js-yaml ───────────────────────────────────────────────
|
|
4
|
+
// Vite/ESM environments don't support require(). We attempt a static import at
|
|
5
|
+
// module initialisation time. If js-yaml is not installed the import will throw
|
|
6
|
+
// and jsYamlDump stays undefined — the serializer then shows a clear error.
|
|
7
|
+
|
|
8
|
+
type JsYamlDump = (data: unknown, options?: object) => string
|
|
9
|
+
|
|
10
|
+
let jsYamlDump: JsYamlDump | undefined
|
|
11
|
+
|
|
12
|
+
const jsonReplacer = (_key: string, value: unknown) => {
|
|
13
|
+
// JSON doesn't support bigint values; preserve intent as a tagged string.
|
|
14
|
+
if (typeof value === "bigint") {
|
|
15
|
+
return `${value}n`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return value
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
// Static import string must be a literal so bundlers can analyse it.
|
|
23
|
+
// The `@vite-ignore` comment suppresses the "dynamic import" warning.
|
|
24
|
+
const mod = await import(/* @vite-ignore */ "js-yaml")
|
|
25
|
+
jsYamlDump = (mod.dump ?? mod.default?.dump) as JsYamlDump | undefined
|
|
26
|
+
} catch {
|
|
27
|
+
// js-yaml not installed — yamlFormat.serialize() will throw a helpful message
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ─── XML ──────────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Recursively converts a plain JS object / array to XML element strings.
|
|
34
|
+
* - Object keys become element names (invalid XML chars are stripped).
|
|
35
|
+
* - Arrays emit repeated elements using the parent key name (singularised naively).
|
|
36
|
+
* - Primitive values become text content.
|
|
37
|
+
*/
|
|
38
|
+
function toXmlElements(value: unknown, tag: string, indent: string): string {
|
|
39
|
+
const pad = indent + " "
|
|
40
|
+
|
|
41
|
+
if (value === null || value === undefined) {
|
|
42
|
+
return `${indent}<${tag}/>`
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (Array.isArray(value)) {
|
|
46
|
+
// Emit each item wrapped in a singular version of the tag.
|
|
47
|
+
const child = tag.replace(/s$/, "") || "item"
|
|
48
|
+
return value
|
|
49
|
+
.map((item) => toXmlElements(item, child, indent))
|
|
50
|
+
.join("\n")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (typeof value === "object") {
|
|
54
|
+
const children = Object.entries(value as Record<string, unknown>)
|
|
55
|
+
.map(([k, v]) => toXmlElements(v, sanitiseTag(k), pad))
|
|
56
|
+
.join("\n")
|
|
57
|
+
return `${indent}<${tag}>\n${children}\n${indent}</${tag}>`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Primitive — escape XML special chars.
|
|
61
|
+
const escaped = String(value)
|
|
62
|
+
.replace(/&/g, "&")
|
|
63
|
+
.replace(/</g, "<")
|
|
64
|
+
.replace(/>/g, ">")
|
|
65
|
+
.replace(/"/g, """)
|
|
66
|
+
.replace(/'/g, "'")
|
|
67
|
+
return `${indent}<${tag}>${escaped}</${tag}>`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function sanitiseTag(key: string): string {
|
|
71
|
+
// XML element names must start with a letter or underscore.
|
|
72
|
+
const cleaned = key.replace(/[^a-zA-Z0-9_.-]/g, "_")
|
|
73
|
+
return /^[^a-zA-Z_]/.test(cleaned) ? `_${cleaned}` : cleaned
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const xmlFormat: ExportFormat = {
|
|
77
|
+
id: "xml",
|
|
78
|
+
label: "XML",
|
|
79
|
+
extension: "xml",
|
|
80
|
+
mimeType: "application/xml",
|
|
81
|
+
serialize(data) {
|
|
82
|
+
const body = toXmlElements(data, "root", "")
|
|
83
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n${body}`
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ─── JSON ─────────────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
export const jsonFormat: ExportFormat = {
|
|
90
|
+
id: "json",
|
|
91
|
+
label: "JSON",
|
|
92
|
+
extension: "json",
|
|
93
|
+
mimeType: "application/json",
|
|
94
|
+
serialize(data) {
|
|
95
|
+
return JSON.stringify(data, jsonReplacer, 2)
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ─── YAML ─────────────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
export const yamlFormat: ExportFormat = {
|
|
102
|
+
id: "yaml",
|
|
103
|
+
label: "YAML",
|
|
104
|
+
extension: "yaml",
|
|
105
|
+
mimeType: "application/yaml",
|
|
106
|
+
serialize(data) {
|
|
107
|
+
if (typeof jsYamlDump !== "function") {
|
|
108
|
+
throw new Error(
|
|
109
|
+
'The "js-yaml" package is required for YAML export.\n' +
|
|
110
|
+
"Install it with: npm install js-yaml\n" +
|
|
111
|
+
"Types (optional): npm install -D @types/js-yaml",
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
return jsYamlDump(data, { indent: 2, lineWidth: 120, noRefs: true })
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ─── Registry ─────────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
export const BUILTIN_FORMATS: Record<string, ExportFormat> = {
|
|
121
|
+
json: jsonFormat,
|
|
122
|
+
yaml: yamlFormat,
|
|
123
|
+
xml: xmlFormat,
|
|
124
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { computed, ref, watch } from "vue"
|
|
2
|
+
import { useClipboard } from "@vueuse/core"
|
|
3
|
+
import { BUILTIN_FORMATS } from "./formats/builtins"
|
|
4
|
+
import type {
|
|
5
|
+
ExportFormat,
|
|
6
|
+
FormatEntry,
|
|
7
|
+
SerializeResult,
|
|
8
|
+
ExportSuccessPayload,
|
|
9
|
+
ExportErrorPayload,
|
|
10
|
+
} from "./types"
|
|
11
|
+
|
|
12
|
+
export * from "./types"
|
|
13
|
+
|
|
14
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
interface UseExportOptions {
|
|
17
|
+
data: () => unknown
|
|
18
|
+
formats: () => FormatEntry[]
|
|
19
|
+
defaultFormat: () => string | undefined
|
|
20
|
+
filename: () => string
|
|
21
|
+
enabled?: () => boolean
|
|
22
|
+
onSuccess: (payload: ExportSuccessPayload) => void
|
|
23
|
+
onError: (payload: ExportErrorPayload) => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─── Composable ───────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export function useExport(options: UseExportOptions) {
|
|
29
|
+
const {
|
|
30
|
+
data,
|
|
31
|
+
formats,
|
|
32
|
+
defaultFormat,
|
|
33
|
+
filename,
|
|
34
|
+
enabled,
|
|
35
|
+
onSuccess,
|
|
36
|
+
onError,
|
|
37
|
+
} = options
|
|
38
|
+
|
|
39
|
+
// ── Resolve format list ────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
const resolvedFormats = computed<ExportFormat[]>(() => {
|
|
42
|
+
return formats()
|
|
43
|
+
.map((entry): ExportFormat | null => {
|
|
44
|
+
if (typeof entry === "string") {
|
|
45
|
+
return BUILTIN_FORMATS[entry] ?? null
|
|
46
|
+
}
|
|
47
|
+
// Custom format — validate minimum required fields
|
|
48
|
+
if (
|
|
49
|
+
entry &&
|
|
50
|
+
typeof entry.id === "string" &&
|
|
51
|
+
typeof entry.serialize === "function"
|
|
52
|
+
) {
|
|
53
|
+
return entry as ExportFormat
|
|
54
|
+
}
|
|
55
|
+
console.warn("[RacletteExport] Invalid format entry, skipping:", entry)
|
|
56
|
+
return null
|
|
57
|
+
})
|
|
58
|
+
.filter((f): f is ExportFormat => f !== null)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// ── Active format ──────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
const activeFormatId = ref<string>("")
|
|
64
|
+
|
|
65
|
+
// Initialize / sync when formats or defaultFormat changes
|
|
66
|
+
watch(
|
|
67
|
+
[resolvedFormats, defaultFormat],
|
|
68
|
+
([fmts, dflt]) => {
|
|
69
|
+
if (!fmts.length) {
|
|
70
|
+
activeFormatId.value = ""
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
const preferred = dflt ?? ""
|
|
74
|
+
const found = fmts.find((f) => f.id === preferred)
|
|
75
|
+
// Keep current selection if still valid, otherwise fall back to first
|
|
76
|
+
const currentStillValid = fmts.some((f) => f.id === activeFormatId.value)
|
|
77
|
+
if (!currentStillValid) {
|
|
78
|
+
activeFormatId.value = found?.id ?? fmts[0].id
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{ immediate: true },
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
const activeFormat = computed<ExportFormat | undefined>(() =>
|
|
85
|
+
resolvedFormats.value.find((f) => f.id === activeFormatId.value),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
// ── Serialization ──────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
const serializeResult = computed<SerializeResult>(() => {
|
|
91
|
+
if (enabled && !enabled()) {
|
|
92
|
+
// Defer serialization until explicitly enabled by the caller.
|
|
93
|
+
return { ok: true, value: "" }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const fmt = activeFormat.value
|
|
97
|
+
if (!fmt) return { ok: false, error: "No format selected." }
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const value = fmt.serialize(data())
|
|
101
|
+
if (typeof value !== "string") {
|
|
102
|
+
throw new TypeError(
|
|
103
|
+
`serialize() must return a string, got ${typeof value}`,
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
return { ok: true, value }
|
|
107
|
+
} catch (err) {
|
|
108
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
109
|
+
onError({ formatId: fmt.id, action: "serialize", error: err })
|
|
110
|
+
return { ok: false, error: message }
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// ── Clipboard ──────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
const { copy, isSupported: clipboardSupported } = useClipboard()
|
|
117
|
+
const copyState = ref<"idle" | "success" | "error">("idle")
|
|
118
|
+
let copyResetTimer: ReturnType<typeof setTimeout> | null = null
|
|
119
|
+
|
|
120
|
+
async function copyToClipboard() {
|
|
121
|
+
const fmt = activeFormat.value
|
|
122
|
+
if (!fmt) return
|
|
123
|
+
if (serializeResult.value.ok === false) return
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
await copy(serializeResult.value.value)
|
|
127
|
+
copyState.value = "success"
|
|
128
|
+
onSuccess({ formatId: fmt.id, action: "copy" })
|
|
129
|
+
} catch (err) {
|
|
130
|
+
copyState.value = "error"
|
|
131
|
+
onError({ formatId: fmt.id, action: "copy", error: err })
|
|
132
|
+
} finally {
|
|
133
|
+
if (copyResetTimer) clearTimeout(copyResetTimer)
|
|
134
|
+
copyResetTimer = setTimeout(() => {
|
|
135
|
+
copyState.value = "idle"
|
|
136
|
+
}, 2000)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── Download ───────────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
function downloadFile() {
|
|
143
|
+
const fmt = activeFormat.value
|
|
144
|
+
if (!fmt) return
|
|
145
|
+
if (serializeResult.value.ok === false) return
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const blob = new Blob([serializeResult.value.value], {
|
|
149
|
+
type: `${fmt.mimeType};charset=utf-8`,
|
|
150
|
+
})
|
|
151
|
+
const url = URL.createObjectURL(blob)
|
|
152
|
+
const anchor = document.createElement("a")
|
|
153
|
+
anchor.href = url
|
|
154
|
+
anchor.download = `${filename() || "export"}.${fmt.extension}`
|
|
155
|
+
anchor.style.display = "none"
|
|
156
|
+
document.body.appendChild(anchor)
|
|
157
|
+
anchor.click()
|
|
158
|
+
document.body.removeChild(anchor)
|
|
159
|
+
URL.revokeObjectURL(url)
|
|
160
|
+
onSuccess({ formatId: fmt.id, action: "download" })
|
|
161
|
+
} catch (err) {
|
|
162
|
+
onError({ formatId: fmt.id, action: "download", error: err })
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
resolvedFormats,
|
|
168
|
+
activeFormatId,
|
|
169
|
+
activeFormat,
|
|
170
|
+
serializeResult,
|
|
171
|
+
copyToClipboard,
|
|
172
|
+
downloadFile,
|
|
173
|
+
copyState,
|
|
174
|
+
clipboardSupported,
|
|
175
|
+
}
|
|
176
|
+
}
|