@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.
Files changed (192) hide show
  1. package/README.md +401 -155
  2. package/config/jest.setup.js +10 -0
  3. package/dist/ts-res-ui-components.d.ts +1657 -76
  4. package/lib/components/common/QualifierContextControl.js +4 -1
  5. package/lib/components/common/ResourceTreeView.js +4 -1
  6. package/lib/components/forms/GenericQualifierTypeEditForm.d.ts +26 -0
  7. package/lib/components/forms/GenericQualifierTypeEditForm.js +166 -0
  8. package/lib/components/forms/QualifierEditForm.d.ts +1 -1
  9. package/lib/components/forms/index.d.ts +2 -0
  10. package/lib/components/forms/index.js +1 -0
  11. package/lib/components/orchestrator/ResourceOrchestrator.d.ts +3 -0
  12. package/lib/components/orchestrator/ResourceOrchestrator.js +118 -51
  13. package/lib/components/pickers/ResourcePicker/ResourcePickerTree.js +32 -10
  14. package/lib/components/pickers/ResourcePicker/index.js +4 -2
  15. package/lib/components/views/CompiledView/index.js +75 -16
  16. package/lib/components/views/ConfigurationView/index.js +94 -35
  17. package/lib/components/views/FilterView/index.js +7 -4
  18. package/lib/components/views/GridView/EditableGridCell.d.ts +76 -0
  19. package/lib/components/views/GridView/EditableGridCell.js +224 -0
  20. package/lib/components/views/GridView/GridSelector.d.ts +43 -0
  21. package/lib/components/views/GridView/GridSelector.js +89 -0
  22. package/lib/components/views/GridView/MultiGridView.d.ts +85 -0
  23. package/lib/components/views/GridView/MultiGridView.js +196 -0
  24. package/lib/components/views/GridView/ResourceGrid.d.ts +38 -0
  25. package/lib/components/views/GridView/ResourceGrid.js +232 -0
  26. package/lib/components/views/GridView/SharedContextControls.d.ts +47 -0
  27. package/lib/components/views/GridView/SharedContextControls.js +95 -0
  28. package/lib/components/views/GridView/cells/BooleanCell.d.ts +44 -0
  29. package/lib/components/views/GridView/cells/BooleanCell.js +49 -0
  30. package/lib/components/views/GridView/cells/DropdownCell.d.ts +58 -0
  31. package/lib/components/views/GridView/cells/DropdownCell.js +182 -0
  32. package/lib/components/views/GridView/cells/StringCell.d.ts +57 -0
  33. package/lib/components/views/GridView/cells/StringCell.js +106 -0
  34. package/lib/components/views/GridView/cells/TriStateCell.d.ts +54 -0
  35. package/lib/components/views/GridView/cells/TriStateCell.js +112 -0
  36. package/lib/components/views/GridView/cells/index.d.ts +15 -0
  37. package/lib/components/views/GridView/cells/index.js +11 -0
  38. package/lib/components/views/GridView/index.d.ts +53 -0
  39. package/lib/components/views/GridView/index.js +212 -0
  40. package/lib/components/views/ImportView/index.js +22 -19
  41. package/lib/components/views/MessagesWindow/index.js +4 -1
  42. package/lib/components/views/ResolutionView/index.js +8 -5
  43. package/lib/contexts/ObservabilityContext.d.ts +85 -0
  44. package/lib/contexts/ObservabilityContext.js +98 -0
  45. package/lib/contexts/index.d.ts +2 -0
  46. package/lib/contexts/index.js +24 -0
  47. package/lib/hooks/useConfigurationState.d.ts +3 -3
  48. package/lib/hooks/useResolutionState.js +850 -246
  49. package/lib/hooks/useResourceData.d.ts +7 -4
  50. package/lib/hooks/useResourceData.js +185 -184
  51. package/lib/index.d.ts +5 -1
  52. package/lib/index.js +8 -1
  53. package/lib/namespaces/GridTools.d.ts +136 -0
  54. package/lib/namespaces/GridTools.js +138 -0
  55. package/lib/namespaces/ObservabilityTools.d.ts +3 -0
  56. package/lib/namespaces/ObservabilityTools.js +23 -0
  57. package/lib/namespaces/ResolutionTools.d.ts +2 -1
  58. package/lib/namespaces/ResolutionTools.js +2 -0
  59. package/lib/namespaces/index.d.ts +2 -0
  60. package/lib/namespaces/index.js +2 -0
  61. package/lib/test/integration/observability.integration.test.d.ts +2 -0
  62. package/lib/test/unit/hooks/useResourceData.test.d.ts +2 -0
  63. package/lib/test/unit/utils/downloadHelper.test.d.ts +2 -0
  64. package/lib/test/unit/workflows/resolutionWorkflows.test.d.ts +2 -0
  65. package/lib/test/unit/workflows/resourceCreation.test.d.ts +2 -0
  66. package/lib/test/unit/workflows/resultPatternExtensions.test.d.ts +2 -0
  67. package/lib/test/unit/workflows/validation.test.d.ts +2 -0
  68. package/lib/types/index.d.ts +387 -20
  69. package/lib/types/index.js +2 -1
  70. package/lib/utils/cellValidation.d.ts +113 -0
  71. package/lib/utils/cellValidation.js +248 -0
  72. package/lib/utils/downloadHelper.d.ts +66 -0
  73. package/lib/utils/downloadHelper.js +195 -0
  74. package/lib/utils/observability/factories.d.ts +29 -0
  75. package/lib/utils/observability/factories.js +58 -0
  76. package/lib/utils/observability/implementations.d.ts +61 -0
  77. package/lib/utils/observability/implementations.js +103 -0
  78. package/lib/utils/observability/index.d.ts +4 -0
  79. package/lib/utils/observability/index.js +26 -0
  80. package/lib/utils/observability/interfaces.d.ts +30 -0
  81. package/lib/utils/observability/interfaces.js +23 -0
  82. package/lib/utils/resolutionEditing.js +2 -1
  83. package/lib/utils/resourceSelector.d.ts +97 -0
  84. package/lib/utils/resourceSelector.js +195 -0
  85. package/lib/utils/resourceSelectors.d.ts +146 -0
  86. package/lib/utils/resourceSelectors.js +233 -0
  87. package/lib/utils/tsResIntegration.d.ts +6 -41
  88. package/lib/utils/tsResIntegration.js +20 -16
  89. package/lib/utils/zipLoader/zipProcessingHelpers.d.ts +3 -2
  90. package/lib/utils/zipLoader/zipProcessingHelpers.js +6 -5
  91. package/lib-commonjs/components/common/QualifierContextControl.js +4 -1
  92. package/lib-commonjs/components/common/ResourceTreeView.js +4 -1
  93. package/lib-commonjs/components/forms/GenericQualifierTypeEditForm.js +171 -0
  94. package/lib-commonjs/components/forms/index.js +3 -1
  95. package/lib-commonjs/components/orchestrator/ResourceOrchestrator.js +118 -51
  96. package/lib-commonjs/components/pickers/ResourcePicker/ResourcePickerTree.js +32 -10
  97. package/lib-commonjs/components/pickers/ResourcePicker/index.js +4 -2
  98. package/lib-commonjs/components/views/CompiledView/index.js +75 -16
  99. package/lib-commonjs/components/views/ConfigurationView/index.js +93 -34
  100. package/lib-commonjs/components/views/FilterView/index.js +7 -4
  101. package/lib-commonjs/components/views/GridView/EditableGridCell.js +232 -0
  102. package/lib-commonjs/components/views/GridView/GridSelector.js +94 -0
  103. package/lib-commonjs/components/views/GridView/MultiGridView.js +201 -0
  104. package/lib-commonjs/components/views/GridView/ResourceGrid.js +237 -0
  105. package/lib-commonjs/components/views/GridView/SharedContextControls.js +100 -0
  106. package/lib-commonjs/components/views/GridView/cells/BooleanCell.js +54 -0
  107. package/lib-commonjs/components/views/GridView/cells/DropdownCell.js +187 -0
  108. package/lib-commonjs/components/views/GridView/cells/StringCell.js +111 -0
  109. package/lib-commonjs/components/views/GridView/cells/TriStateCell.js +117 -0
  110. package/lib-commonjs/components/views/GridView/cells/index.js +18 -0
  111. package/lib-commonjs/components/views/GridView/index.js +217 -0
  112. package/lib-commonjs/components/views/ImportView/index.js +22 -19
  113. package/lib-commonjs/components/views/MessagesWindow/index.js +4 -1
  114. package/lib-commonjs/components/views/ResolutionView/index.js +8 -5
  115. package/lib-commonjs/contexts/ObservabilityContext.js +104 -0
  116. package/lib-commonjs/contexts/index.js +30 -0
  117. package/lib-commonjs/hooks/useResolutionState.js +849 -245
  118. package/lib-commonjs/hooks/useResourceData.js +184 -215
  119. package/lib-commonjs/index.js +15 -1
  120. package/lib-commonjs/namespaces/GridTools.js +161 -0
  121. package/lib-commonjs/namespaces/ObservabilityTools.js +33 -0
  122. package/lib-commonjs/namespaces/ResolutionTools.js +10 -1
  123. package/lib-commonjs/namespaces/index.js +3 -1
  124. package/lib-commonjs/types/index.js +10 -0
  125. package/lib-commonjs/utils/cellValidation.js +253 -0
  126. package/lib-commonjs/utils/downloadHelper.js +198 -0
  127. package/lib-commonjs/utils/observability/factories.js +63 -0
  128. package/lib-commonjs/utils/observability/implementations.js +109 -0
  129. package/lib-commonjs/utils/observability/index.js +36 -0
  130. package/lib-commonjs/utils/observability/interfaces.js +24 -0
  131. package/lib-commonjs/utils/resolutionEditing.js +2 -1
  132. package/lib-commonjs/utils/resourceSelector.js +200 -0
  133. package/lib-commonjs/utils/resourceSelectors.js +242 -0
  134. package/lib-commonjs/utils/tsResIntegration.js +21 -16
  135. package/lib-commonjs/utils/zipLoader/zipProcessingHelpers.js +7 -5
  136. package/package.json +7 -7
  137. package/src/components/common/QualifierContextControl.tsx +0 -338
  138. package/src/components/common/ResolutionContextOptionsControl.tsx +0 -450
  139. package/src/components/common/ResolutionResults/index.tsx +0 -481
  140. package/src/components/common/ResourceListView.tsx +0 -167
  141. package/src/components/common/ResourcePickerOptionsControl.tsx +0 -351
  142. package/src/components/common/ResourceTreeView.tsx +0 -417
  143. package/src/components/common/SourceResourceDetail/index.tsx +0 -493
  144. package/src/components/forms/HierarchyEditor.tsx +0 -285
  145. package/src/components/forms/QualifierEditForm.tsx +0 -487
  146. package/src/components/forms/QualifierTypeEditForm.tsx +0 -458
  147. package/src/components/forms/ResourceTypeEditForm.tsx +0 -437
  148. package/src/components/forms/index.ts +0 -11
  149. package/src/components/orchestrator/ResourceOrchestrator.tsx +0 -444
  150. package/src/components/pickers/ResourcePicker/README.md +0 -570
  151. package/src/components/pickers/ResourcePicker/ResourceItem.tsx +0 -127
  152. package/src/components/pickers/ResourcePicker/ResourcePickerList.tsx +0 -114
  153. package/src/components/pickers/ResourcePicker/ResourcePickerTree.tsx +0 -461
  154. package/src/components/pickers/ResourcePicker/index.tsx +0 -234
  155. package/src/components/pickers/ResourcePicker/types.ts +0 -301
  156. package/src/components/pickers/ResourcePicker/utils/treeNavigation.ts +0 -210
  157. package/src/components/views/CompiledView/index.tsx +0 -1342
  158. package/src/components/views/ConfigurationView/index.tsx +0 -848
  159. package/src/components/views/FilterView/index.tsx +0 -681
  160. package/src/components/views/ImportView/index.tsx +0 -789
  161. package/src/components/views/MessagesWindow/index.tsx +0 -325
  162. package/src/components/views/ResolutionView/EditableJsonView.tsx +0 -386
  163. package/src/components/views/ResolutionView/NewResourceModal.tsx +0 -158
  164. package/src/components/views/ResolutionView/UnifiedChangeControls.tsx +0 -163
  165. package/src/components/views/ResolutionView/index.tsx +0 -751
  166. package/src/components/views/SourceView/index.tsx +0 -291
  167. package/src/hooks/useConfigurationState.ts +0 -436
  168. package/src/hooks/useFilterState.ts +0 -150
  169. package/src/hooks/useResolutionState.ts +0 -893
  170. package/src/hooks/useResourceData.ts +0 -596
  171. package/src/hooks/useViewState.ts +0 -97
  172. package/src/index.ts +0 -68
  173. package/src/namespaces/ConfigurationTools.ts +0 -59
  174. package/src/namespaces/FilterTools.ts +0 -47
  175. package/src/namespaces/ImportTools.ts +0 -42
  176. package/src/namespaces/PickerTools.ts +0 -104
  177. package/src/namespaces/ResolutionTools.ts +0 -68
  178. package/src/namespaces/ResourceTools.ts +0 -106
  179. package/src/namespaces/TsResTools.ts +0 -49
  180. package/src/namespaces/ViewStateTools.ts +0 -91
  181. package/src/namespaces/ZipTools.ts +0 -49
  182. package/src/namespaces/index.ts +0 -49
  183. package/src/types/index.ts +0 -1273
  184. package/src/utils/configurationUtils.ts +0 -339
  185. package/src/utils/fileProcessing.ts +0 -164
  186. package/src/utils/filterResources.ts +0 -356
  187. package/src/utils/resolutionEditing.ts +0 -346
  188. package/src/utils/resolutionUtils.ts +0 -740
  189. package/src/utils/tsResIntegration.ts +0 -475
  190. package/src/utils/zipLoader/index.ts +0 -5
  191. package/src/utils/zipLoader/zipProcessingHelpers.ts +0 -46
  192. 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;