@raclettejs/workbench 0.1.33-canary.3 → 0.1.33
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/.raclette/backend/raclette.config.js +2 -2
- package/.raclette/frontend/raclette.config.js +2 -2
- package/.raclette/frontend/yarn.lock +143 -126
- package/.raclette/virtual/backend/raclette.config.js +2 -2
- package/.raclette/virtual/frontend/raclette.config.js +2 -2
- package/.raclette/virtual/frontend/yarn.lock +143 -126
- package/CHANGELOG.md +19 -3
- package/i18n/de-DE.json +15 -0
- package/i18n/en-EU.json +15 -0
- package/i18n/sk-SK.json +15 -0
- package/package.json +5 -4
- package/plugins/pacifico__compositions/frontend/widgets/CompositionListWidget.vue +39 -21
- package/plugins/pacifico__interactionLinks/frontend/widgets/InteractionLinkListWidget.vue +39 -21
- package/plugins/pacifico__plugins/frontend/index.ts +3 -3
- package/plugins/pacifico__plugins/frontend/widgets/PluginListWidget.vue +11 -12
- package/plugins/pacifico__tags/frontend/widgets/TagListWidget.vue +39 -21
- package/plugins/pacifico__users/frontend/widgets/UserListWidget.vue +79 -57
- package/services/frontend/src/app/components/dynamicForm/DynamicForm.vue +139 -51
- package/services/frontend/src/app/components/dynamicForm/DynamicInput.vue +95 -5
- package/services/frontend/src/app/components/dynamicForm/ObjectListInput.vue +78 -41
- package/services/frontend/src/app/components/dynamicForm/TextDivider.vue +46 -6
- package/services/frontend/src/app/components/dynamicForm/typedInputs/ListInput.vue +118 -27
- package/services/frontend/src/app/components/dynamicForm/typedInputs/RecordInput.vue +131 -21
- package/services/frontend/src/app/components/dynamicForm/typedInputs/TagsInput.vue +1 -0
- package/services/frontend/src/app/components/dynamicForm/typedInputs/TriggerInput.vue +4 -4
- package/services/frontend/src/app/composables/useWorkbenchTableActions.ts +29 -0
- package/services/frontend/src/orchestrator/assets/styles/themes/light.ts +4 -3
|
@@ -1,69 +1,97 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
<h1 v-if="title" class="tw:text-2xl tw:font-bold">
|
|
12
|
-
{{ title }}
|
|
13
|
-
</h1>
|
|
14
|
-
</div>
|
|
15
|
-
|
|
16
|
-
<v-form @submit.prevent="onFormSubmit">
|
|
17
|
-
<!-- body -->
|
|
18
|
-
<div class="tw:grid tw:gap-6">
|
|
19
|
-
<template v-for="(value, key) in props.data" :key="key">
|
|
20
|
-
<TextDivider v-if="'divider' in value" :label="value.divider" />
|
|
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
|
-
<v-btn type="submit" class="tw:hidden!" />
|
|
36
|
-
<div
|
|
37
|
-
class="tw:mt-8 tw:flex tw:gap-4 tw:justify-end"
|
|
38
|
-
v-if="$slots?.['button-area']"
|
|
2
|
+
<div :class="wrapperClasses">
|
|
3
|
+
<v-form
|
|
4
|
+
class="tw:flex tw:min-h-0 tw:w-full tw:flex-col"
|
|
5
|
+
@submit.prevent="onFormSubmit"
|
|
6
|
+
>
|
|
7
|
+
<div class="tw:flex tw:min-h-0 tw:w-full tw:flex-col">
|
|
8
|
+
<header
|
|
9
|
+
v-if="title || routeBackInteractionLinkId"
|
|
10
|
+
class="tw:flex tw:items-center tw:justify-between tw:gap-3 tw:pt-3"
|
|
39
11
|
>
|
|
40
|
-
<
|
|
41
|
-
|
|
12
|
+
<div class="tw:flex tw:min-w-0 tw:items-center tw:gap-2">
|
|
13
|
+
<v-btn
|
|
14
|
+
v-if="routeBackInteractionLinkId"
|
|
15
|
+
@click="navigateBack"
|
|
16
|
+
:density="vuetifyDensity"
|
|
17
|
+
icon="mdi-chevron-left"
|
|
18
|
+
variant="text"
|
|
19
|
+
aria-label="Go back"
|
|
20
|
+
/>
|
|
21
|
+
<h1
|
|
22
|
+
v-if="title"
|
|
23
|
+
class="tw:min-w-0 tw:truncate tw:font-semibold tw:tracking-tight tw:text-[color:rgb(var(--v-theme-on-surface))]"
|
|
24
|
+
:class="titleClasses"
|
|
25
|
+
>
|
|
26
|
+
{{ title }}
|
|
27
|
+
</h1>
|
|
28
|
+
</div>
|
|
29
|
+
</header>
|
|
30
|
+
|
|
31
|
+
<!-- body -->
|
|
32
|
+
<div class="tw:grid" :class="bodyClasses">
|
|
33
|
+
<template v-for="(value, key) in props.data" :key="key">
|
|
34
|
+
<TextDivider
|
|
35
|
+
v-if="'divider' in value"
|
|
36
|
+
:label="value.divider"
|
|
37
|
+
:density="density"
|
|
38
|
+
/>
|
|
39
|
+
<DynamicInput
|
|
40
|
+
v-else
|
|
41
|
+
v-model="modelProxy[value.field]"
|
|
42
|
+
:input-type="value.inputType"
|
|
43
|
+
:value="value"
|
|
44
|
+
:disabled="!!disabled"
|
|
45
|
+
:error-message="getValidationError(value)"
|
|
46
|
+
:density="density"
|
|
47
|
+
@validate="validateField(value)"
|
|
48
|
+
/>
|
|
49
|
+
</template>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<!-- button area -->
|
|
53
|
+
<slot name="button-area-override">
|
|
54
|
+
<v-btn type="submit" class="tw:hidden!" />
|
|
55
|
+
<footer
|
|
56
|
+
v-if="$slots?.['button-area']"
|
|
57
|
+
class="tw:flex tw:flex-wrap tw:items-center tw:justify-end"
|
|
58
|
+
:class="actionClasses"
|
|
59
|
+
>
|
|
60
|
+
<slot name="button-area" v-bind="{ isValid, validateAllFields }" />
|
|
61
|
+
<!-- <v-btn color="primary" variant="outlined"> Validate Form </v-btn>
|
|
42
62
|
<v-btn color="secondary" variant="outlined"> Reset Form </v-btn>
|
|
43
63
|
<v-btn color="info" variant="outlined"> Show Form Data </v-btn> -->
|
|
44
|
-
|
|
45
|
-
|
|
64
|
+
</footer>
|
|
65
|
+
</slot>
|
|
66
|
+
</div>
|
|
46
67
|
</v-form>
|
|
47
68
|
</div>
|
|
48
69
|
</template>
|
|
49
70
|
|
|
50
71
|
<script setup lang="ts" generic="T">
|
|
51
|
-
import { computed, ref, watch, watchEffect } from "vue"
|
|
72
|
+
import { computed, ref, watch, watchEffect, withDefaults } from "vue"
|
|
52
73
|
import DynamicInput from "./DynamicInput.vue"
|
|
53
74
|
import TextDivider from "./TextDivider.vue"
|
|
54
75
|
import { DynamicFormItem, DynamicFormField } from "./DynamicFormTypes"
|
|
55
76
|
import { useI18n } from "vue-i18n"
|
|
56
77
|
import { useRouteState } from "@raclettejs/core/orchestrator/composables"
|
|
57
78
|
|
|
58
|
-
const props =
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
79
|
+
const props = withDefaults(
|
|
80
|
+
defineProps<{
|
|
81
|
+
title?: string
|
|
82
|
+
data: DynamicFormItem<T>[]
|
|
83
|
+
modelValue: T
|
|
84
|
+
disabled?: Boolean
|
|
85
|
+
validateOnChange?: boolean
|
|
86
|
+
/** When false, constrains width (previous default). Default true fills the layout. */
|
|
87
|
+
fluid?: boolean
|
|
88
|
+
routeBackInteractionLinkId?: string
|
|
89
|
+
density?: "compact" | "comfortable" | "spacious"
|
|
90
|
+
}>(),
|
|
91
|
+
{
|
|
92
|
+
fluid: true,
|
|
93
|
+
},
|
|
94
|
+
)
|
|
67
95
|
|
|
68
96
|
const emit = defineEmits<{
|
|
69
97
|
(e: "update:modelValue", value: T): void
|
|
@@ -75,6 +103,66 @@ const { t } = useI18n()
|
|
|
75
103
|
const { triggerInteractionLinkById } = useRouteState()
|
|
76
104
|
|
|
77
105
|
const validationErrors = ref<Record<string, string>>({})
|
|
106
|
+
const density = computed(() => props.density ?? "comfortable")
|
|
107
|
+
|
|
108
|
+
const vuetifyDensity = computed(() => {
|
|
109
|
+
if (density.value === "spacious") {
|
|
110
|
+
return "default"
|
|
111
|
+
}
|
|
112
|
+
return density.value
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const horizontalInsetClasses = computed(() => {
|
|
116
|
+
if (density.value === "compact") {
|
|
117
|
+
return "tw:px-3 sm:tw:px-4"
|
|
118
|
+
}
|
|
119
|
+
if (density.value === "spacious") {
|
|
120
|
+
return "tw:px-5 tw:mdpx-6"
|
|
121
|
+
}
|
|
122
|
+
return "tw:px-4 tw:mdpx-5"
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const wrapperClasses = computed(() => [
|
|
126
|
+
"tw:box-border tw:flex tw:min-h-0 tw:w-full tw:flex-col",
|
|
127
|
+
horizontalInsetClasses.value,
|
|
128
|
+
!props.fluid ? "tw:mx-auto tw:max-w-6xl" : "",
|
|
129
|
+
])
|
|
130
|
+
|
|
131
|
+
const titleClasses = computed(() => {
|
|
132
|
+
if (density.value === "compact") {
|
|
133
|
+
return "tw:text-lg"
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (density.value === "spacious") {
|
|
137
|
+
return "tw:text-2xl"
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return "tw:text-xl"
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
const bodyClasses = computed(() => {
|
|
144
|
+
if (density.value === "compact") {
|
|
145
|
+
return "tw:gap-0"
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (density.value === "spacious") {
|
|
149
|
+
return "tw:gap-4"
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return "tw:gap-2"
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
const actionClasses = computed(() => {
|
|
156
|
+
if (density.value === "compact") {
|
|
157
|
+
return "tw:gap-2 tw:py-3"
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (density.value === "spacious") {
|
|
161
|
+
return "tw:gap-4 tw:py-5"
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return "tw:gap-3 tw:py-4"
|
|
165
|
+
})
|
|
78
166
|
|
|
79
167
|
const isValid = computed(() => Object.keys(validationErrors.value).length === 0)
|
|
80
168
|
|
|
@@ -1,11 +1,33 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
<div
|
|
3
|
+
class="tw:grid tw:grid-cols-1 tw:rounded-md tw:p-2 tw:grid-cols-12"
|
|
4
|
+
:class="rowGapClasses"
|
|
5
|
+
>
|
|
6
|
+
<label
|
|
7
|
+
class="tw:col-span-5"
|
|
8
|
+
:class="labelSpacingClasses"
|
|
9
|
+
:for="inputId"
|
|
10
|
+
v-if="hasTitleOrDescription"
|
|
11
|
+
>
|
|
12
|
+
<div
|
|
13
|
+
class="tw:font-semibold tw:leading-5 tw:text-[color:rgb(var(--v-theme-on-surface))]"
|
|
14
|
+
:class="labelTextClasses"
|
|
15
|
+
>
|
|
5
16
|
{{ value.title }}
|
|
6
|
-
<span
|
|
17
|
+
<span
|
|
18
|
+
v-if="value.validation?.required"
|
|
19
|
+
class="tw:ml-0.5 tw:text-[color:rgb(var(--v-theme-error))]"
|
|
20
|
+
aria-hidden="true"
|
|
21
|
+
>
|
|
22
|
+
*
|
|
23
|
+
</span>
|
|
7
24
|
</div>
|
|
8
|
-
<div
|
|
25
|
+
<div
|
|
26
|
+
v-if="value.description"
|
|
27
|
+
:id="descriptionHtmlId ?? undefined"
|
|
28
|
+
class="tw:mt-1 tw:max-w-prose tw:leading-snug tw:text-[color:rgb(var(--v-theme-on-surface))]"
|
|
29
|
+
:class="descriptionTextClasses"
|
|
30
|
+
>
|
|
9
31
|
{{ value.description }}
|
|
10
32
|
</div>
|
|
11
33
|
</label>
|
|
@@ -66,6 +88,7 @@
|
|
|
66
88
|
<RecordInput
|
|
67
89
|
v-model="recordModel"
|
|
68
90
|
:input-id="inputId"
|
|
91
|
+
v-bind="additionalInputBindings"
|
|
69
92
|
@blur="$emit('validate')"
|
|
70
93
|
/>
|
|
71
94
|
</template>
|
|
@@ -87,6 +110,7 @@
|
|
|
87
110
|
v-model="objectArrayModel"
|
|
88
111
|
:fields="value.fields"
|
|
89
112
|
:disabled="!!disabled"
|
|
113
|
+
:density="density"
|
|
90
114
|
@validate="$emit('validate')"
|
|
91
115
|
/>
|
|
92
116
|
</template>
|
|
@@ -239,6 +263,7 @@ const props = defineProps<{
|
|
|
239
263
|
disabled?: boolean
|
|
240
264
|
errorMessage?: string
|
|
241
265
|
hideDetails?: boolean
|
|
266
|
+
density?: "compact" | "comfortable" | "spacious"
|
|
242
267
|
}>()
|
|
243
268
|
|
|
244
269
|
defineEmits<{
|
|
@@ -262,6 +287,69 @@ const id = useId()
|
|
|
262
287
|
const attrs = useAttrs()
|
|
263
288
|
|
|
264
289
|
const inputId = computed(() => `dynamic-input-${id}`)
|
|
290
|
+
const descriptionHtmlId = computed(() =>
|
|
291
|
+
props.value.description ? `${inputId.value}-description` : undefined,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
const density = computed(() => props.density ?? "comfortable")
|
|
295
|
+
|
|
296
|
+
const vuetifyDensity = computed(() => {
|
|
297
|
+
if (density.value === "spacious") {
|
|
298
|
+
return "default"
|
|
299
|
+
}
|
|
300
|
+
return density.value
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
const rowGapClasses = computed(() => {
|
|
304
|
+
if (density.value === "compact") {
|
|
305
|
+
return "tw:gap-3 tw:md:gap-4"
|
|
306
|
+
}
|
|
307
|
+
if (density.value === "spacious") {
|
|
308
|
+
return "tw:gap-6"
|
|
309
|
+
}
|
|
310
|
+
return "tw:gap-4 tw:md:gap-5"
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
const labelSpacingClasses = computed(() => {
|
|
314
|
+
if (density.value === "spacious") {
|
|
315
|
+
return "tw:pt-1"
|
|
316
|
+
}
|
|
317
|
+
return ""
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
const labelTextClasses = computed(() => {
|
|
321
|
+
if (density.value === "compact") {
|
|
322
|
+
return "tw:text-sm"
|
|
323
|
+
}
|
|
324
|
+
if (density.value === "spacious") {
|
|
325
|
+
return "tw:text-base"
|
|
326
|
+
}
|
|
327
|
+
return "tw:text-sm"
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
const descriptionTextClasses = computed(() => {
|
|
331
|
+
if (density.value === "compact") {
|
|
332
|
+
return "tw:text-xs"
|
|
333
|
+
}
|
|
334
|
+
return "tw:text-sm"
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
const accessibilityFieldBindings = computed(() => {
|
|
338
|
+
const o: Record<string, unknown> = {
|
|
339
|
+
color: "primary",
|
|
340
|
+
}
|
|
341
|
+
const descId = descriptionHtmlId.value
|
|
342
|
+
if (descId) {
|
|
343
|
+
o["aria-describedby"] = descId
|
|
344
|
+
}
|
|
345
|
+
if (props.value.validation?.required) {
|
|
346
|
+
o["aria-required"] = true
|
|
347
|
+
}
|
|
348
|
+
if (props.errorMessage) {
|
|
349
|
+
o["aria-invalid"] = true
|
|
350
|
+
}
|
|
351
|
+
return o
|
|
352
|
+
})
|
|
265
353
|
|
|
266
354
|
const createTypedModel = <T extends DynamicModelType>(
|
|
267
355
|
types: DynamicInputType[],
|
|
@@ -333,8 +421,10 @@ const additionalInputBindings = computed(() => {
|
|
|
333
421
|
} = props.value
|
|
334
422
|
|
|
335
423
|
const result: Record<string, unknown> = {
|
|
424
|
+
...accessibilityFieldBindings.value,
|
|
336
425
|
prefix,
|
|
337
426
|
suffix,
|
|
427
|
+
density: vuetifyDensity.value,
|
|
338
428
|
prependIcon: prependIcon ? `mdi-${prependIcon}` : undefined,
|
|
339
429
|
prependInnerIcon: prependInnerIcon ? `mdi-${prependInnerIcon}` : undefined,
|
|
340
430
|
appendIcon: appendIcon ? `mdi-${appendIcon}` : undefined,
|
|
@@ -1,75 +1,110 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="tw:
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
<div class="tw:flex tw:flex-col tw:gap-4">
|
|
3
|
+
<div
|
|
4
|
+
class="tw:rounded-lg tw:border tw:border-solid tw:border-[color:rgb(var(--v-theme-border-default))] tw:bg-[rgb(var(--v-theme-primary)/0.04)] tw:pb-2 tw:pl-2 tw:pr-2 tw:pt-2"
|
|
5
|
+
>
|
|
6
|
+
<v-expansion-panels
|
|
7
|
+
v-model="openPanels"
|
|
8
|
+
class="dynamic-object-list-panels tw:rounded-lg tw:bg-transparent"
|
|
9
|
+
flat
|
|
10
|
+
multiple
|
|
11
|
+
rounded="lg"
|
|
8
12
|
>
|
|
9
|
-
<v-expansion-panel
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
:
|
|
29
|
-
:value="field"
|
|
13
|
+
<v-expansion-panel
|
|
14
|
+
v-for="(item, index) in internalItems"
|
|
15
|
+
:key="index"
|
|
16
|
+
:value="index"
|
|
17
|
+
class="tw:bg-[rgb(var(--v-theme-primary)/0.02)] [&:first-child]:tw:rounded-t-lg [&:last-child]:tw:rounded-b-lg"
|
|
18
|
+
>
|
|
19
|
+
<v-expansion-panel-title
|
|
20
|
+
class="tw:flex tw:min-h-14 tw:items-center tw:gap-2 tw:py-4 tw:text-[color:rgb(var(--v-theme-on-surface))]"
|
|
21
|
+
>
|
|
22
|
+
<span class="tw:flex-1 tw:truncate tw:font-semibold tw:text-sm">
|
|
23
|
+
{{ getPanelTitle(item, index) }}
|
|
24
|
+
</span>
|
|
25
|
+
<v-btn
|
|
26
|
+
icon="mdi-minus"
|
|
27
|
+
size="small"
|
|
28
|
+
variant="text"
|
|
29
|
+
color="error"
|
|
30
|
+
class="tw:shrink-0"
|
|
31
|
+
type="button"
|
|
32
|
+
:density="panelButtonDensity"
|
|
30
33
|
:disabled="disabled"
|
|
31
|
-
|
|
32
|
-
@
|
|
34
|
+
:aria-label="t('workbench.dynamicInputs.objectList.removeEntry')"
|
|
35
|
+
@click.stop="removeItem(index)"
|
|
33
36
|
/>
|
|
34
|
-
</
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
</v-expansion-panel-title>
|
|
38
|
+
|
|
39
|
+
<v-expansion-panel-text
|
|
40
|
+
class="tw:border-t tw:border-solid tw:border-[color:rgb(var(--v-theme-border-default))]"
|
|
41
|
+
>
|
|
42
|
+
<div class="tw:grid tw:gap-4 tw:pb-4 tw:pt-4">
|
|
43
|
+
<DynamicInput
|
|
44
|
+
v-for="field in fields"
|
|
45
|
+
:key="String(field.field)"
|
|
46
|
+
v-model="internalItems[index][field.field]"
|
|
47
|
+
:input-type="field.inputType"
|
|
48
|
+
:value="field"
|
|
49
|
+
:disabled="disabled"
|
|
50
|
+
:density="density"
|
|
51
|
+
@update:model-value="onItemChange(index)"
|
|
52
|
+
@validate="$emit('validate')"
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
</v-expansion-panel-text>
|
|
56
|
+
</v-expansion-panel>
|
|
57
|
+
</v-expansion-panels>
|
|
58
|
+
</div>
|
|
38
59
|
|
|
39
60
|
<v-btn
|
|
40
61
|
v-if="canAddNewItem"
|
|
41
|
-
|
|
42
|
-
|
|
62
|
+
variant="flat"
|
|
63
|
+
color="secondary"
|
|
43
64
|
prepend-icon="mdi-plus"
|
|
65
|
+
class="tw:self-start tw:!rounded-lg"
|
|
66
|
+
size="small"
|
|
67
|
+
type="button"
|
|
68
|
+
:density="panelButtonDensity"
|
|
44
69
|
:disabled="disabled"
|
|
45
70
|
@click="addNewItem"
|
|
46
71
|
>
|
|
47
|
-
|
|
72
|
+
{{ t("workbench.dynamicInputs.objectList.addEntry") }}
|
|
48
73
|
</v-btn>
|
|
49
74
|
</div>
|
|
50
75
|
</template>
|
|
51
76
|
|
|
52
77
|
<script setup lang="ts" generic="T extends Record<string, any>">
|
|
53
|
-
import { ref, watch
|
|
78
|
+
import { computed, ref, watch } from "vue"
|
|
79
|
+
import { useI18n } from "vue-i18n"
|
|
54
80
|
import DynamicInput from "./DynamicInput.vue"
|
|
55
81
|
import type { DynamicFormField } from "./DynamicFormTypes"
|
|
56
82
|
|
|
83
|
+
const { t } = useI18n()
|
|
84
|
+
|
|
57
85
|
const props = defineProps<{
|
|
58
86
|
fields?: DynamicFormField<T>[]
|
|
59
87
|
disabled?: boolean
|
|
88
|
+
density?: "compact" | "comfortable" | "spacious"
|
|
60
89
|
}>()
|
|
61
90
|
|
|
62
91
|
defineEmits<{
|
|
63
92
|
validate: []
|
|
64
93
|
}>()
|
|
65
94
|
|
|
95
|
+
const density = computed(() => props.density ?? "comfortable")
|
|
96
|
+
|
|
97
|
+
const panelButtonDensity = computed(() =>
|
|
98
|
+
density.value === "compact" ? "compact" : "comfortable",
|
|
99
|
+
)
|
|
100
|
+
|
|
66
101
|
const modelValue = defineModel<T[]>({ required: true, default: () => [] })
|
|
67
102
|
|
|
68
103
|
const internalItems = ref<T[]>([])
|
|
69
104
|
const openPanels = ref<number[]>([0])
|
|
70
105
|
|
|
71
106
|
const initializeEmptyItem = (): T => {
|
|
72
|
-
const item:
|
|
107
|
+
const item: Record<string, unknown> = {}
|
|
73
108
|
props.fields?.forEach((field) => {
|
|
74
109
|
const fieldKey = field.field as string
|
|
75
110
|
|
|
@@ -127,7 +162,9 @@ const isItemEmpty = (item: T): boolean => {
|
|
|
127
162
|
|
|
128
163
|
const getPanelTitle = (item: T, index: number): string => {
|
|
129
164
|
if (!props.fields || props.fields.length === 0) {
|
|
130
|
-
return
|
|
165
|
+
return t("workbench.dynamicInputs.objectList.unnamedEntry", {
|
|
166
|
+
n: index + 1,
|
|
167
|
+
})
|
|
131
168
|
}
|
|
132
169
|
|
|
133
170
|
const firstField = props.fields[0]
|
|
@@ -137,7 +174,7 @@ const getPanelTitle = (item: T, index: number): string => {
|
|
|
137
174
|
return String(firstValue)
|
|
138
175
|
}
|
|
139
176
|
|
|
140
|
-
return
|
|
177
|
+
return t("workbench.dynamicInputs.objectList.unnamedEntry", { n: index + 1 })
|
|
141
178
|
}
|
|
142
179
|
|
|
143
180
|
const syncToModelValue = () => {
|
|
@@ -145,7 +182,7 @@ const syncToModelValue = () => {
|
|
|
145
182
|
modelValue.value = nonEmptyItems
|
|
146
183
|
}
|
|
147
184
|
|
|
148
|
-
const onItemChange = (
|
|
185
|
+
const onItemChange = (_index: number) => {
|
|
149
186
|
syncToModelValue()
|
|
150
187
|
}
|
|
151
188
|
|
|
@@ -1,13 +1,53 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
<div
|
|
3
|
+
class="tw:flex tw:w-full tw:items-center"
|
|
4
|
+
:class="containerClasses"
|
|
5
|
+
role="separator"
|
|
6
|
+
:aria-label="label"
|
|
7
|
+
>
|
|
8
|
+
<v-divider
|
|
9
|
+
class="tw:opacity-80"
|
|
10
|
+
color="border-default"
|
|
11
|
+
thickness="1"
|
|
12
|
+
/>
|
|
13
|
+
<div
|
|
14
|
+
class="tw:text-nowrap tw:font-medium tw:tracking-wide tw:text-[color:rgb(var(--v-theme-on-surface))]"
|
|
15
|
+
:class="labelClasses"
|
|
16
|
+
>
|
|
17
|
+
{{ label }}
|
|
18
|
+
</div>
|
|
19
|
+
<v-divider
|
|
20
|
+
class="tw:opacity-80"
|
|
21
|
+
color="border-default"
|
|
22
|
+
thickness="1"
|
|
23
|
+
/>
|
|
6
24
|
</div>
|
|
7
25
|
</template>
|
|
8
26
|
|
|
9
27
|
<script setup lang="ts">
|
|
10
|
-
|
|
11
|
-
|
|
28
|
+
import { computed } from "vue"
|
|
29
|
+
|
|
30
|
+
const props = defineProps<{
|
|
31
|
+
label: string
|
|
32
|
+
density?: "compact" | "comfortable" | "spacious"
|
|
33
|
+
}>()
|
|
34
|
+
|
|
35
|
+
const density = computed(() => props.density ?? "comfortable")
|
|
36
|
+
|
|
37
|
+
const containerClasses = computed(() => {
|
|
38
|
+
if (density.value === "compact") {
|
|
39
|
+
return "tw:gap-2 tw:py-2"
|
|
40
|
+
}
|
|
41
|
+
if (density.value === "spacious") {
|
|
42
|
+
return "tw:gap-4 tw:py-4"
|
|
43
|
+
}
|
|
44
|
+
return "tw:gap-3 tw:py-3"
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const labelClasses = computed(() => {
|
|
48
|
+
if (density.value === "compact") {
|
|
49
|
+
return "tw:text-xs"
|
|
50
|
+
}
|
|
51
|
+
return "tw:text-sm"
|
|
12
52
|
})
|
|
13
53
|
</script>
|