@raclettejs/workbench 0.0.1
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/README.md +1 -0
- package/config/compositions.js +282 -0
- package/config/interactionLinks.js +164 -0
- package/i18n/de-DE.json +245 -0
- package/i18n/en-EU.json +245 -0
- package/i18n/sk-SK.json +245 -0
- package/package.json +63 -0
- package/packages.json +33 -0
- package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/CompositionConfiguration.vue +140 -0
- package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/CompositionIntegration.vue +3 -0
- package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/CompositionWidgetsLayout.vue +129 -0
- package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/widgetsLayout/WidgetContextMenu.vue +53 -0
- package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/widgetsLayout/WidgetIcon.vue +99 -0
- package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/widgetsLayout/WidgetList.vue +122 -0
- package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/widgetsLayout/WidgetRenderer.vue +65 -0
- package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/widgetsLayout/WidgetSettings.vue +96 -0
- package/plugins/pacifico__compositions/frontend/widgets/CompositionCreateWidget.vue +51 -0
- package/plugins/pacifico__compositions/frontend/widgets/CompositionEditWidget.vue +66 -0
- package/plugins/pacifico__compositions/frontend/widgets/CompositionListWidget.vue +132 -0
- package/plugins/pacifico__compositions/raclette.plugin.ts +8 -0
- package/plugins/pacifico__core/backend/index.ts +25 -0
- package/plugins/pacifico__core/backend/routes.ts +220 -0
- package/plugins/pacifico__core/raclette.plugin.ts +9 -0
- package/plugins/pacifico__interactionLinks/frontend/components/InteractionLinkConfiguration.vue +145 -0
- package/plugins/pacifico__interactionLinks/frontend/widgets/InteractionLinkCreateWidget.vue +48 -0
- package/plugins/pacifico__interactionLinks/frontend/widgets/InteractionLinkEditWidget.vue +66 -0
- package/plugins/pacifico__interactionLinks/frontend/widgets/InteractionLinkListWidget.vue +126 -0
- package/plugins/pacifico__interactionLinks/raclette.plugin.ts +8 -0
- package/plugins/pacifico__plugins/frontend/index.ts +42 -0
- package/plugins/pacifico__plugins/frontend/widgets/PluginDetailWidget.vue +26 -0
- package/plugins/pacifico__plugins/frontend/widgets/PluginListWidget.vue +59 -0
- package/plugins/pacifico__plugins/raclette.plugin.ts +8 -0
- package/plugins/pacifico__tags/frontend/components/TagConfiguration.vue +99 -0
- package/plugins/pacifico__tags/frontend/widgets/TagCreateWidget.vue +43 -0
- package/plugins/pacifico__tags/frontend/widgets/TagEditWidget.vue +64 -0
- package/plugins/pacifico__tags/frontend/widgets/TagListWidget.vue +71 -0
- package/plugins/pacifico__tags/raclette.plugin.ts +8 -0
- package/plugins/pacifico__users/frontend/components/UserConfiguration.vue +110 -0
- package/plugins/pacifico__users/frontend/widgets/UserCreateWidget.vue +48 -0
- package/plugins/pacifico__users/frontend/widgets/UserEditWidget.vue +64 -0
- package/plugins/pacifico__users/frontend/widgets/UserListWidget.vue +86 -0
- package/plugins/pacifico__users/raclette.plugin.ts +8 -0
- package/raclette.config.js +150 -0
- package/services/frontend/.DS_Store +0 -0
- package/services/frontend/public/favicon.png +0 -0
- package/services/frontend/public/home-icon.png +0 -0
- package/services/frontend/public/logo.png +0 -0
- package/services/frontend/src/app/.DS_Store +0 -0
- package/services/frontend/src/app/components/BaseDataTable.vue +284 -0
- package/services/frontend/src/app/components/dynamicForm/DynamicForm.vue +207 -0
- package/services/frontend/src/app/components/dynamicForm/DynamicFormTypes.ts +63 -0
- package/services/frontend/src/app/components/dynamicForm/DynamicInput.vue +344 -0
- package/services/frontend/src/app/components/dynamicForm/TextDivider.vue +13 -0
- package/services/frontend/src/app/components/dynamicForm/testing/DynamicFormInputTesting.vue +477 -0
- package/services/frontend/src/app/components/dynamicForm/typedInputs/CheckboxInput.vue +16 -0
- package/services/frontend/src/app/components/dynamicForm/typedInputs/ColorInput.vue +52 -0
- package/services/frontend/src/app/components/dynamicForm/typedInputs/CompositionInput.vue +32 -0
- package/services/frontend/src/app/components/dynamicForm/typedInputs/IconInput.vue +40 -0
- package/services/frontend/src/app/components/dynamicForm/typedInputs/ListInput.vue +51 -0
- package/services/frontend/src/app/components/dynamicForm/typedInputs/LocalizedInput.vue +51 -0
- package/services/frontend/src/app/components/dynamicForm/typedInputs/PasswordInput.vue +23 -0
- package/services/frontend/src/app/components/dynamicForm/typedInputs/RecordInput.vue +91 -0
- package/services/frontend/src/app/components/dynamicForm/typedInputs/SwitchInput.vue +17 -0
- package/services/frontend/src/app/components/dynamicForm/typedInputs/TagsInput.vue +58 -0
- package/services/frontend/src/app/components/dynamicForm/typedInputs/TriggerInput.vue +159 -0
- package/services/frontend/src/app/components/dynamicForm/typedInputs/UserInput.vue +63 -0
- package/services/frontend/src/app/components/dynamicForm/typedInputs/mdi-icons.ts +7002 -0
- package/services/frontend/src/app/components/dynamicForm/validators.ts +19 -0
- package/services/frontend/src/app/components/stepNavigator/StepNavigator.vue +331 -0
- package/services/frontend/src/app/components/stepNavigator/StepNavigatorTypes.ts +12 -0
- package/services/frontend/src/app/composables/useUserHelper.ts +254 -0
- package/services/frontend/src/app/composables/useVantaFog.ts +37 -0
- package/services/frontend/src/app/composables/useWorkbenchPlugins.ts +44 -0
- package/services/frontend/src/orchestrator/.DS_Store +0 -0
- package/services/frontend/src/orchestrator/LoginApp.vue +62 -0
- package/services/frontend/src/orchestrator/WelcomeScreen.vue +39 -0
- package/services/frontend/src/orchestrator/assets/styles/themes/dark.ts +39 -0
- package/services/frontend/src/orchestrator/assets/styles/themes/highContrast.ts +39 -0
- package/services/frontend/src/orchestrator/assets/styles/themes/light.ts +39 -0
- package/services/frontend/src/orchestrator/components/InitProjectScreen.vue +298 -0
- package/services/frontend/src/orchestrator/components/LoginScreen.vue +73 -0
- package/services/frontend/src/orchestrator/components/RacletteAdCorner.vue +313 -0
- package/services/frontend/src/orchestrator/components/layout/AppButton.vue +109 -0
- package/services/frontend/src/orchestrator/components/layout/AppHeadline.vue +46 -0
- package/services/frontend/src/orchestrator/components/layout/AppLabelledInput.vue +94 -0
- package/services/frontend/src/orchestrator/components/layout/AppModal.vue +28 -0
- package/services/frontend/src/orchestrator/components/layout/SvgButtonArrow.vue +14 -0
- package/services/frontend/src/orchestrator/composables/useWelcomeScreen.ts +19 -0
- package/yarn.lock +3435 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<!-- Search and Actions Bar -->
|
|
4
|
+
<div class="tw:flex tw:gap-2 tw:items-center tw:mb-6">
|
|
5
|
+
<v-text-field
|
|
6
|
+
v-model="search"
|
|
7
|
+
variant="outlined"
|
|
8
|
+
hide-details
|
|
9
|
+
prepend-inner-icon="mdi-magnify"
|
|
10
|
+
clearable
|
|
11
|
+
:placeholder="
|
|
12
|
+
$t('workbench.baseDataTable.searchPlaceholder', {
|
|
13
|
+
dataName,
|
|
14
|
+
})
|
|
15
|
+
"
|
|
16
|
+
density="compact"
|
|
17
|
+
/>
|
|
18
|
+
|
|
19
|
+
<AppButton
|
|
20
|
+
v-if="showCreateButton"
|
|
21
|
+
@click="handleCreate"
|
|
22
|
+
variant="secondary"
|
|
23
|
+
class="!tw:h-10"
|
|
24
|
+
>
|
|
25
|
+
<template #prepend-icon>
|
|
26
|
+
<v-icon icon="mdi-plus-circle-outline" />
|
|
27
|
+
</template>
|
|
28
|
+
{{
|
|
29
|
+
$t("workbench.baseDataTable.createDataButton", {
|
|
30
|
+
dataName,
|
|
31
|
+
})
|
|
32
|
+
}}
|
|
33
|
+
</AppButton>
|
|
34
|
+
|
|
35
|
+
<!-- Additional action buttons slot -->
|
|
36
|
+
<slot name="actions" />
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<!-- Data Table -->
|
|
40
|
+
<v-data-table
|
|
41
|
+
v-model:search="search"
|
|
42
|
+
density="comfortable"
|
|
43
|
+
:items-per-page="itemsPerPage"
|
|
44
|
+
sticky
|
|
45
|
+
fixed-header
|
|
46
|
+
:headers="computedHeaders"
|
|
47
|
+
:items="items"
|
|
48
|
+
:loading="loading"
|
|
49
|
+
@click:row="handleRowClick"
|
|
50
|
+
>
|
|
51
|
+
<!-- Built-in tags column -->
|
|
52
|
+
<template #item.tags="{ item }" v-if="!customSlots.includes('item.tags')">
|
|
53
|
+
<div class="tw:flex tw:gap-2" v-if="item.tags?.length">
|
|
54
|
+
<v-chip
|
|
55
|
+
v-for="tag in item.tags"
|
|
56
|
+
:key="tag._id || tag.id || tag"
|
|
57
|
+
:color="tag.color"
|
|
58
|
+
label
|
|
59
|
+
variant="outlined"
|
|
60
|
+
size="small"
|
|
61
|
+
>
|
|
62
|
+
{{ tag.title || tag.name || tag }}
|
|
63
|
+
</v-chip>
|
|
64
|
+
</div>
|
|
65
|
+
</template>
|
|
66
|
+
|
|
67
|
+
<!-- Dynamic slots for custom column rendering -->
|
|
68
|
+
<template v-for="slot in customSlots" :key="slot" #[slot]="slotProps">
|
|
69
|
+
<slot :name="slot" v-bind="slotProps" />
|
|
70
|
+
</template>
|
|
71
|
+
|
|
72
|
+
<!-- Actions column template -->
|
|
73
|
+
<template #item.actions="{ item }" v-if="showActionsColumn">
|
|
74
|
+
<slot name="row-actions" :item="item">
|
|
75
|
+
<!-- Default delete action -->
|
|
76
|
+
<v-icon
|
|
77
|
+
v-if="showDeleteAction && !isDeleteDisabled(item)"
|
|
78
|
+
color="red"
|
|
79
|
+
@click.stop.prevent="handleDelete(item)"
|
|
80
|
+
class="mr-2"
|
|
81
|
+
:title="
|
|
82
|
+
$t('workbench.baseDataTable.deleteDataTitle', {
|
|
83
|
+
dataName,
|
|
84
|
+
})
|
|
85
|
+
"
|
|
86
|
+
icon="mdi-delete"
|
|
87
|
+
/>
|
|
88
|
+
</slot>
|
|
89
|
+
</template>
|
|
90
|
+
|
|
91
|
+
<!-- Loading template -->
|
|
92
|
+
<template #loader>
|
|
93
|
+
<v-skeleton-loader
|
|
94
|
+
class="mx-auto border"
|
|
95
|
+
:height="500"
|
|
96
|
+
width="100%"
|
|
97
|
+
type="list-item"
|
|
98
|
+
:loading="true"
|
|
99
|
+
/>
|
|
100
|
+
</template>
|
|
101
|
+
|
|
102
|
+
<!-- Additional table slots (this caused the table to be empty) -->
|
|
103
|
+
<!-- <slot name="table-slots" /> -->
|
|
104
|
+
</v-data-table>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<!-- Delete Confirmation Dialog -->
|
|
108
|
+
<v-dialog
|
|
109
|
+
v-model="deleteDialog"
|
|
110
|
+
max-width="400"
|
|
111
|
+
v-if="showDeleteAction && itemToDelete"
|
|
112
|
+
>
|
|
113
|
+
<v-card>
|
|
114
|
+
<v-card-title class="text-h6">
|
|
115
|
+
{{
|
|
116
|
+
$t("workbench.baseDataTable.deleteDataTitle", {
|
|
117
|
+
dataName,
|
|
118
|
+
})
|
|
119
|
+
}}
|
|
120
|
+
</v-card-title>
|
|
121
|
+
|
|
122
|
+
<v-card-text
|
|
123
|
+
v-html="
|
|
124
|
+
$t('workbench.baseDataTable.deleteDataConfirmMessage', {
|
|
125
|
+
article: dataArticle,
|
|
126
|
+
dataName,
|
|
127
|
+
value:
|
|
128
|
+
itemToDelete.email ||
|
|
129
|
+
itemToDelete.title ||
|
|
130
|
+
itemToDelete.name ||
|
|
131
|
+
itemToDelete._id,
|
|
132
|
+
})
|
|
133
|
+
"
|
|
134
|
+
/>
|
|
135
|
+
<v-card-actions>
|
|
136
|
+
<v-spacer />
|
|
137
|
+
<v-btn variant="text" @click="deleteDialog = false">
|
|
138
|
+
{{ $t("workbench.baseDataTable.deleteDataConfirmCancel") }}
|
|
139
|
+
</v-btn>
|
|
140
|
+
<v-btn color="red" variant="flat" @click="confirmDelete">
|
|
141
|
+
{{ $t("workbench.baseDataTable.deleteDataConfirmButton") }}
|
|
142
|
+
</v-btn>
|
|
143
|
+
</v-card-actions>
|
|
144
|
+
</v-card>
|
|
145
|
+
</v-dialog>
|
|
146
|
+
|
|
147
|
+
<!-- Additional dialogs slot -->
|
|
148
|
+
<slot name="dialogs" />
|
|
149
|
+
</template>
|
|
150
|
+
|
|
151
|
+
<script setup lang="ts" generic="T extends Record<string, any>">
|
|
152
|
+
import { computed, ref, useSlots } from "vue"
|
|
153
|
+
import { useRouteState } from "@raclettejs/core/orchestrator/composables"
|
|
154
|
+
import AppButton from "../../orchestrator/components/layout/AppButton.vue"
|
|
155
|
+
|
|
156
|
+
export interface BaseDataTableProps<T> {
|
|
157
|
+
uuid?: string
|
|
158
|
+
// Data props
|
|
159
|
+
items?: T[]
|
|
160
|
+
loading?: boolean
|
|
161
|
+
|
|
162
|
+
// Table configuration
|
|
163
|
+
headers: Array<{
|
|
164
|
+
title: string
|
|
165
|
+
key: keyof T | string
|
|
166
|
+
align?: "start" | "center" | "end"
|
|
167
|
+
sortable?: boolean
|
|
168
|
+
width?: string | number
|
|
169
|
+
}>
|
|
170
|
+
itemsPerPage?: number
|
|
171
|
+
|
|
172
|
+
// Create button
|
|
173
|
+
showCreateButton?: boolean
|
|
174
|
+
createInteractionLinkId?: string
|
|
175
|
+
|
|
176
|
+
// Row actions
|
|
177
|
+
showActionsColumn?: boolean
|
|
178
|
+
showDeleteAction?: boolean
|
|
179
|
+
|
|
180
|
+
// Navigation
|
|
181
|
+
editInteractionLinkId?: string
|
|
182
|
+
|
|
183
|
+
// Item processing
|
|
184
|
+
deleteValidator?: (item: T) => boolean
|
|
185
|
+
rowClickHandler?: (item: T) => void
|
|
186
|
+
|
|
187
|
+
dataName: string
|
|
188
|
+
dataArticle: string
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const props = withDefaults(defineProps<BaseDataTableProps<T>>(), {
|
|
192
|
+
uuid: undefined,
|
|
193
|
+
items: () => [],
|
|
194
|
+
itemsPerPage: 50,
|
|
195
|
+
showCreateButton: true,
|
|
196
|
+
showActionsColumn: true,
|
|
197
|
+
showDeleteAction: true,
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
const emit = defineEmits<{
|
|
201
|
+
create: []
|
|
202
|
+
edit: [item: T]
|
|
203
|
+
delete: [item: T]
|
|
204
|
+
rowClick: [item: T]
|
|
205
|
+
}>()
|
|
206
|
+
|
|
207
|
+
const slots = useSlots()
|
|
208
|
+
const { triggerInteractionLinkById } = useRouteState()
|
|
209
|
+
|
|
210
|
+
// Reactive data
|
|
211
|
+
const search = ref("")
|
|
212
|
+
const deleteDialog = ref(false)
|
|
213
|
+
const itemToDelete = ref<T | null>(null)
|
|
214
|
+
|
|
215
|
+
const computedHeaders = computed(() => {
|
|
216
|
+
if (!props.headers || !props.headers?.length) {
|
|
217
|
+
return null
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const headers = [...props.headers]
|
|
221
|
+
|
|
222
|
+
// Add actions column if needed
|
|
223
|
+
if (props.showActionsColumn) {
|
|
224
|
+
headers.push({
|
|
225
|
+
title: "Actions",
|
|
226
|
+
key: "actions",
|
|
227
|
+
align: "end" as const,
|
|
228
|
+
sortable: false,
|
|
229
|
+
})
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return headers
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
const customSlots = computed(() => {
|
|
236
|
+
return Object.keys(slots).filter(
|
|
237
|
+
(slot) => slot.startsWith("item.") && slot !== "item.actions",
|
|
238
|
+
)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
// Methods
|
|
242
|
+
const handleCreate = () => {
|
|
243
|
+
if (props.createInteractionLinkId) {
|
|
244
|
+
triggerInteractionLinkById(props.createInteractionLinkId)
|
|
245
|
+
}
|
|
246
|
+
emit("create")
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const handleRowClick = (_: PointerEvent, row: { item: T }) => {
|
|
250
|
+
if (props.rowClickHandler) {
|
|
251
|
+
props.rowClickHandler(row.item)
|
|
252
|
+
return
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (props.editInteractionLinkId) {
|
|
256
|
+
const item = row.item as any
|
|
257
|
+
|
|
258
|
+
triggerInteractionLinkById(props.editInteractionLinkId, {
|
|
259
|
+
id: item._id,
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
emit("edit", row.item)
|
|
264
|
+
emit("rowClick", row.item)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const handleDelete = (item: T) => {
|
|
268
|
+
itemToDelete.value = item
|
|
269
|
+
deleteDialog.value = true
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const confirmDelete = () => {
|
|
273
|
+
if (itemToDelete.value) {
|
|
274
|
+
emit("delete", itemToDelete.value)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
itemToDelete.value = null
|
|
278
|
+
deleteDialog.value = false
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const isDeleteDisabled = (item: T): boolean => {
|
|
282
|
+
return props.deleteValidator ? !props.deleteValidator(item) : false
|
|
283
|
+
}
|
|
284
|
+
</script>
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="tw:p-6" :class="{ 'tw:max-w-6xl': !fluid }">
|
|
3
|
+
<div v-if="title" class="tw:mb-6 tw:flex tw:gap-1 tw:items-center">
|
|
4
|
+
<v-btn
|
|
5
|
+
v-if="routeBackInteractionLinkId"
|
|
6
|
+
@click="navigateBack"
|
|
7
|
+
density="comfortable"
|
|
8
|
+
icon="mdi-chevron-left"
|
|
9
|
+
flat
|
|
10
|
+
/>
|
|
11
|
+
<h1 v-if="title" class="tw:text-2xl tw:font-bold">
|
|
12
|
+
{{ title }}
|
|
13
|
+
</h1>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<!-- body -->
|
|
17
|
+
<div class="tw:grid tw:gap-6">
|
|
18
|
+
<template v-for="(value, key) in props.data" :key="key">
|
|
19
|
+
<TextDivider v-if="'divider' in value" :label="value.divider" />
|
|
20
|
+
|
|
21
|
+
<DynamicInput
|
|
22
|
+
v-else
|
|
23
|
+
v-model="modelProxy[value.field]"
|
|
24
|
+
:input-type="value.inputType"
|
|
25
|
+
:value="value"
|
|
26
|
+
:disabled="!!disabled"
|
|
27
|
+
:error-message="getValidationError(value)"
|
|
28
|
+
@validate="validateField(value)"
|
|
29
|
+
/>
|
|
30
|
+
</template>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<!-- button area -->
|
|
34
|
+
<slot name="button-area-override">
|
|
35
|
+
<div
|
|
36
|
+
class="tw:mt-8 tw:flex tw:gap-4 tw:justify-end"
|
|
37
|
+
v-if="$slots?.['button-area']"
|
|
38
|
+
>
|
|
39
|
+
<slot name="button-area" v-bind="{ isValid, validateAllFields }" />
|
|
40
|
+
<!-- <v-btn color="primary" variant="outlined"> Validate Form </v-btn>
|
|
41
|
+
<v-btn color="secondary" variant="outlined"> Reset Form </v-btn>
|
|
42
|
+
<v-btn color="info" variant="outlined"> Show Form Data </v-btn> -->
|
|
43
|
+
</div>
|
|
44
|
+
</slot>
|
|
45
|
+
</div>
|
|
46
|
+
</template>
|
|
47
|
+
|
|
48
|
+
<script setup lang="ts" generic="T">
|
|
49
|
+
import { computed, ref, watch, watchEffect } from "vue"
|
|
50
|
+
import DynamicInput from "./DynamicInput.vue"
|
|
51
|
+
import TextDivider from "./TextDivider.vue"
|
|
52
|
+
import { DynamicFormItem, DynamicFormField } from "./DynamicFormTypes"
|
|
53
|
+
import { useI18n } from "vue-i18n"
|
|
54
|
+
import { useRouteState } from "@raclettejs/core/orchestrator/composables"
|
|
55
|
+
|
|
56
|
+
const props = defineProps<{
|
|
57
|
+
title?: string
|
|
58
|
+
data: DynamicFormItem<T>[]
|
|
59
|
+
modelValue: T
|
|
60
|
+
disabled?: Boolean
|
|
61
|
+
validateOnChange?: boolean
|
|
62
|
+
fluid?: boolean
|
|
63
|
+
routeBackInteractionLinkId?: string
|
|
64
|
+
}>()
|
|
65
|
+
|
|
66
|
+
const emit = defineEmits<{
|
|
67
|
+
(e: "update:modelValue", value: T): void
|
|
68
|
+
(e: "validation-change", isValid: boolean): void
|
|
69
|
+
}>()
|
|
70
|
+
|
|
71
|
+
const { t } = useI18n()
|
|
72
|
+
const { triggerInteractionLinkById } = useRouteState()
|
|
73
|
+
|
|
74
|
+
const validationErrors = ref<Record<string, string>>({})
|
|
75
|
+
|
|
76
|
+
const isValid = computed(() => Object.keys(validationErrors.value).length === 0)
|
|
77
|
+
|
|
78
|
+
const modelProxy = computed({
|
|
79
|
+
get: () => props.modelValue,
|
|
80
|
+
set: (value: T) => emit("update:modelValue", value),
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
const validateValue = (
|
|
84
|
+
field: DynamicFormField<T>,
|
|
85
|
+
value: any,
|
|
86
|
+
): string | null => {
|
|
87
|
+
const validation = field.validation
|
|
88
|
+
|
|
89
|
+
if (!validation) {
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const { required, minLength, maxLength, pattern, customValidator } =
|
|
94
|
+
validation
|
|
95
|
+
|
|
96
|
+
// Required validation
|
|
97
|
+
if (required) {
|
|
98
|
+
if (
|
|
99
|
+
value === null ||
|
|
100
|
+
value === undefined ||
|
|
101
|
+
value === "" ||
|
|
102
|
+
(Array.isArray(value) && value.length === 0) ||
|
|
103
|
+
(typeof value === "object" && Object.keys(value).length === 0)
|
|
104
|
+
) {
|
|
105
|
+
return t("workbench.dynamicInputs.validation.required", {
|
|
106
|
+
field: field.title,
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Skip other validations if value is empty and not required
|
|
112
|
+
if (!required && (!value || value === "")) {
|
|
113
|
+
return null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Min length validation
|
|
117
|
+
if (minLength && typeof value === "string" && value.length < minLength) {
|
|
118
|
+
return t("workbench.dynamicInputs.validation.minLength", {
|
|
119
|
+
field: field.title,
|
|
120
|
+
minLength,
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Max length validation
|
|
125
|
+
if (maxLength && typeof value === "string" && value.length > maxLength) {
|
|
126
|
+
return t("workbench.dynamicInputs.validation.maxLength", {
|
|
127
|
+
field: field.title,
|
|
128
|
+
maxLength,
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Pattern validation
|
|
133
|
+
if (pattern && typeof value === "string" && !pattern.test(value)) {
|
|
134
|
+
return t("workbench.dynamicInputs.validation.invalidFormat", {
|
|
135
|
+
field: field.title,
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (required && field.inputType === "localized") {
|
|
140
|
+
if (!value.default) {
|
|
141
|
+
return t("workbench.dynamicInputs.validation.defaultValueRequired")
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Custom validator
|
|
146
|
+
if (customValidator) {
|
|
147
|
+
return t(customValidator(value) || "")
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return null
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Validate a single field
|
|
154
|
+
const validateField = (field: DynamicFormField<T>) => {
|
|
155
|
+
const value = props.modelValue[field.field]
|
|
156
|
+
const error = validateValue(field, value)
|
|
157
|
+
|
|
158
|
+
if (error) {
|
|
159
|
+
validationErrors.value[field.field as string] = error
|
|
160
|
+
} else {
|
|
161
|
+
delete validationErrors.value[field.field as string]
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Emit validation status
|
|
165
|
+
emit("validation-change", Object.keys(validationErrors.value).length === 0)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Get validation error for a field
|
|
169
|
+
const getValidationError = (field: DynamicFormField<T>): string | undefined => {
|
|
170
|
+
return validationErrors.value[field.field as string]
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Validate all fields
|
|
174
|
+
const validateAllFields = () => {
|
|
175
|
+
props.data.forEach((item) => {
|
|
176
|
+
if ("field" in item) {
|
|
177
|
+
validateField(item)
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
return isValid.value
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const navigateBack = () => {
|
|
185
|
+
if (props.routeBackInteractionLinkId) {
|
|
186
|
+
triggerInteractionLinkById(props.routeBackInteractionLinkId)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
watch(
|
|
191
|
+
() => props.modelValue,
|
|
192
|
+
() => {
|
|
193
|
+
if (props.validateOnChange) {
|
|
194
|
+
validateAllFields()
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
{ deep: true },
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
watchEffect(() => emit("validation-change", isValid.value))
|
|
201
|
+
|
|
202
|
+
defineExpose({
|
|
203
|
+
validateAllFields,
|
|
204
|
+
isValid,
|
|
205
|
+
errors: computed(() => validationErrors.value),
|
|
206
|
+
})
|
|
207
|
+
</script>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { MdiIcon } from "./typedInputs/mdi-icons"
|
|
2
|
+
|
|
3
|
+
export type DynamicInputType =
|
|
4
|
+
| "permissions"
|
|
5
|
+
| "tags"
|
|
6
|
+
| "localized"
|
|
7
|
+
| "composition"
|
|
8
|
+
| "triggers"
|
|
9
|
+
| "record"
|
|
10
|
+
| "list"
|
|
11
|
+
| "text"
|
|
12
|
+
| "textarea"
|
|
13
|
+
| "number"
|
|
14
|
+
| "checkbox"
|
|
15
|
+
| "switch"
|
|
16
|
+
| "select"
|
|
17
|
+
| "multiselect"
|
|
18
|
+
| "password"
|
|
19
|
+
| "color"
|
|
20
|
+
| "user"
|
|
21
|
+
|
|
22
|
+
interface DynamicFormItemBase<T> {
|
|
23
|
+
title: string
|
|
24
|
+
description?: string
|
|
25
|
+
inputType: DynamicInputType
|
|
26
|
+
field: keyof T
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface InputAttributes {
|
|
30
|
+
prependIcon?: MdiIcon
|
|
31
|
+
prependInnerIcon?: MdiIcon
|
|
32
|
+
appendIcon?: MdiIcon
|
|
33
|
+
appendInnerIcon?: MdiIcon
|
|
34
|
+
prefix?: string
|
|
35
|
+
suffix?: string
|
|
36
|
+
// click handlers
|
|
37
|
+
onClickPrepend?: (event: PointerEvent) => void
|
|
38
|
+
onClickPrependInner?: (event: PointerEvent) => void
|
|
39
|
+
onClickAppend?: (event: PointerEvent) => void
|
|
40
|
+
onClickAppendInner?: (event: PointerEvent) => void
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface SelectionProps {
|
|
44
|
+
selectItems?: string[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ValidationProps {
|
|
48
|
+
required?: boolean
|
|
49
|
+
minLength?: number
|
|
50
|
+
maxLength?: number
|
|
51
|
+
pattern?: RegExp
|
|
52
|
+
customValidator?: (value: any) => string | null // return error message or null if valid
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface DynamicFormDivider {
|
|
56
|
+
divider: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type DynamicFormField<T> = DynamicFormItemBase<T> &
|
|
60
|
+
InputAttributes &
|
|
61
|
+
SelectionProps & { validation?: ValidationProps }
|
|
62
|
+
|
|
63
|
+
export type DynamicFormItem<T> = DynamicFormField<T> | DynamicFormDivider
|