@fgv/ts-res-ui-components 5.0.0-21 → 5.0.0-23
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/README.md +401 -155
- package/config/jest.setup.js +10 -0
- package/dist/ts-res-ui-components.d.ts +1657 -76
- package/lib/components/common/QualifierContextControl.js +4 -1
- package/lib/components/common/ResourceTreeView.js +4 -1
- package/lib/components/forms/GenericQualifierTypeEditForm.d.ts +26 -0
- package/lib/components/forms/GenericQualifierTypeEditForm.js +166 -0
- package/lib/components/forms/QualifierEditForm.d.ts +1 -1
- package/lib/components/forms/index.d.ts +2 -0
- package/lib/components/forms/index.js +1 -0
- package/lib/components/orchestrator/ResourceOrchestrator.d.ts +3 -0
- package/lib/components/orchestrator/ResourceOrchestrator.js +118 -51
- package/lib/components/pickers/ResourcePicker/ResourcePickerTree.js +32 -10
- package/lib/components/pickers/ResourcePicker/index.js +4 -2
- package/lib/components/views/CompiledView/index.js +75 -16
- package/lib/components/views/ConfigurationView/index.js +94 -35
- package/lib/components/views/FilterView/index.js +7 -4
- package/lib/components/views/GridView/EditableGridCell.d.ts +76 -0
- package/lib/components/views/GridView/EditableGridCell.js +224 -0
- package/lib/components/views/GridView/GridSelector.d.ts +43 -0
- package/lib/components/views/GridView/GridSelector.js +89 -0
- package/lib/components/views/GridView/MultiGridView.d.ts +85 -0
- package/lib/components/views/GridView/MultiGridView.js +196 -0
- package/lib/components/views/GridView/ResourceGrid.d.ts +38 -0
- package/lib/components/views/GridView/ResourceGrid.js +232 -0
- package/lib/components/views/GridView/SharedContextControls.d.ts +47 -0
- package/lib/components/views/GridView/SharedContextControls.js +95 -0
- package/lib/components/views/GridView/cells/BooleanCell.d.ts +44 -0
- package/lib/components/views/GridView/cells/BooleanCell.js +49 -0
- package/lib/components/views/GridView/cells/DropdownCell.d.ts +58 -0
- package/lib/components/views/GridView/cells/DropdownCell.js +182 -0
- package/lib/components/views/GridView/cells/StringCell.d.ts +57 -0
- package/lib/components/views/GridView/cells/StringCell.js +106 -0
- package/lib/components/views/GridView/cells/TriStateCell.d.ts +54 -0
- package/lib/components/views/GridView/cells/TriStateCell.js +112 -0
- package/lib/components/views/GridView/cells/index.d.ts +15 -0
- package/lib/components/views/GridView/cells/index.js +11 -0
- package/lib/components/views/GridView/index.d.ts +53 -0
- package/lib/components/views/GridView/index.js +212 -0
- package/lib/components/views/ImportView/index.js +22 -19
- package/lib/components/views/MessagesWindow/index.js +4 -1
- package/lib/components/views/ResolutionView/index.js +8 -5
- package/lib/contexts/ObservabilityContext.d.ts +85 -0
- package/lib/contexts/ObservabilityContext.js +98 -0
- package/lib/contexts/index.d.ts +2 -0
- package/lib/contexts/index.js +24 -0
- package/lib/hooks/useConfigurationState.d.ts +3 -3
- package/lib/hooks/useResolutionState.js +850 -246
- package/lib/hooks/useResourceData.d.ts +7 -4
- package/lib/hooks/useResourceData.js +185 -184
- package/lib/index.d.ts +5 -1
- package/lib/index.js +8 -1
- package/lib/namespaces/GridTools.d.ts +136 -0
- package/lib/namespaces/GridTools.js +138 -0
- package/lib/namespaces/ObservabilityTools.d.ts +3 -0
- package/lib/namespaces/ObservabilityTools.js +23 -0
- package/lib/namespaces/ResolutionTools.d.ts +2 -1
- package/lib/namespaces/ResolutionTools.js +2 -0
- package/lib/namespaces/index.d.ts +2 -0
- package/lib/namespaces/index.js +2 -0
- package/lib/test/integration/observability.integration.test.d.ts +2 -0
- package/lib/test/unit/hooks/useResourceData.test.d.ts +2 -0
- package/lib/test/unit/utils/downloadHelper.test.d.ts +2 -0
- package/lib/test/unit/workflows/resolutionWorkflows.test.d.ts +2 -0
- package/lib/test/unit/workflows/resourceCreation.test.d.ts +2 -0
- package/lib/test/unit/workflows/resultPatternExtensions.test.d.ts +2 -0
- package/lib/test/unit/workflows/validation.test.d.ts +2 -0
- package/lib/types/index.d.ts +387 -20
- package/lib/types/index.js +2 -1
- package/lib/utils/cellValidation.d.ts +113 -0
- package/lib/utils/cellValidation.js +248 -0
- package/lib/utils/downloadHelper.d.ts +66 -0
- package/lib/utils/downloadHelper.js +195 -0
- package/lib/utils/observability/factories.d.ts +29 -0
- package/lib/utils/observability/factories.js +58 -0
- package/lib/utils/observability/implementations.d.ts +61 -0
- package/lib/utils/observability/implementations.js +103 -0
- package/lib/utils/observability/index.d.ts +4 -0
- package/lib/utils/observability/index.js +26 -0
- package/lib/utils/observability/interfaces.d.ts +30 -0
- package/lib/utils/observability/interfaces.js +23 -0
- package/lib/utils/resolutionEditing.js +2 -1
- package/lib/utils/resourceSelector.d.ts +97 -0
- package/lib/utils/resourceSelector.js +195 -0
- package/lib/utils/resourceSelectors.d.ts +146 -0
- package/lib/utils/resourceSelectors.js +233 -0
- package/lib/utils/tsResIntegration.d.ts +6 -41
- package/lib/utils/tsResIntegration.js +20 -16
- package/lib/utils/zipLoader/zipProcessingHelpers.d.ts +3 -2
- package/lib/utils/zipLoader/zipProcessingHelpers.js +6 -5
- package/lib-commonjs/components/common/QualifierContextControl.js +4 -1
- package/lib-commonjs/components/common/ResourceTreeView.js +4 -1
- package/lib-commonjs/components/forms/GenericQualifierTypeEditForm.js +171 -0
- package/lib-commonjs/components/forms/index.js +3 -1
- package/lib-commonjs/components/orchestrator/ResourceOrchestrator.js +118 -51
- package/lib-commonjs/components/pickers/ResourcePicker/ResourcePickerTree.js +32 -10
- package/lib-commonjs/components/pickers/ResourcePicker/index.js +4 -2
- package/lib-commonjs/components/views/CompiledView/index.js +75 -16
- package/lib-commonjs/components/views/ConfigurationView/index.js +93 -34
- package/lib-commonjs/components/views/FilterView/index.js +7 -4
- package/lib-commonjs/components/views/GridView/EditableGridCell.js +232 -0
- package/lib-commonjs/components/views/GridView/GridSelector.js +94 -0
- package/lib-commonjs/components/views/GridView/MultiGridView.js +201 -0
- package/lib-commonjs/components/views/GridView/ResourceGrid.js +237 -0
- package/lib-commonjs/components/views/GridView/SharedContextControls.js +100 -0
- package/lib-commonjs/components/views/GridView/cells/BooleanCell.js +54 -0
- package/lib-commonjs/components/views/GridView/cells/DropdownCell.js +187 -0
- package/lib-commonjs/components/views/GridView/cells/StringCell.js +111 -0
- package/lib-commonjs/components/views/GridView/cells/TriStateCell.js +117 -0
- package/lib-commonjs/components/views/GridView/cells/index.js +18 -0
- package/lib-commonjs/components/views/GridView/index.js +217 -0
- package/lib-commonjs/components/views/ImportView/index.js +22 -19
- package/lib-commonjs/components/views/MessagesWindow/index.js +4 -1
- package/lib-commonjs/components/views/ResolutionView/index.js +8 -5
- package/lib-commonjs/contexts/ObservabilityContext.js +104 -0
- package/lib-commonjs/contexts/index.js +30 -0
- package/lib-commonjs/hooks/useResolutionState.js +849 -245
- package/lib-commonjs/hooks/useResourceData.js +184 -215
- package/lib-commonjs/index.js +15 -1
- package/lib-commonjs/namespaces/GridTools.js +161 -0
- package/lib-commonjs/namespaces/ObservabilityTools.js +33 -0
- package/lib-commonjs/namespaces/ResolutionTools.js +10 -1
- package/lib-commonjs/namespaces/index.js +3 -1
- package/lib-commonjs/types/index.js +10 -0
- package/lib-commonjs/utils/cellValidation.js +253 -0
- package/lib-commonjs/utils/downloadHelper.js +198 -0
- package/lib-commonjs/utils/observability/factories.js +63 -0
- package/lib-commonjs/utils/observability/implementations.js +109 -0
- package/lib-commonjs/utils/observability/index.js +36 -0
- package/lib-commonjs/utils/observability/interfaces.js +24 -0
- package/lib-commonjs/utils/resolutionEditing.js +2 -1
- package/lib-commonjs/utils/resourceSelector.js +200 -0
- package/lib-commonjs/utils/resourceSelectors.js +242 -0
- package/lib-commonjs/utils/tsResIntegration.js +21 -16
- package/lib-commonjs/utils/zipLoader/zipProcessingHelpers.js +7 -5
- package/package.json +7 -7
- package/src/components/common/QualifierContextControl.tsx +0 -338
- package/src/components/common/ResolutionContextOptionsControl.tsx +0 -450
- package/src/components/common/ResolutionResults/index.tsx +0 -481
- package/src/components/common/ResourceListView.tsx +0 -167
- package/src/components/common/ResourcePickerOptionsControl.tsx +0 -351
- package/src/components/common/ResourceTreeView.tsx +0 -417
- package/src/components/common/SourceResourceDetail/index.tsx +0 -493
- package/src/components/forms/HierarchyEditor.tsx +0 -285
- package/src/components/forms/QualifierEditForm.tsx +0 -487
- package/src/components/forms/QualifierTypeEditForm.tsx +0 -458
- package/src/components/forms/ResourceTypeEditForm.tsx +0 -437
- package/src/components/forms/index.ts +0 -11
- package/src/components/orchestrator/ResourceOrchestrator.tsx +0 -444
- package/src/components/pickers/ResourcePicker/README.md +0 -570
- package/src/components/pickers/ResourcePicker/ResourceItem.tsx +0 -127
- package/src/components/pickers/ResourcePicker/ResourcePickerList.tsx +0 -114
- package/src/components/pickers/ResourcePicker/ResourcePickerTree.tsx +0 -461
- package/src/components/pickers/ResourcePicker/index.tsx +0 -234
- package/src/components/pickers/ResourcePicker/types.ts +0 -301
- package/src/components/pickers/ResourcePicker/utils/treeNavigation.ts +0 -210
- package/src/components/views/CompiledView/index.tsx +0 -1342
- package/src/components/views/ConfigurationView/index.tsx +0 -848
- package/src/components/views/FilterView/index.tsx +0 -681
- package/src/components/views/ImportView/index.tsx +0 -789
- package/src/components/views/MessagesWindow/index.tsx +0 -325
- package/src/components/views/ResolutionView/EditableJsonView.tsx +0 -386
- package/src/components/views/ResolutionView/NewResourceModal.tsx +0 -158
- package/src/components/views/ResolutionView/UnifiedChangeControls.tsx +0 -163
- package/src/components/views/ResolutionView/index.tsx +0 -751
- package/src/components/views/SourceView/index.tsx +0 -291
- package/src/hooks/useConfigurationState.ts +0 -436
- package/src/hooks/useFilterState.ts +0 -150
- package/src/hooks/useResolutionState.ts +0 -893
- package/src/hooks/useResourceData.ts +0 -596
- package/src/hooks/useViewState.ts +0 -97
- package/src/index.ts +0 -68
- package/src/namespaces/ConfigurationTools.ts +0 -59
- package/src/namespaces/FilterTools.ts +0 -47
- package/src/namespaces/ImportTools.ts +0 -42
- package/src/namespaces/PickerTools.ts +0 -104
- package/src/namespaces/ResolutionTools.ts +0 -68
- package/src/namespaces/ResourceTools.ts +0 -106
- package/src/namespaces/TsResTools.ts +0 -49
- package/src/namespaces/ViewStateTools.ts +0 -91
- package/src/namespaces/ZipTools.ts +0 -49
- package/src/namespaces/index.ts +0 -49
- package/src/types/index.ts +0 -1273
- package/src/utils/configurationUtils.ts +0 -339
- package/src/utils/fileProcessing.ts +0 -164
- package/src/utils/filterResources.ts +0 -356
- package/src/utils/resolutionEditing.ts +0 -346
- package/src/utils/resolutionUtils.ts +0 -740
- package/src/utils/tsResIntegration.ts +0 -475
- package/src/utils/zipLoader/index.ts +0 -5
- package/src/utils/zipLoader/zipProcessingHelpers.ts +0 -46
- package/src/utils/zipLoader/zipUtils.ts +0 -7
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import React, { useMemo } from 'react';
|
|
2
|
-
import { ResourcePickerListProps } from './types';
|
|
3
|
-
import { ResourceItem } from './ResourceItem';
|
|
4
|
-
import { mergeWithPendingResources, filterTreeBranch } from './utils/treeNavigation';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* List view for the ResourcePicker component
|
|
8
|
-
* Enhanced version of ResourceListView with annotation and pending resource support
|
|
9
|
-
*/
|
|
10
|
-
export const ResourcePickerList = <T = unknown,>({
|
|
11
|
-
resourceIds,
|
|
12
|
-
pendingResources,
|
|
13
|
-
selectedResourceId,
|
|
14
|
-
onResourceSelect,
|
|
15
|
-
resourceAnnotations,
|
|
16
|
-
searchTerm = '',
|
|
17
|
-
rootPath,
|
|
18
|
-
hideRootNode,
|
|
19
|
-
className = '',
|
|
20
|
-
emptyMessage = 'No resources available'
|
|
21
|
-
}: ResourcePickerListProps<T>) => {
|
|
22
|
-
// Merge existing and pending resources
|
|
23
|
-
const allResourceIds = useMemo(() => {
|
|
24
|
-
return mergeWithPendingResources(resourceIds, pendingResources);
|
|
25
|
-
}, [resourceIds, pendingResources]);
|
|
26
|
-
|
|
27
|
-
// Apply branch isolation filtering
|
|
28
|
-
const branchFilteredIds = useMemo(() => {
|
|
29
|
-
return filterTreeBranch(allResourceIds, rootPath, hideRootNode);
|
|
30
|
-
}, [allResourceIds, rootPath, hideRootNode]);
|
|
31
|
-
|
|
32
|
-
// Filter by search term and sort
|
|
33
|
-
const filteredResourceIds = useMemo(() => {
|
|
34
|
-
const filtered = searchTerm
|
|
35
|
-
? branchFilteredIds.filter((id) => id.toLowerCase().includes(searchTerm.toLowerCase()))
|
|
36
|
-
: branchFilteredIds;
|
|
37
|
-
|
|
38
|
-
return filtered.sort();
|
|
39
|
-
}, [branchFilteredIds, searchTerm]);
|
|
40
|
-
|
|
41
|
-
// Helper function to get display name with prefix truncation
|
|
42
|
-
const getDisplayName = useMemo(() => {
|
|
43
|
-
return (resourceId: string, pendingDisplayName?: string) => {
|
|
44
|
-
// For all resources (existing and pending), apply prefix truncation to show full relative ID
|
|
45
|
-
if (rootPath) {
|
|
46
|
-
if (hideRootNode && resourceId.startsWith(rootPath + '.')) {
|
|
47
|
-
// Remove the root path prefix completely - show the full relative path
|
|
48
|
-
return resourceId.substring(rootPath.length + 1);
|
|
49
|
-
} else if (resourceId === rootPath) {
|
|
50
|
-
// For the root node itself, show the full path
|
|
51
|
-
return rootPath;
|
|
52
|
-
} else if (resourceId.startsWith(rootPath + '.')) {
|
|
53
|
-
// Show relative to root path - the full relative path
|
|
54
|
-
return resourceId.substring(rootPath.length + 1);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Default: show the full resource ID
|
|
59
|
-
return resourceId;
|
|
60
|
-
};
|
|
61
|
-
}, [rootPath, hideRootNode]);
|
|
62
|
-
|
|
63
|
-
// Create a map of pending resources for quick lookup
|
|
64
|
-
const pendingResourceMap = useMemo(() => {
|
|
65
|
-
const map = new Map<
|
|
66
|
-
string,
|
|
67
|
-
{ isPending: boolean; type?: 'new' | 'modified' | 'deleted'; resourceData?: T }
|
|
68
|
-
>();
|
|
69
|
-
pendingResources?.forEach((pr) => {
|
|
70
|
-
map.set(pr.id, {
|
|
71
|
-
isPending: true,
|
|
72
|
-
type: pr.type,
|
|
73
|
-
resourceData: pr.resourceData
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
return map;
|
|
77
|
-
}, [pendingResources]);
|
|
78
|
-
|
|
79
|
-
if (filteredResourceIds.length === 0) {
|
|
80
|
-
return (
|
|
81
|
-
<div className={`${className} p-4 text-center text-gray-500`}>
|
|
82
|
-
<p>{searchTerm ? 'No resources match your search' : emptyMessage}</p>
|
|
83
|
-
</div>
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return (
|
|
88
|
-
<div className={`${className} overflow-y-auto`}>
|
|
89
|
-
{filteredResourceIds.map((resourceId) => {
|
|
90
|
-
const pendingInfo = pendingResourceMap.get(resourceId);
|
|
91
|
-
const isPending = Boolean(pendingInfo?.isPending);
|
|
92
|
-
const pendingResource = pendingResources?.find((pr) => pr.id === resourceId);
|
|
93
|
-
const truncatedDisplayName = getDisplayName(resourceId, pendingResource?.displayName);
|
|
94
|
-
|
|
95
|
-
return (
|
|
96
|
-
<ResourceItem<T>
|
|
97
|
-
key={resourceId}
|
|
98
|
-
resourceId={resourceId}
|
|
99
|
-
displayName={truncatedDisplayName}
|
|
100
|
-
isSelected={selectedResourceId === resourceId}
|
|
101
|
-
isPending={isPending}
|
|
102
|
-
annotation={resourceAnnotations?.[resourceId]}
|
|
103
|
-
onClick={onResourceSelect}
|
|
104
|
-
searchTerm={searchTerm}
|
|
105
|
-
resourceData={pendingInfo?.resourceData}
|
|
106
|
-
pendingType={pendingInfo?.type}
|
|
107
|
-
/>
|
|
108
|
-
);
|
|
109
|
-
})}
|
|
110
|
-
</div>
|
|
111
|
-
);
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
export default ResourcePickerList;
|
|
@@ -1,461 +0,0 @@
|
|
|
1
|
-
import React, { useState, useMemo, useCallback } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
ChevronRightIcon,
|
|
4
|
-
ChevronDownIcon,
|
|
5
|
-
FolderIcon,
|
|
6
|
-
FolderOpenIcon,
|
|
7
|
-
DocumentTextIcon
|
|
8
|
-
} from '@heroicons/react/24/outline';
|
|
9
|
-
import { ResourcePickerTreeProps, PendingResource } from './types';
|
|
10
|
-
import { Runtime } from '@fgv/ts-res';
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Virtual tree node that can represent both real and pending resources
|
|
14
|
-
*/
|
|
15
|
-
interface VirtualTreeNode<T = unknown> {
|
|
16
|
-
id: string;
|
|
17
|
-
name: string;
|
|
18
|
-
isLeaf: boolean;
|
|
19
|
-
isPending: boolean;
|
|
20
|
-
pendingResource?: PendingResource<T>;
|
|
21
|
-
realNode?: Runtime.ResourceTree.IReadOnlyResourceTreeNode<any>;
|
|
22
|
-
children: Map<string, VirtualTreeNode<T>>;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Creates a virtual tree by merging real resource tree with pending resources
|
|
27
|
-
*/
|
|
28
|
-
function createVirtualTree<T = unknown>(
|
|
29
|
-
realTree: Runtime.ResourceTree.IReadOnlyResourceTreeRoot<any> | null,
|
|
30
|
-
pendingResources: PendingResource<T>[] = []
|
|
31
|
-
): VirtualTreeNode<T> | null {
|
|
32
|
-
if (!realTree) return null;
|
|
33
|
-
|
|
34
|
-
// Helper to convert real node to virtual node
|
|
35
|
-
const convertRealNode = (
|
|
36
|
-
realNode: Runtime.ResourceTree.IReadOnlyResourceTreeNode<any>
|
|
37
|
-
): VirtualTreeNode<T> => {
|
|
38
|
-
const virtualNode: VirtualTreeNode<T> = {
|
|
39
|
-
id: realNode.id,
|
|
40
|
-
name: realNode.name,
|
|
41
|
-
isLeaf: realNode.isLeaf,
|
|
42
|
-
isPending: false,
|
|
43
|
-
realNode,
|
|
44
|
-
children: new Map()
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
// Convert children
|
|
48
|
-
if (!realNode.isLeaf && realNode.children) {
|
|
49
|
-
for (const child of realNode.children.values()) {
|
|
50
|
-
const virtualChild = convertRealNode(child);
|
|
51
|
-
virtualNode.children.set(child.id, virtualChild);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return virtualNode;
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
// Start with the real tree structure
|
|
59
|
-
const rootNode: VirtualTreeNode<T> = {
|
|
60
|
-
id: '',
|
|
61
|
-
name: 'root',
|
|
62
|
-
isLeaf: false,
|
|
63
|
-
isPending: false,
|
|
64
|
-
children: new Map()
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
// Convert all real children
|
|
68
|
-
for (const realChild of realTree.children.values()) {
|
|
69
|
-
const virtualChild = convertRealNode(realChild);
|
|
70
|
-
rootNode.children.set(realChild.id, virtualChild);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Add pending resources
|
|
74
|
-
for (const pendingResource of pendingResources) {
|
|
75
|
-
if (pendingResource.type === 'deleted') continue; // Skip deleted resources
|
|
76
|
-
|
|
77
|
-
const pathParts = pendingResource.id.split('.');
|
|
78
|
-
const displayName = pendingResource.displayName || pathParts[pathParts.length - 1];
|
|
79
|
-
|
|
80
|
-
// Find or create parent nodes
|
|
81
|
-
let currentNode = rootNode;
|
|
82
|
-
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
83
|
-
const partialPath = pathParts.slice(0, i + 1).join('.');
|
|
84
|
-
|
|
85
|
-
if (!currentNode.children.has(partialPath)) {
|
|
86
|
-
// Create virtual parent node
|
|
87
|
-
const parentNode: VirtualTreeNode<T> = {
|
|
88
|
-
id: partialPath,
|
|
89
|
-
name: pathParts[i],
|
|
90
|
-
isLeaf: false,
|
|
91
|
-
isPending: false,
|
|
92
|
-
children: new Map()
|
|
93
|
-
};
|
|
94
|
-
currentNode.children.set(partialPath, parentNode);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
currentNode = currentNode.children.get(partialPath)!;
|
|
98
|
-
currentNode.isLeaf = false; // Ensure parent is not a leaf
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Create the pending resource node
|
|
102
|
-
const pendingNode: VirtualTreeNode<T> = {
|
|
103
|
-
id: pendingResource.id,
|
|
104
|
-
name: displayName,
|
|
105
|
-
isLeaf: true,
|
|
106
|
-
isPending: true,
|
|
107
|
-
pendingResource,
|
|
108
|
-
children: new Map()
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
currentNode.children.set(pendingResource.id, pendingNode);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return rootNode;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Tree view for the ResourcePicker component
|
|
119
|
-
* Enhanced version of ResourceTreeView with branch isolation and annotation support
|
|
120
|
-
*/
|
|
121
|
-
export const ResourcePickerTree = <T = unknown,>({
|
|
122
|
-
resources,
|
|
123
|
-
pendingResources,
|
|
124
|
-
selectedResourceId,
|
|
125
|
-
onResourceSelect,
|
|
126
|
-
resourceAnnotations,
|
|
127
|
-
searchTerm = '',
|
|
128
|
-
rootPath,
|
|
129
|
-
hideRootNode,
|
|
130
|
-
className = '',
|
|
131
|
-
emptyMessage = 'No resources available'
|
|
132
|
-
}: ResourcePickerTreeProps<T>) => {
|
|
133
|
-
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set());
|
|
134
|
-
|
|
135
|
-
// Build the virtual tree structure from resources and pending resources
|
|
136
|
-
const virtualTree = useMemo(() => {
|
|
137
|
-
if (!resources) return null;
|
|
138
|
-
|
|
139
|
-
// Get the tree from the resource manager
|
|
140
|
-
const resourceManager = resources.system.resourceManager;
|
|
141
|
-
const treeResult = resourceManager.getBuiltResourceTree();
|
|
142
|
-
if (treeResult.isFailure()) {
|
|
143
|
-
console.error('Failed to build resource tree:', treeResult.message);
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Create virtual tree that includes pending resources
|
|
148
|
-
return createVirtualTree(treeResult.value, pendingResources);
|
|
149
|
-
}, [resources, pendingResources]);
|
|
150
|
-
|
|
151
|
-
// Find the effective root node(s) to display
|
|
152
|
-
const effectiveRootNodes = useMemo(() => {
|
|
153
|
-
if (!virtualTree) return [];
|
|
154
|
-
|
|
155
|
-
// If no rootPath, show all top-level nodes
|
|
156
|
-
if (!rootPath) {
|
|
157
|
-
return Array.from(virtualTree.children.values());
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Find the target node in the virtual tree
|
|
161
|
-
const findVirtualNodeById = (node: VirtualTreeNode<T>, targetId: string): VirtualTreeNode<T> | null => {
|
|
162
|
-
if (node.id === targetId) {
|
|
163
|
-
return node;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (!node.isLeaf && node.children) {
|
|
167
|
-
for (const child of node.children.values()) {
|
|
168
|
-
const found = findVirtualNodeById(child, targetId);
|
|
169
|
-
if (found) return found;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return null;
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
// Search through all top-level children to find the target
|
|
177
|
-
let targetNode: VirtualTreeNode<T> | null = null;
|
|
178
|
-
for (const child of virtualTree.children.values()) {
|
|
179
|
-
targetNode = findVirtualNodeById(child, rootPath);
|
|
180
|
-
if (targetNode) break;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (!targetNode) {
|
|
184
|
-
return []; // Target node not found
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// If hideRootNode is true, show the target's children instead of the target itself
|
|
188
|
-
if (hideRootNode && !targetNode.isLeaf && targetNode.children) {
|
|
189
|
-
return Array.from(targetNode.children.values());
|
|
190
|
-
} else {
|
|
191
|
-
// Show the target node as the new root
|
|
192
|
-
return [targetNode];
|
|
193
|
-
}
|
|
194
|
-
}, [virtualTree, rootPath, hideRootNode]);
|
|
195
|
-
|
|
196
|
-
// Create a map of pending resource IDs for quick lookup
|
|
197
|
-
const pendingResourceMap = useMemo(() => {
|
|
198
|
-
const map = new Map<string, PendingResource<T>>();
|
|
199
|
-
pendingResources?.forEach((pr) => {
|
|
200
|
-
map.set(pr.id, pr);
|
|
201
|
-
});
|
|
202
|
-
return map;
|
|
203
|
-
}, [pendingResources]);
|
|
204
|
-
|
|
205
|
-
const toggleNode = useCallback((nodeId: string) => {
|
|
206
|
-
setExpandedNodes((prev) => {
|
|
207
|
-
const newSet = new Set(prev);
|
|
208
|
-
if (newSet.has(nodeId)) {
|
|
209
|
-
newSet.delete(nodeId);
|
|
210
|
-
} else {
|
|
211
|
-
newSet.add(nodeId);
|
|
212
|
-
}
|
|
213
|
-
return newSet;
|
|
214
|
-
});
|
|
215
|
-
}, []);
|
|
216
|
-
|
|
217
|
-
const renderTreeNode = (node: VirtualTreeNode<T>, level: number = 0): React.ReactElement | null => {
|
|
218
|
-
const isExpanded = expandedNodes.has(node.id);
|
|
219
|
-
const isSelected = selectedResourceId === node.id;
|
|
220
|
-
const nodeIdLower = node.id.toLowerCase();
|
|
221
|
-
const searchLower = searchTerm.toLowerCase();
|
|
222
|
-
const matchesSearch = !searchTerm || nodeIdLower.includes(searchLower);
|
|
223
|
-
const isPending = node.isPending;
|
|
224
|
-
const pendingResource = node.pendingResource;
|
|
225
|
-
const displayName = node.name; // Virtual node already has the correct display name
|
|
226
|
-
|
|
227
|
-
// Determine if this resource should be shown (not deleted)
|
|
228
|
-
const isDeleted = pendingResource?.type === 'deleted';
|
|
229
|
-
if (isDeleted) {
|
|
230
|
-
return null; // Don't render deleted resources
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Check if any children match
|
|
234
|
-
let hasMatchingChildren = false;
|
|
235
|
-
if (!node.isLeaf && node.children && searchTerm) {
|
|
236
|
-
const checkChildren = (n: VirtualTreeNode): boolean => {
|
|
237
|
-
if (n.id.toLowerCase().includes(searchLower)) return true;
|
|
238
|
-
if (!n.isLeaf && n.children) {
|
|
239
|
-
for (const child of n.children.values()) {
|
|
240
|
-
if (checkChildren(child)) return true;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
return false;
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
for (const child of node.children.values()) {
|
|
247
|
-
if (checkChildren(child)) {
|
|
248
|
-
hasMatchingChildren = true;
|
|
249
|
-
break;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Hide nodes that don't match search and don't have matching children
|
|
255
|
-
if (searchTerm && !matchesSearch && !hasMatchingChildren) {
|
|
256
|
-
return null;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
return (
|
|
260
|
-
<div key={node.id} className="select-none">
|
|
261
|
-
<div
|
|
262
|
-
className={`
|
|
263
|
-
flex items-center px-2 py-1 cursor-pointer hover:bg-gray-100
|
|
264
|
-
${isSelected ? 'bg-purple-50 border-l-2 border-purple-500' : ''}
|
|
265
|
-
${matchesSearch && searchTerm ? 'bg-yellow-50' : ''}
|
|
266
|
-
${
|
|
267
|
-
isPending && pendingResource?.type === 'new'
|
|
268
|
-
? 'bg-emerald-25 border-l-2 border-emerald-300'
|
|
269
|
-
: ''
|
|
270
|
-
}
|
|
271
|
-
${
|
|
272
|
-
isPending && pendingResource?.type === 'modified'
|
|
273
|
-
? 'bg-amber-25 border-l-2 border-amber-300'
|
|
274
|
-
: ''
|
|
275
|
-
}
|
|
276
|
-
`}
|
|
277
|
-
style={{ paddingLeft: `${level * 20 + 8}px` }}
|
|
278
|
-
onClick={() => {
|
|
279
|
-
if (node.isLeaf) {
|
|
280
|
-
onResourceSelect({
|
|
281
|
-
resourceId: node.id,
|
|
282
|
-
resourceData: node.pendingResource?.resourceData,
|
|
283
|
-
isPending: node.isPending,
|
|
284
|
-
pendingType: node.pendingResource?.type
|
|
285
|
-
});
|
|
286
|
-
} else {
|
|
287
|
-
toggleNode(node.id);
|
|
288
|
-
}
|
|
289
|
-
}}
|
|
290
|
-
>
|
|
291
|
-
{/* Expand/Collapse chevron */}
|
|
292
|
-
{!node.isLeaf && (
|
|
293
|
-
<button
|
|
294
|
-
onClick={(e) => {
|
|
295
|
-
e.stopPropagation();
|
|
296
|
-
toggleNode(node.id);
|
|
297
|
-
}}
|
|
298
|
-
className="mr-1 hover:bg-gray-200 rounded p-0.5"
|
|
299
|
-
>
|
|
300
|
-
{isExpanded ? (
|
|
301
|
-
<ChevronDownIcon className="w-3 h-3 text-gray-600" />
|
|
302
|
-
) : (
|
|
303
|
-
<ChevronRightIcon className="w-3 h-3 text-gray-600" />
|
|
304
|
-
)}
|
|
305
|
-
</button>
|
|
306
|
-
)}
|
|
307
|
-
|
|
308
|
-
{/* Spacer for alignment when no children */}
|
|
309
|
-
{node.isLeaf && <div className="w-4 mr-1" />}
|
|
310
|
-
|
|
311
|
-
{/* Folder/Document icon */}
|
|
312
|
-
{node.isLeaf ? (
|
|
313
|
-
<DocumentTextIcon className="w-4 h-4 text-green-500 mr-2 flex-shrink-0" />
|
|
314
|
-
) : isExpanded ? (
|
|
315
|
-
<FolderOpenIcon className="w-4 h-4 text-blue-500 mr-2 flex-shrink-0" />
|
|
316
|
-
) : (
|
|
317
|
-
<FolderIcon className="w-4 h-4 text-blue-500 mr-2 flex-shrink-0" />
|
|
318
|
-
)}
|
|
319
|
-
|
|
320
|
-
{/* Node name with search highlighting */}
|
|
321
|
-
<span
|
|
322
|
-
className={`
|
|
323
|
-
text-sm truncate flex-1
|
|
324
|
-
${isSelected ? 'font-medium text-purple-900' : 'text-gray-700'}
|
|
325
|
-
${isPending && pendingResource?.type === 'new' ? 'font-medium text-emerald-800' : ''}
|
|
326
|
-
${isPending && pendingResource?.type === 'modified' ? 'font-medium text-amber-800' : ''}
|
|
327
|
-
${matchesSearch && searchTerm ? 'font-medium' : ''}
|
|
328
|
-
`}
|
|
329
|
-
title={node.id}
|
|
330
|
-
>
|
|
331
|
-
{searchTerm ? <HighlightedText text={displayName} searchTerm={searchTerm} /> : displayName}
|
|
332
|
-
</span>
|
|
333
|
-
|
|
334
|
-
{/* Annotations for any nodes */}
|
|
335
|
-
{resourceAnnotations?.[node.id] && (
|
|
336
|
-
<div className="flex items-center gap-1 ml-2">
|
|
337
|
-
{renderAnnotation(resourceAnnotations[node.id])}
|
|
338
|
-
</div>
|
|
339
|
-
)}
|
|
340
|
-
|
|
341
|
-
{/* Show child count for branches */}
|
|
342
|
-
{!node.isLeaf && node.children && (
|
|
343
|
-
<span className="ml-2 text-xs text-gray-500">({node.children.size})</span>
|
|
344
|
-
)}
|
|
345
|
-
</div>
|
|
346
|
-
|
|
347
|
-
{/* Render children if expanded */}
|
|
348
|
-
{!node.isLeaf && node.children && isExpanded && (
|
|
349
|
-
<div>
|
|
350
|
-
{Array.from(node.children.values())
|
|
351
|
-
.sort((a: VirtualTreeNode<T>, b: VirtualTreeNode<T>) => {
|
|
352
|
-
// Sort folders first, then by name
|
|
353
|
-
if (a.isLeaf !== b.isLeaf) {
|
|
354
|
-
return a.isLeaf ? 1 : -1;
|
|
355
|
-
}
|
|
356
|
-
return a.name.localeCompare(b.name);
|
|
357
|
-
})
|
|
358
|
-
.map((child) => renderTreeNode(child, level + 1))}
|
|
359
|
-
</div>
|
|
360
|
-
)}
|
|
361
|
-
</div>
|
|
362
|
-
);
|
|
363
|
-
};
|
|
364
|
-
|
|
365
|
-
const renderAnnotation = (annotation: any) => {
|
|
366
|
-
const elements: React.ReactNode[] = [];
|
|
367
|
-
|
|
368
|
-
if (annotation.indicator) {
|
|
369
|
-
elements.push(
|
|
370
|
-
<span key="indicator" className="text-xs" title={annotation.indicator.tooltip}>
|
|
371
|
-
{annotation.indicator.type === 'dot' ? (
|
|
372
|
-
<span className="text-orange-500">●</span>
|
|
373
|
-
) : (
|
|
374
|
-
annotation.indicator.value
|
|
375
|
-
)}
|
|
376
|
-
</span>
|
|
377
|
-
);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
if (annotation.badge) {
|
|
381
|
-
const getBadgeClasses = (variant: string) => {
|
|
382
|
-
const baseClasses = 'px-1.5 py-0.5 text-xs font-medium rounded';
|
|
383
|
-
const variantClasses: Record<string, string> = {
|
|
384
|
-
info: 'bg-blue-100 text-blue-800',
|
|
385
|
-
warning: 'bg-yellow-100 text-yellow-800',
|
|
386
|
-
success: 'bg-green-100 text-green-800',
|
|
387
|
-
error: 'bg-red-100 text-red-800',
|
|
388
|
-
edited: 'bg-purple-100 text-purple-800',
|
|
389
|
-
new: 'bg-emerald-100 text-emerald-800'
|
|
390
|
-
};
|
|
391
|
-
return `${baseClasses} ${variantClasses[variant] || variantClasses.info}`;
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
elements.push(
|
|
395
|
-
<span key="badge" className={getBadgeClasses(annotation.badge.variant)}>
|
|
396
|
-
{annotation.badge.text}
|
|
397
|
-
</span>
|
|
398
|
-
);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
if (annotation.suffix) {
|
|
402
|
-
elements.push(
|
|
403
|
-
<span key="suffix" className="text-xs text-gray-500">
|
|
404
|
-
{annotation.suffix}
|
|
405
|
-
</span>
|
|
406
|
-
);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
return elements;
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
if (!virtualTree || effectiveRootNodes.length === 0) {
|
|
413
|
-
return (
|
|
414
|
-
<div className={`${className} p-4 text-center text-gray-500`}>
|
|
415
|
-
<p>{emptyMessage}</p>
|
|
416
|
-
</div>
|
|
417
|
-
);
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
return (
|
|
421
|
-
<div className={`${className} overflow-y-auto`}>
|
|
422
|
-
{effectiveRootNodes
|
|
423
|
-
.sort((a: VirtualTreeNode, b: VirtualTreeNode) => {
|
|
424
|
-
// Sort folders first, then by name
|
|
425
|
-
if (a.isLeaf !== b.isLeaf) {
|
|
426
|
-
return a.isLeaf ? 1 : -1;
|
|
427
|
-
}
|
|
428
|
-
return a.name.localeCompare(b.name);
|
|
429
|
-
})
|
|
430
|
-
.map((child) => renderTreeNode(child))}
|
|
431
|
-
</div>
|
|
432
|
-
);
|
|
433
|
-
};
|
|
434
|
-
|
|
435
|
-
/**
|
|
436
|
-
* Component to highlight search terms in text
|
|
437
|
-
*/
|
|
438
|
-
const HighlightedText: React.FC<{ text: string; searchTerm: string }> = ({ text, searchTerm }) => {
|
|
439
|
-
if (!searchTerm) {
|
|
440
|
-
return <span>{text}</span>;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
const regex = new RegExp(`(${searchTerm})`, 'gi');
|
|
444
|
-
const parts = text.split(regex);
|
|
445
|
-
|
|
446
|
-
return (
|
|
447
|
-
<span>
|
|
448
|
-
{parts.map((part, index) =>
|
|
449
|
-
regex.test(part) ? (
|
|
450
|
-
<mark key={index} className="bg-yellow-200">
|
|
451
|
-
{part}
|
|
452
|
-
</mark>
|
|
453
|
-
) : (
|
|
454
|
-
<span key={index}>{part}</span>
|
|
455
|
-
)
|
|
456
|
-
)}
|
|
457
|
-
</span>
|
|
458
|
-
);
|
|
459
|
-
};
|
|
460
|
-
|
|
461
|
-
export default ResourcePickerTree;
|