@raclettejs/workbench 0.1.22 → 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/CHANGELOG.md CHANGED
@@ -7,6 +7,28 @@ 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
+
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>
27
+
28
+ ### Fixed
29
+ - workbench workspace dependency protocol
30
+
31
+
10
32
  ## [0.1.21] - 2026-03-05 <a href="https://gitlab.com/raclettejs/core-dev/-/compare/v0.1.20...v0.1.21" target="_blank" rel="noopener"><b>Overview of all changes</b></a>
11
33
 
12
34
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raclettejs/workbench",
3
- "version": "0.1.22",
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.19",
34
+ "@raclettejs/core": "^0.1.24",
35
35
  "@vueuse/integrations": "14.2.1",
36
36
  "fuse.js": "7.1.0",
37
- "three": "0.180.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.19",
43
- "@sinclair/typebox": "0.34.41",
44
+ "@raclettejs/types": "^0.1.24",
45
+ "@sinclair/typebox": "0.34.48",
44
46
  "@types/ramda": "0.31.1",
45
- "@vueuse/core": "13.9.0",
46
- "@vueuse/rxjs": "13.9.0",
47
- "axios": "1.12.2",
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": "10.1.8",
52
+ "eslint-config-prettier": "9.1.2",
51
53
  "eslint-plugin-import": "2.32.0",
52
- "eslint-plugin-prefer-arrow-functions": "3.8.1",
53
- "eslint-plugin-prettier": "5.5.4",
54
- "eslint-plugin-vue": "10.4.0",
55
- "fastify": "5.6.0",
56
- "globals": "16.4.0",
57
- "prettier": "3.6.2",
58
- "ramda": "0.31.3",
59
- "typescript": "5.9.2",
60
- "typescript-eslint": "8.44.0",
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": "7.1.5",
63
- "vue": "3.5.21",
64
- "vue-eslint-parser": "10.2.0",
65
- "vue-i18n": "11.1.12",
66
- "vuetify": "3.10.2"
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: module.color,
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': module.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="module.icon"
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
- props.module.icon.endsWith(".svg") ||
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
- (newModule) => {
86
- if (
87
- newModule.icon.endsWith(".svg") ||
88
- newModule.icon.startsWith("data:image/svg+xml")
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 :src="activeWidget?.images[0] || activeWidget.icon" />
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, item }">
14
- <v-list-item v-bind="props" :prepend-icon="`mdi-${item.raw}`" />
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="{ item }">
18
- <v-icon :icon="`mdi-${item.value}`" class="me-2" />
19
- {{ item.value }}
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="{ item }">
15
+ <template #chip="{ internalItem }">
16
16
  <v-chip
17
- :color="item.raw.color"
17
+ :color="internalItem.raw.color"
18
18
  label
19
19
  variant="outlined"
20
20
  closable
21
- @click:close="removeItem(item.value)"
21
+ @click:close="removeItem(internalItem.value)"
22
22
  >
23
- {{ item.title }}
23
+ {{ internalItem.title }}
24
24
  </v-chip>
25
25
  </template>
26
26
 
27
- <template #item="{ props, item }">
27
+ <template #item="{ props, internalItem }">
28
28
  <v-list-item v-bind="props" title="">
29
- <v-chip :color="item.raw.color" label variant="outlined" size="small">
30
- {{ item.title }}
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: PluginMetadata[] = []
21
- forEachObjIndexed(({ default: pluginFile }) => {
22
- const fileContent = pluginFile
23
- fileContent.pluginKey = generatePluginKey(
24
- pluginFile.name,
25
- pluginFile.author,
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(fileContent)
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
  }