@fgv/ts-res-ui-components 5.0.0-10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.rush/temp/03c8b056281d9db0a97d8a6e25eea798a160d393.tar.log +271 -0
- package/.rush/temp/chunked-rush-logs/ts-res-ui-components.build.chunks.jsonl +9 -0
- package/.rush/temp/operation/build/all.log +9 -0
- package/.rush/temp/operation/build/log-chunks.jsonl +9 -0
- package/.rush/temp/operation/build/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +1111 -0
- package/README.md +18 -0
- package/REFACTORING_PLAN.md +171 -0
- package/config/jest.config.json +16 -0
- package/config/jest.setup.js +64 -0
- package/config/rig.json +16 -0
- package/lib/components/common/QualifierContextControl.d.ts +14 -0
- package/lib/components/common/QualifierContextControl.d.ts.map +1 -0
- package/lib/components/common/QualifierContextControl.js +78 -0
- package/lib/components/common/QualifierContextControl.js.map +1 -0
- package/lib/components/common/ResourceListView.d.ts +11 -0
- package/lib/components/common/ResourceListView.d.ts.map +1 -0
- package/lib/components/common/ResourceListView.js +20 -0
- package/lib/components/common/ResourceListView.js.map +1 -0
- package/lib/components/common/ResourceTreeView.d.ts +12 -0
- package/lib/components/common/ResourceTreeView.d.ts.map +1 -0
- package/lib/components/common/ResourceTreeView.js +162 -0
- package/lib/components/common/ResourceTreeView.js.map +1 -0
- package/lib/components/forms/HierarchyEditor.d.ts +10 -0
- package/lib/components/forms/HierarchyEditor.d.ts.map +1 -0
- package/lib/components/forms/HierarchyEditor.js +106 -0
- package/lib/components/forms/HierarchyEditor.js.map +1 -0
- package/lib/components/forms/QualifierEditForm.d.ts +11 -0
- package/lib/components/forms/QualifierEditForm.d.ts.map +1 -0
- package/lib/components/forms/QualifierEditForm.js +181 -0
- package/lib/components/forms/QualifierEditForm.js.map +1 -0
- package/lib/components/forms/QualifierTypeEditForm.d.ts +10 -0
- package/lib/components/forms/QualifierTypeEditForm.d.ts.map +1 -0
- package/lib/components/forms/QualifierTypeEditForm.js +172 -0
- package/lib/components/forms/QualifierTypeEditForm.js.map +1 -0
- package/lib/components/forms/ResourceTypeEditForm.d.ts +10 -0
- package/lib/components/forms/ResourceTypeEditForm.d.ts.map +1 -0
- package/lib/components/forms/ResourceTypeEditForm.js +188 -0
- package/lib/components/forms/ResourceTypeEditForm.js.map +1 -0
- package/lib/components/forms/index.d.ts +9 -0
- package/lib/components/forms/index.d.ts.map +1 -0
- package/lib/components/forms/index.js +5 -0
- package/lib/components/forms/index.js.map +1 -0
- package/lib/components/orchestrator/ResourceOrchestrator.d.ts +14 -0
- package/lib/components/orchestrator/ResourceOrchestrator.d.ts.map +1 -0
- package/lib/components/orchestrator/ResourceOrchestrator.js +278 -0
- package/lib/components/orchestrator/ResourceOrchestrator.js.map +1 -0
- package/lib/components/views/CompiledView/index.d.ts +5 -0
- package/lib/components/views/CompiledView/index.d.ts.map +1 -0
- package/lib/components/views/CompiledView/index.js +595 -0
- package/lib/components/views/CompiledView/index.js.map +1 -0
- package/lib/components/views/ConfigurationView/index.d.ts +5 -0
- package/lib/components/views/ConfigurationView/index.d.ts.map +1 -0
- package/lib/components/views/ConfigurationView/index.js +363 -0
- package/lib/components/views/ConfigurationView/index.js.map +1 -0
- package/lib/components/views/FilterView/index.d.ts +5 -0
- package/lib/components/views/FilterView/index.d.ts.map +1 -0
- package/lib/components/views/FilterView/index.js +463 -0
- package/lib/components/views/FilterView/index.js.map +1 -0
- package/lib/components/views/ImportView/index.d.ts +5 -0
- package/lib/components/views/ImportView/index.d.ts.map +1 -0
- package/lib/components/views/ImportView/index.js +514 -0
- package/lib/components/views/ImportView/index.js.map +1 -0
- package/lib/components/views/ResolutionView/EditableJsonView.d.ts +21 -0
- package/lib/components/views/ResolutionView/EditableJsonView.d.ts.map +1 -0
- package/lib/components/views/ResolutionView/EditableJsonView.js +109 -0
- package/lib/components/views/ResolutionView/EditableJsonView.js.map +1 -0
- package/lib/components/views/ResolutionView/ResolutionEditControls.d.ts +19 -0
- package/lib/components/views/ResolutionView/ResolutionEditControls.d.ts.map +1 -0
- package/lib/components/views/ResolutionView/ResolutionEditControls.js +82 -0
- package/lib/components/views/ResolutionView/ResolutionEditControls.js.map +1 -0
- package/lib/components/views/ResolutionView/index.d.ts +5 -0
- package/lib/components/views/ResolutionView/index.d.ts.map +1 -0
- package/lib/components/views/ResolutionView/index.js +255 -0
- package/lib/components/views/ResolutionView/index.js.map +1 -0
- package/lib/components/views/SourceView/index.d.ts +5 -0
- package/lib/components/views/SourceView/index.d.ts.map +1 -0
- package/lib/components/views/SourceView/index.js +316 -0
- package/lib/components/views/SourceView/index.js.map +1 -0
- package/lib/components/views/ZipLoaderView/index.d.ts +5 -0
- package/lib/components/views/ZipLoaderView/index.d.ts.map +1 -0
- package/lib/components/views/ZipLoaderView/index.js +313 -0
- package/lib/components/views/ZipLoaderView/index.js.map +1 -0
- package/lib/hooks/useConfigurationState.d.ts +46 -0
- package/lib/hooks/useConfigurationState.d.ts.map +1 -0
- package/lib/hooks/useConfigurationState.js +239 -0
- package/lib/hooks/useConfigurationState.js.map +1 -0
- package/lib/hooks/useFilterState.d.ts +7 -0
- package/lib/hooks/useFilterState.d.ts.map +1 -0
- package/lib/hooks/useFilterState.js +80 -0
- package/lib/hooks/useFilterState.js.map +1 -0
- package/lib/hooks/useResolutionState.d.ts +8 -0
- package/lib/hooks/useResolutionState.d.ts.map +1 -0
- package/lib/hooks/useResolutionState.js +253 -0
- package/lib/hooks/useResolutionState.js.map +1 -0
- package/lib/hooks/useResourceData.d.ts +19 -0
- package/lib/hooks/useResourceData.d.ts.map +1 -0
- package/lib/hooks/useResourceData.js +368 -0
- package/lib/hooks/useResourceData.js.map +1 -0
- package/lib/hooks/useViewState.d.ts +10 -0
- package/lib/hooks/useViewState.d.ts.map +1 -0
- package/lib/hooks/useViewState.js +29 -0
- package/lib/hooks/useViewState.js.map +1 -0
- package/lib/index.d.ts +27 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +34 -0
- package/lib/index.js.map +1 -0
- package/lib/test/helpers/testDataLoader.d.ts +37 -0
- package/lib/test/helpers/testDataLoader.d.ts.map +1 -0
- package/lib/test/helpers/testDataLoader.js +171 -0
- package/lib/test/helpers/testDataLoader.js.map +1 -0
- package/lib/test/unit/utils/configurationUtils.test.d.ts +2 -0
- package/lib/test/unit/utils/configurationUtils.test.d.ts.map +1 -0
- package/lib/test/unit/utils/configurationUtils.test.js +497 -0
- package/lib/test/unit/utils/configurationUtils.test.js.map +1 -0
- package/lib/test/unit/utils/fileProcessing.test.d.ts +2 -0
- package/lib/test/unit/utils/fileProcessing.test.d.ts.map +1 -0
- package/lib/test/unit/utils/fileProcessing.test.js +321 -0
- package/lib/test/unit/utils/fileProcessing.test.js.map +1 -0
- package/lib/test/unit/utils/filterResources.test.d.ts +2 -0
- package/lib/test/unit/utils/filterResources.test.d.ts.map +1 -0
- package/lib/test/unit/utils/filterResources.test.js +403 -0
- package/lib/test/unit/utils/filterResources.test.js.map +1 -0
- package/lib/test/unit/utils/resolutionEditing.test.d.ts +2 -0
- package/lib/test/unit/utils/resolutionEditing.test.d.ts.map +1 -0
- package/lib/test/unit/utils/resolutionEditing.test.js +439 -0
- package/lib/test/unit/utils/resolutionEditing.test.js.map +1 -0
- package/lib/test/unit/utils/resolutionUtils.test.d.ts +2 -0
- package/lib/test/unit/utils/resolutionUtils.test.d.ts.map +1 -0
- package/lib/test/unit/utils/resolutionUtils.test.js +397 -0
- package/lib/test/unit/utils/resolutionUtils.test.js.map +1 -0
- package/lib/test/unit/utils/tsResIntegration.test.d.ts +2 -0
- package/lib/test/unit/utils/tsResIntegration.test.d.ts.map +1 -0
- package/lib/test/unit/utils/tsResIntegration.test.js +376 -0
- package/lib/test/unit/utils/tsResIntegration.test.js.map +1 -0
- package/lib/types/index.d.ts +251 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index.js +2 -0
- package/lib/types/index.js.map +1 -0
- package/lib/utils/configurationUtils.d.ts +74 -0
- package/lib/utils/configurationUtils.d.ts.map +1 -0
- package/lib/utils/configurationUtils.js +359 -0
- package/lib/utils/configurationUtils.js.map +1 -0
- package/lib/utils/fileProcessing.d.ts +18 -0
- package/lib/utils/fileProcessing.d.ts.map +1 -0
- package/lib/utils/fileProcessing.js +142 -0
- package/lib/utils/fileProcessing.js.map +1 -0
- package/lib/utils/filterResources.d.ts +38 -0
- package/lib/utils/filterResources.d.ts.map +1 -0
- package/lib/utils/filterResources.js +153 -0
- package/lib/utils/filterResources.js.map +1 -0
- package/lib/utils/resolutionEditing.d.ts +58 -0
- package/lib/utils/resolutionEditing.d.ts.map +1 -0
- package/lib/utils/resolutionEditing.js +246 -0
- package/lib/utils/resolutionEditing.js.map +1 -0
- package/lib/utils/resolutionUtils.d.ts +28 -0
- package/lib/utils/resolutionUtils.d.ts.map +1 -0
- package/lib/utils/resolutionUtils.js +216 -0
- package/lib/utils/resolutionUtils.js.map +1 -0
- package/lib/utils/tsResIntegration.d.ts +71 -0
- package/lib/utils/tsResIntegration.d.ts.map +1 -0
- package/lib/utils/tsResIntegration.js +294 -0
- package/lib/utils/tsResIntegration.js.map +1 -0
- package/lib/utils/zipLoader/browserZipLoader.d.ts +48 -0
- package/lib/utils/zipLoader/browserZipLoader.d.ts.map +1 -0
- package/lib/utils/zipLoader/browserZipLoader.js +247 -0
- package/lib/utils/zipLoader/browserZipLoader.js.map +1 -0
- package/lib/utils/zipLoader/index.d.ts +8 -0
- package/lib/utils/zipLoader/index.d.ts.map +1 -0
- package/lib/utils/zipLoader/index.js +13 -0
- package/lib/utils/zipLoader/index.js.map +1 -0
- package/lib/utils/zipLoader/nodeZipBuilder.d.ts +55 -0
- package/lib/utils/zipLoader/nodeZipBuilder.d.ts.map +1 -0
- package/lib/utils/zipLoader/nodeZipBuilder.js +98 -0
- package/lib/utils/zipLoader/nodeZipBuilder.js.map +1 -0
- package/lib/utils/zipLoader/types.d.ts +139 -0
- package/lib/utils/zipLoader/types.d.ts.map +1 -0
- package/lib/utils/zipLoader/types.js +2 -0
- package/lib/utils/zipLoader/types.js.map +1 -0
- package/lib/utils/zipLoader/zipUtils.d.ts +53 -0
- package/lib/utils/zipLoader/zipUtils.d.ts.map +1 -0
- package/lib/utils/zipLoader/zipUtils.js +229 -0
- package/lib/utils/zipLoader/zipUtils.js.map +1 -0
- package/package.json +69 -0
- package/rush-logs/ts-res-ui-components.build.cache.log +3 -0
- package/rush-logs/ts-res-ui-components.build.log +9 -0
- package/src/components/common/QualifierContextControl.tsx +151 -0
- package/src/components/common/ResourceListView.tsx +63 -0
- package/src/components/common/ResourceTreeView.tsx +271 -0
- package/src/components/forms/HierarchyEditor.tsx +204 -0
- package/src/components/forms/QualifierEditForm.tsx +355 -0
- package/src/components/forms/QualifierTypeEditForm.tsx +347 -0
- package/src/components/forms/ResourceTypeEditForm.tsx +331 -0
- package/src/components/forms/index.ts +11 -0
- package/src/components/orchestrator/ResourceOrchestrator.tsx +372 -0
- package/src/components/views/CompiledView/index.tsx +922 -0
- package/src/components/views/ConfigurationView/index.tsx +800 -0
- package/src/components/views/FilterView/index.tsx +825 -0
- package/src/components/views/ImportView/index.tsx +717 -0
- package/src/components/views/ResolutionView/EditableJsonView.tsx +214 -0
- package/src/components/views/ResolutionView/ResolutionEditControls.tsx +170 -0
- package/src/components/views/ResolutionView/index.tsx +591 -0
- package/src/components/views/SourceView/index.tsx +536 -0
- package/src/components/views/ZipLoaderView/index.tsx +485 -0
- package/src/hooks/useConfigurationState.ts +374 -0
- package/src/hooks/useFilterState.ts +97 -0
- package/src/hooks/useResolutionState.ts +355 -0
- package/src/hooks/useResourceData.ts +467 -0
- package/src/hooks/useViewState.ts +44 -0
- package/src/index.ts +45 -0
- package/src/test/helpers/testDataLoader.ts +195 -0
- package/src/test/unit/utils/configurationUtils.test.ts +630 -0
- package/src/test/unit/utils/fileProcessing.test.ts +391 -0
- package/src/test/unit/utils/filterResources.test.ts +574 -0
- package/src/test/unit/utils/resolutionEditing.test.ts +556 -0
- package/src/test/unit/utils/resolutionUtils.test.ts +521 -0
- package/src/test/unit/utils/tsResIntegration.test.ts +433 -0
- package/src/types/index.ts +322 -0
- package/src/utils/configurationUtils.ts +424 -0
- package/src/utils/fileProcessing.ts +160 -0
- package/src/utils/filterResources.ts +206 -0
- package/src/utils/resolutionEditing.ts +319 -0
- package/src/utils/resolutionUtils.ts +289 -0
- package/src/utils/tsResIntegration.ts +440 -0
- package/src/utils/zipLoader/browserZipLoader.ts +319 -0
- package/src/utils/zipLoader/index.ts +26 -0
- package/src/utils/zipLoader/nodeZipBuilder.ts +153 -0
- package/src/utils/zipLoader/types.ts +175 -0
- package/src/utils/zipLoader/zipUtils.ts +266 -0
- package/temp/build/typescript/ts_gZid87Hu.json +1 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { Result, succeed, fail } from '@fgv/ts-utils';
|
|
2
|
+
import { Runtime, Import, Resources } from '@fgv/ts-res';
|
|
3
|
+
import { ProcessedResources } from '../types';
|
|
4
|
+
|
|
5
|
+
export interface FilterOptions {
|
|
6
|
+
partialContextMatch?: boolean;
|
|
7
|
+
enableDebugLogging?: boolean;
|
|
8
|
+
reduceQualifiers?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface FilteredResource {
|
|
12
|
+
id: string;
|
|
13
|
+
originalCandidateCount: number;
|
|
14
|
+
filteredCandidateCount: number;
|
|
15
|
+
hasWarning: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface FilterResult {
|
|
19
|
+
success: boolean;
|
|
20
|
+
filteredResources: FilteredResource[];
|
|
21
|
+
processedResources?: ProcessedResources;
|
|
22
|
+
error?: string;
|
|
23
|
+
warnings: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Helper function for conditional debug logging
|
|
27
|
+
const debugLog = (enableDebug: boolean, ...args: unknown[]) => {
|
|
28
|
+
if (enableDebug) {
|
|
29
|
+
console.log(...args);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check if filter values object has any meaningful values
|
|
35
|
+
*/
|
|
36
|
+
export function hasFilterValues(values: Record<string, string | undefined>): boolean {
|
|
37
|
+
return Object.values(values).some((value) => value !== undefined && value !== '');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get a summary string of active filter values
|
|
42
|
+
*/
|
|
43
|
+
export function getFilterSummary(values: Record<string, string | undefined>): string {
|
|
44
|
+
const activeFilters = Object.entries(values)
|
|
45
|
+
.filter(([, value]) => value !== undefined && value !== '')
|
|
46
|
+
.map(([key, value]) => `${key}=${value}`);
|
|
47
|
+
return activeFilters.length > 0 ? activeFilters.join(', ') : 'No filters';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Creates a filtered resource manager using the ResourceManagerBuilder.clone() method.
|
|
52
|
+
* This is a simplified implementation that leverages the built-in filtering functionality.
|
|
53
|
+
*/
|
|
54
|
+
export const createFilteredResourceManagerSimple = async (
|
|
55
|
+
originalSystem: ProcessedResources['system'],
|
|
56
|
+
partialContext: Record<string, string | undefined>,
|
|
57
|
+
options: FilterOptions = { partialContextMatch: true }
|
|
58
|
+
): Promise<Result<ProcessedResources>> => {
|
|
59
|
+
const enableDebug = options.enableDebugLogging === true;
|
|
60
|
+
|
|
61
|
+
debugLog(enableDebug, '=== SIMPLE FILTER CREATION ===');
|
|
62
|
+
debugLog(enableDebug, 'Original system:', originalSystem);
|
|
63
|
+
debugLog(enableDebug, 'Partial context:', partialContext);
|
|
64
|
+
|
|
65
|
+
// Validate the original system
|
|
66
|
+
if (!originalSystem?.resourceManager) {
|
|
67
|
+
return fail('Original system or resourceManager is undefined');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Filter out undefined values from the context before processing
|
|
71
|
+
const filteredContext = Object.fromEntries(
|
|
72
|
+
Object.entries(partialContext).filter(([, value]) => value !== undefined)
|
|
73
|
+
) as Record<string, string>;
|
|
74
|
+
|
|
75
|
+
// Try to use ResourceManagerBuilder.clone() for proper filtering first
|
|
76
|
+
debugLog(enableDebug, 'Using ResourceManagerBuilder for proper filtering');
|
|
77
|
+
debugLog(enableDebug, 'Validating context and cloning manager:', filteredContext);
|
|
78
|
+
const resourceManagerBuilder = originalSystem.resourceManager;
|
|
79
|
+
|
|
80
|
+
return resourceManagerBuilder
|
|
81
|
+
.validateContext(filteredContext)
|
|
82
|
+
.onSuccess((validatedContext) => {
|
|
83
|
+
debugLog(enableDebug, 'Context validated, creating clone with context:', validatedContext);
|
|
84
|
+
return resourceManagerBuilder.clone({
|
|
85
|
+
filterForContext: validatedContext,
|
|
86
|
+
reduceQualifiers: options.reduceQualifiers
|
|
87
|
+
});
|
|
88
|
+
})
|
|
89
|
+
.withErrorFormat((e) => `Failed to validate context or clone: ${e}`)
|
|
90
|
+
.onSuccess((filteredManager) => {
|
|
91
|
+
debugLog(enableDebug, 'Filtered manager created:', filteredManager);
|
|
92
|
+
|
|
93
|
+
// Create new ImportManager for the filtered system
|
|
94
|
+
return Import.ImportManager.create({
|
|
95
|
+
resources: filteredManager
|
|
96
|
+
})
|
|
97
|
+
.withErrorFormat((e) => `Failed to create filtered import manager: ${e}`)
|
|
98
|
+
.onSuccess((newImportManager) => {
|
|
99
|
+
// Create new ContextQualifierProvider for the filtered system
|
|
100
|
+
return Runtime.ValidatingSimpleContextQualifierProvider.create({
|
|
101
|
+
qualifiers: originalSystem.qualifiers
|
|
102
|
+
})
|
|
103
|
+
.withErrorFormat((e) => `Failed to create filtered context provider: ${e}`)
|
|
104
|
+
.onSuccess((newContextQualifierProvider) => {
|
|
105
|
+
// Build the new system object
|
|
106
|
+
const newSystem = {
|
|
107
|
+
qualifierTypes: originalSystem.qualifierTypes,
|
|
108
|
+
qualifiers: originalSystem.qualifiers,
|
|
109
|
+
resourceTypes: originalSystem.resourceTypes,
|
|
110
|
+
resourceManager: filteredManager,
|
|
111
|
+
importManager: newImportManager,
|
|
112
|
+
contextQualifierProvider: newContextQualifierProvider
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Get compiled collection from the filtered manager
|
|
116
|
+
return filteredManager
|
|
117
|
+
.getCompiledResourceCollection({ includeMetadata: true })
|
|
118
|
+
.withErrorFormat((e) => `Failed to get compiled collection: ${e}`)
|
|
119
|
+
.onSuccess((compiledCollection) => {
|
|
120
|
+
// Create resolver for the filtered system
|
|
121
|
+
return Runtime.ResourceResolver.create({
|
|
122
|
+
resourceManager: filteredManager,
|
|
123
|
+
qualifierTypes: originalSystem.qualifierTypes,
|
|
124
|
+
contextQualifierProvider: newContextQualifierProvider
|
|
125
|
+
})
|
|
126
|
+
.withErrorFormat((e) => `Failed to create resolver: ${e}`)
|
|
127
|
+
.onSuccess((resolver) => {
|
|
128
|
+
// Create summary
|
|
129
|
+
const resourceIds = Array.from(filteredManager.resources.keys());
|
|
130
|
+
const summary = {
|
|
131
|
+
totalResources: resourceIds.length,
|
|
132
|
+
resourceIds,
|
|
133
|
+
errorCount: 0,
|
|
134
|
+
warnings: [] as string[]
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const processedResources: ProcessedResources = {
|
|
138
|
+
system: newSystem,
|
|
139
|
+
compiledCollection,
|
|
140
|
+
resolver,
|
|
141
|
+
resourceCount: resourceIds.length,
|
|
142
|
+
summary
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
debugLog(enableDebug, '=== FILTERED PROCESSING COMPLETE ===');
|
|
146
|
+
debugLog(enableDebug, 'Filtered resource count:', resourceIds.length);
|
|
147
|
+
debugLog(enableDebug, 'Filtered resource IDs:', resourceIds);
|
|
148
|
+
|
|
149
|
+
return succeed(processedResources);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
})
|
|
155
|
+
.onFailure((error) => {
|
|
156
|
+
debugLog(enableDebug, 'Failed to create filtered resource manager:', error);
|
|
157
|
+
return fail(`Failed to create filtered resource manager: ${error}`);
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Analyze filtered resources compared to original resources
|
|
163
|
+
*/
|
|
164
|
+
export function analyzeFilteredResources(
|
|
165
|
+
originalResourceIds: string[],
|
|
166
|
+
filteredProcessedResources: ProcessedResources,
|
|
167
|
+
originalProcessedResources: ProcessedResources
|
|
168
|
+
): FilterResult {
|
|
169
|
+
const filteredResources: FilteredResource[] = [];
|
|
170
|
+
const warnings: string[] = [];
|
|
171
|
+
|
|
172
|
+
for (const resourceId of originalResourceIds) {
|
|
173
|
+
// Get original resource info
|
|
174
|
+
const originalResourceResult =
|
|
175
|
+
originalProcessedResources.system.resourceManager.getBuiltResource(resourceId);
|
|
176
|
+
const originalCandidateCount = originalResourceResult.isSuccess()
|
|
177
|
+
? originalResourceResult.value.candidates.length
|
|
178
|
+
: 0;
|
|
179
|
+
|
|
180
|
+
// Get filtered resource info
|
|
181
|
+
const filteredResourceResult =
|
|
182
|
+
filteredProcessedResources.system.resourceManager.getBuiltResource(resourceId);
|
|
183
|
+
const filteredCandidateCount = filteredResourceResult.isSuccess()
|
|
184
|
+
? filteredResourceResult.value.candidates.length
|
|
185
|
+
: 0;
|
|
186
|
+
|
|
187
|
+
const hasWarning = filteredCandidateCount === 0 && originalCandidateCount > 0;
|
|
188
|
+
if (hasWarning) {
|
|
189
|
+
warnings.push(`Resource ${resourceId} has no matching candidates after filtering`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
filteredResources.push({
|
|
193
|
+
id: resourceId,
|
|
194
|
+
originalCandidateCount,
|
|
195
|
+
filteredCandidateCount,
|
|
196
|
+
hasWarning
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
success: true,
|
|
202
|
+
filteredResources,
|
|
203
|
+
processedResources: filteredProcessedResources,
|
|
204
|
+
warnings
|
|
205
|
+
};
|
|
206
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { Result, succeed, fail } from '@fgv/ts-utils';
|
|
2
|
+
import { ResourceJson, Resources, Runtime } from '@fgv/ts-res';
|
|
3
|
+
import { Diff } from '@fgv/ts-json';
|
|
4
|
+
import { JsonObject } from '@fgv/ts-json-base';
|
|
5
|
+
import { ProcessedResources, JsonValue } from '../types';
|
|
6
|
+
|
|
7
|
+
export interface EditedResourceInfo {
|
|
8
|
+
resourceId: string;
|
|
9
|
+
originalValue: JsonValue;
|
|
10
|
+
editedValue: JsonValue;
|
|
11
|
+
timestamp: Date;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface EditValidationResult {
|
|
15
|
+
isValid: boolean;
|
|
16
|
+
errors: string[];
|
|
17
|
+
warnings: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validates an edited resource JSON value
|
|
22
|
+
*/
|
|
23
|
+
export function validateEditedResource(editedValue: JsonValue): EditValidationResult {
|
|
24
|
+
const errors: string[] = [];
|
|
25
|
+
const warnings: string[] = [];
|
|
26
|
+
|
|
27
|
+
// Basic JSON validation
|
|
28
|
+
if (editedValue === null || editedValue === undefined) {
|
|
29
|
+
errors.push('Resource value cannot be null or undefined');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Check if it's valid JSON-serializable
|
|
33
|
+
try {
|
|
34
|
+
JSON.stringify(editedValue);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
errors.push(`Invalid JSON: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Type-specific validation
|
|
40
|
+
if (typeof editedValue === 'object' && editedValue !== null) {
|
|
41
|
+
// Object validation - check for circular references
|
|
42
|
+
const seen = new Set<unknown>();
|
|
43
|
+
const checkCircular = (obj: unknown): boolean => {
|
|
44
|
+
if (seen.has(obj)) return true;
|
|
45
|
+
seen.add(obj);
|
|
46
|
+
if (typeof obj === 'object' && obj !== null) {
|
|
47
|
+
for (const key in obj as Record<string, unknown>) {
|
|
48
|
+
const objTyped = obj as Record<string, unknown>;
|
|
49
|
+
if (typeof objTyped[key] === 'object' && objTyped[key] !== null) {
|
|
50
|
+
if (checkCircular(objTyped[key])) return true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
seen.delete(obj);
|
|
55
|
+
return false;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
if (checkCircular(editedValue)) {
|
|
59
|
+
errors.push('Resource contains circular references');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
isValid: errors.length === 0,
|
|
65
|
+
errors,
|
|
66
|
+
warnings
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Computes a 3-way diff between base, resolved, and edited values to create minimal delta
|
|
72
|
+
* @param baseValue - The base/original value before resolution (if available)
|
|
73
|
+
* @param resolvedValue - The fully resolved/composed value shown to user
|
|
74
|
+
* @param editedValue - The value after user edits
|
|
75
|
+
* @returns A minimal delta object with only the changes, or null if no changes
|
|
76
|
+
*/
|
|
77
|
+
export function computeResourceDelta(
|
|
78
|
+
baseValue: JsonValue | undefined,
|
|
79
|
+
resolvedValue: JsonValue,
|
|
80
|
+
editedValue: JsonValue
|
|
81
|
+
): Result<JsonValue> {
|
|
82
|
+
// Use ts-json's three-way diff for proper delta computation
|
|
83
|
+
const diffResult = Diff.jsonThreeWayDiff(resolvedValue, editedValue);
|
|
84
|
+
|
|
85
|
+
if (diffResult.isFailure()) {
|
|
86
|
+
// Fall back to full replacement on diff failure
|
|
87
|
+
console.error('Failed to compute three-way diff:', diffResult.message);
|
|
88
|
+
return succeed(editedValue);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const diff = diffResult.value;
|
|
92
|
+
|
|
93
|
+
// If identical, no changes needed
|
|
94
|
+
if (diff.identical) {
|
|
95
|
+
return succeed(null);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Build a proper delta that includes deletions as null values
|
|
99
|
+
const delta: Record<string, JsonValue> = {};
|
|
100
|
+
|
|
101
|
+
// Add all changes/additions from onlyInB
|
|
102
|
+
if (diff.onlyInB !== null) {
|
|
103
|
+
Object.assign(delta, diff.onlyInB);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Add deletions as null values from onlyInA
|
|
107
|
+
// onlyInA contains properties that existed in resolved but not in edited
|
|
108
|
+
if (diff.onlyInA !== null) {
|
|
109
|
+
// Add null entries for all deleted properties
|
|
110
|
+
addDeletionsAsNull(diff.onlyInA, delta);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// If delta is empty, no changes
|
|
114
|
+
if (Object.keys(delta).length === 0) {
|
|
115
|
+
return succeed(null);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return succeed(delta);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Recursively adds null values to delta for all properties in the deleted object
|
|
123
|
+
*/
|
|
124
|
+
function addDeletionsAsNull(deleted: JsonValue, delta: Record<string, JsonValue>): void {
|
|
125
|
+
if (typeof deleted === 'object' && deleted !== null && !Array.isArray(deleted)) {
|
|
126
|
+
const deletedObj = deleted as Record<string, JsonValue>;
|
|
127
|
+
for (const key in deletedObj) {
|
|
128
|
+
if (deletedObj.hasOwnProperty(key)) {
|
|
129
|
+
// If this key already exists in delta (from onlyInB), it means the property
|
|
130
|
+
// was modified, not deleted, so don't override with null
|
|
131
|
+
if (!(key in delta)) {
|
|
132
|
+
delta[key] = null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Creates candidate declarations for edited resources with proper delta handling
|
|
141
|
+
*/
|
|
142
|
+
export function createCandidateDeclarations(
|
|
143
|
+
editedResources: Map<string, { originalValue: JsonValue; editedValue: JsonValue; delta: JsonValue }>,
|
|
144
|
+
currentContext: Record<string, string>
|
|
145
|
+
): ResourceJson.Json.ILooseResourceCandidateDecl[] {
|
|
146
|
+
const declarations: ResourceJson.Json.ILooseResourceCandidateDecl[] = [];
|
|
147
|
+
|
|
148
|
+
for (const [resourceId, resourceEdit] of editedResources.entries()) {
|
|
149
|
+
// Create conditions from current context (using array format)
|
|
150
|
+
const conditions: ResourceJson.Json.ILooseConditionDecl[] = [];
|
|
151
|
+
|
|
152
|
+
for (const [qualifierName, qualifierValue] of Object.entries(currentContext)) {
|
|
153
|
+
if (qualifierValue && qualifierValue.trim() !== '') {
|
|
154
|
+
conditions.push({
|
|
155
|
+
qualifierName,
|
|
156
|
+
operator: 'matches',
|
|
157
|
+
value: qualifierValue,
|
|
158
|
+
priority: 900 // High priority for user edits
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Always use the delta if we have one (which should be the minimal changes)
|
|
164
|
+
// The delta will be null if there are no changes, or the delta itself if there are changes
|
|
165
|
+
const hasChanges = resourceEdit.delta !== null && resourceEdit.delta !== undefined;
|
|
166
|
+
|
|
167
|
+
if (!hasChanges) {
|
|
168
|
+
// No changes, skip this resource
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Always save as partial with just the delta when we have changes
|
|
173
|
+
// This ensures minimal, clean resource files
|
|
174
|
+
declarations.push({
|
|
175
|
+
id: resourceId,
|
|
176
|
+
conditions: conditions.length > 0 ? conditions : undefined,
|
|
177
|
+
json: resourceEdit.delta as JsonObject, // Always use the delta (minimal changes only)
|
|
178
|
+
isPartial: true, // Always partial when saving deltas
|
|
179
|
+
mergeMethod: 'augment' // Always augment to merge the delta with base
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return declarations;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Rebuilds the resource system with edited candidates using deltas
|
|
188
|
+
*/
|
|
189
|
+
export async function rebuildSystemWithEdits(
|
|
190
|
+
originalSystem: ProcessedResources['system'],
|
|
191
|
+
editedResources: Map<string, { originalValue: JsonValue; editedValue: JsonValue; delta: JsonValue }>,
|
|
192
|
+
currentContext: Record<string, string>
|
|
193
|
+
): Promise<Result<ProcessedResources>> {
|
|
194
|
+
try {
|
|
195
|
+
const candidateDeclarations = createCandidateDeclarations(editedResources, currentContext);
|
|
196
|
+
|
|
197
|
+
const clonedManager = originalSystem.resourceManager.clone({
|
|
198
|
+
candidates: candidateDeclarations
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (clonedManager.isFailure()) {
|
|
202
|
+
return fail(`Failed to clone manager: ${clonedManager.message}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Get compiled collection from the updated manager
|
|
206
|
+
const compiledResult = clonedManager.value.getCompiledResourceCollection({ includeMetadata: true });
|
|
207
|
+
if (compiledResult.isFailure()) {
|
|
208
|
+
return fail(`Failed to get compiled collection: ${compiledResult.message}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Create resolver for the updated system
|
|
212
|
+
const resolverResult = Runtime.ResourceResolver.create({
|
|
213
|
+
resourceManager: clonedManager.value,
|
|
214
|
+
qualifierTypes: originalSystem.qualifierTypes,
|
|
215
|
+
contextQualifierProvider: originalSystem.contextQualifierProvider
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
if (resolverResult.isFailure()) {
|
|
219
|
+
return fail(`Failed to create resolver: ${resolverResult.message}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Create summary
|
|
223
|
+
const resourceIds = Array.from(clonedManager.value.resources.keys());
|
|
224
|
+
const summary = {
|
|
225
|
+
totalResources: resourceIds.length,
|
|
226
|
+
resourceIds,
|
|
227
|
+
errorCount: 0,
|
|
228
|
+
warnings: []
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const updatedSystem: ProcessedResources = {
|
|
232
|
+
system: {
|
|
233
|
+
qualifierTypes: originalSystem.qualifierTypes,
|
|
234
|
+
qualifiers: originalSystem.qualifiers,
|
|
235
|
+
resourceTypes: originalSystem.resourceTypes,
|
|
236
|
+
resourceManager: clonedManager.value,
|
|
237
|
+
importManager: originalSystem.importManager,
|
|
238
|
+
contextQualifierProvider: originalSystem.contextQualifierProvider
|
|
239
|
+
},
|
|
240
|
+
compiledCollection: compiledResult.value,
|
|
241
|
+
resolver: resolverResult.value,
|
|
242
|
+
resourceCount: resourceIds.length,
|
|
243
|
+
summary
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
return succeed(updatedSystem);
|
|
247
|
+
} catch (error) {
|
|
248
|
+
return fail(
|
|
249
|
+
`Failed to rebuild system with edits: ${error instanceof Error ? error.message : String(error)}`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Extracts the current resolution context from resolver state
|
|
256
|
+
*/
|
|
257
|
+
export function extractResolutionContext(
|
|
258
|
+
resolver: Runtime.ResourceResolver,
|
|
259
|
+
contextValues: Record<string, string>
|
|
260
|
+
): Record<string, string> {
|
|
261
|
+
// Filter out empty/undefined context values
|
|
262
|
+
const cleanContext: Record<string, string> = {};
|
|
263
|
+
|
|
264
|
+
for (const [key, value] of Object.entries(contextValues)) {
|
|
265
|
+
if (value && value.trim() !== '') {
|
|
266
|
+
cleanContext[key] = value.trim();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return cleanContext;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Creates a collision detection key for tracking edit conflicts
|
|
275
|
+
*/
|
|
276
|
+
export function createEditCollisionKey(resourceId: string, context: Record<string, string>): string {
|
|
277
|
+
const contextEntries = Object.entries(context)
|
|
278
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
279
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
280
|
+
.join('&');
|
|
281
|
+
|
|
282
|
+
return `${resourceId}?${contextEntries}`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Checks for potential edit conflicts with existing candidates
|
|
287
|
+
*/
|
|
288
|
+
export function checkEditConflicts(
|
|
289
|
+
resourceManager: Resources.ResourceManagerBuilder | Runtime.IResourceManager,
|
|
290
|
+
editedResources: Map<string, JsonValue>,
|
|
291
|
+
currentContext: Record<string, string>
|
|
292
|
+
): { conflicts: string[]; warnings: string[] } {
|
|
293
|
+
const conflicts: string[] = [];
|
|
294
|
+
const warnings: string[] = [];
|
|
295
|
+
|
|
296
|
+
for (const [resourceId] of editedResources) {
|
|
297
|
+
try {
|
|
298
|
+
// Get the current resource to check for conflicts
|
|
299
|
+
const resourceResult = resourceManager.getBuiltResource(resourceId);
|
|
300
|
+
if (resourceResult.isSuccess()) {
|
|
301
|
+
const resource = resourceResult.value;
|
|
302
|
+
|
|
303
|
+
// Check if we're likely to create a conflict
|
|
304
|
+
if (resource.candidates.length > 1) {
|
|
305
|
+
warnings.push(
|
|
306
|
+
`Resource ${resourceId} has ${resource.candidates.length} candidates - edits may create conflicts`
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Could add more sophisticated conflict detection here
|
|
311
|
+
// based on condition overlap analysis
|
|
312
|
+
}
|
|
313
|
+
} catch (error) {
|
|
314
|
+
// Ignore errors in conflict checking
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return { conflicts, warnings };
|
|
319
|
+
}
|