@raclettejs/workbench 0.1.23 → 0.1.24
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 +515 -0
- package/.raclette/backend/yarn.lock +3708 -0
- package/.raclette/frontend/raclette.config.js +45 -0
- package/.raclette/frontend/yarn.lock +3320 -0
- package/.raclette/virtual/backend/README.md +172 -0
- package/.raclette/virtual/backend/raclette.config.js +515 -0
- package/.raclette/virtual/backend/yarn.lock +3708 -0
- package/.raclette/virtual/frontend/README.md +511 -0
- package/.raclette/virtual/frontend/raclette.config.js +45 -0
- package/.raclette/virtual/frontend/src/core/lib/eggs/readme.md +75 -0
- package/.raclette/virtual/frontend/yarn.lock +3320 -0
- package/CHANGELOG.md +16 -0
- package/package.json +26 -23
- package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/CompositionConfiguration.vue +100 -2
- package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/widgetsLayout/WidgetIcon.vue +56 -15
- package/plugins/pacifico__compositions/frontend/components/compositionConfiguration/widgetsLayout/WidgetRenderer.vue +75 -2
- package/services/frontend/src/app/components/dynamicForm/typedInputs/IconInput.vue +5 -5
- package/services/frontend/src/app/components/dynamicForm/typedInputs/TagsInput.vue +12 -7
- package/services/frontend/src/app/composables/useWorkbenchPlugins.ts +92 -8
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
|
|
11
|
+
## [0.1.24] - 2026-04-15 <a href="https://gitlab.com/raclettejs/core-dev/-/compare/v0.1.23...v0.1.24" target="_blank" rel="noopener"><b>Overview of all changes</b></a>
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Composition create flow: pathname is now auto-generated from title using a basic slug strategy (lowercase, spaces to dashes, special characters removed). Sync is create-only and stops once pathname is manually overridden.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Composition widget layout fallbacks improved: missing widget icon/image/color metadata now renders deterministic placeholders (avatar initials and preview fallback card with widget + plugin labels) to support fast widget prototyping without required assets.
|
|
20
|
+
|
|
21
|
+
### Updated
|
|
22
|
+
|
|
23
|
+
- Dependency Versions
|
|
24
|
+
|
|
25
|
+
|
|
10
26
|
## [0.1.23] - 2026-03-11 <a href="https://gitlab.com/raclettejs/core-dev/-/compare/v0.1.21...v0.1.23" target="_blank" rel="noopener"><b>Overview of all changes</b></a>
|
|
11
27
|
|
|
12
28
|
### Fixed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@raclettejs/workbench",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.24",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "racletteJS Workbench - used to configure your application, for dev and prod",
|
|
6
6
|
"repository": "https://gitlab.com/raclettejs/workbench",
|
|
@@ -31,38 +31,41 @@
|
|
|
31
31
|
".": "./index.ts"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@raclettejs/core": "^0.1.
|
|
34
|
+
"@raclettejs/core": "^0.1.24",
|
|
35
35
|
"@vueuse/integrations": "14.2.1",
|
|
36
36
|
"fuse.js": "7.1.0",
|
|
37
|
-
"three": "0.
|
|
37
|
+
"three": "0.183.2",
|
|
38
38
|
"vanta": "0.5.24"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
+
"@emnapi/core": "1.9.2",
|
|
42
|
+
"@emnapi/runtime": "1.9.2",
|
|
41
43
|
"@eslint/js": "9.35.0",
|
|
42
|
-
"@raclettejs/types": "^0.1.
|
|
43
|
-
"@sinclair/typebox": "0.34.
|
|
44
|
+
"@raclettejs/types": "^0.1.24",
|
|
45
|
+
"@sinclair/typebox": "0.34.48",
|
|
44
46
|
"@types/ramda": "0.31.1",
|
|
45
|
-
"@vueuse/core": "
|
|
46
|
-
"@vueuse/rxjs": "
|
|
47
|
-
"axios": "1.
|
|
47
|
+
"@vueuse/core": "14.2.1",
|
|
48
|
+
"@vueuse/rxjs": "14.2.1",
|
|
49
|
+
"axios": "1.13.6",
|
|
48
50
|
"date-fns": "4.1.0",
|
|
49
51
|
"eslint": "9.35.0",
|
|
50
|
-
"eslint-config-prettier": "
|
|
52
|
+
"eslint-config-prettier": "9.1.2",
|
|
51
53
|
"eslint-plugin-import": "2.32.0",
|
|
52
|
-
"eslint-plugin-prefer-arrow-functions": "3.
|
|
53
|
-
"eslint-plugin-prettier": "5.5.
|
|
54
|
-
"eslint-plugin-vue": "
|
|
55
|
-
"fastify": "5.
|
|
56
|
-
"globals": "
|
|
57
|
-
"prettier": "3.
|
|
58
|
-
"ramda": "0.
|
|
59
|
-
"
|
|
60
|
-
"typescript
|
|
54
|
+
"eslint-plugin-prefer-arrow-functions": "3.9.1",
|
|
55
|
+
"eslint-plugin-prettier": "5.5.5",
|
|
56
|
+
"eslint-plugin-vue": "9.33.0",
|
|
57
|
+
"fastify": "5.8.2",
|
|
58
|
+
"globals": "17.4.0",
|
|
59
|
+
"prettier": "3.8.1",
|
|
60
|
+
"ramda": "0.32.0",
|
|
61
|
+
"rxjs": "7.8.2",
|
|
62
|
+
"typescript": "5.9.3",
|
|
63
|
+
"typescript-eslint": "8.57.1",
|
|
61
64
|
"uuid": "13.0.0",
|
|
62
|
-
"vite": "
|
|
63
|
-
"vue": "3.5.
|
|
64
|
-
"vue-eslint-parser": "
|
|
65
|
-
"vue-i18n": "11.
|
|
66
|
-
"vuetify": "
|
|
65
|
+
"vite": "8.0.1",
|
|
66
|
+
"vue": "3.5.30",
|
|
67
|
+
"vue-eslint-parser": "9.4.3",
|
|
68
|
+
"vue-i18n": "11.3.0",
|
|
69
|
+
"vuetify": "4.0.5"
|
|
67
70
|
}
|
|
68
71
|
}
|
|
@@ -46,13 +46,13 @@ import type { CompositionCreate, CompositionUpdate } from "@raclettejs/core"
|
|
|
46
46
|
import DynamicForm from "@app/components/dynamicForm/DynamicForm.vue"
|
|
47
47
|
import type { DynamicFormItem } from "@app/components/dynamicForm/DynamicFormTypes"
|
|
48
48
|
import { useI18n } from "vue-i18n"
|
|
49
|
-
import { computed, useTemplateRef } from "vue"
|
|
49
|
+
import { computed, ref, useTemplateRef, watch } from "vue"
|
|
50
50
|
import CompositionWidgetsLayout from "./CompositionWidgetsLayout.vue"
|
|
51
51
|
import WidgetList from "./widgetsLayout/WidgetList.vue"
|
|
52
52
|
import StepNavigator from "@app/components/stepNavigator/StepNavigator.vue"
|
|
53
53
|
import { Step } from "@app/components/stepNavigator/StepNavigatorTypes"
|
|
54
54
|
|
|
55
|
-
defineProps<{ disabled?: boolean; isEditing?: boolean }>()
|
|
55
|
+
const props = defineProps<{ disabled?: boolean; isEditing?: boolean }>()
|
|
56
56
|
|
|
57
57
|
defineEmits<{
|
|
58
58
|
(e: "save"): void
|
|
@@ -67,6 +67,61 @@ const stepNavigatorRef = useTemplateRef("stepNavigatorRef")
|
|
|
67
67
|
const dynamicFormRef = useTemplateRef("dynamicFormRef")
|
|
68
68
|
|
|
69
69
|
const { t } = useI18n()
|
|
70
|
+
const isUpdatingPathname = ref(false)
|
|
71
|
+
const isPathSyncEnabled = ref(!props.isEditing)
|
|
72
|
+
const lastAutoPathSignature = ref("")
|
|
73
|
+
|
|
74
|
+
const slugifyPath = (value: string): string =>
|
|
75
|
+
value
|
|
76
|
+
.normalize("NFD")
|
|
77
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
78
|
+
.toLowerCase()
|
|
79
|
+
.trim()
|
|
80
|
+
.replace(/[^a-z0-9\s-]/g, "")
|
|
81
|
+
.replace(/[\s_]+/g, "-")
|
|
82
|
+
.replace(/-+/g, "-")
|
|
83
|
+
.replace(/^-|-$/g, "")
|
|
84
|
+
|
|
85
|
+
const getPathSignature = (pathname: unknown): string => JSON.stringify(pathname)
|
|
86
|
+
|
|
87
|
+
const isPathnameEmpty = (pathname: unknown): boolean => {
|
|
88
|
+
if (!pathname) {
|
|
89
|
+
return true
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (typeof pathname === "string") {
|
|
93
|
+
return pathname.trim().length === 0
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (typeof pathname === "object") {
|
|
97
|
+
return Object.values(pathname as Record<string, unknown>).every(
|
|
98
|
+
(value) => typeof value === "string" && value.trim().length === 0,
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return true
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const buildPathnameFromSlug = (
|
|
106
|
+
currentPathname: CompositionCreate["pathname"] | CompositionUpdate["pathname"],
|
|
107
|
+
slug: string,
|
|
108
|
+
) => {
|
|
109
|
+
if (typeof currentPathname === "string") {
|
|
110
|
+
return slug
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (currentPathname && typeof currentPathname === "object") {
|
|
114
|
+
return Object.keys(currentPathname).reduce<Record<string, string>>(
|
|
115
|
+
(acc, locale) => {
|
|
116
|
+
acc[locale] = slug
|
|
117
|
+
return acc
|
|
118
|
+
},
|
|
119
|
+
{},
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return slug
|
|
124
|
+
}
|
|
70
125
|
|
|
71
126
|
const inputFields = computed<
|
|
72
127
|
DynamicFormItem<CompositionCreate | CompositionUpdate>[]
|
|
@@ -135,6 +190,49 @@ const validateConfigStep = (): boolean => {
|
|
|
135
190
|
|
|
136
191
|
const onDynamicFormSubmit = () => stepNavigatorRef.value?.nextStep()
|
|
137
192
|
|
|
193
|
+
watch(
|
|
194
|
+
() => composition.value.pathname,
|
|
195
|
+
(pathname) => {
|
|
196
|
+
if (props.isEditing || isUpdatingPathname.value || !isPathSyncEnabled.value) {
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const currentSignature = getPathSignature(pathname)
|
|
201
|
+
const isAutoGeneratedValue =
|
|
202
|
+
lastAutoPathSignature.value.length > 0 &&
|
|
203
|
+
currentSignature === lastAutoPathSignature.value
|
|
204
|
+
|
|
205
|
+
if (!isAutoGeneratedValue && !isPathnameEmpty(pathname)) {
|
|
206
|
+
isPathSyncEnabled.value = false
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
{ deep: true },
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
watch(
|
|
213
|
+
() => composition.value.title,
|
|
214
|
+
(title) => {
|
|
215
|
+
if (props.isEditing || !isPathSyncEnabled.value) {
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const slug = slugifyPath(title || "")
|
|
220
|
+
const nextPath = buildPathnameFromSlug(composition.value.pathname, slug)
|
|
221
|
+
const nextSignature = getPathSignature(nextPath)
|
|
222
|
+
|
|
223
|
+
if (nextSignature === getPathSignature(composition.value.pathname)) {
|
|
224
|
+
lastAutoPathSignature.value = nextSignature
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
isUpdatingPathname.value = true
|
|
229
|
+
composition.value.pathname = nextPath
|
|
230
|
+
lastAutoPathSignature.value = nextSignature
|
|
231
|
+
isUpdatingPathname.value = false
|
|
232
|
+
},
|
|
233
|
+
{ immediate: true },
|
|
234
|
+
)
|
|
235
|
+
|
|
138
236
|
const steps = computed<Step[]>(() => [
|
|
139
237
|
{
|
|
140
238
|
id: "config",
|
|
@@ -4,24 +4,31 @@
|
|
|
4
4
|
<span
|
|
5
5
|
class="tw:flex tw:h-8 tw:w-8 tw:items-center tw:justify-center tw:rounded tw:shadow-none"
|
|
6
6
|
:style="{
|
|
7
|
-
backgroundColor:
|
|
7
|
+
backgroundColor: resolvedColor,
|
|
8
8
|
}"
|
|
9
9
|
>
|
|
10
10
|
<span
|
|
11
11
|
v-if="svgContent"
|
|
12
12
|
class="tw:flex tw:h-6 tw:w-6 tw:items-center tw:justify-center"
|
|
13
13
|
:style="{
|
|
14
|
-
'--highlight-color':
|
|
14
|
+
'--highlight-color': resolvedColor,
|
|
15
15
|
'--base-color': baseColor,
|
|
16
16
|
}"
|
|
17
17
|
v-html="svgContent"
|
|
18
18
|
/>
|
|
19
|
+
<span
|
|
20
|
+
v-else-if="!resolvedIcon"
|
|
21
|
+
class="tw:text-xs tw:font-semibold tw:uppercase tw:text-white"
|
|
22
|
+
>
|
|
23
|
+
{{ initials }}
|
|
24
|
+
</span>
|
|
19
25
|
|
|
20
26
|
<img
|
|
21
27
|
v-else
|
|
22
|
-
:src="
|
|
28
|
+
:src="resolvedIcon"
|
|
23
29
|
:alt="`${module.title} icon`"
|
|
24
30
|
class="tw:size-6"
|
|
31
|
+
@error="onImageError"
|
|
25
32
|
/>
|
|
26
33
|
</span>
|
|
27
34
|
</figure>
|
|
@@ -33,6 +40,10 @@ import { WidgetDeclaration } from "@raclettejs/core"
|
|
|
33
40
|
import { ref, onMounted, watch, computed } from "vue"
|
|
34
41
|
import type { PropType } from "vue"
|
|
35
42
|
import { useTheme } from "vuetify"
|
|
43
|
+
import {
|
|
44
|
+
generateColorFromName,
|
|
45
|
+
getWidgetInitials,
|
|
46
|
+
} from "@raclettejs/core/orchestrator"
|
|
36
47
|
|
|
37
48
|
const props = defineProps({
|
|
38
49
|
module: {
|
|
@@ -44,11 +55,41 @@ const props = defineProps({
|
|
|
44
55
|
const vuetifyTheme = useTheme()
|
|
45
56
|
|
|
46
57
|
const svgContent = ref("")
|
|
58
|
+
const hasImageError = ref(false)
|
|
47
59
|
|
|
48
60
|
const baseColor = computed(() =>
|
|
49
61
|
vuetifyTheme.global.name.value === "dark" ? "#000" : "#fff",
|
|
50
62
|
)
|
|
51
63
|
|
|
64
|
+
const initials = computed(() => {
|
|
65
|
+
return getWidgetInitials(props.module.title || "Widget")
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const resolvedColor = computed(() =>
|
|
69
|
+
props.module.color?.trim()
|
|
70
|
+
? props.module.color
|
|
71
|
+
: generateColorFromName(props.module.title || "widget"),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
const resolvedIcon = computed(() => {
|
|
75
|
+
if (hasImageError.value) {
|
|
76
|
+
return ""
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const icon = props.module.icon
|
|
80
|
+
if (typeof icon !== "string") {
|
|
81
|
+
return ""
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return icon.trim()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const isSvgIcon = computed(
|
|
88
|
+
() =>
|
|
89
|
+
resolvedIcon.value.endsWith(".svg") ||
|
|
90
|
+
resolvedIcon.value.startsWith("data:image/svg+xml"),
|
|
91
|
+
)
|
|
92
|
+
|
|
52
93
|
const loadSvgContent = async (iconPath: string) => {
|
|
53
94
|
if (iconPath.startsWith("data:image/svg+xml;base64,")) {
|
|
54
95
|
const base64Data = iconPath.split(",")[1]
|
|
@@ -72,25 +113,25 @@ const loadSvgContent = async (iconPath: string) => {
|
|
|
72
113
|
}
|
|
73
114
|
|
|
74
115
|
onMounted(() => {
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
props.module.icon.startsWith("data:image/svg+xml")
|
|
78
|
-
) {
|
|
79
|
-
loadSvgContent(props.module.icon)
|
|
116
|
+
if (isSvgIcon.value && resolvedIcon.value) {
|
|
117
|
+
loadSvgContent(resolvedIcon.value)
|
|
80
118
|
}
|
|
81
119
|
})
|
|
82
120
|
|
|
83
121
|
watch(
|
|
84
|
-
() => props.module,
|
|
85
|
-
(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
) {
|
|
90
|
-
loadSvgContent(newModule.icon)
|
|
122
|
+
() => [props.module.icon, props.module.title],
|
|
123
|
+
() => {
|
|
124
|
+
hasImageError.value = false
|
|
125
|
+
if (isSvgIcon.value && resolvedIcon.value) {
|
|
126
|
+
loadSvgContent(resolvedIcon.value)
|
|
91
127
|
} else {
|
|
92
128
|
svgContent.value = ""
|
|
93
129
|
}
|
|
94
130
|
},
|
|
95
131
|
)
|
|
132
|
+
|
|
133
|
+
const onImageError = () => {
|
|
134
|
+
hasImageError.value = true
|
|
135
|
+
svgContent.value = ""
|
|
136
|
+
}
|
|
96
137
|
</script>
|
|
@@ -21,7 +21,27 @@
|
|
|
21
21
|
|
|
22
22
|
<div class="tw:pointer-events-none">
|
|
23
23
|
<div v-if="activeWidget">
|
|
24
|
-
<v-img
|
|
24
|
+
<v-img
|
|
25
|
+
v-if="previewSrc"
|
|
26
|
+
:src="previewSrc"
|
|
27
|
+
cover
|
|
28
|
+
class="tw:min-h-24"
|
|
29
|
+
@error="onPreviewError"
|
|
30
|
+
/>
|
|
31
|
+
<div
|
|
32
|
+
v-else
|
|
33
|
+
class="tw:flex tw:min-h-24 tw:flex-col tw:items-center tw:justify-center tw:gap-1 tw:px-3 tw:py-6 tw:text-center tw:text-white"
|
|
34
|
+
:style="{
|
|
35
|
+
backgroundColor: placeholderColor,
|
|
36
|
+
}"
|
|
37
|
+
>
|
|
38
|
+
<span class="tw:text-base tw:font-semibold">
|
|
39
|
+
{{ placeholderLabel }}
|
|
40
|
+
</span>
|
|
41
|
+
<span class="tw:text-sm tw:font-medium tw:opacity-90">
|
|
42
|
+
{{ pluginLabel }}
|
|
43
|
+
</span>
|
|
44
|
+
</div>
|
|
25
45
|
</div>
|
|
26
46
|
<div v-else class="tw:bg-red-700 tw:text-2xl tw:text-white">
|
|
27
47
|
Module not found: "{{ widgetConfig.widget.name }}"
|
|
@@ -31,12 +51,13 @@
|
|
|
31
51
|
</template>
|
|
32
52
|
|
|
33
53
|
<script setup lang="ts">
|
|
34
|
-
import { computed, type PropType } from "vue"
|
|
54
|
+
import { computed, ref, watch, type PropType } from "vue"
|
|
35
55
|
import DraggableElement from "@raclettejs/core/orchestrator/components/dragAndDrop/DraggableElement.vue"
|
|
36
56
|
import WidgetContextMenu from "./WidgetContextMenu.vue"
|
|
37
57
|
import useWidgets from "@raclettejs/core/orchestrator/composables/useWidgets"
|
|
38
58
|
import { OnResizeFunction } from "@raclettejs/core/orchestrator/composables/useDragAndDrop"
|
|
39
59
|
import { WidgetSlot } from "@raclettejs/core/frontend/store/types"
|
|
60
|
+
import { generateColorFromName } from "@raclettejs/core/orchestrator"
|
|
40
61
|
|
|
41
62
|
const props = defineProps({
|
|
42
63
|
widgetConfig: {
|
|
@@ -53,13 +74,65 @@ const emit = defineEmits<{
|
|
|
53
74
|
}>()
|
|
54
75
|
|
|
55
76
|
const { widgets } = useWidgets()
|
|
77
|
+
const hasPreviewError = ref(false)
|
|
56
78
|
|
|
57
79
|
const activeWidget = computed(() =>
|
|
58
80
|
findWidgetByName(props.widgetConfig.widget.name),
|
|
59
81
|
)
|
|
60
82
|
|
|
83
|
+
const placeholderLabel = computed(
|
|
84
|
+
() => activeWidget.value?.title || props.widgetConfig.widget.name || "Widget",
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
const pluginLabel = computed(() => {
|
|
88
|
+
const pluginKey = activeWidget.value?.pluginKey
|
|
89
|
+
if (!pluginKey) {
|
|
90
|
+
return "Unknown Plugin"
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return pluginKey
|
|
94
|
+
.replace(/__/g, " / ")
|
|
95
|
+
.replace(/_/g, " ")
|
|
96
|
+
.replace(/\b\w/g, (char) => char.toUpperCase())
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const placeholderColor = computed(() => {
|
|
100
|
+
const widgetColor = activeWidget.value?.color?.trim()
|
|
101
|
+
if (widgetColor) {
|
|
102
|
+
return widgetColor
|
|
103
|
+
}
|
|
104
|
+
return generateColorFromName(placeholderLabel.value)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
const previewSrc = computed(() => {
|
|
108
|
+
if (hasPreviewError.value || !activeWidget.value) {
|
|
109
|
+
return ""
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const image = activeWidget.value.images?.find(
|
|
113
|
+
(value: string) => typeof value === "string" && value.trim().length > 0,
|
|
114
|
+
)
|
|
115
|
+
if (image) {
|
|
116
|
+
return image
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const icon = activeWidget.value.icon?.trim()
|
|
120
|
+
return icon || ""
|
|
121
|
+
})
|
|
122
|
+
|
|
61
123
|
const onResize: OnResizeFunction = ({ cols }) => emit("resize", cols)
|
|
62
124
|
|
|
63
125
|
const findWidgetByName = (widgetName?: string) =>
|
|
64
126
|
widgets.value.find((widget) => widget.name === widgetName)
|
|
127
|
+
|
|
128
|
+
const onPreviewError = () => {
|
|
129
|
+
hasPreviewError.value = true
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
watch(
|
|
133
|
+
() => activeWidget.value?.name,
|
|
134
|
+
() => {
|
|
135
|
+
hasPreviewError.value = false
|
|
136
|
+
},
|
|
137
|
+
)
|
|
65
138
|
</script>
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
:menu-props="{ maxHeight: 400 }"
|
|
11
11
|
:virtual-scroll="true"
|
|
12
12
|
>
|
|
13
|
-
<template #item="{ props,
|
|
14
|
-
<v-list-item v-bind="props" :prepend-icon="`mdi-${
|
|
13
|
+
<template #item="{ props, internalItem }">
|
|
14
|
+
<v-list-item v-bind="props" :prepend-icon="`mdi-${internalItem.raw}`" />
|
|
15
15
|
</template>
|
|
16
16
|
|
|
17
|
-
<template #selection="{
|
|
18
|
-
<v-icon :icon="`mdi-${
|
|
19
|
-
{{
|
|
17
|
+
<template #selection="{ internalItem }">
|
|
18
|
+
<v-icon :icon="`mdi-${internalItem.value}`" class="me-2" />
|
|
19
|
+
{{ internalItem.value }}
|
|
20
20
|
</template>
|
|
21
21
|
</v-autocomplete>
|
|
22
22
|
</template>
|
|
@@ -12,22 +12,27 @@
|
|
|
12
12
|
variant="outlined"
|
|
13
13
|
chips
|
|
14
14
|
>
|
|
15
|
-
<template #chip="{
|
|
15
|
+
<template #chip="{ internalItem }">
|
|
16
16
|
<v-chip
|
|
17
|
-
:color="
|
|
17
|
+
:color="internalItem.raw.color"
|
|
18
18
|
label
|
|
19
19
|
variant="outlined"
|
|
20
20
|
closable
|
|
21
|
-
@click:close="removeItem(
|
|
21
|
+
@click:close="removeItem(internalItem.value)"
|
|
22
22
|
>
|
|
23
|
-
{{
|
|
23
|
+
{{ internalItem.title }}
|
|
24
24
|
</v-chip>
|
|
25
25
|
</template>
|
|
26
26
|
|
|
27
|
-
<template #item="{ props,
|
|
27
|
+
<template #item="{ props, internalItem }">
|
|
28
28
|
<v-list-item v-bind="props" title="">
|
|
29
|
-
<v-chip
|
|
30
|
-
|
|
29
|
+
<v-chip
|
|
30
|
+
:color="internalItem.raw.color"
|
|
31
|
+
label
|
|
32
|
+
variant="outlined"
|
|
33
|
+
size="small"
|
|
34
|
+
>
|
|
35
|
+
{{ internalItem.title }}
|
|
31
36
|
</v-chip>
|
|
32
37
|
</v-list-item>
|
|
33
38
|
</template>
|
|
@@ -7,6 +7,10 @@ import { computed } from "vue"
|
|
|
7
7
|
|
|
8
8
|
const PLUGIN_SETTINGS_WIDGET_NAME = "PluginSettings"
|
|
9
9
|
|
|
10
|
+
type WorkbenchPluginListItem = PluginMetadata & {
|
|
11
|
+
resolvedFrontendPath: string
|
|
12
|
+
}
|
|
13
|
+
|
|
10
14
|
const PLUGIN_METADATA_FILES: { [key: string]: { default: PluginMetadata } } =
|
|
11
15
|
import.meta.glob<{ default: PluginMetadata }>(
|
|
12
16
|
"@racletteCore/../../external-app-plugins/**/raclette.plugin.ts",
|
|
@@ -15,16 +19,95 @@ const PLUGIN_METADATA_FILES: { [key: string]: { default: PluginMetadata } } =
|
|
|
15
19
|
},
|
|
16
20
|
)
|
|
17
21
|
|
|
22
|
+
// Build-time keys; glob `**` matches both sibling `frontend` and `src/frontend` layouts.
|
|
23
|
+
const externalAppPluginFrontendIndexKeys = new Set(
|
|
24
|
+
Object.keys(
|
|
25
|
+
import.meta.glob("@racletteCore/../../external-app-plugins/**/frontend/index.ts", {
|
|
26
|
+
eager: false,
|
|
27
|
+
}),
|
|
28
|
+
),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
const externalAppPluginWidgetPaths = new Set(
|
|
32
|
+
Object.keys(
|
|
33
|
+
import.meta.glob(
|
|
34
|
+
"@racletteCore/../../external-app-plugins/**/frontend/widgets/**/*Widget.vue",
|
|
35
|
+
{ eager: false },
|
|
36
|
+
),
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
const resolveExternalAppPluginFrontendPath = (pluginPath: string, metadata: PluginMetadata): string => {
|
|
41
|
+
const explicit = metadata.frontendDir?.replace(/^\.\//, "")
|
|
42
|
+
if (explicit) {
|
|
43
|
+
return `${pluginPath}/${explicit}`
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const direct = `${pluginPath}/frontend`
|
|
47
|
+
const underSrc = `${pluginPath}/src/frontend`
|
|
48
|
+
const directIndex = `${direct}/index.ts`
|
|
49
|
+
const srcIndex = `${underSrc}/index.ts`
|
|
50
|
+
|
|
51
|
+
const hasDirect = externalAppPluginFrontendIndexKeys.has(directIndex)
|
|
52
|
+
const hasUnderSrc = externalAppPluginFrontendIndexKeys.has(srcIndex)
|
|
53
|
+
|
|
54
|
+
if (hasDirect && hasUnderSrc) {
|
|
55
|
+
return direct
|
|
56
|
+
}
|
|
57
|
+
if (hasUnderSrc) {
|
|
58
|
+
return underSrc
|
|
59
|
+
}
|
|
60
|
+
return direct
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const findExternalPluginDirByKey = (pluginKey: string): string | null => {
|
|
64
|
+
for (const configPath of Object.keys(PLUGIN_METADATA_FILES)) {
|
|
65
|
+
const meta = PLUGIN_METADATA_FILES[configPath]?.default
|
|
66
|
+
if (!meta) continue
|
|
67
|
+
if (generatePluginKey(meta.name, meta.author) === pluginKey) {
|
|
68
|
+
return configPath.replace(/\/raclette\.plugin\.ts$/, "")
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const externalAppPluginHasSettingsWidget = (pluginKey: string): boolean => {
|
|
75
|
+
const pluginDir = findExternalPluginDirByKey(pluginKey)
|
|
76
|
+
if (!pluginDir) return false
|
|
77
|
+
|
|
78
|
+
const configPath = Object.keys(PLUGIN_METADATA_FILES).find(
|
|
79
|
+
(p) => p.replace(/\/raclette\.plugin\.ts$/, "") === pluginDir,
|
|
80
|
+
)
|
|
81
|
+
const meta = configPath ? PLUGIN_METADATA_FILES[configPath]?.default : undefined
|
|
82
|
+
if (!meta) return false
|
|
83
|
+
|
|
84
|
+
const frontendPath = resolveExternalAppPluginFrontendPath(pluginDir, meta)
|
|
85
|
+
const widgetsPrefix = `${frontendPath}/widgets/`
|
|
86
|
+
|
|
87
|
+
return [...externalAppPluginWidgetPaths].some(
|
|
88
|
+
(p) =>
|
|
89
|
+
p.includes(widgetsPrefix) &&
|
|
90
|
+
p.endsWith(`${PLUGIN_SETTINGS_WIDGET_NAME}Widget.vue`),
|
|
91
|
+
)
|
|
92
|
+
};
|
|
93
|
+
|
|
18
94
|
export default () => {
|
|
19
95
|
const pluginMetadataFiles = computed(() => {
|
|
20
|
-
const pluginFiles:
|
|
21
|
-
forEachObjIndexed(({ default: pluginFile }) => {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
96
|
+
const pluginFiles: WorkbenchPluginListItem[] = []
|
|
97
|
+
forEachObjIndexed(({ default: pluginFile }, configPath) => {
|
|
98
|
+
const pluginPath = String(configPath).replace(
|
|
99
|
+
/\/raclette\.plugin\.ts$/,
|
|
100
|
+
"",
|
|
101
|
+
)
|
|
102
|
+
const resolvedFrontendPath = resolveExternalAppPluginFrontendPath(
|
|
103
|
+
pluginPath,
|
|
104
|
+
pluginFile,
|
|
26
105
|
)
|
|
27
|
-
pluginFiles.push(
|
|
106
|
+
pluginFiles.push({
|
|
107
|
+
...pluginFile,
|
|
108
|
+
pluginKey: generatePluginKey(pluginFile.name, pluginFile.author),
|
|
109
|
+
resolvedFrontendPath,
|
|
110
|
+
})
|
|
28
111
|
}, PLUGIN_METADATA_FILES)
|
|
29
112
|
return pluginFiles
|
|
30
113
|
})
|
|
@@ -38,7 +121,8 @@ export default () => {
|
|
|
38
121
|
}
|
|
39
122
|
|
|
40
123
|
const hasPluginSettingsWidget = (pluginKey: string) =>
|
|
41
|
-
!!registeredPlugins?.[pluginKey]?.widgets?.[PLUGIN_SETTINGS_WIDGET_NAME]
|
|
124
|
+
!!registeredPlugins?.[pluginKey]?.widgets?.[PLUGIN_SETTINGS_WIDGET_NAME] ||
|
|
125
|
+
externalAppPluginHasSettingsWidget(pluginKey)
|
|
42
126
|
|
|
43
127
|
return { pluginMetadataFiles, getWorkbenchPlugin, hasPluginSettingsWidget }
|
|
44
128
|
}
|