@fgv/ts-res-ui-components 5.0.0-10
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/.rush/temp/03c8b056281d9db0a97d8a6e25eea798a160d393.tar.log +271 -0
- package/.rush/temp/chunked-rush-logs/ts-res-ui-components.build.chunks.jsonl +9 -0
- package/.rush/temp/operation/build/all.log +9 -0
- package/.rush/temp/operation/build/log-chunks.jsonl +9 -0
- package/.rush/temp/operation/build/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +1111 -0
- package/README.md +18 -0
- package/REFACTORING_PLAN.md +171 -0
- package/config/jest.config.json +16 -0
- package/config/jest.setup.js +64 -0
- package/config/rig.json +16 -0
- package/lib/components/common/QualifierContextControl.d.ts +14 -0
- package/lib/components/common/QualifierContextControl.d.ts.map +1 -0
- package/lib/components/common/QualifierContextControl.js +78 -0
- package/lib/components/common/QualifierContextControl.js.map +1 -0
- package/lib/components/common/ResourceListView.d.ts +11 -0
- package/lib/components/common/ResourceListView.d.ts.map +1 -0
- package/lib/components/common/ResourceListView.js +20 -0
- package/lib/components/common/ResourceListView.js.map +1 -0
- package/lib/components/common/ResourceTreeView.d.ts +12 -0
- package/lib/components/common/ResourceTreeView.d.ts.map +1 -0
- package/lib/components/common/ResourceTreeView.js +162 -0
- package/lib/components/common/ResourceTreeView.js.map +1 -0
- package/lib/components/forms/HierarchyEditor.d.ts +10 -0
- package/lib/components/forms/HierarchyEditor.d.ts.map +1 -0
- package/lib/components/forms/HierarchyEditor.js +106 -0
- package/lib/components/forms/HierarchyEditor.js.map +1 -0
- package/lib/components/forms/QualifierEditForm.d.ts +11 -0
- package/lib/components/forms/QualifierEditForm.d.ts.map +1 -0
- package/lib/components/forms/QualifierEditForm.js +181 -0
- package/lib/components/forms/QualifierEditForm.js.map +1 -0
- package/lib/components/forms/QualifierTypeEditForm.d.ts +10 -0
- package/lib/components/forms/QualifierTypeEditForm.d.ts.map +1 -0
- package/lib/components/forms/QualifierTypeEditForm.js +172 -0
- package/lib/components/forms/QualifierTypeEditForm.js.map +1 -0
- package/lib/components/forms/ResourceTypeEditForm.d.ts +10 -0
- package/lib/components/forms/ResourceTypeEditForm.d.ts.map +1 -0
- package/lib/components/forms/ResourceTypeEditForm.js +188 -0
- package/lib/components/forms/ResourceTypeEditForm.js.map +1 -0
- package/lib/components/forms/index.d.ts +9 -0
- package/lib/components/forms/index.d.ts.map +1 -0
- package/lib/components/forms/index.js +5 -0
- package/lib/components/forms/index.js.map +1 -0
- package/lib/components/orchestrator/ResourceOrchestrator.d.ts +14 -0
- package/lib/components/orchestrator/ResourceOrchestrator.d.ts.map +1 -0
- package/lib/components/orchestrator/ResourceOrchestrator.js +278 -0
- package/lib/components/orchestrator/ResourceOrchestrator.js.map +1 -0
- package/lib/components/views/CompiledView/index.d.ts +5 -0
- package/lib/components/views/CompiledView/index.d.ts.map +1 -0
- package/lib/components/views/CompiledView/index.js +595 -0
- package/lib/components/views/CompiledView/index.js.map +1 -0
- package/lib/components/views/ConfigurationView/index.d.ts +5 -0
- package/lib/components/views/ConfigurationView/index.d.ts.map +1 -0
- package/lib/components/views/ConfigurationView/index.js +363 -0
- package/lib/components/views/ConfigurationView/index.js.map +1 -0
- package/lib/components/views/FilterView/index.d.ts +5 -0
- package/lib/components/views/FilterView/index.d.ts.map +1 -0
- package/lib/components/views/FilterView/index.js +463 -0
- package/lib/components/views/FilterView/index.js.map +1 -0
- package/lib/components/views/ImportView/index.d.ts +5 -0
- package/lib/components/views/ImportView/index.d.ts.map +1 -0
- package/lib/components/views/ImportView/index.js +514 -0
- package/lib/components/views/ImportView/index.js.map +1 -0
- package/lib/components/views/ResolutionView/EditableJsonView.d.ts +21 -0
- package/lib/components/views/ResolutionView/EditableJsonView.d.ts.map +1 -0
- package/lib/components/views/ResolutionView/EditableJsonView.js +109 -0
- package/lib/components/views/ResolutionView/EditableJsonView.js.map +1 -0
- package/lib/components/views/ResolutionView/ResolutionEditControls.d.ts +19 -0
- package/lib/components/views/ResolutionView/ResolutionEditControls.d.ts.map +1 -0
- package/lib/components/views/ResolutionView/ResolutionEditControls.js +82 -0
- package/lib/components/views/ResolutionView/ResolutionEditControls.js.map +1 -0
- package/lib/components/views/ResolutionView/index.d.ts +5 -0
- package/lib/components/views/ResolutionView/index.d.ts.map +1 -0
- package/lib/components/views/ResolutionView/index.js +255 -0
- package/lib/components/views/ResolutionView/index.js.map +1 -0
- package/lib/components/views/SourceView/index.d.ts +5 -0
- package/lib/components/views/SourceView/index.d.ts.map +1 -0
- package/lib/components/views/SourceView/index.js +316 -0
- package/lib/components/views/SourceView/index.js.map +1 -0
- package/lib/components/views/ZipLoaderView/index.d.ts +5 -0
- package/lib/components/views/ZipLoaderView/index.d.ts.map +1 -0
- package/lib/components/views/ZipLoaderView/index.js +313 -0
- package/lib/components/views/ZipLoaderView/index.js.map +1 -0
- package/lib/hooks/useConfigurationState.d.ts +46 -0
- package/lib/hooks/useConfigurationState.d.ts.map +1 -0
- package/lib/hooks/useConfigurationState.js +239 -0
- package/lib/hooks/useConfigurationState.js.map +1 -0
- package/lib/hooks/useFilterState.d.ts +7 -0
- package/lib/hooks/useFilterState.d.ts.map +1 -0
- package/lib/hooks/useFilterState.js +80 -0
- package/lib/hooks/useFilterState.js.map +1 -0
- package/lib/hooks/useResolutionState.d.ts +8 -0
- package/lib/hooks/useResolutionState.d.ts.map +1 -0
- package/lib/hooks/useResolutionState.js +253 -0
- package/lib/hooks/useResolutionState.js.map +1 -0
- package/lib/hooks/useResourceData.d.ts +19 -0
- package/lib/hooks/useResourceData.d.ts.map +1 -0
- package/lib/hooks/useResourceData.js +368 -0
- package/lib/hooks/useResourceData.js.map +1 -0
- package/lib/hooks/useViewState.d.ts +10 -0
- package/lib/hooks/useViewState.d.ts.map +1 -0
- package/lib/hooks/useViewState.js +29 -0
- package/lib/hooks/useViewState.js.map +1 -0
- package/lib/index.d.ts +27 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +34 -0
- package/lib/index.js.map +1 -0
- package/lib/test/helpers/testDataLoader.d.ts +37 -0
- package/lib/test/helpers/testDataLoader.d.ts.map +1 -0
- package/lib/test/helpers/testDataLoader.js +171 -0
- package/lib/test/helpers/testDataLoader.js.map +1 -0
- package/lib/test/unit/utils/configurationUtils.test.d.ts +2 -0
- package/lib/test/unit/utils/configurationUtils.test.d.ts.map +1 -0
- package/lib/test/unit/utils/configurationUtils.test.js +497 -0
- package/lib/test/unit/utils/configurationUtils.test.js.map +1 -0
- package/lib/test/unit/utils/fileProcessing.test.d.ts +2 -0
- package/lib/test/unit/utils/fileProcessing.test.d.ts.map +1 -0
- package/lib/test/unit/utils/fileProcessing.test.js +321 -0
- package/lib/test/unit/utils/fileProcessing.test.js.map +1 -0
- package/lib/test/unit/utils/filterResources.test.d.ts +2 -0
- package/lib/test/unit/utils/filterResources.test.d.ts.map +1 -0
- package/lib/test/unit/utils/filterResources.test.js +403 -0
- package/lib/test/unit/utils/filterResources.test.js.map +1 -0
- package/lib/test/unit/utils/resolutionEditing.test.d.ts +2 -0
- package/lib/test/unit/utils/resolutionEditing.test.d.ts.map +1 -0
- package/lib/test/unit/utils/resolutionEditing.test.js +439 -0
- package/lib/test/unit/utils/resolutionEditing.test.js.map +1 -0
- package/lib/test/unit/utils/resolutionUtils.test.d.ts +2 -0
- package/lib/test/unit/utils/resolutionUtils.test.d.ts.map +1 -0
- package/lib/test/unit/utils/resolutionUtils.test.js +397 -0
- package/lib/test/unit/utils/resolutionUtils.test.js.map +1 -0
- package/lib/test/unit/utils/tsResIntegration.test.d.ts +2 -0
- package/lib/test/unit/utils/tsResIntegration.test.d.ts.map +1 -0
- package/lib/test/unit/utils/tsResIntegration.test.js +376 -0
- package/lib/test/unit/utils/tsResIntegration.test.js.map +1 -0
- package/lib/types/index.d.ts +251 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +2 -0
- package/lib/types/index.js.map +1 -0
- package/lib/utils/configurationUtils.d.ts +74 -0
- package/lib/utils/configurationUtils.d.ts.map +1 -0
- package/lib/utils/configurationUtils.js +359 -0
- package/lib/utils/configurationUtils.js.map +1 -0
- package/lib/utils/fileProcessing.d.ts +18 -0
- package/lib/utils/fileProcessing.d.ts.map +1 -0
- package/lib/utils/fileProcessing.js +142 -0
- package/lib/utils/fileProcessing.js.map +1 -0
- package/lib/utils/filterResources.d.ts +38 -0
- package/lib/utils/filterResources.d.ts.map +1 -0
- package/lib/utils/filterResources.js +153 -0
- package/lib/utils/filterResources.js.map +1 -0
- package/lib/utils/resolutionEditing.d.ts +58 -0
- package/lib/utils/resolutionEditing.d.ts.map +1 -0
- package/lib/utils/resolutionEditing.js +246 -0
- package/lib/utils/resolutionEditing.js.map +1 -0
- package/lib/utils/resolutionUtils.d.ts +28 -0
- package/lib/utils/resolutionUtils.d.ts.map +1 -0
- package/lib/utils/resolutionUtils.js +216 -0
- package/lib/utils/resolutionUtils.js.map +1 -0
- package/lib/utils/tsResIntegration.d.ts +71 -0
- package/lib/utils/tsResIntegration.d.ts.map +1 -0
- package/lib/utils/tsResIntegration.js +294 -0
- package/lib/utils/tsResIntegration.js.map +1 -0
- package/lib/utils/zipLoader/browserZipLoader.d.ts +48 -0
- package/lib/utils/zipLoader/browserZipLoader.d.ts.map +1 -0
- package/lib/utils/zipLoader/browserZipLoader.js +247 -0
- package/lib/utils/zipLoader/browserZipLoader.js.map +1 -0
- package/lib/utils/zipLoader/index.d.ts +8 -0
- package/lib/utils/zipLoader/index.d.ts.map +1 -0
- package/lib/utils/zipLoader/index.js +13 -0
- package/lib/utils/zipLoader/index.js.map +1 -0
- package/lib/utils/zipLoader/nodeZipBuilder.d.ts +55 -0
- package/lib/utils/zipLoader/nodeZipBuilder.d.ts.map +1 -0
- package/lib/utils/zipLoader/nodeZipBuilder.js +98 -0
- package/lib/utils/zipLoader/nodeZipBuilder.js.map +1 -0
- package/lib/utils/zipLoader/types.d.ts +139 -0
- package/lib/utils/zipLoader/types.d.ts.map +1 -0
- package/lib/utils/zipLoader/types.js +2 -0
- package/lib/utils/zipLoader/types.js.map +1 -0
- package/lib/utils/zipLoader/zipUtils.d.ts +53 -0
- package/lib/utils/zipLoader/zipUtils.d.ts.map +1 -0
- package/lib/utils/zipLoader/zipUtils.js +229 -0
- package/lib/utils/zipLoader/zipUtils.js.map +1 -0
- package/package.json +69 -0
- package/rush-logs/ts-res-ui-components.build.cache.log +3 -0
- package/rush-logs/ts-res-ui-components.build.log +9 -0
- package/src/components/common/QualifierContextControl.tsx +151 -0
- package/src/components/common/ResourceListView.tsx +63 -0
- package/src/components/common/ResourceTreeView.tsx +271 -0
- package/src/components/forms/HierarchyEditor.tsx +204 -0
- package/src/components/forms/QualifierEditForm.tsx +355 -0
- package/src/components/forms/QualifierTypeEditForm.tsx +347 -0
- package/src/components/forms/ResourceTypeEditForm.tsx +331 -0
- package/src/components/forms/index.ts +11 -0
- package/src/components/orchestrator/ResourceOrchestrator.tsx +372 -0
- package/src/components/views/CompiledView/index.tsx +922 -0
- package/src/components/views/ConfigurationView/index.tsx +800 -0
- package/src/components/views/FilterView/index.tsx +825 -0
- package/src/components/views/ImportView/index.tsx +717 -0
- package/src/components/views/ResolutionView/EditableJsonView.tsx +214 -0
- package/src/components/views/ResolutionView/ResolutionEditControls.tsx +170 -0
- package/src/components/views/ResolutionView/index.tsx +591 -0
- package/src/components/views/SourceView/index.tsx +536 -0
- package/src/components/views/ZipLoaderView/index.tsx +485 -0
- package/src/hooks/useConfigurationState.ts +374 -0
- package/src/hooks/useFilterState.ts +97 -0
- package/src/hooks/useResolutionState.ts +355 -0
- package/src/hooks/useResourceData.ts +467 -0
- package/src/hooks/useViewState.ts +44 -0
- package/src/index.ts +45 -0
- package/src/test/helpers/testDataLoader.ts +195 -0
- package/src/test/unit/utils/configurationUtils.test.ts +630 -0
- package/src/test/unit/utils/fileProcessing.test.ts +391 -0
- package/src/test/unit/utils/filterResources.test.ts +574 -0
- package/src/test/unit/utils/resolutionEditing.test.ts +556 -0
- package/src/test/unit/utils/resolutionUtils.test.ts +521 -0
- package/src/test/unit/utils/tsResIntegration.test.ts +433 -0
- package/src/types/index.ts +322 -0
- package/src/utils/configurationUtils.ts +424 -0
- package/src/utils/fileProcessing.ts +160 -0
- package/src/utils/filterResources.ts +206 -0
- package/src/utils/resolutionEditing.ts +319 -0
- package/src/utils/resolutionUtils.ts +289 -0
- package/src/utils/tsResIntegration.ts +440 -0
- package/src/utils/zipLoader/browserZipLoader.ts +319 -0
- package/src/utils/zipLoader/index.ts +26 -0
- package/src/utils/zipLoader/nodeZipBuilder.ts +153 -0
- package/src/utils/zipLoader/types.ts +175 -0
- package/src/utils/zipLoader/zipUtils.ts +266 -0
- package/temp/build/typescript/ts_gZid87Hu.json +1 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { ProcessedResources } from '../../types';
|
|
3
|
+
|
|
4
|
+
export interface QualifierContextControlProps {
|
|
5
|
+
qualifierName: string;
|
|
6
|
+
value: string | undefined;
|
|
7
|
+
onChange: (qualifierName: string, value: string | undefined) => void;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
placeholder?: string;
|
|
10
|
+
resources?: ProcessedResources | null;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const QualifierContextControl: React.FC<QualifierContextControlProps> = ({
|
|
15
|
+
qualifierName,
|
|
16
|
+
value,
|
|
17
|
+
onChange,
|
|
18
|
+
disabled = false,
|
|
19
|
+
placeholder,
|
|
20
|
+
resources,
|
|
21
|
+
className = ''
|
|
22
|
+
}) => {
|
|
23
|
+
// Extract qualifier type information from system configuration
|
|
24
|
+
const qualifierInfo = useMemo(() => {
|
|
25
|
+
if (!resources?.system?.qualifiers) {
|
|
26
|
+
return { hasEnumeratedValues: false, enumeratedValues: [] };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// Get qualifier declaration
|
|
31
|
+
const qualifierResult = resources.system.qualifiers.validating.get(qualifierName);
|
|
32
|
+
|
|
33
|
+
if (!qualifierResult.isSuccess()) {
|
|
34
|
+
return { hasEnumeratedValues: false, enumeratedValues: [] };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const qualifier = qualifierResult.value;
|
|
38
|
+
|
|
39
|
+
// Access the instantiated qualifier type
|
|
40
|
+
if (qualifier.type) {
|
|
41
|
+
const qualifierType = qualifier.type;
|
|
42
|
+
// Use type assertion to access properties that may exist on specific subtypes
|
|
43
|
+
const qtAny = qualifierType as unknown as Record<string, unknown>;
|
|
44
|
+
const config = (qtAny.configuration || {}) as Record<string, unknown>;
|
|
45
|
+
|
|
46
|
+
// Look for enumerated values in different possible locations
|
|
47
|
+
const enumeratedValues =
|
|
48
|
+
config.enumeratedValues ||
|
|
49
|
+
config.allowedTerritories ||
|
|
50
|
+
qtAny.enumeratedValues ||
|
|
51
|
+
qtAny.allowedTerritories ||
|
|
52
|
+
[];
|
|
53
|
+
|
|
54
|
+
if (enumeratedValues && Array.isArray(enumeratedValues) && enumeratedValues.length > 0) {
|
|
55
|
+
return {
|
|
56
|
+
hasEnumeratedValues: true,
|
|
57
|
+
enumeratedValues: enumeratedValues as string[],
|
|
58
|
+
systemType: (qtAny.systemType as string) || 'literal',
|
|
59
|
+
caseSensitive: config.caseSensitive !== false
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { hasEnumeratedValues: false, enumeratedValues: [] };
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.warn(`Failed to extract qualifier type info for ${qualifierName}:`, error);
|
|
67
|
+
return { hasEnumeratedValues: false, enumeratedValues: [] };
|
|
68
|
+
}
|
|
69
|
+
}, [qualifierName, resources?.system?.qualifiers]);
|
|
70
|
+
|
|
71
|
+
const handleChange = (newValue: string) => {
|
|
72
|
+
onChange(qualifierName, newValue || undefined);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const handleClear = () => {
|
|
76
|
+
onChange(qualifierName, undefined);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const effectiveValue = value ?? '';
|
|
80
|
+
const hasEnumeratedValues = qualifierInfo.hasEnumeratedValues && qualifierInfo.enumeratedValues.length > 0;
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div className={`bg-white rounded border border-gray-200 p-2 ${className}`}>
|
|
84
|
+
<div className="flex items-center gap-2">
|
|
85
|
+
<label className="text-sm font-medium text-gray-700 min-w-0 flex-shrink-0">{qualifierName}:</label>
|
|
86
|
+
<div className="flex-1 flex items-center gap-1">
|
|
87
|
+
{hasEnumeratedValues ? (
|
|
88
|
+
// Dropdown for enumerated values
|
|
89
|
+
<select
|
|
90
|
+
value={effectiveValue}
|
|
91
|
+
onChange={(e) => handleChange(e.target.value)}
|
|
92
|
+
disabled={disabled}
|
|
93
|
+
className={`flex-1 px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-transparent text-sm min-w-0 ${
|
|
94
|
+
disabled ? 'bg-gray-100 text-gray-400' : ''
|
|
95
|
+
}`}
|
|
96
|
+
>
|
|
97
|
+
<option value="">
|
|
98
|
+
{disabled
|
|
99
|
+
? 'Disabled'
|
|
100
|
+
: value === undefined
|
|
101
|
+
? '(undefined)'
|
|
102
|
+
: placeholder || 'Select value...'}
|
|
103
|
+
</option>
|
|
104
|
+
{qualifierInfo.enumeratedValues.map((enumValue: string) => (
|
|
105
|
+
<option key={enumValue} value={enumValue}>
|
|
106
|
+
{enumValue}
|
|
107
|
+
</option>
|
|
108
|
+
))}
|
|
109
|
+
</select>
|
|
110
|
+
) : (
|
|
111
|
+
// Text input for non-enumerated values
|
|
112
|
+
<input
|
|
113
|
+
type="text"
|
|
114
|
+
value={effectiveValue}
|
|
115
|
+
onChange={(e) => handleChange(e.target.value)}
|
|
116
|
+
disabled={disabled}
|
|
117
|
+
className={`flex-1 px-2 py-1 border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-transparent text-sm min-w-0 ${
|
|
118
|
+
disabled ? 'bg-gray-100 text-gray-400' : ''
|
|
119
|
+
}`}
|
|
120
|
+
placeholder={
|
|
121
|
+
disabled
|
|
122
|
+
? 'Disabled'
|
|
123
|
+
: value === undefined
|
|
124
|
+
? '(undefined)'
|
|
125
|
+
: placeholder || `Enter ${qualifierName} value`
|
|
126
|
+
}
|
|
127
|
+
/>
|
|
128
|
+
)}
|
|
129
|
+
{!disabled && value !== undefined && (
|
|
130
|
+
<button
|
|
131
|
+
type="button"
|
|
132
|
+
onClick={handleClear}
|
|
133
|
+
className="px-2 py-1 text-xs text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded transition-colors"
|
|
134
|
+
title="Set to undefined"
|
|
135
|
+
>
|
|
136
|
+
✕
|
|
137
|
+
</button>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
</div>
|
|
141
|
+
{/* Show enumerated values indicator */}
|
|
142
|
+
{hasEnumeratedValues && (
|
|
143
|
+
<div className="mt-1 text-xs text-blue-600">
|
|
144
|
+
{qualifierInfo.enumeratedValues.length} predefined values
|
|
145
|
+
</div>
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export default QualifierContextControl;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { DocumentTextIcon } from '@heroicons/react/24/outline';
|
|
3
|
+
|
|
4
|
+
interface ResourceListViewProps {
|
|
5
|
+
resourceIds: string[];
|
|
6
|
+
selectedResourceId: string | null;
|
|
7
|
+
onResourceSelect: (resourceId: string) => void;
|
|
8
|
+
searchTerm?: string;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const ResourceListView: React.FC<ResourceListViewProps> = ({
|
|
13
|
+
resourceIds,
|
|
14
|
+
selectedResourceId,
|
|
15
|
+
onResourceSelect,
|
|
16
|
+
searchTerm = '',
|
|
17
|
+
className = ''
|
|
18
|
+
}) => {
|
|
19
|
+
// Filter and sort resource IDs
|
|
20
|
+
const filteredResourceIds = React.useMemo(() => {
|
|
21
|
+
const filtered = searchTerm
|
|
22
|
+
? resourceIds.filter((id) => id.toLowerCase().includes(searchTerm.toLowerCase()))
|
|
23
|
+
: resourceIds;
|
|
24
|
+
|
|
25
|
+
return filtered.sort();
|
|
26
|
+
}, [resourceIds, searchTerm]);
|
|
27
|
+
|
|
28
|
+
if (filteredResourceIds.length === 0) {
|
|
29
|
+
return (
|
|
30
|
+
<div className={`${className} p-4 text-center text-gray-500`}>
|
|
31
|
+
<p>{searchTerm ? 'No resources match your search' : 'No resources available'}</p>
|
|
32
|
+
</div>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className={`${className} overflow-y-auto`}>
|
|
38
|
+
{filteredResourceIds.map((resourceId) => (
|
|
39
|
+
<div
|
|
40
|
+
key={resourceId}
|
|
41
|
+
className={`flex items-center px-3 py-2 cursor-pointer hover:bg-gray-100 border-b border-gray-100 last:border-b-0 ${
|
|
42
|
+
selectedResourceId === resourceId ? 'bg-purple-50 border-l-2 border-purple-500' : ''
|
|
43
|
+
} ${
|
|
44
|
+
searchTerm && resourceId.toLowerCase().includes(searchTerm.toLowerCase()) ? 'bg-yellow-50' : ''
|
|
45
|
+
}`}
|
|
46
|
+
onClick={() => onResourceSelect(resourceId)}
|
|
47
|
+
>
|
|
48
|
+
<DocumentTextIcon className="w-4 h-4 text-green-500 mr-2 flex-shrink-0" />
|
|
49
|
+
<span
|
|
50
|
+
className={`text-sm truncate ${
|
|
51
|
+
selectedResourceId === resourceId ? 'font-medium text-purple-900' : 'text-gray-700'
|
|
52
|
+
}`}
|
|
53
|
+
title={resourceId}
|
|
54
|
+
>
|
|
55
|
+
{resourceId}
|
|
56
|
+
</span>
|
|
57
|
+
</div>
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export default ResourceListView;
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import React, { useState, useMemo, useCallback } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ChevronRightIcon,
|
|
4
|
+
ChevronDownIcon,
|
|
5
|
+
DocumentTextIcon,
|
|
6
|
+
FolderIcon,
|
|
7
|
+
FolderOpenIcon
|
|
8
|
+
} from '@heroicons/react/24/outline';
|
|
9
|
+
import { Resources, Runtime } from '@fgv/ts-res';
|
|
10
|
+
|
|
11
|
+
interface ResourceTreeViewProps {
|
|
12
|
+
resources: Resources.ResourceManagerBuilder | Runtime.CompiledResourceCollection;
|
|
13
|
+
selectedResourceId: string | null;
|
|
14
|
+
onResourceSelect: (resourceId: string) => void;
|
|
15
|
+
searchTerm?: string;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface TreeNode {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
isLeaf: boolean;
|
|
23
|
+
resource?: Resources.Resource | Runtime.IResource;
|
|
24
|
+
children?: Map<string, TreeNode>;
|
|
25
|
+
isExpanded?: boolean;
|
|
26
|
+
isVisible?: boolean;
|
|
27
|
+
matchesSearch?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const ResourceTreeView: React.FC<ResourceTreeViewProps> = ({
|
|
31
|
+
resources,
|
|
32
|
+
selectedResourceId,
|
|
33
|
+
onResourceSelect,
|
|
34
|
+
searchTerm = '',
|
|
35
|
+
className = ''
|
|
36
|
+
}) => {
|
|
37
|
+
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set());
|
|
38
|
+
|
|
39
|
+
// Build the tree structure from resources
|
|
40
|
+
const treeData = useMemo(() => {
|
|
41
|
+
if (!resources) return null;
|
|
42
|
+
|
|
43
|
+
// Get the tree from the resources
|
|
44
|
+
const treeResult = resources.getBuiltResourceTree();
|
|
45
|
+
if (treeResult.isFailure()) {
|
|
46
|
+
console.error('Failed to build resource tree:', treeResult.message);
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return treeResult.value;
|
|
51
|
+
}, [resources]);
|
|
52
|
+
|
|
53
|
+
// Filter tree based on search term
|
|
54
|
+
const filteredTree = useMemo(() => {
|
|
55
|
+
if (!treeData || !searchTerm) return treeData;
|
|
56
|
+
|
|
57
|
+
// Helper function to check if a node or its descendants match the search
|
|
58
|
+
const markMatchingNodes = (
|
|
59
|
+
node: Runtime.ResourceTree.IReadOnlyResourceTreeNode<any>,
|
|
60
|
+
searchLower: string
|
|
61
|
+
): boolean => {
|
|
62
|
+
const nodeIdLower = node.id.toLowerCase();
|
|
63
|
+
let matches = nodeIdLower.includes(searchLower);
|
|
64
|
+
|
|
65
|
+
if (!node.isLeaf && node.children) {
|
|
66
|
+
// Check children recursively
|
|
67
|
+
for (const child of node.children.values()) {
|
|
68
|
+
if (markMatchingNodes(child, searchLower)) {
|
|
69
|
+
matches = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return matches;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Mark all matching nodes
|
|
78
|
+
const searchLower = searchTerm.toLowerCase();
|
|
79
|
+
for (const child of treeData.children.values()) {
|
|
80
|
+
markMatchingNodes(child, searchLower);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return treeData;
|
|
84
|
+
}, [treeData, searchTerm]);
|
|
85
|
+
|
|
86
|
+
// Toggle node expansion
|
|
87
|
+
const toggleNode = useCallback((nodeId: string) => {
|
|
88
|
+
setExpandedNodes((prev) => {
|
|
89
|
+
const newExpanded = new Set(prev);
|
|
90
|
+
if (newExpanded.has(nodeId)) {
|
|
91
|
+
newExpanded.delete(nodeId);
|
|
92
|
+
} else {
|
|
93
|
+
newExpanded.add(nodeId);
|
|
94
|
+
}
|
|
95
|
+
return newExpanded;
|
|
96
|
+
});
|
|
97
|
+
}, []);
|
|
98
|
+
|
|
99
|
+
// Expand all nodes that contain search matches
|
|
100
|
+
const expandMatchingNodes = useCallback(() => {
|
|
101
|
+
if (!searchTerm || !filteredTree) return;
|
|
102
|
+
|
|
103
|
+
const searchLower = searchTerm.toLowerCase();
|
|
104
|
+
const nodesToExpand = new Set<string>();
|
|
105
|
+
|
|
106
|
+
const checkNode = (node: Runtime.ResourceTree.IReadOnlyResourceTreeNode<any>) => {
|
|
107
|
+
if (node.id.toLowerCase().includes(searchLower)) {
|
|
108
|
+
// Expand all parent nodes
|
|
109
|
+
const parts = node.id.split('.');
|
|
110
|
+
for (let i = 1; i < parts.length; i++) {
|
|
111
|
+
nodesToExpand.add(parts.slice(0, i).join('.'));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!node.isLeaf && node.children) {
|
|
116
|
+
for (const child of node.children.values()) {
|
|
117
|
+
checkNode(child);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
for (const child of filteredTree.children.values()) {
|
|
123
|
+
checkNode(child);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
setExpandedNodes(nodesToExpand);
|
|
127
|
+
}, [searchTerm, filteredTree]);
|
|
128
|
+
|
|
129
|
+
// Auto-expand when search term changes
|
|
130
|
+
React.useEffect(() => {
|
|
131
|
+
if (searchTerm) {
|
|
132
|
+
expandMatchingNodes();
|
|
133
|
+
}
|
|
134
|
+
}, [searchTerm, expandMatchingNodes]);
|
|
135
|
+
|
|
136
|
+
// Render a single tree node
|
|
137
|
+
const renderTreeNode = (
|
|
138
|
+
node: Runtime.ResourceTree.IReadOnlyResourceTreeNode<Resources.Resource | Runtime.IResource>,
|
|
139
|
+
level: number = 0
|
|
140
|
+
): React.ReactElement | null => {
|
|
141
|
+
const isExpanded = expandedNodes.has(node.id);
|
|
142
|
+
const isSelected = selectedResourceId === node.id;
|
|
143
|
+
const nodeIdLower = node.id.toLowerCase();
|
|
144
|
+
const searchLower = searchTerm.toLowerCase();
|
|
145
|
+
const matchesSearch = !searchTerm || nodeIdLower.includes(searchLower);
|
|
146
|
+
|
|
147
|
+
// Check if any children match
|
|
148
|
+
let hasMatchingChildren = false;
|
|
149
|
+
if (!node.isLeaf && node.children && searchTerm) {
|
|
150
|
+
const checkChildren = (n: Runtime.ResourceTree.IReadOnlyResourceTreeNode<any>): boolean => {
|
|
151
|
+
if (n.id.toLowerCase().includes(searchLower)) return true;
|
|
152
|
+
if (!n.isLeaf && n.children) {
|
|
153
|
+
for (const child of n.children.values()) {
|
|
154
|
+
if (checkChildren(child)) return true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
for (const child of node.children.values()) {
|
|
161
|
+
if (checkChildren(child)) {
|
|
162
|
+
hasMatchingChildren = true;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Hide nodes that don't match search and don't have matching children
|
|
169
|
+
if (searchTerm && !matchesSearch && !hasMatchingChildren) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<div key={node.id}>
|
|
175
|
+
<div
|
|
176
|
+
className={`flex items-center px-2 py-1 cursor-pointer hover:bg-gray-100 ${
|
|
177
|
+
isSelected ? 'bg-purple-50 border-l-2 border-purple-500' : ''
|
|
178
|
+
} ${matchesSearch && searchTerm ? 'bg-yellow-50' : ''}`}
|
|
179
|
+
style={{ paddingLeft: `${level * 20 + 8}px` }}
|
|
180
|
+
onClick={() => {
|
|
181
|
+
if (node.isLeaf) {
|
|
182
|
+
onResourceSelect(node.id);
|
|
183
|
+
} else {
|
|
184
|
+
toggleNode(node.id);
|
|
185
|
+
}
|
|
186
|
+
}}
|
|
187
|
+
>
|
|
188
|
+
{/* Expand/Collapse icon for branches */}
|
|
189
|
+
{!node.isLeaf && (
|
|
190
|
+
<button
|
|
191
|
+
onClick={(e) => {
|
|
192
|
+
e.stopPropagation();
|
|
193
|
+
toggleNode(node.id);
|
|
194
|
+
}}
|
|
195
|
+
className="mr-1 p-0.5 hover:bg-gray-200 rounded"
|
|
196
|
+
>
|
|
197
|
+
{isExpanded ? (
|
|
198
|
+
<ChevronDownIcon className="w-3 h-3 text-gray-600" />
|
|
199
|
+
) : (
|
|
200
|
+
<ChevronRightIcon className="w-3 h-3 text-gray-600" />
|
|
201
|
+
)}
|
|
202
|
+
</button>
|
|
203
|
+
)}
|
|
204
|
+
|
|
205
|
+
{/* Folder or document icon */}
|
|
206
|
+
{node.isLeaf ? (
|
|
207
|
+
<DocumentTextIcon className="w-4 h-4 text-green-500 mr-2 flex-shrink-0" />
|
|
208
|
+
) : isExpanded ? (
|
|
209
|
+
<FolderOpenIcon className="w-4 h-4 text-blue-500 mr-2 flex-shrink-0" />
|
|
210
|
+
) : (
|
|
211
|
+
<FolderIcon className="w-4 h-4 text-blue-500 mr-2 flex-shrink-0" />
|
|
212
|
+
)}
|
|
213
|
+
|
|
214
|
+
{/* Node name */}
|
|
215
|
+
<span
|
|
216
|
+
className={`text-sm truncate ${isSelected ? 'font-medium text-purple-900' : 'text-gray-700'} ${
|
|
217
|
+
matchesSearch && searchTerm ? 'font-medium' : ''
|
|
218
|
+
}`}
|
|
219
|
+
title={node.id}
|
|
220
|
+
>
|
|
221
|
+
{node.name}
|
|
222
|
+
</span>
|
|
223
|
+
|
|
224
|
+
{/* Show child count for branches */}
|
|
225
|
+
{!node.isLeaf && node.children && (
|
|
226
|
+
<span className="ml-2 text-xs text-gray-500">({node.children.size})</span>
|
|
227
|
+
)}
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
{/* Render children if expanded */}
|
|
231
|
+
{!node.isLeaf && node.children && isExpanded && (
|
|
232
|
+
<div>
|
|
233
|
+
{Array.from(node.children.values())
|
|
234
|
+
.sort((a, b) => {
|
|
235
|
+
// Sort folders first, then by name
|
|
236
|
+
if (a.isLeaf !== b.isLeaf) {
|
|
237
|
+
return a.isLeaf ? 1 : -1;
|
|
238
|
+
}
|
|
239
|
+
return a.name.localeCompare(b.name);
|
|
240
|
+
})
|
|
241
|
+
.map((child) => renderTreeNode(child, level + 1))}
|
|
242
|
+
</div>
|
|
243
|
+
)}
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
if (!filteredTree) {
|
|
249
|
+
return (
|
|
250
|
+
<div className={`${className} p-4 text-center text-gray-500`}>
|
|
251
|
+
<p>No resources available</p>
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return (
|
|
257
|
+
<div className={`${className} overflow-y-auto`}>
|
|
258
|
+
{Array.from(filteredTree.children.values())
|
|
259
|
+
.sort((a, b) => {
|
|
260
|
+
// Sort folders first, then by name
|
|
261
|
+
if (a.isLeaf !== b.isLeaf) {
|
|
262
|
+
return a.isLeaf ? 1 : -1;
|
|
263
|
+
}
|
|
264
|
+
return a.name.localeCompare(b.name);
|
|
265
|
+
})
|
|
266
|
+
.map((child) => renderTreeNode(child))}
|
|
267
|
+
</div>
|
|
268
|
+
);
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
export default ResourceTreeView;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface HierarchyEditorProps {
|
|
4
|
+
hierarchy: Record<string, string>;
|
|
5
|
+
onChange: (hierarchy: Record<string, string>) => void;
|
|
6
|
+
availableValues: string[];
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const HierarchyEditor: React.FC<HierarchyEditorProps> = ({
|
|
11
|
+
hierarchy,
|
|
12
|
+
onChange,
|
|
13
|
+
availableValues,
|
|
14
|
+
className = ''
|
|
15
|
+
}) => {
|
|
16
|
+
const [newChild, setNewChild] = useState('');
|
|
17
|
+
const [newParent, setNewParent] = useState('');
|
|
18
|
+
|
|
19
|
+
// Ensure hierarchy is a valid object with string values
|
|
20
|
+
const safeHierarchy = React.useMemo(() => {
|
|
21
|
+
if (!hierarchy || typeof hierarchy !== 'object') {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
// Filter out any non-string values
|
|
25
|
+
const safe: Record<string, string> = {};
|
|
26
|
+
for (const [key, value] of Object.entries(hierarchy)) {
|
|
27
|
+
if (typeof value === 'string') {
|
|
28
|
+
safe[key] = value;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return safe;
|
|
32
|
+
}, [hierarchy]);
|
|
33
|
+
|
|
34
|
+
const handleAddRelationship = () => {
|
|
35
|
+
if (newChild && newParent && newChild !== newParent) {
|
|
36
|
+
const updatedHierarchy = { ...safeHierarchy, [newChild]: newParent };
|
|
37
|
+
onChange(updatedHierarchy);
|
|
38
|
+
setNewChild('');
|
|
39
|
+
setNewParent('');
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const handleRemoveRelationship = (child: string) => {
|
|
44
|
+
const updatedHierarchy = { ...safeHierarchy };
|
|
45
|
+
delete updatedHierarchy[child];
|
|
46
|
+
onChange(updatedHierarchy);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const getHierarchyTree = () => {
|
|
50
|
+
const roots = new Set(availableValues);
|
|
51
|
+
const children = new Set(Object.keys(safeHierarchy));
|
|
52
|
+
const parents = new Set(Object.values(safeHierarchy));
|
|
53
|
+
|
|
54
|
+
// Remove children from roots (they have parents)
|
|
55
|
+
children.forEach((child) => roots.delete(child));
|
|
56
|
+
|
|
57
|
+
// Add parents that aren't in available values (for display purposes)
|
|
58
|
+
parents.forEach((parent) => {
|
|
59
|
+
if (!availableValues.includes(parent)) {
|
|
60
|
+
roots.add(parent);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const buildTree = (value: string, level = 0): any => {
|
|
65
|
+
const childrenOfValue = Object.entries(safeHierarchy).filter(([, parent]) => parent === value);
|
|
66
|
+
return {
|
|
67
|
+
value,
|
|
68
|
+
level,
|
|
69
|
+
children: childrenOfValue.map(([child]) => buildTree(child, level + 1))
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return Array.from(roots).map((root) => buildTree(root));
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const renderTree = (nodes: any[]): React.ReactNode => {
|
|
77
|
+
return nodes.map((node) => {
|
|
78
|
+
const parentValue = safeHierarchy[node.value];
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div key={node.value} className="ml-4">
|
|
82
|
+
<div className="flex items-center space-x-2 py-1">
|
|
83
|
+
<span className="text-sm text-gray-700" style={{ marginLeft: `${node.level * 20}px` }}>
|
|
84
|
+
{node.level > 0 && '└─ '}
|
|
85
|
+
{node.value}
|
|
86
|
+
</span>
|
|
87
|
+
{parentValue && <span className="text-xs text-gray-500">→ {parentValue}</span>}
|
|
88
|
+
</div>
|
|
89
|
+
{node.children.length > 0 && renderTree(node.children)}
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div className={className}>
|
|
97
|
+
<label className="block text-sm font-medium text-gray-700 mb-2">Value Hierarchy</label>
|
|
98
|
+
<div className="border border-gray-300 rounded-md p-3 bg-white">
|
|
99
|
+
{/* Add new relationship form */}
|
|
100
|
+
<div className="mb-4 p-3 bg-gray-50 rounded border">
|
|
101
|
+
<div className="text-sm font-medium text-gray-700 mb-2">Add Parent-Child Relationship</div>
|
|
102
|
+
<div className="grid grid-cols-3 gap-2 items-end">
|
|
103
|
+
<div>
|
|
104
|
+
<label className="block text-xs text-gray-600">Child Value</label>
|
|
105
|
+
{availableValues.length > 0 ? (
|
|
106
|
+
<select
|
|
107
|
+
value={newChild}
|
|
108
|
+
onChange={(e) => setNewChild(e.target.value)}
|
|
109
|
+
className="w-full text-sm rounded border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
|
110
|
+
>
|
|
111
|
+
<option value="">Select child...</option>
|
|
112
|
+
{availableValues.map((value) => (
|
|
113
|
+
<option key={value} value={value}>
|
|
114
|
+
{value}
|
|
115
|
+
</option>
|
|
116
|
+
))}
|
|
117
|
+
</select>
|
|
118
|
+
) : (
|
|
119
|
+
<input
|
|
120
|
+
type="text"
|
|
121
|
+
value={newChild}
|
|
122
|
+
onChange={(e) => setNewChild(e.target.value)}
|
|
123
|
+
placeholder="Enter child value"
|
|
124
|
+
className="w-full text-sm rounded border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
|
125
|
+
/>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
<div>
|
|
129
|
+
<label className="block text-xs text-gray-600">Parent Value</label>
|
|
130
|
+
<input
|
|
131
|
+
type="text"
|
|
132
|
+
value={newParent}
|
|
133
|
+
onChange={(e) => setNewParent(e.target.value)}
|
|
134
|
+
placeholder="Enter parent value"
|
|
135
|
+
className="w-full text-sm rounded border-gray-300 focus:border-blue-500 focus:ring-blue-500"
|
|
136
|
+
/>
|
|
137
|
+
</div>
|
|
138
|
+
<div>
|
|
139
|
+
<button
|
|
140
|
+
onClick={handleAddRelationship}
|
|
141
|
+
disabled={!newChild || !newParent || newChild === newParent}
|
|
142
|
+
className="w-full px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed"
|
|
143
|
+
>
|
|
144
|
+
Add
|
|
145
|
+
</button>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
<div className="text-xs text-gray-500 mt-1">
|
|
149
|
+
{availableValues.length > 0
|
|
150
|
+
? "Define which values are children of other values. The parent doesn't need to be in the enumerated values list."
|
|
151
|
+
: 'Define which values are children of other values. Enter any valid values for this qualifier type.'}
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{/* Current relationships */}
|
|
156
|
+
{Object.keys(safeHierarchy).length > 0 && (
|
|
157
|
+
<div className="mb-4">
|
|
158
|
+
<div className="text-sm font-medium text-gray-700 mb-2">
|
|
159
|
+
Current Relationships ({Object.keys(safeHierarchy).length})
|
|
160
|
+
</div>
|
|
161
|
+
<div className="max-h-24 overflow-y-auto border border-gray-200 rounded bg-white p-2">
|
|
162
|
+
<div className="space-y-1">
|
|
163
|
+
{Object.entries(safeHierarchy).map(([child, parent]) => (
|
|
164
|
+
<div key={child} className="flex items-center justify-between bg-gray-50 px-2 py-1 rounded">
|
|
165
|
+
<span className="text-sm">
|
|
166
|
+
<span className="font-medium">{child}</span> →{' '}
|
|
167
|
+
<span className="text-gray-600">{parent}</span>
|
|
168
|
+
</span>
|
|
169
|
+
<button
|
|
170
|
+
onClick={() => handleRemoveRelationship(child)}
|
|
171
|
+
className="text-red-600 hover:text-red-800 text-xs ml-2 flex-shrink-0"
|
|
172
|
+
>
|
|
173
|
+
Remove
|
|
174
|
+
</button>
|
|
175
|
+
</div>
|
|
176
|
+
))}
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
)}
|
|
181
|
+
|
|
182
|
+
{/* Hierarchy visualization */}
|
|
183
|
+
{(availableValues.length > 0 || Object.keys(safeHierarchy).length > 0) && (
|
|
184
|
+
<div>
|
|
185
|
+
<div className="text-sm font-medium text-gray-700 mb-2">Hierarchy Tree</div>
|
|
186
|
+
<div className="bg-gray-50 border rounded max-h-32 overflow-y-auto">
|
|
187
|
+
<div className="p-2 text-sm font-mono">
|
|
188
|
+
{getHierarchyTree().length > 0 ? (
|
|
189
|
+
renderTree(getHierarchyTree())
|
|
190
|
+
) : (
|
|
191
|
+
<div className="text-gray-500 text-center py-2">
|
|
192
|
+
No hierarchy defined. Add relationships above to see the tree structure.
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
export default HierarchyEditor;
|