@solidxai/core-ui 0.1.9-beta.18 → 0.1.9-beta.19
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/dist/adapters/auth/handleLogout.d.ts +1 -1
- package/dist/adapters/auth/handleLogout.d.ts.map +1 -1
- package/dist/adapters/auth/handleLogout.js +20 -32
- package/dist/adapters/auth/handleLogout.js.map +1 -1
- package/dist/adapters/auth/handleLogout.ts +7 -14
- package/dist/components/core/common/SolidAccountSettings/SolidChangePassword.d.ts.map +1 -1
- package/dist/components/core/common/SolidAccountSettings/SolidChangePassword.js +1 -10
- package/dist/components/core/common/SolidAccountSettings/SolidChangePassword.js.map +1 -1
- package/dist/components/core/common/SolidAccountSettings/SolidChangePassword.tsx +1 -8
- package/dist/components/core/common/SolidAccountSettings/SolidNotifications.d.ts.map +1 -1
- package/dist/components/core/common/SolidAccountSettings/SolidNotifications.js +6 -11
- package/dist/components/core/common/SolidAccountSettings/SolidNotifications.js.map +1 -1
- package/dist/components/core/common/SolidAccountSettings/SolidNotifications.tsx +5 -31
- package/dist/components/core/common/SolidGlobalSearchElement.d.ts.map +1 -1
- package/dist/components/core/common/SolidGlobalSearchElement.js +47 -13
- package/dist/components/core/common/SolidGlobalSearchElement.js.map +1 -1
- package/dist/components/core/common/SolidGlobalSearchElement.tsx +38 -4
- package/dist/components/core/extension/solid-core/metadata/MetadataExplorerFormWidget.d.ts +16 -0
- package/dist/components/core/extension/solid-core/metadata/MetadataExplorerFormWidget.d.ts.map +1 -0
- package/dist/components/core/extension/solid-core/metadata/MetadataExplorerFormWidget.js +31 -0
- package/dist/components/core/extension/solid-core/metadata/MetadataExplorerFormWidget.js.map +1 -0
- package/dist/components/core/extension/solid-core/metadata/MetadataExplorerFormWidget.tsx +43 -0
- package/dist/components/core/extension/solid-core/moduleMetadata/list/ClearModulePackageRuntimeHeaderAction.d.ts +3 -0
- package/dist/components/core/extension/solid-core/moduleMetadata/list/ClearModulePackageRuntimeHeaderAction.d.ts.map +1 -0
- package/dist/components/core/extension/solid-core/moduleMetadata/list/ClearModulePackageRuntimeHeaderAction.js +141 -0
- package/dist/components/core/extension/solid-core/moduleMetadata/list/ClearModulePackageRuntimeHeaderAction.js.map +1 -0
- package/dist/components/core/extension/solid-core/moduleMetadata/list/ClearModulePackageRuntimeHeaderAction.tsx +205 -0
- package/dist/components/core/extension/solid-core/moduleMetadata/list/ExportModulePackageRowAction.d.ts +4 -0
- package/dist/components/core/extension/solid-core/moduleMetadata/list/ExportModulePackageRowAction.d.ts.map +1 -0
- package/dist/components/core/extension/solid-core/moduleMetadata/list/ExportModulePackageRowAction.js +112 -0
- package/dist/components/core/extension/solid-core/moduleMetadata/list/ExportModulePackageRowAction.js.map +1 -0
- package/dist/components/core/extension/solid-core/moduleMetadata/list/ExportModulePackageRowAction.tsx +146 -0
- package/dist/components/core/extension/solid-core/moduleMetadata/list/ModuleImportListHeaderAction.d.ts +11 -0
- package/dist/components/core/extension/solid-core/moduleMetadata/list/ModuleImportListHeaderAction.d.ts.map +1 -0
- package/dist/components/core/extension/solid-core/moduleMetadata/list/ModuleImportListHeaderAction.js +22 -0
- package/dist/components/core/extension/solid-core/moduleMetadata/list/ModuleImportListHeaderAction.js.map +1 -0
- package/dist/components/core/extension/solid-core/moduleMetadata/list/ModuleImportListHeaderAction.tsx +36 -0
- package/dist/components/core/extension/solid-core/moduleMetadata/list/moduleMetadataListOnLoad.d.ts +3 -0
- package/dist/components/core/extension/solid-core/moduleMetadata/list/moduleMetadataListOnLoad.d.ts.map +1 -0
- package/dist/components/core/extension/solid-core/moduleMetadata/list/moduleMetadataListOnLoad.js +133 -0
- package/dist/components/core/extension/solid-core/moduleMetadata/list/moduleMetadataListOnLoad.js.map +1 -0
- package/dist/components/core/extension/solid-core/moduleMetadata/list/moduleMetadataListOnLoad.ts +76 -0
- package/dist/components/core/form/fields/relations/RolesGroupedByModuleWidget.d.ts +12 -0
- package/dist/components/core/form/fields/relations/RolesGroupedByModuleWidget.d.ts.map +1 -0
- package/dist/components/core/form/fields/relations/RolesGroupedByModuleWidget.js +168 -0
- package/dist/components/core/form/fields/relations/RolesGroupedByModuleWidget.js.map +1 -0
- package/dist/components/core/form/fields/relations/RolesGroupedByModuleWidget.tsx +241 -0
- package/dist/components/core/list/SolidEmptyListViewPlaceholder.d.ts.map +1 -1
- package/dist/components/core/list/SolidEmptyListViewPlaceholder.js +3 -0
- package/dist/components/core/list/SolidEmptyListViewPlaceholder.js.map +1 -1
- package/dist/components/core/list/SolidEmptyListViewPlaceholder.tsx +2 -0
- package/dist/components/core/list/SolidListView.d.ts.map +1 -1
- package/dist/components/core/list/SolidListView.js +74 -68
- package/dist/components/core/list/SolidListView.js.map +1 -1
- package/dist/components/core/list/SolidListView.tsx +13 -4
- package/dist/components/core/list/SolidListViewColumn.d.ts.map +1 -1
- package/dist/components/core/list/SolidListViewColumn.js +0 -2
- package/dist/components/core/list/SolidListViewColumn.js.map +1 -1
- package/dist/components/core/list/SolidListViewColumn.tsx +0 -1
- package/dist/components/core/list/SolidListViewConfigure.d.ts.map +1 -1
- package/dist/components/core/list/SolidListViewConfigure.js +10 -9
- package/dist/components/core/list/SolidListViewConfigure.js.map +1 -1
- package/dist/components/core/list/SolidListViewConfigure.tsx +3 -1
- package/dist/components/core/list/SolidListViewHeaderButton.d.ts.map +1 -1
- package/dist/components/core/list/SolidListViewHeaderButton.js +3 -0
- package/dist/components/core/list/SolidListViewHeaderButton.js.map +1 -1
- package/dist/components/core/list/SolidListViewHeaderButton.tsx +2 -0
- package/dist/components/core/list/SolidListViewHeaderContextMenuButton.d.ts.map +1 -1
- package/dist/components/core/list/SolidListViewHeaderContextMenuButton.js +3 -0
- package/dist/components/core/list/SolidListViewHeaderContextMenuButton.js.map +1 -1
- package/dist/components/core/list/SolidListViewHeaderContextMenuButton.tsx +2 -0
- package/dist/components/core/list/SolidListViewRowActionsMenu.d.ts.map +1 -1
- package/dist/components/core/list/SolidListViewRowActionsMenu.js +7 -1
- package/dist/components/core/list/SolidListViewRowActionsMenu.js.map +1 -1
- package/dist/components/core/list/SolidListViewRowActionsMenu.tsx +5 -1
- package/dist/components/core/list/columns/SolidBooleanColumn.d.ts.map +1 -1
- package/dist/components/core/list/columns/SolidBooleanColumn.js +4 -6
- package/dist/components/core/list/columns/SolidBooleanColumn.js.map +1 -1
- package/dist/components/core/list/columns/SolidBooleanColumn.tsx +1 -3
- package/dist/components/core/list/columns/SolidMediaMultipleColumn.d.ts.map +1 -1
- package/dist/components/core/list/columns/SolidMediaMultipleColumn.js +3 -2
- package/dist/components/core/list/columns/SolidMediaMultipleColumn.js.map +1 -1
- package/dist/components/core/list/columns/SolidMediaMultipleColumn.tsx +3 -2
- package/dist/components/core/list/columns/SolidMediaSingleColumn.d.ts.map +1 -1
- package/dist/components/core/list/columns/SolidMediaSingleColumn.js +3 -2
- package/dist/components/core/list/columns/SolidMediaSingleColumn.js.map +1 -1
- package/dist/components/core/list/columns/SolidMediaSingleColumn.tsx +3 -2
- package/dist/components/core/model/CreateModel.css +48 -0
- package/dist/components/core/model/CreateModel.d.ts +1 -0
- package/dist/components/core/model/CreateModel.d.ts.map +1 -1
- package/dist/components/core/model/CreateModel.js +69 -104
- package/dist/components/core/model/CreateModel.js.map +1 -1
- package/dist/components/core/model/CreateModel.tsx +92 -204
- package/dist/components/core/model/FieldMetaData.d.ts +1 -1
- package/dist/components/core/model/FieldMetaData.d.ts.map +1 -1
- package/dist/components/core/model/FieldMetaData.js +6 -22
- package/dist/components/core/model/FieldMetaData.js.map +1 -1
- package/dist/components/core/model/FieldMetaData.tsx +6 -45
- package/dist/components/core/model/FieldMetaDataForm.d.ts.map +1 -1
- package/dist/components/core/model/FieldMetaDataForm.js +196 -222
- package/dist/components/core/model/FieldMetaDataForm.js.map +1 -1
- package/dist/components/core/model/FieldMetaDataForm.tsx +0 -72
- package/dist/components/core/model/ModelMetaData.d.ts.map +1 -1
- package/dist/components/core/model/ModelMetaData.js +7 -7
- package/dist/components/core/model/ModelMetaData.js.map +1 -1
- package/dist/components/core/model/ModelMetaData.tsx +7 -0
- package/dist/components/core/module/CreateModule.css +44 -0
- package/dist/components/core/module/CreateModule.d.ts +1 -0
- package/dist/components/core/module/CreateModule.d.ts.map +1 -1
- package/dist/components/core/module/CreateModule.js +40 -43
- package/dist/components/core/module/CreateModule.js.map +1 -1
- package/dist/components/core/module/CreateModule.tsx +243 -247
- package/dist/components/core/module/ModuleListViewData.d.ts.map +1 -1
- package/dist/components/core/module/ModuleListViewData.js +47 -7
- package/dist/components/core/module/ModuleListViewData.js.map +1 -1
- package/dist/components/core/module/ModuleListViewData.tsx +58 -0
- package/dist/components/core/module/ModuleMetadataExplorer.css +638 -0
- package/dist/components/core/module/ModuleMetadataExplorer.d.ts +15 -0
- package/dist/components/core/module/ModuleMetadataExplorer.d.ts.map +1 -0
- package/dist/components/core/module/ModuleMetadataExplorer.js +666 -0
- package/dist/components/core/module/ModuleMetadataExplorer.js.map +1 -0
- package/dist/components/core/module/ModuleMetadataExplorer.tsx +1000 -0
- package/dist/components/core/module/ModulePackageDialog.css +540 -0
- package/dist/components/core/module/ModulePackageDialog.d.ts +16 -0
- package/dist/components/core/module/ModulePackageDialog.d.ts.map +1 -0
- package/dist/components/core/module/ModulePackageDialog.js +625 -0
- package/dist/components/core/module/ModulePackageDialog.js.map +1 -0
- package/dist/components/core/module/ModulePackageDialog.tsx +906 -0
- package/dist/components/layout/user-profile-menu.d.ts.map +1 -1
- package/dist/components/layout/user-profile-menu.js +57 -33
- package/dist/components/layout/user-profile-menu.js.map +1 -1
- package/dist/components/layout/user-profile-menu.tsx +11 -59
- package/dist/helpers/buttonEnvironment.d.ts +5 -0
- package/dist/helpers/buttonEnvironment.d.ts.map +1 -0
- package/dist/helpers/buttonEnvironment.js +30 -0
- package/dist/helpers/buttonEnvironment.js.map +1 -0
- package/dist/helpers/buttonEnvironment.ts +35 -0
- package/dist/helpers/fileTypeIcon.d.ts +12 -0
- package/dist/helpers/fileTypeIcon.d.ts.map +1 -0
- package/dist/helpers/fileTypeIcon.js +37 -0
- package/dist/helpers/fileTypeIcon.js.map +1 -0
- package/dist/helpers/fileTypeIcon.tsx +65 -0
- package/dist/helpers/registry.d.ts.map +1 -1
- package/dist/helpers/registry.js +10 -0
- package/dist/helpers/registry.js.map +1 -1
- package/dist/helpers/registry.ts +10 -0
- package/dist/helpers/waitForBackendAvailability.d.ts +7 -0
- package/dist/helpers/waitForBackendAvailability.d.ts.map +1 -0
- package/dist/helpers/waitForBackendAvailability.js +86 -0
- package/dist/helpers/waitForBackendAvailability.js.map +1 -0
- package/dist/helpers/waitForBackendAvailability.ts +37 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/index.ts +18 -0
- package/dist/redux/api/moduleApi.d.ts +26 -1
- package/dist/redux/api/moduleApi.d.ts.map +1 -1
- package/dist/redux/api/moduleApi.js +237 -1
- package/dist/redux/api/moduleApi.js.map +1 -1
- package/dist/redux/api/moduleApi.ts +181 -1
- package/dist/redux/store/defaultStoreConfig.d.ts +22 -0
- package/dist/redux/store/defaultStoreConfig.d.ts.map +1 -1
- package/dist/redux/store/solidEntityApiPool.d.ts +1 -0
- package/dist/redux/store/solidEntityApiPool.d.ts.map +1 -1
- package/dist/redux/store/solidEntityApiPool.js +4 -0
- package/dist/redux/store/solidEntityApiPool.js.map +1 -1
- package/dist/redux/store/solidEntityApiPool.ts +4 -0
- package/dist/resources/globals.css +56 -0
- package/dist/resources/shadcn-base.css +5 -0
- package/dist/routes/AppEventListener.d.ts.map +1 -1
- package/dist/routes/AppEventListener.js +58 -8
- package/dist/routes/AppEventListener.js.map +1 -1
- package/dist/routes/AppEventListener.tsx +69 -8
- package/dist/types/index.d.ts +1 -0
- package/dist/types/jsoneditor.d.ts +4 -0
- package/package.json +2 -1
|
@@ -0,0 +1,1000 @@
|
|
|
1
|
+
import { ChevronDown, ChevronRight, DatabaseZap, FileSearch, Save, Search, Settings, Wand2 } from "lucide-react";
|
|
2
|
+
import React, { useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { useDispatch } from "react-redux";
|
|
4
|
+
import {
|
|
5
|
+
useGetModuleMetadataExplorerDocumentQuery,
|
|
6
|
+
useGetModuleMetadataExplorerManifestQuery,
|
|
7
|
+
useSeedModuleMetadataMutation,
|
|
8
|
+
useUpdateModuleMetadataExplorerSectionMutation,
|
|
9
|
+
useValidateModuleMetadataExplorerSectionMutation,
|
|
10
|
+
} from "../../../redux/api/moduleApi";
|
|
11
|
+
import { showToast } from "../../../redux/features/toastSlice";
|
|
12
|
+
import {
|
|
13
|
+
SolidButton,
|
|
14
|
+
SolidDialog,
|
|
15
|
+
SolidDialogBody,
|
|
16
|
+
SolidDialogFooter,
|
|
17
|
+
SolidDialogHeader,
|
|
18
|
+
SolidDialogSeparator,
|
|
19
|
+
SolidDialogTitle,
|
|
20
|
+
SolidDropdownMenu,
|
|
21
|
+
SolidDropdownMenuContent,
|
|
22
|
+
SolidDropdownMenuItem,
|
|
23
|
+
SolidDropdownMenuTrigger,
|
|
24
|
+
SolidInput,
|
|
25
|
+
SolidMessage,
|
|
26
|
+
SolidTag,
|
|
27
|
+
} from "../../shad-cn-ui";
|
|
28
|
+
import "jsoneditor/dist/jsoneditor.css";
|
|
29
|
+
import "./ModuleMetadataExplorer.css";
|
|
30
|
+
|
|
31
|
+
type ModuleMetadataExplorerProps = {
|
|
32
|
+
moduleName?: string;
|
|
33
|
+
moduleId?: number;
|
|
34
|
+
modelSingularName?: string;
|
|
35
|
+
scopedArrayPath?: string;
|
|
36
|
+
scopedItemValue?: string;
|
|
37
|
+
scopedItemField?: string;
|
|
38
|
+
readOnly?: boolean;
|
|
39
|
+
allowSeed?: boolean;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type MetadataIssue = {
|
|
43
|
+
path: string;
|
|
44
|
+
message: string;
|
|
45
|
+
severity: "error" | "warning";
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type ExplorerSectionDefinition = {
|
|
49
|
+
key: string;
|
|
50
|
+
title: string;
|
|
51
|
+
jsonPath: string;
|
|
52
|
+
type: "object" | "array";
|
|
53
|
+
description: string;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
type ExplorerTreeNode = {
|
|
57
|
+
id: string;
|
|
58
|
+
label: string;
|
|
59
|
+
path: string;
|
|
60
|
+
parentPath: string | null;
|
|
61
|
+
sectionKey: string;
|
|
62
|
+
sectionTitle: string;
|
|
63
|
+
children: ExplorerTreeNode[];
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type JsonEditorHandle = {
|
|
67
|
+
format: () => void;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
function extractApiErrorMessage(error: any) {
|
|
71
|
+
const message = error?.data?.message ?? error?.message ?? "Something went wrong";
|
|
72
|
+
return Array.isArray(message) ? message.join(", ") : String(message);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function serializeJson(value: any) {
|
|
76
|
+
try {
|
|
77
|
+
return JSON.stringify(value);
|
|
78
|
+
} catch (_error) {
|
|
79
|
+
return "";
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function deepCloneJson<T>(value: T): T {
|
|
84
|
+
return JSON.parse(JSON.stringify(value));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function humanizeKey(value: string) {
|
|
88
|
+
return value
|
|
89
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
90
|
+
.replace(/[_-]+/g, " ")
|
|
91
|
+
.replace(/\s+/g, " ")
|
|
92
|
+
.trim()
|
|
93
|
+
.replace(/^./, (char) => char.toUpperCase());
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function tokenizePath(path: string) {
|
|
97
|
+
const tokens: Array<string | number> = [];
|
|
98
|
+
const regex = /([^[.\]]+)|\[(\d+)\]/g;
|
|
99
|
+
let match: RegExpExecArray | null;
|
|
100
|
+
while ((match = regex.exec(path)) !== null) {
|
|
101
|
+
if (match[1] !== undefined) {
|
|
102
|
+
tokens.push(match[1]);
|
|
103
|
+
} else if (match[2] !== undefined) {
|
|
104
|
+
tokens.push(Number(match[2]));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return tokens;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function getValueAtJsonPath(source: any, path: string) {
|
|
111
|
+
if (!path) return source;
|
|
112
|
+
return tokenizePath(path).reduce((current, token) => {
|
|
113
|
+
if (current === undefined || current === null) return undefined;
|
|
114
|
+
return current[token as keyof typeof current];
|
|
115
|
+
}, source);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function setValueAtJsonPath(target: any, path: string, value: any) {
|
|
119
|
+
const tokens = tokenizePath(path);
|
|
120
|
+
if (!tokens.length) return;
|
|
121
|
+
|
|
122
|
+
let current = target;
|
|
123
|
+
for (let index = 0; index < tokens.length - 1; index += 1) {
|
|
124
|
+
current = current[tokens[index] as keyof typeof current];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
current[tokens[tokens.length - 1] as keyof typeof current] = value;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function appendPath(basePath: string, key: string) {
|
|
131
|
+
return basePath ? `${basePath}.${key}` : key;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getArrayItemLabel(item: any, index: number, parentKey: string) {
|
|
135
|
+
const labelCandidates = [
|
|
136
|
+
"singularName",
|
|
137
|
+
"name",
|
|
138
|
+
"displayName",
|
|
139
|
+
"pluralName",
|
|
140
|
+
"label",
|
|
141
|
+
"key",
|
|
142
|
+
"title",
|
|
143
|
+
"id",
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
for (const candidate of labelCandidates) {
|
|
147
|
+
const nextValue = item?.[candidate];
|
|
148
|
+
if (nextValue !== undefined && nextValue !== null && `${nextValue}`.trim()) {
|
|
149
|
+
return `${nextValue}`;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return `${humanizeKey(parentKey || "Item")} ${index + 1}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function buildTreeNode(
|
|
157
|
+
value: any,
|
|
158
|
+
options: {
|
|
159
|
+
label: string;
|
|
160
|
+
path: string;
|
|
161
|
+
parentPath: string | null;
|
|
162
|
+
sectionKey: string;
|
|
163
|
+
sectionTitle: string;
|
|
164
|
+
hintKey: string;
|
|
165
|
+
},
|
|
166
|
+
): ExplorerTreeNode {
|
|
167
|
+
const { label, path, parentPath, sectionKey, sectionTitle, hintKey } = options;
|
|
168
|
+
|
|
169
|
+
let children: ExplorerTreeNode[] = [];
|
|
170
|
+
|
|
171
|
+
if (Array.isArray(value)) {
|
|
172
|
+
children = value
|
|
173
|
+
.filter((item) => item !== null && typeof item === "object")
|
|
174
|
+
.map((item, index) => buildTreeNode(item, {
|
|
175
|
+
label: getArrayItemLabel(item, index, hintKey),
|
|
176
|
+
path: `${path}[${index}]`,
|
|
177
|
+
parentPath: path,
|
|
178
|
+
sectionKey,
|
|
179
|
+
sectionTitle,
|
|
180
|
+
hintKey,
|
|
181
|
+
}));
|
|
182
|
+
} else if (value && typeof value === "object") {
|
|
183
|
+
children = Object.entries(value)
|
|
184
|
+
.filter(([, nextValue]) => nextValue !== null && typeof nextValue === "object")
|
|
185
|
+
.map(([key, nextValue]) => buildTreeNode(nextValue, {
|
|
186
|
+
label: humanizeKey(key),
|
|
187
|
+
path: appendPath(path, key),
|
|
188
|
+
parentPath: path,
|
|
189
|
+
sectionKey,
|
|
190
|
+
sectionTitle,
|
|
191
|
+
hintKey: key,
|
|
192
|
+
}));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
id: path,
|
|
197
|
+
label,
|
|
198
|
+
path,
|
|
199
|
+
parentPath,
|
|
200
|
+
sectionKey,
|
|
201
|
+
sectionTitle,
|
|
202
|
+
children,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function buildTreeFromDocument(sections: ExplorerSectionDefinition[], documentValue: any) {
|
|
207
|
+
const nodes = sections
|
|
208
|
+
.map((section) => {
|
|
209
|
+
const sectionValue = getValueAtJsonPath(documentValue, section.jsonPath);
|
|
210
|
+
if (sectionValue === undefined) return null;
|
|
211
|
+
return buildTreeNode(sectionValue, {
|
|
212
|
+
label: section.title,
|
|
213
|
+
path: section.jsonPath,
|
|
214
|
+
parentPath: null,
|
|
215
|
+
sectionKey: section.key,
|
|
216
|
+
sectionTitle: section.title,
|
|
217
|
+
hintKey: section.key,
|
|
218
|
+
});
|
|
219
|
+
})
|
|
220
|
+
.filter(Boolean) as ExplorerTreeNode[];
|
|
221
|
+
|
|
222
|
+
const nodeMap = new Map<string, ExplorerTreeNode>();
|
|
223
|
+
const visit = (nextNodes: ExplorerTreeNode[]) => {
|
|
224
|
+
nextNodes.forEach((node) => {
|
|
225
|
+
nodeMap.set(node.path, node);
|
|
226
|
+
if (node.children.length) visit(node.children);
|
|
227
|
+
});
|
|
228
|
+
};
|
|
229
|
+
visit(nodes);
|
|
230
|
+
|
|
231
|
+
return { nodes, nodeMap };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function filterTreeNodes(nodes: ExplorerTreeNode[], query: string): ExplorerTreeNode[] {
|
|
235
|
+
const needle = query.trim().toLowerCase();
|
|
236
|
+
if (!needle) return nodes;
|
|
237
|
+
|
|
238
|
+
const visit = (node: ExplorerTreeNode): ExplorerTreeNode | null => {
|
|
239
|
+
const filteredChildren = node.children
|
|
240
|
+
.map(visit)
|
|
241
|
+
.filter(Boolean) as ExplorerTreeNode[];
|
|
242
|
+
const matches = node.label.toLowerCase().includes(needle) || node.path.toLowerCase().includes(needle);
|
|
243
|
+
|
|
244
|
+
if (matches || filteredChildren.length) {
|
|
245
|
+
return { ...node, children: filteredChildren };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return null;
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
return nodes.map(visit).filter(Boolean) as ExplorerTreeNode[];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function getAncestorPaths(nodeMap: Map<string, ExplorerTreeNode>, path: string) {
|
|
255
|
+
const paths: string[] = [];
|
|
256
|
+
let current = nodeMap.get(path);
|
|
257
|
+
|
|
258
|
+
while (current?.parentPath) {
|
|
259
|
+
paths.push(current.parentPath);
|
|
260
|
+
current = nodeMap.get(current.parentPath);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return paths;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function getSectionForPath(sections: ExplorerSectionDefinition[], path: string) {
|
|
267
|
+
return sections
|
|
268
|
+
.filter((section) => path === section.jsonPath || path.startsWith(`${section.jsonPath}.`) || path.startsWith(`${section.jsonPath}[`))
|
|
269
|
+
.sort((left, right) => right.jsonPath.length - left.jsonPath.length)[0] ?? null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function filterIssuesForPath(issues: MetadataIssue[], path: string) {
|
|
273
|
+
return issues.filter((issue) => {
|
|
274
|
+
if (!issue.path) return false;
|
|
275
|
+
return issue.path === path || issue.path.startsWith(`${path}.`) || issue.path.startsWith(`${path}[`);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const JsonEditorSurface = React.forwardRef<JsonEditorHandle, {
|
|
280
|
+
value: any;
|
|
281
|
+
resetToken: string;
|
|
282
|
+
readOnly?: boolean;
|
|
283
|
+
onValueChange: (value: any) => void;
|
|
284
|
+
onErrorChange: (message: string | null) => void;
|
|
285
|
+
}>(function JsonEditorSurface({ value, resetToken, readOnly = false, onValueChange, onErrorChange }, ref) {
|
|
286
|
+
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
287
|
+
const editorRef = useRef<any>(null);
|
|
288
|
+
// Keep a ref to the latest value so the async init can seed the editor correctly
|
|
289
|
+
const latestValueRef = useRef<any>(value);
|
|
290
|
+
|
|
291
|
+
// Sync latest value into ref so async editor init can use it
|
|
292
|
+
useEffect(() => { latestValueRef.current = value; }, [value]);
|
|
293
|
+
|
|
294
|
+
const emitState = () => {
|
|
295
|
+
const editor = editorRef.current;
|
|
296
|
+
if (!editor) return;
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
const nextValue = editor.get();
|
|
300
|
+
onValueChange(nextValue);
|
|
301
|
+
onErrorChange(null);
|
|
302
|
+
} catch (error: any) {
|
|
303
|
+
onErrorChange(error?.message ? String(error.message) : "The JSON is currently invalid.");
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
useImperativeHandle(ref, () => ({
|
|
308
|
+
format() {
|
|
309
|
+
try {
|
|
310
|
+
editorRef.current?.format?.();
|
|
311
|
+
} catch (_error) {
|
|
312
|
+
// jsoneditor already surfaces parse states through onErrorChange
|
|
313
|
+
}
|
|
314
|
+
emitState();
|
|
315
|
+
},
|
|
316
|
+
}), []);
|
|
317
|
+
|
|
318
|
+
useEffect(() => {
|
|
319
|
+
if (!containerRef.current || editorRef.current) return;
|
|
320
|
+
|
|
321
|
+
let destroyed = false;
|
|
322
|
+
|
|
323
|
+
// @ts-ignore
|
|
324
|
+
void import("jsoneditor").then((jsonEditorModule: any) => {
|
|
325
|
+
if (destroyed || !containerRef.current || editorRef.current) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const JSONEditor = jsonEditorModule?.default ?? jsonEditorModule;
|
|
330
|
+
editorRef.current = new JSONEditor(containerRef.current, {
|
|
331
|
+
mode: "code",
|
|
332
|
+
modes: ["code"],
|
|
333
|
+
mainMenuBar: true,
|
|
334
|
+
navigationBar: true,
|
|
335
|
+
statusBar: true,
|
|
336
|
+
search: true,
|
|
337
|
+
enableSort: false,
|
|
338
|
+
enableTransform: false,
|
|
339
|
+
onChange: () => emitState(),
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// Seed editor with current value (which may already be set before async init resolved)
|
|
343
|
+
try { editorRef.current.set(latestValueRef.current); } catch (_e) {}
|
|
344
|
+
emitState();
|
|
345
|
+
|
|
346
|
+
if (editorRef.current?.aceEditor?.setReadOnly) {
|
|
347
|
+
editorRef.current.aceEditor.setReadOnly(readOnly);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
return () => {
|
|
352
|
+
destroyed = true;
|
|
353
|
+
editorRef.current?.destroy?.();
|
|
354
|
+
editorRef.current = null;
|
|
355
|
+
};
|
|
356
|
+
}, []);
|
|
357
|
+
|
|
358
|
+
useEffect(() => {
|
|
359
|
+
if (!editorRef.current) return;
|
|
360
|
+
editorRef.current.set(value);
|
|
361
|
+
emitState();
|
|
362
|
+
}, [resetToken, value]);
|
|
363
|
+
|
|
364
|
+
useEffect(() => {
|
|
365
|
+
if (!editorRef.current?.aceEditor?.setReadOnly) return;
|
|
366
|
+
editorRef.current.aceEditor.setReadOnly(readOnly);
|
|
367
|
+
}, [readOnly]);
|
|
368
|
+
|
|
369
|
+
return <div ref={containerRef} className={`solid-module-explorer-editor-host solid-module-explorer-jsoneditor ${readOnly ? "is-readonly" : ""}`} />;
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
function MetadataTree({
|
|
373
|
+
nodes,
|
|
374
|
+
activePath,
|
|
375
|
+
expandedPaths,
|
|
376
|
+
onToggle,
|
|
377
|
+
onSelect,
|
|
378
|
+
depth = 0,
|
|
379
|
+
}: {
|
|
380
|
+
nodes: ExplorerTreeNode[];
|
|
381
|
+
activePath: string;
|
|
382
|
+
expandedPaths: Set<string>;
|
|
383
|
+
onToggle: (path: string) => void;
|
|
384
|
+
onSelect: (path: string) => void;
|
|
385
|
+
depth?: number;
|
|
386
|
+
}) {
|
|
387
|
+
return (
|
|
388
|
+
<>
|
|
389
|
+
{nodes.map((node) => {
|
|
390
|
+
const isExpanded = expandedPaths.has(node.path);
|
|
391
|
+
const hasChildren = node.children.length > 0;
|
|
392
|
+
const isActive = activePath === node.path;
|
|
393
|
+
|
|
394
|
+
return (
|
|
395
|
+
<div key={node.id} className="solid-module-explorer-tree-node">
|
|
396
|
+
<div
|
|
397
|
+
className={`solid-module-explorer-tree-row ${isActive ? "is-active" : ""}`}
|
|
398
|
+
style={{ paddingLeft: `${depth * 0.9}rem` }}
|
|
399
|
+
>
|
|
400
|
+
<button
|
|
401
|
+
type="button"
|
|
402
|
+
className="solid-module-explorer-tree-toggle"
|
|
403
|
+
onClick={() => hasChildren && onToggle(node.path)}
|
|
404
|
+
aria-label={hasChildren ? (isExpanded ? "Collapse node" : "Expand node") : "Leaf node"}
|
|
405
|
+
disabled={!hasChildren}
|
|
406
|
+
>
|
|
407
|
+
{hasChildren ? (isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />) : <span className="solid-module-explorer-tree-dot" />}
|
|
408
|
+
</button>
|
|
409
|
+
<button
|
|
410
|
+
type="button"
|
|
411
|
+
className="solid-module-explorer-tree-label"
|
|
412
|
+
onClick={() => onSelect(node.path)}
|
|
413
|
+
title={node.path}
|
|
414
|
+
>
|
|
415
|
+
{node.label}
|
|
416
|
+
</button>
|
|
417
|
+
</div>
|
|
418
|
+
|
|
419
|
+
{hasChildren && isExpanded && (
|
|
420
|
+
<MetadataTree
|
|
421
|
+
nodes={node.children}
|
|
422
|
+
activePath={activePath}
|
|
423
|
+
expandedPaths={expandedPaths}
|
|
424
|
+
onToggle={onToggle}
|
|
425
|
+
onSelect={onSelect}
|
|
426
|
+
depth={depth + 1}
|
|
427
|
+
/>
|
|
428
|
+
)}
|
|
429
|
+
</div>
|
|
430
|
+
);
|
|
431
|
+
})}
|
|
432
|
+
</>
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export function ModuleMetadataExplorer({
|
|
437
|
+
moduleName,
|
|
438
|
+
moduleId,
|
|
439
|
+
modelSingularName,
|
|
440
|
+
scopedArrayPath,
|
|
441
|
+
scopedItemValue,
|
|
442
|
+
scopedItemField,
|
|
443
|
+
readOnly = false,
|
|
444
|
+
allowSeed = true,
|
|
445
|
+
}: ModuleMetadataExplorerProps) {
|
|
446
|
+
const dispatch = useDispatch();
|
|
447
|
+
const editorRef = useRef<JsonEditorHandle | null>(null);
|
|
448
|
+
const explorerRef = useRef<HTMLDivElement | null>(null);
|
|
449
|
+
const isResizingRef = useRef(false);
|
|
450
|
+
const [activePath, setActivePath] = useState("");
|
|
451
|
+
const [expandedPaths, setExpandedPaths] = useState<Set<string>>(new Set());
|
|
452
|
+
const [treeFilter, setTreeFilter] = useState("");
|
|
453
|
+
const [sidebarWidth, setSidebarWidth] = useState(280);
|
|
454
|
+
const [editorValue, setEditorValue] = useState<any>({});
|
|
455
|
+
const [savedValue, setSavedValue] = useState<any>({});
|
|
456
|
+
const [editorError, setEditorError] = useState<string | null>(null);
|
|
457
|
+
const [editorResetToken, setEditorResetToken] = useState("initial");
|
|
458
|
+
const [validationIssues, setValidationIssues] = useState<MetadataIssue[] | null>(null);
|
|
459
|
+
const [localActionSummary, setLocalActionSummary] = useState("");
|
|
460
|
+
const [isContextDialogOpen, setIsContextDialogOpen] = useState(false);
|
|
461
|
+
const [isSeedDialogOpen, setIsSeedDialogOpen] = useState(false);
|
|
462
|
+
|
|
463
|
+
const {
|
|
464
|
+
data: manifestData,
|
|
465
|
+
error: manifestError,
|
|
466
|
+
refetch: refetchManifest,
|
|
467
|
+
} = useGetModuleMetadataExplorerManifestQuery(moduleName as string, {
|
|
468
|
+
skip: !moduleName,
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
const {
|
|
472
|
+
data: documentData,
|
|
473
|
+
isLoading: isDocumentLoading,
|
|
474
|
+
error: documentError,
|
|
475
|
+
refetch: refetchDocument,
|
|
476
|
+
} = useGetModuleMetadataExplorerDocumentQuery(moduleName as string, {
|
|
477
|
+
skip: !moduleName,
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
const [updateSection, { isLoading: isSavingSection }] = useUpdateModuleMetadataExplorerSectionMutation();
|
|
481
|
+
const [validateSection, { isLoading: isValidatingSection }] = useValidateModuleMetadataExplorerSectionMutation();
|
|
482
|
+
const [seedModuleMetadata, { isLoading: isSeedingModule }] = useSeedModuleMetadataMutation();
|
|
483
|
+
const isReadOnlyExplorer = readOnly || moduleName?.toLowerCase() === "solid-core";
|
|
484
|
+
|
|
485
|
+
const sections = (manifestData?.sections ?? []) as ExplorerSectionDefinition[];
|
|
486
|
+
const fullDocument = documentData?.value ?? {};
|
|
487
|
+
const loadError = !manifestData || !documentData ? (manifestError ?? documentError) : null;
|
|
488
|
+
|
|
489
|
+
const { nodes: treeNodes, nodeMap } = useMemo(
|
|
490
|
+
() => buildTreeFromDocument(sections, fullDocument),
|
|
491
|
+
[sections, fullDocument],
|
|
492
|
+
);
|
|
493
|
+
|
|
494
|
+
const scopedRootPath = useMemo(() => {
|
|
495
|
+
// Scope to a specific model definition
|
|
496
|
+
if (modelSingularName) {
|
|
497
|
+
const models = getValueAtJsonPath(fullDocument, "moduleMetadata.models");
|
|
498
|
+
if (!Array.isArray(models)) return "";
|
|
499
|
+
const modelIndex = models.findIndex((model) => model?.singularName === modelSingularName);
|
|
500
|
+
return modelIndex >= 0 ? `moduleMetadata.models[${modelIndex}]` : "";
|
|
501
|
+
}
|
|
502
|
+
// Scope to a specific item in a root-level array (e.g. views, actions, menus)
|
|
503
|
+
if (scopedArrayPath && scopedItemValue) {
|
|
504
|
+
const arr = getValueAtJsonPath(fullDocument, scopedArrayPath);
|
|
505
|
+
if (!Array.isArray(arr)) return "";
|
|
506
|
+
const matchField = scopedItemField ?? "name";
|
|
507
|
+
const itemIndex = arr.findIndex((item) => item?.[matchField] === scopedItemValue);
|
|
508
|
+
return itemIndex >= 0 ? `${scopedArrayPath}[${itemIndex}]` : "";
|
|
509
|
+
}
|
|
510
|
+
return "";
|
|
511
|
+
}, [fullDocument, modelSingularName, scopedArrayPath, scopedItemValue, scopedItemField]);
|
|
512
|
+
|
|
513
|
+
const scopedTreeNodes = useMemo(() => {
|
|
514
|
+
if (!scopedRootPath) return treeNodes;
|
|
515
|
+
const scopedNode = nodeMap.get(scopedRootPath);
|
|
516
|
+
return scopedNode ? [scopedNode] : [];
|
|
517
|
+
}, [nodeMap, scopedRootPath, treeNodes]);
|
|
518
|
+
|
|
519
|
+
const filteredTreeNodes = useMemo(
|
|
520
|
+
() => filterTreeNodes(scopedTreeNodes, treeFilter),
|
|
521
|
+
[scopedTreeNodes, treeFilter],
|
|
522
|
+
);
|
|
523
|
+
|
|
524
|
+
const visibleExpandedPaths = useMemo(() => {
|
|
525
|
+
if (!treeFilter.trim()) return expandedPaths;
|
|
526
|
+
const nextExpanded = new Set<string>();
|
|
527
|
+
const visit = (nodesToVisit: ExplorerTreeNode[]) => {
|
|
528
|
+
nodesToVisit.forEach((node) => {
|
|
529
|
+
if (node.children.length) {
|
|
530
|
+
nextExpanded.add(node.path);
|
|
531
|
+
visit(node.children);
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
};
|
|
535
|
+
visit(filteredTreeNodes);
|
|
536
|
+
return nextExpanded;
|
|
537
|
+
}, [expandedPaths, filteredTreeNodes, treeFilter]);
|
|
538
|
+
|
|
539
|
+
const activeNode = activePath ? nodeMap.get(activePath) ?? null : null;
|
|
540
|
+
const activeSection = activePath ? getSectionForPath(sections, activePath) : null;
|
|
541
|
+
const activeValidationIssues = filterIssuesForPath(validationIssues ?? [], activePath);
|
|
542
|
+
const savedSerialized = serializeJson(savedValue);
|
|
543
|
+
const editorSerialized = serializeJson(editorValue);
|
|
544
|
+
const isDirty = editorError ? true : savedSerialized !== editorSerialized;
|
|
545
|
+
const hasMutableChanges = !isReadOnlyExplorer && isDirty;
|
|
546
|
+
const canSave = !isReadOnlyExplorer && isDirty && !editorError;
|
|
547
|
+
const hasValidationFailure = Boolean(editorError) || activeValidationIssues.length > 0;
|
|
548
|
+
const validationSummary = editorError
|
|
549
|
+
? editorError
|
|
550
|
+
: activeValidationIssues.length
|
|
551
|
+
? `${activeValidationIssues.length} validation issue${activeValidationIssues.length === 1 ? "" : "s"}: ${activeValidationIssues[0]?.message ?? "Review this JSON before saving."}`
|
|
552
|
+
: "";
|
|
553
|
+
|
|
554
|
+
useEffect(() => {
|
|
555
|
+
setValidationIssues(documentData?.validation?.issues ?? []);
|
|
556
|
+
}, [documentData?.lastModifiedAt]);
|
|
557
|
+
|
|
558
|
+
useEffect(() => {
|
|
559
|
+
const handlePointerMove = (event: PointerEvent) => {
|
|
560
|
+
if (!isResizingRef.current || !explorerRef.current) return;
|
|
561
|
+
const bounds = explorerRef.current.getBoundingClientRect();
|
|
562
|
+
const nextWidth = Math.min(Math.max(event.clientX - bounds.left, 220), 520);
|
|
563
|
+
setSidebarWidth(nextWidth);
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
const handlePointerUp = () => {
|
|
567
|
+
isResizingRef.current = false;
|
|
568
|
+
document.body.style.cursor = "";
|
|
569
|
+
document.body.style.userSelect = "";
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
window.addEventListener("pointermove", handlePointerMove);
|
|
573
|
+
window.addEventListener("pointerup", handlePointerUp);
|
|
574
|
+
|
|
575
|
+
return () => {
|
|
576
|
+
window.removeEventListener("pointermove", handlePointerMove);
|
|
577
|
+
window.removeEventListener("pointerup", handlePointerUp);
|
|
578
|
+
};
|
|
579
|
+
}, []);
|
|
580
|
+
|
|
581
|
+
useEffect(() => {
|
|
582
|
+
if (!scopedTreeNodes.length) return;
|
|
583
|
+
|
|
584
|
+
const isPathInsideScope =
|
|
585
|
+
!scopedRootPath ||
|
|
586
|
+
activePath === scopedRootPath ||
|
|
587
|
+
activePath.startsWith(`${scopedRootPath}.`) ||
|
|
588
|
+
activePath.startsWith(`${scopedRootPath}[`);
|
|
589
|
+
|
|
590
|
+
if (!activePath || !nodeMap.has(activePath) || !isPathInsideScope) {
|
|
591
|
+
const defaultPath = scopedTreeNodes[0].path;
|
|
592
|
+
setActivePath(defaultPath);
|
|
593
|
+
setExpandedPaths(new Set([defaultPath]));
|
|
594
|
+
}
|
|
595
|
+
}, [activePath, nodeMap, scopedRootPath, scopedTreeNodes]);
|
|
596
|
+
|
|
597
|
+
useEffect(() => {
|
|
598
|
+
if (!activePath) return;
|
|
599
|
+
const nextValue = getValueAtJsonPath(fullDocument, activePath);
|
|
600
|
+
if (nextValue === undefined) return;
|
|
601
|
+
setSavedValue(nextValue);
|
|
602
|
+
setEditorValue(nextValue);
|
|
603
|
+
setEditorError(null);
|
|
604
|
+
setEditorResetToken(`${activePath}:${documentData?.lastModifiedAt ?? ""}`);
|
|
605
|
+
}, [activePath, documentData?.lastModifiedAt, fullDocument]);
|
|
606
|
+
|
|
607
|
+
const showErrorToast = (error: any) => {
|
|
608
|
+
dispatch(showToast({
|
|
609
|
+
severity: "error",
|
|
610
|
+
summary: "Metadata Explorer",
|
|
611
|
+
detail: extractApiErrorMessage(error),
|
|
612
|
+
life: 4000,
|
|
613
|
+
}));
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
const toggleExpandedPath = (path: string) => {
|
|
617
|
+
setExpandedPaths((current) => {
|
|
618
|
+
const next = new Set(current);
|
|
619
|
+
if (next.has(path)) {
|
|
620
|
+
next.delete(path);
|
|
621
|
+
} else {
|
|
622
|
+
next.add(path);
|
|
623
|
+
}
|
|
624
|
+
return next;
|
|
625
|
+
});
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
const selectPath = (path: string) => {
|
|
629
|
+
if (path === activePath) return;
|
|
630
|
+
if (!isReadOnlyExplorer && isDirty) {
|
|
631
|
+
const shouldProceed = window.confirm("You have unsaved metadata changes. Switch nodes and discard them?");
|
|
632
|
+
if (!shouldProceed) return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const ancestorPaths = getAncestorPaths(nodeMap, path);
|
|
636
|
+
setExpandedPaths((current) => {
|
|
637
|
+
const next = new Set(current);
|
|
638
|
+
ancestorPaths.forEach((ancestorPath) => next.add(ancestorPath));
|
|
639
|
+
return next;
|
|
640
|
+
});
|
|
641
|
+
setActivePath(path);
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
const handleSidebarResizeStart = () => {
|
|
645
|
+
isResizingRef.current = true;
|
|
646
|
+
document.body.style.cursor = "col-resize";
|
|
647
|
+
document.body.style.userSelect = "none";
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
const handleCollapseAll = () => {
|
|
651
|
+
setExpandedPaths(new Set());
|
|
652
|
+
};
|
|
653
|
+
|
|
654
|
+
const handleFormat = () => {
|
|
655
|
+
editorRef.current?.format();
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
const buildPatchedDocument = () => {
|
|
659
|
+
const nextDocument = deepCloneJson(fullDocument);
|
|
660
|
+
setValueAtJsonPath(nextDocument, activePath, editorValue);
|
|
661
|
+
return nextDocument;
|
|
662
|
+
};
|
|
663
|
+
|
|
664
|
+
const buildPatchedSectionValue = () => {
|
|
665
|
+
if (!activeSection) return undefined;
|
|
666
|
+
return getValueAtJsonPath(buildPatchedDocument(), activeSection.jsonPath);
|
|
667
|
+
};
|
|
668
|
+
|
|
669
|
+
const handleValidate = async () => {
|
|
670
|
+
if (!moduleName || !activePath || !activeSection) return;
|
|
671
|
+
|
|
672
|
+
if (editorError) {
|
|
673
|
+
setLocalActionSummary("Validation blocked because the JSON is currently invalid.");
|
|
674
|
+
dispatch(showToast({
|
|
675
|
+
severity: "error",
|
|
676
|
+
summary: "Metadata Explorer",
|
|
677
|
+
detail: "Fix the invalid JSON before validating.",
|
|
678
|
+
life: 3500,
|
|
679
|
+
}));
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
try {
|
|
684
|
+
const response: any = await validateSection({
|
|
685
|
+
moduleName,
|
|
686
|
+
sectionKey: activeSection.key,
|
|
687
|
+
value: buildPatchedSectionValue(),
|
|
688
|
+
}).unwrap();
|
|
689
|
+
|
|
690
|
+
setValidationIssues(response?.issues ?? []);
|
|
691
|
+
setLocalActionSummary(response?.valid ? "Validation passed for the current JSON fragment." : "Validation reported issues for the current JSON fragment.");
|
|
692
|
+
dispatch(showToast({
|
|
693
|
+
severity: response?.valid ? "success" : "warn",
|
|
694
|
+
summary: "Metadata Explorer",
|
|
695
|
+
detail: response?.valid ? "Validation passed." : "Validation reported issues.",
|
|
696
|
+
life: 3000,
|
|
697
|
+
}));
|
|
698
|
+
} catch (error) {
|
|
699
|
+
showErrorToast(error);
|
|
700
|
+
}
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
const handleSave = async () => {
|
|
704
|
+
if (!moduleName || !activePath || !activeSection) return;
|
|
705
|
+
|
|
706
|
+
if (editorError) {
|
|
707
|
+
dispatch(showToast({
|
|
708
|
+
severity: "error",
|
|
709
|
+
summary: "Metadata Explorer",
|
|
710
|
+
detail: "Fix the invalid JSON before saving.",
|
|
711
|
+
life: 3500,
|
|
712
|
+
}));
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
try {
|
|
717
|
+
const response: any = await updateSection({
|
|
718
|
+
moduleName,
|
|
719
|
+
sectionKey: activeSection.key,
|
|
720
|
+
value: buildPatchedSectionValue(),
|
|
721
|
+
}).unwrap();
|
|
722
|
+
|
|
723
|
+
setValidationIssues(response?.validation?.issues ?? []);
|
|
724
|
+
setLocalActionSummary("JSON fragment saved to the metadata file.");
|
|
725
|
+
dispatch(showToast({
|
|
726
|
+
severity: "success",
|
|
727
|
+
summary: "Metadata Explorer",
|
|
728
|
+
detail: "Metadata saved successfully.",
|
|
729
|
+
life: 3000,
|
|
730
|
+
}));
|
|
731
|
+
refetchManifest();
|
|
732
|
+
refetchDocument();
|
|
733
|
+
} catch (error) {
|
|
734
|
+
showErrorToast(error);
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
const handleSeedModule = async () => {
|
|
739
|
+
if (!moduleId) return;
|
|
740
|
+
|
|
741
|
+
try {
|
|
742
|
+
const response: any = await seedModuleMetadata({ id: moduleId }).unwrap();
|
|
743
|
+
setIsSeedDialogOpen(false);
|
|
744
|
+
setLocalActionSummary(response?.message ?? `Seeded metadata for module ${moduleName}.`);
|
|
745
|
+
dispatch(showToast({
|
|
746
|
+
severity: "success",
|
|
747
|
+
summary: "Metadata Explorer",
|
|
748
|
+
detail: response?.message ?? "Module metadata seeded successfully.",
|
|
749
|
+
life: 3000,
|
|
750
|
+
}));
|
|
751
|
+
refetchManifest();
|
|
752
|
+
refetchDocument();
|
|
753
|
+
} catch (error) {
|
|
754
|
+
showErrorToast(error);
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
if (!moduleName) {
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return (
|
|
763
|
+
<div className="solid-module-explorer">
|
|
764
|
+
<div className="solid-module-explorer-panel">
|
|
765
|
+
{loadError && (
|
|
766
|
+
<div className="solid-module-explorer-alert">
|
|
767
|
+
<SolidMessage
|
|
768
|
+
severity="error"
|
|
769
|
+
text={`Unable to load metadata explorer: ${extractApiErrorMessage(loadError)}`}
|
|
770
|
+
className="justify-content-start"
|
|
771
|
+
/>
|
|
772
|
+
</div>
|
|
773
|
+
)}
|
|
774
|
+
|
|
775
|
+
<div
|
|
776
|
+
ref={explorerRef}
|
|
777
|
+
className="solid-module-explorer-workspace"
|
|
778
|
+
style={{ gridTemplateColumns: `${sidebarWidth}px 8px minmax(0, 1fr)` }}
|
|
779
|
+
>
|
|
780
|
+
<aside className="solid-module-explorer-sidebar">
|
|
781
|
+
<div className="solid-module-explorer-sidebar-section solid-module-explorer-sidebar-sections">
|
|
782
|
+
<div className="solid-module-explorer-sidebar-heading">
|
|
783
|
+
<div className="solid-module-explorer-sidebar-heading-main">
|
|
784
|
+
<div className="solid-module-explorer-sidebar-title">{modelSingularName ? "Explorer" : "Sections"}</div>
|
|
785
|
+
<SolidTag tone="info">{modelSingularName ? scopedTreeNodes.length : sections.length}</SolidTag>
|
|
786
|
+
</div>
|
|
787
|
+
<button
|
|
788
|
+
type="button"
|
|
789
|
+
className="solid-module-explorer-sidebar-action"
|
|
790
|
+
onClick={handleCollapseAll}
|
|
791
|
+
>
|
|
792
|
+
Collapse all
|
|
793
|
+
</button>
|
|
794
|
+
</div>
|
|
795
|
+
|
|
796
|
+
<div className="solid-module-explorer-sidebar-search">
|
|
797
|
+
<SolidInput
|
|
798
|
+
value={treeFilter}
|
|
799
|
+
onChange={(event) => setTreeFilter(event.target.value)}
|
|
800
|
+
placeholder="Search metadata tree"
|
|
801
|
+
/>
|
|
802
|
+
<div className="solid-module-explorer-sidebar-search-icon">
|
|
803
|
+
<Search size={14} />
|
|
804
|
+
</div>
|
|
805
|
+
</div>
|
|
806
|
+
|
|
807
|
+
<div className="solid-module-explorer-section-list solid-module-explorer-tree-list">
|
|
808
|
+
<MetadataTree
|
|
809
|
+
nodes={filteredTreeNodes}
|
|
810
|
+
activePath={activePath}
|
|
811
|
+
expandedPaths={visibleExpandedPaths}
|
|
812
|
+
onToggle={toggleExpandedPath}
|
|
813
|
+
onSelect={selectPath}
|
|
814
|
+
/>
|
|
815
|
+
</div>
|
|
816
|
+
</div>
|
|
817
|
+
</aside>
|
|
818
|
+
|
|
819
|
+
<div
|
|
820
|
+
className="solid-module-explorer-resizer"
|
|
821
|
+
role="separator"
|
|
822
|
+
aria-orientation="vertical"
|
|
823
|
+
aria-label="Resize metadata navigation panel"
|
|
824
|
+
onPointerDown={handleSidebarResizeStart}
|
|
825
|
+
/>
|
|
826
|
+
|
|
827
|
+
<section className="solid-module-explorer-main">
|
|
828
|
+
<div className="solid-module-explorer-main-header">
|
|
829
|
+
<div className="solid-module-explorer-main-title">
|
|
830
|
+
<div className="solid-module-explorer-main-heading-row">
|
|
831
|
+
<h4>{activeNode?.label ?? activeSection?.title ?? "Metadata"}</h4>
|
|
832
|
+
<p>{activePath || "Choose a metadata node from the tree."}</p>
|
|
833
|
+
</div>
|
|
834
|
+
{hasValidationFailure && (
|
|
835
|
+
<div className="solid-module-explorer-inline-validation" title={validationSummary}>
|
|
836
|
+
{validationSummary}
|
|
837
|
+
</div>
|
|
838
|
+
)}
|
|
839
|
+
{isReadOnlyExplorer && (
|
|
840
|
+
<div className="solid-module-explorer-readonly-note">
|
|
841
|
+
This metadata is browse-only in the explorer.
|
|
842
|
+
</div>
|
|
843
|
+
)}
|
|
844
|
+
</div>
|
|
845
|
+
<div className="solid-module-explorer-main-actions">
|
|
846
|
+
<div className="solid-module-explorer-main-meta">
|
|
847
|
+
{hasMutableChanges && <SolidTag tone="warn">Unsaved changes</SolidTag>}
|
|
848
|
+
{editorError && <SolidTag tone="danger">Invalid JSON</SolidTag>}
|
|
849
|
+
{isReadOnlyExplorer && <SolidTag tone="info">Read only</SolidTag>}
|
|
850
|
+
</div>
|
|
851
|
+
{!isReadOnlyExplorer && (
|
|
852
|
+
<SolidButton
|
|
853
|
+
size="sm"
|
|
854
|
+
leftIcon={<Save size={14} />}
|
|
855
|
+
onClick={handleSave}
|
|
856
|
+
loading={isSavingSection}
|
|
857
|
+
disabled={!canSave}
|
|
858
|
+
>
|
|
859
|
+
Save Section
|
|
860
|
+
</SolidButton>
|
|
861
|
+
)}
|
|
862
|
+
<SolidDropdownMenu>
|
|
863
|
+
<SolidDropdownMenuTrigger asChild>
|
|
864
|
+
<SolidButton
|
|
865
|
+
variant="ghost"
|
|
866
|
+
size="sm"
|
|
867
|
+
className="solid-icon-button"
|
|
868
|
+
aria-label="Explorer actions"
|
|
869
|
+
>
|
|
870
|
+
<Settings size={16} />
|
|
871
|
+
</SolidButton>
|
|
872
|
+
</SolidDropdownMenuTrigger>
|
|
873
|
+
<SolidDropdownMenuContent className="solid-module-explorer-actions-menu">
|
|
874
|
+
{!isReadOnlyExplorer && (
|
|
875
|
+
<SolidDropdownMenuItem onSelect={handleFormat}>
|
|
876
|
+
<div className="solid-module-explorer-actions-item">
|
|
877
|
+
<Wand2 size={14} />
|
|
878
|
+
<span>Format</span>
|
|
879
|
+
</div>
|
|
880
|
+
</SolidDropdownMenuItem>
|
|
881
|
+
)}
|
|
882
|
+
<SolidDropdownMenuItem onSelect={handleValidate} disabled={isValidatingSection}>
|
|
883
|
+
<div className="solid-module-explorer-actions-item">
|
|
884
|
+
<FileSearch size={14} />
|
|
885
|
+
<span>{isValidatingSection ? "Validating..." : "Validate"}</span>
|
|
886
|
+
</div>
|
|
887
|
+
</SolidDropdownMenuItem>
|
|
888
|
+
<SolidDropdownMenuItem onSelect={() => setIsContextDialogOpen(true)}>
|
|
889
|
+
<div className="solid-module-explorer-actions-item">
|
|
890
|
+
<Settings size={14} />
|
|
891
|
+
<span>Context</span>
|
|
892
|
+
</div>
|
|
893
|
+
</SolidDropdownMenuItem>
|
|
894
|
+
{allowSeed && (
|
|
895
|
+
<SolidDropdownMenuItem onSelect={() => setIsSeedDialogOpen(true)} disabled={!moduleId || isSeedingModule}>
|
|
896
|
+
<div className="solid-module-explorer-actions-item">
|
|
897
|
+
<DatabaseZap size={14} />
|
|
898
|
+
<span>{isSeedingModule ? "Seeding..." : "Seed"}</span>
|
|
899
|
+
</div>
|
|
900
|
+
</SolidDropdownMenuItem>
|
|
901
|
+
)}
|
|
902
|
+
</SolidDropdownMenuContent>
|
|
903
|
+
</SolidDropdownMenu>
|
|
904
|
+
</div>
|
|
905
|
+
</div>
|
|
906
|
+
|
|
907
|
+
<div className="solid-module-explorer-editor-wrap">
|
|
908
|
+
<div className="solid-module-explorer-editor-shell">
|
|
909
|
+
{isDocumentLoading ? (
|
|
910
|
+
<div style={{ padding: "1rem" }} className="solid-module-explorer-empty">
|
|
911
|
+
Loading metadata JSON...
|
|
912
|
+
</div>
|
|
913
|
+
) : (
|
|
914
|
+
<JsonEditorSurface
|
|
915
|
+
ref={editorRef}
|
|
916
|
+
value={savedValue}
|
|
917
|
+
resetToken={editorResetToken}
|
|
918
|
+
readOnly={isReadOnlyExplorer}
|
|
919
|
+
onValueChange={setEditorValue}
|
|
920
|
+
onErrorChange={setEditorError}
|
|
921
|
+
/>
|
|
922
|
+
)}
|
|
923
|
+
</div>
|
|
924
|
+
</div>
|
|
925
|
+
</section>
|
|
926
|
+
</div>
|
|
927
|
+
</div>
|
|
928
|
+
|
|
929
|
+
<SolidDialog
|
|
930
|
+
open={isContextDialogOpen}
|
|
931
|
+
onOpenChange={setIsContextDialogOpen}
|
|
932
|
+
header="Context"
|
|
933
|
+
contentClassName="solid-module-explorer-context-dialog"
|
|
934
|
+
>
|
|
935
|
+
<SolidDialogBody>
|
|
936
|
+
<div className="solid-module-explorer-context-dialog-body">
|
|
937
|
+
<div className="solid-module-explorer-context-dialog-header">
|
|
938
|
+
<div className="solid-module-explorer-context-dialog-title">
|
|
939
|
+
{activeNode?.label ?? "Metadata node"}
|
|
940
|
+
</div>
|
|
941
|
+
<div className="solid-module-explorer-context-dialog-meta">
|
|
942
|
+
{activeNode?.sectionTitle && <SolidTag tone="info">{activeNode.sectionTitle}</SolidTag>}
|
|
943
|
+
</div>
|
|
944
|
+
</div>
|
|
945
|
+
|
|
946
|
+
<div className="solid-module-explorer-context-grid">
|
|
947
|
+
<div className="solid-module-explorer-context-row">
|
|
948
|
+
<div className="solid-module-explorer-context-label">JSON Path</div>
|
|
949
|
+
<div className="solid-module-explorer-context-value">{activePath || "-"}</div>
|
|
950
|
+
</div>
|
|
951
|
+
<div className="solid-module-explorer-context-row">
|
|
952
|
+
<div className="solid-module-explorer-context-label">File</div>
|
|
953
|
+
<div className="solid-module-explorer-context-value">{manifestData?.filePath ?? "-"}</div>
|
|
954
|
+
</div>
|
|
955
|
+
{localActionSummary && (
|
|
956
|
+
<div className="solid-module-explorer-context-row">
|
|
957
|
+
<div className="solid-module-explorer-context-label">Last Action</div>
|
|
958
|
+
<div className="solid-module-explorer-context-value">{localActionSummary}</div>
|
|
959
|
+
</div>
|
|
960
|
+
)}
|
|
961
|
+
</div>
|
|
962
|
+
|
|
963
|
+
<div className="solid-module-explorer-context-note">
|
|
964
|
+
Clicking any branch or leaf in the tree opens that exact JSON fragment on the right.
|
|
965
|
+
</div>
|
|
966
|
+
</div>
|
|
967
|
+
</SolidDialogBody>
|
|
968
|
+
</SolidDialog>
|
|
969
|
+
|
|
970
|
+
<SolidDialog
|
|
971
|
+
open={isSeedDialogOpen}
|
|
972
|
+
onOpenChange={setIsSeedDialogOpen}
|
|
973
|
+
contentClassName="solid-module-explorer-seed-dialog"
|
|
974
|
+
>
|
|
975
|
+
<SolidDialogHeader>
|
|
976
|
+
<SolidDialogTitle>Seed Module Metadata</SolidDialogTitle>
|
|
977
|
+
</SolidDialogHeader>
|
|
978
|
+
<SolidDialogSeparator />
|
|
979
|
+
<SolidDialogBody>
|
|
980
|
+
<div className="solid-module-explorer-seed-dialog-body">
|
|
981
|
+
<p>
|
|
982
|
+
Re-seed metadata for <strong>{moduleName}</strong> from its JSON file?
|
|
983
|
+
</p>
|
|
984
|
+
<p className="solid-module-explorer-seed-dialog-note">
|
|
985
|
+
This only re-runs metadata seeding for the current module and refreshes database metadata from the module JSON.
|
|
986
|
+
</p>
|
|
987
|
+
</div>
|
|
988
|
+
</SolidDialogBody>
|
|
989
|
+
<SolidDialogFooter>
|
|
990
|
+
<SolidButton variant="outline" size="sm" onClick={() => setIsSeedDialogOpen(false)} disabled={isSeedingModule}>
|
|
991
|
+
Cancel
|
|
992
|
+
</SolidButton>
|
|
993
|
+
<SolidButton size="sm" onClick={handleSeedModule} loading={isSeedingModule} disabled={!moduleId}>
|
|
994
|
+
Seed
|
|
995
|
+
</SolidButton>
|
|
996
|
+
</SolidDialogFooter>
|
|
997
|
+
</SolidDialog>
|
|
998
|
+
</div>
|
|
999
|
+
);
|
|
1000
|
+
}
|