@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.
Files changed (27) hide show
  1. package/.raclette/backend/raclette.config.js +2 -2
  2. package/.raclette/frontend/raclette.config.js +2 -2
  3. package/.raclette/frontend/yarn.lock +143 -126
  4. package/.raclette/virtual/backend/raclette.config.js +2 -2
  5. package/.raclette/virtual/frontend/raclette.config.js +2 -2
  6. package/.raclette/virtual/frontend/yarn.lock +143 -126
  7. package/CHANGELOG.md +19 -3
  8. package/i18n/de-DE.json +15 -0
  9. package/i18n/en-EU.json +15 -0
  10. package/i18n/sk-SK.json +15 -0
  11. package/package.json +5 -4
  12. package/plugins/pacifico__compositions/frontend/widgets/CompositionListWidget.vue +39 -21
  13. package/plugins/pacifico__interactionLinks/frontend/widgets/InteractionLinkListWidget.vue +39 -21
  14. package/plugins/pacifico__plugins/frontend/index.ts +3 -3
  15. package/plugins/pacifico__plugins/frontend/widgets/PluginListWidget.vue +11 -12
  16. package/plugins/pacifico__tags/frontend/widgets/TagListWidget.vue +39 -21
  17. package/plugins/pacifico__users/frontend/widgets/UserListWidget.vue +79 -57
  18. package/services/frontend/src/app/components/dynamicForm/DynamicForm.vue +139 -51
  19. package/services/frontend/src/app/components/dynamicForm/DynamicInput.vue +95 -5
  20. package/services/frontend/src/app/components/dynamicForm/ObjectListInput.vue +78 -41
  21. package/services/frontend/src/app/components/dynamicForm/TextDivider.vue +46 -6
  22. package/services/frontend/src/app/components/dynamicForm/typedInputs/ListInput.vue +118 -27
  23. package/services/frontend/src/app/components/dynamicForm/typedInputs/RecordInput.vue +131 -21
  24. package/services/frontend/src/app/components/dynamicForm/typedInputs/TagsInput.vue +1 -0
  25. package/services/frontend/src/app/components/dynamicForm/typedInputs/TriggerInput.vue +4 -4
  26. package/services/frontend/src/app/composables/useWorkbenchTableActions.ts +29 -0
  27. package/services/frontend/src/orchestrator/assets/styles/themes/light.ts +4 -3
@@ -1,69 +1,97 @@
1
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
- <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
- <slot name="button-area" v-bind="{ isValid, validateAllFields }" />
41
- <!-- <v-btn color="primary" variant="outlined"> Validate Form </v-btn>
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
- </div>
45
- </slot>
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 = defineProps<{
59
- title?: string
60
- data: DynamicFormItem<T>[]
61
- modelValue: T
62
- disabled?: Boolean
63
- validateOnChange?: boolean
64
- fluid?: boolean
65
- routeBackInteractionLinkId?: string
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 class="tw:grid tw:grid-cols-12 tw:gap-6">
3
- <label class="tw:col-span-5" :for="inputId" v-if="hasTitleOrDescription">
4
- <div class="tw:text-sm tw:font-bold">
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 v-if="value.validation?.required" class="tw:text-red-500">*</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 class="tw:text-xs">
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:grid tw:gap-4">
3
- <v-expansion-panels v-model="openPanels" multiple>
4
- <v-expansion-panel
5
- v-for="(item, index) in internalItems"
6
- :key="index"
7
- :value="index"
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-title class="tw:flex tw:items-center tw:gap-2">
10
- <span class="tw:flex-1">
11
- {{ getPanelTitle(item, index) }}
12
- </span>
13
- <v-btn
14
- icon="mdi-minus"
15
- size="small"
16
- variant="text"
17
- :disabled="disabled"
18
- @click.stop="removeItem(index)"
19
- />
20
- </v-expansion-panel-title>
21
-
22
- <v-expansion-panel-text>
23
- <div class="tw:grid tw:gap-4 tw:pt-4">
24
- <DynamicInput
25
- v-for="field in fields"
26
- :key="field.field as string"
27
- v-model="internalItems[index][field.field]"
28
- :input-type="field.inputType"
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
- @update:model-value="onItemChange(index)"
32
- @validate="$emit('validate')"
34
+ :aria-label="t('workbench.dynamicInputs.objectList.removeEntry')"
35
+ @click.stop="removeItem(index)"
33
36
  />
34
- </div>
35
- </v-expansion-panel-text>
36
- </v-expansion-panel>
37
- </v-expansion-panels>
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
- color="primary"
42
- variant="outlined"
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
- Add Entry
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, computed } from "vue"
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: any = {}
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 `Entry ${index + 1}`
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 `Entry ${index + 1}`
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 = (index: number) => {
185
+ const onItemChange = (_index: number) => {
149
186
  syncToModelValue()
150
187
  }
151
188
 
@@ -1,13 +1,53 @@
1
1
  <template>
2
- <div class="tw:flex tw:w-full tw:items-center tw:gap-2">
3
- <v-divider />
4
- <div class="tw:text-nowrap">{{ label }}</div>
5
- <v-divider />
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
- defineProps({
11
- label: { type: String, required: true },
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>