@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
package/README.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Name
|
|
2
|
+
### @fgv/ts-res-ui-components
|
|
3
|
+
|
|
4
|
+
# Synopsis
|
|
5
|
+
Reusable React components for ts-res resource visualization and management
|
|
6
|
+
|
|
7
|
+
# Description
|
|
8
|
+
|
|
9
|
+
# Example
|
|
10
|
+
|
|
11
|
+
# Install:
|
|
12
|
+
`npm install @fgv/ts-res-ui-components`
|
|
13
|
+
|
|
14
|
+
# Test:
|
|
15
|
+
`npm test`
|
|
16
|
+
|
|
17
|
+
#License:
|
|
18
|
+
MIT
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# ts-res-ui-components Refactoring Plan
|
|
2
|
+
|
|
3
|
+
## Problem Analysis
|
|
4
|
+
|
|
5
|
+
### Current Issues
|
|
6
|
+
The ts-res-ui-components library has accumulated technical debt through attempts to optimize and short-circuit the proper data flow:
|
|
7
|
+
- Widespread use of `any` types (30+ instances in production code)
|
|
8
|
+
- Unsafe type assertions and casts
|
|
9
|
+
- Complex branching logic to handle special cases
|
|
10
|
+
- Polymorphic handling where ResourceManager sometimes contains ResourceManagerBuilder and sometimes CompiledResourceCollection
|
|
11
|
+
- The orchestrator contains business logic instead of pure coordination
|
|
12
|
+
|
|
13
|
+
### Root Cause
|
|
14
|
+
The bundle import feature created a special case that broke the clean flow. Instead of normalizing bundles into the standard pipeline, the code tried to inject CompiledResourceCollection directly into the middle of the flow, leading to type confusion and complex branching.
|
|
15
|
+
|
|
16
|
+
## Architecture Overview
|
|
17
|
+
|
|
18
|
+
### Core Principle: One Clean Flow
|
|
19
|
+
All operations must follow the same pipeline, with no shortcuts or special cases:
|
|
20
|
+
```
|
|
21
|
+
Configuration → ResourceManagerBuilder → [Filter?] → Filtered RMB → CompiledCollection → Resolver
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Fundamental Entities
|
|
25
|
+
|
|
26
|
+
1. **Configuration** (`ISystemConfiguration`)
|
|
27
|
+
- Must exist first
|
|
28
|
+
- Defines qualifier types, qualifiers, and resource types
|
|
29
|
+
- Controls how imports are processed
|
|
30
|
+
|
|
31
|
+
2. **Primary ResourceManagerBuilder**
|
|
32
|
+
- The source of truth for all resources
|
|
33
|
+
- Created by importing files using the configuration
|
|
34
|
+
- All modifications create clones, never mutate
|
|
35
|
+
|
|
36
|
+
3. **Filter Context** (`Map<QualifierName, string | undefined>`)
|
|
37
|
+
- Optional qualifier values for filtering
|
|
38
|
+
- Reduces the resource space
|
|
39
|
+
- Creates a subset of resources/candidates
|
|
40
|
+
|
|
41
|
+
4. **Filtered ResourceManagerBuilder**
|
|
42
|
+
- Clone of primary with filter parameters applied
|
|
43
|
+
- If no filter, this is just a reference to primary (no clone needed)
|
|
44
|
+
- Still a ResourceManagerBuilder, maintaining type consistency
|
|
45
|
+
|
|
46
|
+
### Derived Entities
|
|
47
|
+
|
|
48
|
+
5. **CompiledResourceCollection**
|
|
49
|
+
- Always derived from the current ResourceManagerBuilder (filtered or primary)
|
|
50
|
+
- Pre-compiled for efficient runtime access
|
|
51
|
+
- Regenerated when the source ResourceManagerBuilder changes
|
|
52
|
+
|
|
53
|
+
6. **ResourceResolver**
|
|
54
|
+
- Created from ResourceManagerBuilder + Resolution Context
|
|
55
|
+
- Resolution Context is a DIFFERENT `Map<QualifierName, string | undefined>` than Filter Context
|
|
56
|
+
- Resolves resources based on the resolution context
|
|
57
|
+
|
|
58
|
+
### Key Insight: Two Different Contexts
|
|
59
|
+
- **Filter Context**: Reduces the resource space (affects what resources/candidates exist)
|
|
60
|
+
- **Resolution Context**: Resolves within that space (determines which candidate wins)
|
|
61
|
+
|
|
62
|
+
## Import Normalization
|
|
63
|
+
|
|
64
|
+
All import types must produce a ResourceManagerBuilder:
|
|
65
|
+
|
|
66
|
+
1. **Configuration Import**
|
|
67
|
+
- Sets up the system configuration
|
|
68
|
+
- Prerequisite for all other imports
|
|
69
|
+
|
|
70
|
+
2. **Resource Files/Directories**
|
|
71
|
+
- Import using configuration → ResourceManagerBuilder
|
|
72
|
+
|
|
73
|
+
3. **Bundle Import** (the fix)
|
|
74
|
+
- Extract configuration
|
|
75
|
+
- Extract CompiledResourceCollection
|
|
76
|
+
- **Reconstruct ResourceManagerBuilder from CompiledResourceCollection**
|
|
77
|
+
- Continue with normal flow
|
|
78
|
+
|
|
79
|
+
4. **ZIP File Import**
|
|
80
|
+
- Extract configuration
|
|
81
|
+
- Extract resource files
|
|
82
|
+
- Import files using configuration → ResourceManagerBuilder
|
|
83
|
+
|
|
84
|
+
## Operations via Cloning
|
|
85
|
+
|
|
86
|
+
All modifications create new instances via cloning:
|
|
87
|
+
|
|
88
|
+
1. **Filtering**
|
|
89
|
+
- Clone primary ResourceManagerBuilder
|
|
90
|
+
- Apply filter parameters
|
|
91
|
+
- Produce filtered ResourceManagerBuilder
|
|
92
|
+
|
|
93
|
+
2. **Apply Edits** (from Resolution Viewer)
|
|
94
|
+
- Clone current ResourceManagerBuilder
|
|
95
|
+
- Pass in new conditions/values
|
|
96
|
+
- Let ResourceManagerBuilder do the work
|
|
97
|
+
- Produce updated ResourceManagerBuilder
|
|
98
|
+
|
|
99
|
+
3. **No Filter Case**
|
|
100
|
+
- Filtered ResourceManagerBuilder = reference to Primary
|
|
101
|
+
- No cloning needed, just point to primary
|
|
102
|
+
|
|
103
|
+
## Implementation Guidelines
|
|
104
|
+
|
|
105
|
+
### Result Pattern Usage
|
|
106
|
+
- **Follow RESULT_PATTERN_GUIDE.md** in the root directory for all Result pattern usage
|
|
107
|
+
- Avoid intermediate result variables when possible
|
|
108
|
+
- Use `.onSuccess()` and `.onFailure()` for chaining operations
|
|
109
|
+
- Use `.orDefault()` for acceptable undefined/default values
|
|
110
|
+
- Use `.orThrow()` only in setup/test code, not production paths
|
|
111
|
+
- Use `captureResult` around code that might throw to convert to Results
|
|
112
|
+
|
|
113
|
+
### Type Safety and Validation
|
|
114
|
+
- **NEVER use unsafe casts**, especially for structs
|
|
115
|
+
- Use existing Converters or Validators from ts-res/ts-utils:
|
|
116
|
+
- Converters: Transform and construct new objects (for simple types, primitives)
|
|
117
|
+
- Validators: Validate existing objects without construction (for complex objects, class instances)
|
|
118
|
+
- If no converter/validator exists for a type, **create one** rather than using unsafe casts
|
|
119
|
+
- Example: Instead of `as ResourceType`, use `Convert.resourceType(value)` or create a new converter
|
|
120
|
+
|
|
121
|
+
## Implementation Plan
|
|
122
|
+
|
|
123
|
+
### Phase 1: Type System Cleanup
|
|
124
|
+
- Define proper TypeScript interfaces for all entities
|
|
125
|
+
- Replace all `any` types with specific types (using JsonValue for JSON data)
|
|
126
|
+
- Create validators/converters for new types as needed
|
|
127
|
+
- Establish clear type boundaries between components
|
|
128
|
+
- Import proper types from ts-res (QualifierTypes, Qualifiers, ResourceTypes, etc.)
|
|
129
|
+
|
|
130
|
+
### Phase 2: Import Flow Normalization
|
|
131
|
+
- Refactor bundle import to reconstruct ResourceManagerBuilder
|
|
132
|
+
- Remove special case handling for bundles
|
|
133
|
+
- Ensure all import paths produce ResourceManagerBuilder
|
|
134
|
+
- Eliminate polymorphic resource manager handling
|
|
135
|
+
|
|
136
|
+
### Phase 3: Clone-Based Operations
|
|
137
|
+
- Implement clone-based filtering
|
|
138
|
+
- Implement clone-based edit application
|
|
139
|
+
- Ensure primary ResourceManagerBuilder is never mutated
|
|
140
|
+
- Remove complex branching logic
|
|
141
|
+
|
|
142
|
+
### Phase 4: Orchestrator Simplification
|
|
143
|
+
- Extract business logic to dedicated services
|
|
144
|
+
- Make orchestrator a pure coordination layer
|
|
145
|
+
- Simplify state management
|
|
146
|
+
- Remove redundant effects and callbacks
|
|
147
|
+
|
|
148
|
+
### Phase 5: Testing & Validation
|
|
149
|
+
- Test each import type follows the same flow
|
|
150
|
+
- Verify filtering and editing via cloning
|
|
151
|
+
- Ensure type safety throughout
|
|
152
|
+
- Validate no regressions in functionality
|
|
153
|
+
|
|
154
|
+
## Success Criteria
|
|
155
|
+
|
|
156
|
+
1. **Zero `any` types** in production code
|
|
157
|
+
2. **Single flow path** for all operations
|
|
158
|
+
3. **Type-safe** throughout with proper interfaces
|
|
159
|
+
4. **No special cases** or branching for different import types
|
|
160
|
+
5. **Pure orchestrator** that only coordinates, no business logic
|
|
161
|
+
6. **Immutable operations** via cloning, no mutations
|
|
162
|
+
7. **Clear separation** between Filter Context and Resolution Context
|
|
163
|
+
|
|
164
|
+
## Expected Benefits
|
|
165
|
+
|
|
166
|
+
- **Type Safety**: Full TypeScript benefits with compile-time checking
|
|
167
|
+
- **Maintainability**: Clear, predictable flow without special cases
|
|
168
|
+
- **Debuggability**: Linear flow easier to trace and debug
|
|
169
|
+
- **Extensibility**: New features follow established patterns
|
|
170
|
+
- **Performance**: Simpler code paths, less branching
|
|
171
|
+
- **Reliability**: Fewer edge cases and error conditions
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "@rushstack/heft-web-rig/profiles/library/config/jest.config.json",
|
|
3
|
+
"coveragePathIgnorePatterns": ["index.js", "public.js", "internal.js"],
|
|
4
|
+
"coverageThreshold": {
|
|
5
|
+
"global": {
|
|
6
|
+
"branches": 0,
|
|
7
|
+
"functions": 0,
|
|
8
|
+
"lines": 0,
|
|
9
|
+
"statements": 0
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"collectCoverage": true,
|
|
13
|
+
"coverageReporters": ["text", "lcov", "html"],
|
|
14
|
+
"setupFilesAfterEnv": ["<rootDir>/config/jest.setup.js"],
|
|
15
|
+
"testEnvironment": "jsdom"
|
|
16
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// Setup file for Jest tests
|
|
2
|
+
// Required for ts-utils which uses TextEncoder/TextDecoder
|
|
3
|
+
|
|
4
|
+
// Polyfill TextEncoder and TextDecoder for Node.js environment
|
|
5
|
+
if (typeof TextEncoder === 'undefined') {
|
|
6
|
+
const { TextEncoder, TextDecoder } = require('util');
|
|
7
|
+
global.TextEncoder = TextEncoder;
|
|
8
|
+
global.TextDecoder = TextDecoder;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Mock File constructor for browser file operations
|
|
12
|
+
global.File = class MockFile {
|
|
13
|
+
constructor(bits, name, options = {}) {
|
|
14
|
+
this.bits = bits;
|
|
15
|
+
this.name = name;
|
|
16
|
+
this.type = options.type || '';
|
|
17
|
+
this.webkitRelativePath = options.webkitRelativePath || '';
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Mock FileReader for file reading operations
|
|
22
|
+
global.FileReader = class MockFileReader {
|
|
23
|
+
constructor() {
|
|
24
|
+
this.onload = null;
|
|
25
|
+
this.onerror = null;
|
|
26
|
+
this.result = null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
readAsText(file) {
|
|
30
|
+
setTimeout(() => {
|
|
31
|
+
if (this.onload) {
|
|
32
|
+
this.result = file.bits ? file.bits[0] : '';
|
|
33
|
+
this.onload({ target: { result: this.result } });
|
|
34
|
+
}
|
|
35
|
+
}, 0);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Mock Blob for export operations
|
|
40
|
+
global.Blob = class MockBlob {
|
|
41
|
+
constructor(bits, options = {}) {
|
|
42
|
+
this.bits = bits;
|
|
43
|
+
this.type = options.type || '';
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Mock URL for createObjectURL/revokeObjectURL
|
|
48
|
+
global.URL = {
|
|
49
|
+
createObjectURL: jest.fn(() => 'mock-blob-url'),
|
|
50
|
+
revokeObjectURL: jest.fn()
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Mock document methods for file export
|
|
54
|
+
global.document = {
|
|
55
|
+
createElement: jest.fn(() => ({
|
|
56
|
+
href: '',
|
|
57
|
+
download: '',
|
|
58
|
+
click: jest.fn()
|
|
59
|
+
})),
|
|
60
|
+
body: {
|
|
61
|
+
appendChild: jest.fn(),
|
|
62
|
+
removeChild: jest.fn()
|
|
63
|
+
}
|
|
64
|
+
};
|
package/config/rig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// The "rig.json" file directs tools to look for their config files in an external package.
|
|
2
|
+
// Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package
|
|
3
|
+
{
|
|
4
|
+
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
|
5
|
+
/**
|
|
6
|
+
* (Required) The name of the rig package to inherit from.
|
|
7
|
+
* It should be an NPM package name with the "-rig" suffix.
|
|
8
|
+
*/
|
|
9
|
+
"rigPackageName": "@rushstack/heft-web-rig",
|
|
10
|
+
/**
|
|
11
|
+
* (Optional) Selects a config profile from the rig package. The name must consist of
|
|
12
|
+
* lowercase alphanumeric words separated by hyphens, for example "sample-profile".
|
|
13
|
+
* If omitted, then the "default" profile will be used."
|
|
14
|
+
*/
|
|
15
|
+
"rigProfile": "library"
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ProcessedResources } from '../../types';
|
|
3
|
+
export interface QualifierContextControlProps {
|
|
4
|
+
qualifierName: string;
|
|
5
|
+
value: string | undefined;
|
|
6
|
+
onChange: (qualifierName: string, value: string | undefined) => void;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
resources?: ProcessedResources | null;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare const QualifierContextControl: React.FC<QualifierContextControlProps>;
|
|
13
|
+
export default QualifierContextControl;
|
|
14
|
+
//# sourceMappingURL=QualifierContextControl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"QualifierContextControl.d.ts","sourceRoot":"","sources":["../../../src/components/common/QualifierContextControl.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAkB,MAAM,OAAO,CAAC;AACvC,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,WAAW,4BAA4B;IAC3C,aAAa,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,QAAQ,EAAE,CAAC,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC;IACrE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACtC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,uBAAuB,EAAE,KAAK,CAAC,EAAE,CAAC,4BAA4B,CAuI1E,CAAC;AAEF,eAAe,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
export const QualifierContextControl = ({ qualifierName, value, onChange, disabled = false, placeholder, resources, className = '' }) => {
|
|
3
|
+
// Extract qualifier type information from system configuration
|
|
4
|
+
const qualifierInfo = useMemo(() => {
|
|
5
|
+
if (!resources?.system?.qualifiers) {
|
|
6
|
+
return { hasEnumeratedValues: false, enumeratedValues: [] };
|
|
7
|
+
}
|
|
8
|
+
try {
|
|
9
|
+
// Get qualifier declaration
|
|
10
|
+
const qualifierResult = resources.system.qualifiers.validating.get(qualifierName);
|
|
11
|
+
if (!qualifierResult.isSuccess()) {
|
|
12
|
+
return { hasEnumeratedValues: false, enumeratedValues: [] };
|
|
13
|
+
}
|
|
14
|
+
const qualifier = qualifierResult.value;
|
|
15
|
+
// Access the instantiated qualifier type
|
|
16
|
+
if (qualifier.type) {
|
|
17
|
+
const qualifierType = qualifier.type;
|
|
18
|
+
// Use type assertion to access properties that may exist on specific subtypes
|
|
19
|
+
const qtAny = qualifierType;
|
|
20
|
+
const config = (qtAny.configuration || {});
|
|
21
|
+
// Look for enumerated values in different possible locations
|
|
22
|
+
const enumeratedValues = config.enumeratedValues ||
|
|
23
|
+
config.allowedTerritories ||
|
|
24
|
+
qtAny.enumeratedValues ||
|
|
25
|
+
qtAny.allowedTerritories ||
|
|
26
|
+
[];
|
|
27
|
+
if (enumeratedValues && Array.isArray(enumeratedValues) && enumeratedValues.length > 0) {
|
|
28
|
+
return {
|
|
29
|
+
hasEnumeratedValues: true,
|
|
30
|
+
enumeratedValues: enumeratedValues,
|
|
31
|
+
systemType: qtAny.systemType || 'literal',
|
|
32
|
+
caseSensitive: config.caseSensitive !== false
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { hasEnumeratedValues: false, enumeratedValues: [] };
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
console.warn(`Failed to extract qualifier type info for ${qualifierName}:`, error);
|
|
40
|
+
return { hasEnumeratedValues: false, enumeratedValues: [] };
|
|
41
|
+
}
|
|
42
|
+
}, [qualifierName, resources?.system?.qualifiers]);
|
|
43
|
+
const handleChange = (newValue) => {
|
|
44
|
+
onChange(qualifierName, newValue || undefined);
|
|
45
|
+
};
|
|
46
|
+
const handleClear = () => {
|
|
47
|
+
onChange(qualifierName, undefined);
|
|
48
|
+
};
|
|
49
|
+
const effectiveValue = value ?? '';
|
|
50
|
+
const hasEnumeratedValues = qualifierInfo.hasEnumeratedValues && qualifierInfo.enumeratedValues.length > 0;
|
|
51
|
+
return (React.createElement("div", { className: `bg-white rounded border border-gray-200 p-2 ${className}` },
|
|
52
|
+
React.createElement("div", { className: "flex items-center gap-2" },
|
|
53
|
+
React.createElement("label", { className: "text-sm font-medium text-gray-700 min-w-0 flex-shrink-0" },
|
|
54
|
+
qualifierName,
|
|
55
|
+
":"),
|
|
56
|
+
React.createElement("div", { className: "flex-1 flex items-center gap-1" },
|
|
57
|
+
hasEnumeratedValues ? (
|
|
58
|
+
// Dropdown for enumerated values
|
|
59
|
+
React.createElement("select", { value: effectiveValue, onChange: (e) => handleChange(e.target.value), disabled: disabled, 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 ${disabled ? 'bg-gray-100 text-gray-400' : ''}` },
|
|
60
|
+
React.createElement("option", { value: "" }, disabled
|
|
61
|
+
? 'Disabled'
|
|
62
|
+
: value === undefined
|
|
63
|
+
? '(undefined)'
|
|
64
|
+
: placeholder || 'Select value...'),
|
|
65
|
+
qualifierInfo.enumeratedValues.map((enumValue) => (React.createElement("option", { key: enumValue, value: enumValue }, enumValue))))) : (
|
|
66
|
+
// Text input for non-enumerated values
|
|
67
|
+
React.createElement("input", { type: "text", value: effectiveValue, onChange: (e) => handleChange(e.target.value), disabled: disabled, 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 ${disabled ? 'bg-gray-100 text-gray-400' : ''}`, placeholder: disabled
|
|
68
|
+
? 'Disabled'
|
|
69
|
+
: value === undefined
|
|
70
|
+
? '(undefined)'
|
|
71
|
+
: placeholder || `Enter ${qualifierName} value` })),
|
|
72
|
+
!disabled && value !== undefined && (React.createElement("button", { type: "button", onClick: handleClear, className: "px-2 py-1 text-xs text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded transition-colors", title: "Set to undefined" }, "\u2715")))),
|
|
73
|
+
hasEnumeratedValues && (React.createElement("div", { className: "mt-1 text-xs text-blue-600" },
|
|
74
|
+
qualifierInfo.enumeratedValues.length,
|
|
75
|
+
" predefined values"))));
|
|
76
|
+
};
|
|
77
|
+
export default QualifierContextControl;
|
|
78
|
+
//# sourceMappingURL=QualifierContextControl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"QualifierContextControl.js","sourceRoot":"","sources":["../../../src/components/common/QualifierContextControl.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAavC,MAAM,CAAC,MAAM,uBAAuB,GAA2C,CAAC,EAC9E,aAAa,EACb,KAAK,EACL,QAAQ,EACR,QAAQ,GAAG,KAAK,EAChB,WAAW,EACX,SAAS,EACT,SAAS,GAAG,EAAE,EACf,EAAE,EAAE;IACH,+DAA+D;IAC/D,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,EAAE;QACjC,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;YACnC,OAAO,EAAE,mBAAmB,EAAE,KAAK,EAAE,gBAAgB,EAAE,EAAE,EAAE,CAAC;QAC9D,CAAC;QAED,IAAI,CAAC;YACH,4BAA4B;YAC5B,MAAM,eAAe,GAAG,SAAS,CAAC,MAAM,CAAC,UAAU,CAAC,UAAU,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;YAElF,IAAI,CAAC,eAAe,CAAC,SAAS,EAAE,EAAE,CAAC;gBACjC,OAAO,EAAE,mBAAmB,EAAE,KAAK,EAAE,gBAAgB,EAAE,EAAE,EAAE,CAAC;YAC9D,CAAC;YAED,MAAM,SAAS,GAAG,eAAe,CAAC,KAAK,CAAC;YAExC,yCAAyC;YACzC,IAAI,SAAS,CAAC,IAAI,EAAE,CAAC;gBACnB,MAAM,aAAa,GAAG,SAAS,CAAC,IAAI,CAAC;gBACrC,8EAA8E;gBAC9E,MAAM,KAAK,GAAG,aAAmD,CAAC;gBAClE,MAAM,MAAM,GAAG,CAAC,KAAK,CAAC,aAAa,IAAI,EAAE,CAA4B,CAAC;gBAEtE,6DAA6D;gBAC7D,MAAM,gBAAgB,GACpB,MAAM,CAAC,gBAAgB;oBACvB,MAAM,CAAC,kBAAkB;oBACzB,KAAK,CAAC,gBAAgB;oBACtB,KAAK,CAAC,kBAAkB;oBACxB,EAAE,CAAC;gBAEL,IAAI,gBAAgB,IAAI,KAAK,CAAC,OAAO,CAAC,gBAAgB,CAAC,IAAI,gBAAgB,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBACvF,OAAO;wBACL,mBAAmB,EAAE,IAAI;wBACzB,gBAAgB,EAAE,gBAA4B;wBAC9C,UAAU,EAAG,KAAK,CAAC,UAAqB,IAAI,SAAS;wBACrD,aAAa,EAAE,MAAM,CAAC,aAAa,KAAK,KAAK;qBAC9C,CAAC;gBACJ,CAAC;YACH,CAAC;YAED,OAAO,EAAE,mBAAmB,EAAE,KAAK,EAAE,gBAAgB,EAAE,EAAE,EAAE,CAAC;QAC9D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,CAAC,6CAA6C,aAAa,GAAG,EAAE,KAAK,CAAC,CAAC;YACnF,OAAO,EAAE,mBAAmB,EAAE,KAAK,EAAE,gBAAgB,EAAE,EAAE,EAAE,CAAC;QAC9D,CAAC;IACH,CAAC,EAAE,CAAC,aAAa,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;IAEnD,MAAM,YAAY,GAAG,CAAC,QAAgB,EAAE,EAAE;QACxC,QAAQ,CAAC,aAAa,EAAE,QAAQ,IAAI,SAAS,CAAC,CAAC;IACjD,CAAC,CAAC;IAEF,MAAM,WAAW,GAAG,GAAG,EAAE;QACvB,QAAQ,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;IACrC,CAAC,CAAC;IAEF,MAAM,cAAc,GAAG,KAAK,IAAI,EAAE,CAAC;IACnC,MAAM,mBAAmB,GAAG,aAAa,CAAC,mBAAmB,IAAI,aAAa,CAAC,gBAAgB,CAAC,MAAM,GAAG,CAAC,CAAC;IAE3G,OAAO,CACL,6BAAK,SAAS,EAAE,+CAA+C,SAAS,EAAE;QACxE,6BAAK,SAAS,EAAC,yBAAyB;YACtC,+BAAO,SAAS,EAAC,yDAAyD;gBAAE,aAAa;oBAAU;YACnG,6BAAK,SAAS,EAAC,gCAAgC;gBAC5C,mBAAmB,CAAC,CAAC,CAAC;gBACrB,iCAAiC;gBACjC,gCACE,KAAK,EAAE,cAAc,EACrB,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAC7C,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,gJACT,QAAQ,CAAC,CAAC,CAAC,2BAA2B,CAAC,CAAC,CAAC,EAC3C,EAAE;oBAEF,gCAAQ,KAAK,EAAC,EAAE,IACb,QAAQ;wBACP,CAAC,CAAC,UAAU;wBACZ,CAAC,CAAC,KAAK,KAAK,SAAS;4BACrB,CAAC,CAAC,aAAa;4BACf,CAAC,CAAC,WAAW,IAAI,iBAAiB,CAC7B;oBACR,aAAa,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,SAAiB,EAAE,EAAE,CAAC,CACzD,gCAAQ,GAAG,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,IACrC,SAAS,CACH,CACV,CAAC,CACK,CACV,CAAC,CAAC,CAAC;gBACF,uCAAuC;gBACvC,+BACE,IAAI,EAAC,MAAM,EACX,KAAK,EAAE,cAAc,EACrB,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAC7C,QAAQ,EAAE,QAAQ,EAClB,SAAS,EAAE,gJACT,QAAQ,CAAC,CAAC,CAAC,2BAA2B,CAAC,CAAC,CAAC,EAC3C,EAAE,EACF,WAAW,EACT,QAAQ;wBACN,CAAC,CAAC,UAAU;wBACZ,CAAC,CAAC,KAAK,KAAK,SAAS;4BACrB,CAAC,CAAC,aAAa;4BACf,CAAC,CAAC,WAAW,IAAI,SAAS,aAAa,QAAQ,GAEnD,CACH;gBACA,CAAC,QAAQ,IAAI,KAAK,KAAK,SAAS,IAAI,CACnC,gCACE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,WAAW,EACpB,SAAS,EAAC,iGAAiG,EAC3G,KAAK,EAAC,kBAAkB,aAGjB,CACV,CACG,CACF;QAEL,mBAAmB,IAAI,CACtB,6BAAK,SAAS,EAAC,4BAA4B;YACxC,aAAa,CAAC,gBAAgB,CAAC,MAAM;iCAClC,CACP,CACG,CACP,CAAC;AACJ,CAAC,CAAC;AAEF,eAAe,uBAAuB,CAAC","sourcesContent":["import React, { useMemo } from 'react';\nimport { ProcessedResources } from '../../types';\n\nexport interface QualifierContextControlProps {\n qualifierName: string;\n value: string | undefined;\n onChange: (qualifierName: string, value: string | undefined) => void;\n disabled?: boolean;\n placeholder?: string;\n resources?: ProcessedResources | null;\n className?: string;\n}\n\nexport const QualifierContextControl: React.FC<QualifierContextControlProps> = ({\n qualifierName,\n value,\n onChange,\n disabled = false,\n placeholder,\n resources,\n className = ''\n}) => {\n // Extract qualifier type information from system configuration\n const qualifierInfo = useMemo(() => {\n if (!resources?.system?.qualifiers) {\n return { hasEnumeratedValues: false, enumeratedValues: [] };\n }\n\n try {\n // Get qualifier declaration\n const qualifierResult = resources.system.qualifiers.validating.get(qualifierName);\n\n if (!qualifierResult.isSuccess()) {\n return { hasEnumeratedValues: false, enumeratedValues: [] };\n }\n\n const qualifier = qualifierResult.value;\n\n // Access the instantiated qualifier type\n if (qualifier.type) {\n const qualifierType = qualifier.type;\n // Use type assertion to access properties that may exist on specific subtypes\n const qtAny = qualifierType as unknown as Record<string, unknown>;\n const config = (qtAny.configuration || {}) as Record<string, unknown>;\n\n // Look for enumerated values in different possible locations\n const enumeratedValues =\n config.enumeratedValues ||\n config.allowedTerritories ||\n qtAny.enumeratedValues ||\n qtAny.allowedTerritories ||\n [];\n\n if (enumeratedValues && Array.isArray(enumeratedValues) && enumeratedValues.length > 0) {\n return {\n hasEnumeratedValues: true,\n enumeratedValues: enumeratedValues as string[],\n systemType: (qtAny.systemType as string) || 'literal',\n caseSensitive: config.caseSensitive !== false\n };\n }\n }\n\n return { hasEnumeratedValues: false, enumeratedValues: [] };\n } catch (error) {\n console.warn(`Failed to extract qualifier type info for ${qualifierName}:`, error);\n return { hasEnumeratedValues: false, enumeratedValues: [] };\n }\n }, [qualifierName, resources?.system?.qualifiers]);\n\n const handleChange = (newValue: string) => {\n onChange(qualifierName, newValue || undefined);\n };\n\n const handleClear = () => {\n onChange(qualifierName, undefined);\n };\n\n const effectiveValue = value ?? '';\n const hasEnumeratedValues = qualifierInfo.hasEnumeratedValues && qualifierInfo.enumeratedValues.length > 0;\n\n return (\n <div className={`bg-white rounded border border-gray-200 p-2 ${className}`}>\n <div className=\"flex items-center gap-2\">\n <label className=\"text-sm font-medium text-gray-700 min-w-0 flex-shrink-0\">{qualifierName}:</label>\n <div className=\"flex-1 flex items-center gap-1\">\n {hasEnumeratedValues ? (\n // Dropdown for enumerated values\n <select\n value={effectiveValue}\n onChange={(e) => handleChange(e.target.value)}\n disabled={disabled}\n 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 ${\n disabled ? 'bg-gray-100 text-gray-400' : ''\n }`}\n >\n <option value=\"\">\n {disabled\n ? 'Disabled'\n : value === undefined\n ? '(undefined)'\n : placeholder || 'Select value...'}\n </option>\n {qualifierInfo.enumeratedValues.map((enumValue: string) => (\n <option key={enumValue} value={enumValue}>\n {enumValue}\n </option>\n ))}\n </select>\n ) : (\n // Text input for non-enumerated values\n <input\n type=\"text\"\n value={effectiveValue}\n onChange={(e) => handleChange(e.target.value)}\n disabled={disabled}\n 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 ${\n disabled ? 'bg-gray-100 text-gray-400' : ''\n }`}\n placeholder={\n disabled\n ? 'Disabled'\n : value === undefined\n ? '(undefined)'\n : placeholder || `Enter ${qualifierName} value`\n }\n />\n )}\n {!disabled && value !== undefined && (\n <button\n type=\"button\"\n onClick={handleClear}\n className=\"px-2 py-1 text-xs text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded transition-colors\"\n title=\"Set to undefined\"\n >\n ✕\n </button>\n )}\n </div>\n </div>\n {/* Show enumerated values indicator */}\n {hasEnumeratedValues && (\n <div className=\"mt-1 text-xs text-blue-600\">\n {qualifierInfo.enumeratedValues.length} predefined values\n </div>\n )}\n </div>\n );\n};\n\nexport default QualifierContextControl;\n"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface ResourceListViewProps {
|
|
3
|
+
resourceIds: string[];
|
|
4
|
+
selectedResourceId: string | null;
|
|
5
|
+
onResourceSelect: (resourceId: string) => void;
|
|
6
|
+
searchTerm?: string;
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const ResourceListView: React.FC<ResourceListViewProps>;
|
|
10
|
+
export default ResourceListView;
|
|
11
|
+
//# sourceMappingURL=ResourceListView.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ResourceListView.d.ts","sourceRoot":"","sources":["../../../src/components/common/ResourceListView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAG1B,UAAU,qBAAqB;IAC7B,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,gBAAgB,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,eAAO,MAAM,gBAAgB,EAAE,KAAK,CAAC,EAAE,CAAC,qBAAqB,CAiD5D,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { DocumentTextIcon } from '@heroicons/react/24/outline';
|
|
3
|
+
export const ResourceListView = ({ resourceIds, selectedResourceId, onResourceSelect, searchTerm = '', className = '' }) => {
|
|
4
|
+
// Filter and sort resource IDs
|
|
5
|
+
const filteredResourceIds = React.useMemo(() => {
|
|
6
|
+
const filtered = searchTerm
|
|
7
|
+
? resourceIds.filter((id) => id.toLowerCase().includes(searchTerm.toLowerCase()))
|
|
8
|
+
: resourceIds;
|
|
9
|
+
return filtered.sort();
|
|
10
|
+
}, [resourceIds, searchTerm]);
|
|
11
|
+
if (filteredResourceIds.length === 0) {
|
|
12
|
+
return (React.createElement("div", { className: `${className} p-4 text-center text-gray-500` },
|
|
13
|
+
React.createElement("p", null, searchTerm ? 'No resources match your search' : 'No resources available')));
|
|
14
|
+
}
|
|
15
|
+
return (React.createElement("div", { className: `${className} overflow-y-auto` }, filteredResourceIds.map((resourceId) => (React.createElement("div", { key: resourceId, className: `flex items-center px-3 py-2 cursor-pointer hover:bg-gray-100 border-b border-gray-100 last:border-b-0 ${selectedResourceId === resourceId ? 'bg-purple-50 border-l-2 border-purple-500' : ''} ${searchTerm && resourceId.toLowerCase().includes(searchTerm.toLowerCase()) ? 'bg-yellow-50' : ''}`, onClick: () => onResourceSelect(resourceId) },
|
|
16
|
+
React.createElement(DocumentTextIcon, { className: "w-4 h-4 text-green-500 mr-2 flex-shrink-0" }),
|
|
17
|
+
React.createElement("span", { className: `text-sm truncate ${selectedResourceId === resourceId ? 'font-medium text-purple-900' : 'text-gray-700'}`, title: resourceId }, resourceId))))));
|
|
18
|
+
};
|
|
19
|
+
export default ResourceListView;
|
|
20
|
+
//# sourceMappingURL=ResourceListView.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ResourceListView.js","sourceRoot":"","sources":["../../../src/components/common/ResourceListView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAU/D,MAAM,CAAC,MAAM,gBAAgB,GAAoC,CAAC,EAChE,WAAW,EACX,kBAAkB,EAClB,gBAAgB,EAChB,UAAU,GAAG,EAAE,EACf,SAAS,GAAG,EAAE,EACf,EAAE,EAAE;IACH,+BAA+B;IAC/B,MAAM,mBAAmB,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,EAAE;QAC7C,MAAM,QAAQ,GAAG,UAAU;YACzB,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC,CAAC;YACjF,CAAC,CAAC,WAAW,CAAC;QAEhB,OAAO,QAAQ,CAAC,IAAI,EAAE,CAAC;IACzB,CAAC,EAAE,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC,CAAC;IAE9B,IAAI,mBAAmB,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACrC,OAAO,CACL,6BAAK,SAAS,EAAE,GAAG,SAAS,gCAAgC;YAC1D,+BAAI,UAAU,CAAC,CAAC,CAAC,gCAAgC,CAAC,CAAC,CAAC,wBAAwB,CAAK,CAC7E,CACP,CAAC;IACJ,CAAC;IAED,OAAO,CACL,6BAAK,SAAS,EAAE,GAAG,SAAS,kBAAkB,IAC3C,mBAAmB,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,CACvC,6BACE,GAAG,EAAE,UAAU,EACf,SAAS,EAAE,yGACT,kBAAkB,KAAK,UAAU,CAAC,CAAC,CAAC,2CAA2C,CAAC,CAAC,CAAC,EACpF,IACE,UAAU,IAAI,UAAU,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,EAC/F,EAAE,EACF,OAAO,EAAE,GAAG,EAAE,CAAC,gBAAgB,CAAC,UAAU,CAAC;QAE3C,oBAAC,gBAAgB,IAAC,SAAS,EAAC,2CAA2C,GAAG;QAC1E,8BACE,SAAS,EAAE,oBACT,kBAAkB,KAAK,UAAU,CAAC,CAAC,CAAC,6BAA6B,CAAC,CAAC,CAAC,eACtE,EAAE,EACF,KAAK,EAAE,UAAU,IAEhB,UAAU,CACN,CACH,CACP,CAAC,CACE,CACP,CAAC;AACJ,CAAC,CAAC;AAEF,eAAe,gBAAgB,CAAC","sourcesContent":["import React from 'react';\nimport { DocumentTextIcon } from '@heroicons/react/24/outline';\n\ninterface ResourceListViewProps {\n resourceIds: string[];\n selectedResourceId: string | null;\n onResourceSelect: (resourceId: string) => void;\n searchTerm?: string;\n className?: string;\n}\n\nexport const ResourceListView: React.FC<ResourceListViewProps> = ({\n resourceIds,\n selectedResourceId,\n onResourceSelect,\n searchTerm = '',\n className = ''\n}) => {\n // Filter and sort resource IDs\n const filteredResourceIds = React.useMemo(() => {\n const filtered = searchTerm\n ? resourceIds.filter((id) => id.toLowerCase().includes(searchTerm.toLowerCase()))\n : resourceIds;\n\n return filtered.sort();\n }, [resourceIds, searchTerm]);\n\n if (filteredResourceIds.length === 0) {\n return (\n <div className={`${className} p-4 text-center text-gray-500`}>\n <p>{searchTerm ? 'No resources match your search' : 'No resources available'}</p>\n </div>\n );\n }\n\n return (\n <div className={`${className} overflow-y-auto`}>\n {filteredResourceIds.map((resourceId) => (\n <div\n key={resourceId}\n className={`flex items-center px-3 py-2 cursor-pointer hover:bg-gray-100 border-b border-gray-100 last:border-b-0 ${\n selectedResourceId === resourceId ? 'bg-purple-50 border-l-2 border-purple-500' : ''\n } ${\n searchTerm && resourceId.toLowerCase().includes(searchTerm.toLowerCase()) ? 'bg-yellow-50' : ''\n }`}\n onClick={() => onResourceSelect(resourceId)}\n >\n <DocumentTextIcon className=\"w-4 h-4 text-green-500 mr-2 flex-shrink-0\" />\n <span\n className={`text-sm truncate ${\n selectedResourceId === resourceId ? 'font-medium text-purple-900' : 'text-gray-700'\n }`}\n title={resourceId}\n >\n {resourceId}\n </span>\n </div>\n ))}\n </div>\n );\n};\n\nexport default ResourceListView;\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Resources, Runtime } from '@fgv/ts-res';
|
|
3
|
+
interface ResourceTreeViewProps {
|
|
4
|
+
resources: Resources.ResourceManagerBuilder | Runtime.CompiledResourceCollection;
|
|
5
|
+
selectedResourceId: string | null;
|
|
6
|
+
onResourceSelect: (resourceId: string) => void;
|
|
7
|
+
searchTerm?: string;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const ResourceTreeView: React.FC<ResourceTreeViewProps>;
|
|
11
|
+
export default ResourceTreeView;
|
|
12
|
+
//# sourceMappingURL=ResourceTreeView.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ResourceTreeView.d.ts","sourceRoot":"","sources":["../../../src/components/common/ResourceTreeView.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAyC,MAAM,OAAO,CAAC;AAQ9D,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAEjD,UAAU,qBAAqB;IAC7B,SAAS,EAAE,SAAS,CAAC,sBAAsB,GAAG,OAAO,CAAC,0BAA0B,CAAC;IACjF,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,gBAAgB,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAaD,eAAO,MAAM,gBAAgB,EAAE,KAAK,CAAC,EAAE,CAAC,qBAAqB,CA+O5D,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import React, { useState, useMemo, useCallback } from 'react';
|
|
2
|
+
import { ChevronRightIcon, ChevronDownIcon, DocumentTextIcon, FolderIcon, FolderOpenIcon } from '@heroicons/react/24/outline';
|
|
3
|
+
export const ResourceTreeView = ({ resources, selectedResourceId, onResourceSelect, searchTerm = '', className = '' }) => {
|
|
4
|
+
const [expandedNodes, setExpandedNodes] = useState(new Set());
|
|
5
|
+
// Build the tree structure from resources
|
|
6
|
+
const treeData = useMemo(() => {
|
|
7
|
+
if (!resources)
|
|
8
|
+
return null;
|
|
9
|
+
// Get the tree from the resources
|
|
10
|
+
const treeResult = resources.getBuiltResourceTree();
|
|
11
|
+
if (treeResult.isFailure()) {
|
|
12
|
+
console.error('Failed to build resource tree:', treeResult.message);
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
return treeResult.value;
|
|
16
|
+
}, [resources]);
|
|
17
|
+
// Filter tree based on search term
|
|
18
|
+
const filteredTree = useMemo(() => {
|
|
19
|
+
if (!treeData || !searchTerm)
|
|
20
|
+
return treeData;
|
|
21
|
+
// Helper function to check if a node or its descendants match the search
|
|
22
|
+
const markMatchingNodes = (node, searchLower) => {
|
|
23
|
+
const nodeIdLower = node.id.toLowerCase();
|
|
24
|
+
let matches = nodeIdLower.includes(searchLower);
|
|
25
|
+
if (!node.isLeaf && node.children) {
|
|
26
|
+
// Check children recursively
|
|
27
|
+
for (const child of node.children.values()) {
|
|
28
|
+
if (markMatchingNodes(child, searchLower)) {
|
|
29
|
+
matches = true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return matches;
|
|
34
|
+
};
|
|
35
|
+
// Mark all matching nodes
|
|
36
|
+
const searchLower = searchTerm.toLowerCase();
|
|
37
|
+
for (const child of treeData.children.values()) {
|
|
38
|
+
markMatchingNodes(child, searchLower);
|
|
39
|
+
}
|
|
40
|
+
return treeData;
|
|
41
|
+
}, [treeData, searchTerm]);
|
|
42
|
+
// Toggle node expansion
|
|
43
|
+
const toggleNode = useCallback((nodeId) => {
|
|
44
|
+
setExpandedNodes((prev) => {
|
|
45
|
+
const newExpanded = new Set(prev);
|
|
46
|
+
if (newExpanded.has(nodeId)) {
|
|
47
|
+
newExpanded.delete(nodeId);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
newExpanded.add(nodeId);
|
|
51
|
+
}
|
|
52
|
+
return newExpanded;
|
|
53
|
+
});
|
|
54
|
+
}, []);
|
|
55
|
+
// Expand all nodes that contain search matches
|
|
56
|
+
const expandMatchingNodes = useCallback(() => {
|
|
57
|
+
if (!searchTerm || !filteredTree)
|
|
58
|
+
return;
|
|
59
|
+
const searchLower = searchTerm.toLowerCase();
|
|
60
|
+
const nodesToExpand = new Set();
|
|
61
|
+
const checkNode = (node) => {
|
|
62
|
+
if (node.id.toLowerCase().includes(searchLower)) {
|
|
63
|
+
// Expand all parent nodes
|
|
64
|
+
const parts = node.id.split('.');
|
|
65
|
+
for (let i = 1; i < parts.length; i++) {
|
|
66
|
+
nodesToExpand.add(parts.slice(0, i).join('.'));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (!node.isLeaf && node.children) {
|
|
70
|
+
for (const child of node.children.values()) {
|
|
71
|
+
checkNode(child);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
for (const child of filteredTree.children.values()) {
|
|
76
|
+
checkNode(child);
|
|
77
|
+
}
|
|
78
|
+
setExpandedNodes(nodesToExpand);
|
|
79
|
+
}, [searchTerm, filteredTree]);
|
|
80
|
+
// Auto-expand when search term changes
|
|
81
|
+
React.useEffect(() => {
|
|
82
|
+
if (searchTerm) {
|
|
83
|
+
expandMatchingNodes();
|
|
84
|
+
}
|
|
85
|
+
}, [searchTerm, expandMatchingNodes]);
|
|
86
|
+
// Render a single tree node
|
|
87
|
+
const renderTreeNode = (node, level = 0) => {
|
|
88
|
+
const isExpanded = expandedNodes.has(node.id);
|
|
89
|
+
const isSelected = selectedResourceId === node.id;
|
|
90
|
+
const nodeIdLower = node.id.toLowerCase();
|
|
91
|
+
const searchLower = searchTerm.toLowerCase();
|
|
92
|
+
const matchesSearch = !searchTerm || nodeIdLower.includes(searchLower);
|
|
93
|
+
// Check if any children match
|
|
94
|
+
let hasMatchingChildren = false;
|
|
95
|
+
if (!node.isLeaf && node.children && searchTerm) {
|
|
96
|
+
const checkChildren = (n) => {
|
|
97
|
+
if (n.id.toLowerCase().includes(searchLower))
|
|
98
|
+
return true;
|
|
99
|
+
if (!n.isLeaf && n.children) {
|
|
100
|
+
for (const child of n.children.values()) {
|
|
101
|
+
if (checkChildren(child))
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
};
|
|
107
|
+
for (const child of node.children.values()) {
|
|
108
|
+
if (checkChildren(child)) {
|
|
109
|
+
hasMatchingChildren = true;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Hide nodes that don't match search and don't have matching children
|
|
115
|
+
if (searchTerm && !matchesSearch && !hasMatchingChildren) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
return (React.createElement("div", { key: node.id },
|
|
119
|
+
React.createElement("div", { className: `flex items-center px-2 py-1 cursor-pointer hover:bg-gray-100 ${isSelected ? 'bg-purple-50 border-l-2 border-purple-500' : ''} ${matchesSearch && searchTerm ? 'bg-yellow-50' : ''}`, style: { paddingLeft: `${level * 20 + 8}px` }, onClick: () => {
|
|
120
|
+
if (node.isLeaf) {
|
|
121
|
+
onResourceSelect(node.id);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
toggleNode(node.id);
|
|
125
|
+
}
|
|
126
|
+
} },
|
|
127
|
+
!node.isLeaf && (React.createElement("button", { onClick: (e) => {
|
|
128
|
+
e.stopPropagation();
|
|
129
|
+
toggleNode(node.id);
|
|
130
|
+
}, className: "mr-1 p-0.5 hover:bg-gray-200 rounded" }, isExpanded ? (React.createElement(ChevronDownIcon, { className: "w-3 h-3 text-gray-600" })) : (React.createElement(ChevronRightIcon, { className: "w-3 h-3 text-gray-600" })))),
|
|
131
|
+
node.isLeaf ? (React.createElement(DocumentTextIcon, { className: "w-4 h-4 text-green-500 mr-2 flex-shrink-0" })) : isExpanded ? (React.createElement(FolderOpenIcon, { className: "w-4 h-4 text-blue-500 mr-2 flex-shrink-0" })) : (React.createElement(FolderIcon, { className: "w-4 h-4 text-blue-500 mr-2 flex-shrink-0" })),
|
|
132
|
+
React.createElement("span", { className: `text-sm truncate ${isSelected ? 'font-medium text-purple-900' : 'text-gray-700'} ${matchesSearch && searchTerm ? 'font-medium' : ''}`, title: node.id }, node.name),
|
|
133
|
+
!node.isLeaf && node.children && (React.createElement("span", { className: "ml-2 text-xs text-gray-500" },
|
|
134
|
+
"(",
|
|
135
|
+
node.children.size,
|
|
136
|
+
")"))),
|
|
137
|
+
!node.isLeaf && node.children && isExpanded && (React.createElement("div", null, Array.from(node.children.values())
|
|
138
|
+
.sort((a, b) => {
|
|
139
|
+
// Sort folders first, then by name
|
|
140
|
+
if (a.isLeaf !== b.isLeaf) {
|
|
141
|
+
return a.isLeaf ? 1 : -1;
|
|
142
|
+
}
|
|
143
|
+
return a.name.localeCompare(b.name);
|
|
144
|
+
})
|
|
145
|
+
.map((child) => renderTreeNode(child, level + 1))))));
|
|
146
|
+
};
|
|
147
|
+
if (!filteredTree) {
|
|
148
|
+
return (React.createElement("div", { className: `${className} p-4 text-center text-gray-500` },
|
|
149
|
+
React.createElement("p", null, "No resources available")));
|
|
150
|
+
}
|
|
151
|
+
return (React.createElement("div", { className: `${className} overflow-y-auto` }, Array.from(filteredTree.children.values())
|
|
152
|
+
.sort((a, b) => {
|
|
153
|
+
// Sort folders first, then by name
|
|
154
|
+
if (a.isLeaf !== b.isLeaf) {
|
|
155
|
+
return a.isLeaf ? 1 : -1;
|
|
156
|
+
}
|
|
157
|
+
return a.name.localeCompare(b.name);
|
|
158
|
+
})
|
|
159
|
+
.map((child) => renderTreeNode(child))));
|
|
160
|
+
};
|
|
161
|
+
export default ResourceTreeView;
|
|
162
|
+
//# sourceMappingURL=ResourceTreeView.js.map
|