@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.
Files changed (231) hide show
  1. package/.rush/temp/03c8b056281d9db0a97d8a6e25eea798a160d393.tar.log +271 -0
  2. package/.rush/temp/chunked-rush-logs/ts-res-ui-components.build.chunks.jsonl +9 -0
  3. package/.rush/temp/operation/build/all.log +9 -0
  4. package/.rush/temp/operation/build/log-chunks.jsonl +9 -0
  5. package/.rush/temp/operation/build/state.json +3 -0
  6. package/.rush/temp/shrinkwrap-deps.json +1111 -0
  7. package/README.md +18 -0
  8. package/REFACTORING_PLAN.md +171 -0
  9. package/config/jest.config.json +16 -0
  10. package/config/jest.setup.js +64 -0
  11. package/config/rig.json +16 -0
  12. package/lib/components/common/QualifierContextControl.d.ts +14 -0
  13. package/lib/components/common/QualifierContextControl.d.ts.map +1 -0
  14. package/lib/components/common/QualifierContextControl.js +78 -0
  15. package/lib/components/common/QualifierContextControl.js.map +1 -0
  16. package/lib/components/common/ResourceListView.d.ts +11 -0
  17. package/lib/components/common/ResourceListView.d.ts.map +1 -0
  18. package/lib/components/common/ResourceListView.js +20 -0
  19. package/lib/components/common/ResourceListView.js.map +1 -0
  20. package/lib/components/common/ResourceTreeView.d.ts +12 -0
  21. package/lib/components/common/ResourceTreeView.d.ts.map +1 -0
  22. package/lib/components/common/ResourceTreeView.js +162 -0
  23. package/lib/components/common/ResourceTreeView.js.map +1 -0
  24. package/lib/components/forms/HierarchyEditor.d.ts +10 -0
  25. package/lib/components/forms/HierarchyEditor.d.ts.map +1 -0
  26. package/lib/components/forms/HierarchyEditor.js +106 -0
  27. package/lib/components/forms/HierarchyEditor.js.map +1 -0
  28. package/lib/components/forms/QualifierEditForm.d.ts +11 -0
  29. package/lib/components/forms/QualifierEditForm.d.ts.map +1 -0
  30. package/lib/components/forms/QualifierEditForm.js +181 -0
  31. package/lib/components/forms/QualifierEditForm.js.map +1 -0
  32. package/lib/components/forms/QualifierTypeEditForm.d.ts +10 -0
  33. package/lib/components/forms/QualifierTypeEditForm.d.ts.map +1 -0
  34. package/lib/components/forms/QualifierTypeEditForm.js +172 -0
  35. package/lib/components/forms/QualifierTypeEditForm.js.map +1 -0
  36. package/lib/components/forms/ResourceTypeEditForm.d.ts +10 -0
  37. package/lib/components/forms/ResourceTypeEditForm.d.ts.map +1 -0
  38. package/lib/components/forms/ResourceTypeEditForm.js +188 -0
  39. package/lib/components/forms/ResourceTypeEditForm.js.map +1 -0
  40. package/lib/components/forms/index.d.ts +9 -0
  41. package/lib/components/forms/index.d.ts.map +1 -0
  42. package/lib/components/forms/index.js +5 -0
  43. package/lib/components/forms/index.js.map +1 -0
  44. package/lib/components/orchestrator/ResourceOrchestrator.d.ts +14 -0
  45. package/lib/components/orchestrator/ResourceOrchestrator.d.ts.map +1 -0
  46. package/lib/components/orchestrator/ResourceOrchestrator.js +278 -0
  47. package/lib/components/orchestrator/ResourceOrchestrator.js.map +1 -0
  48. package/lib/components/views/CompiledView/index.d.ts +5 -0
  49. package/lib/components/views/CompiledView/index.d.ts.map +1 -0
  50. package/lib/components/views/CompiledView/index.js +595 -0
  51. package/lib/components/views/CompiledView/index.js.map +1 -0
  52. package/lib/components/views/ConfigurationView/index.d.ts +5 -0
  53. package/lib/components/views/ConfigurationView/index.d.ts.map +1 -0
  54. package/lib/components/views/ConfigurationView/index.js +363 -0
  55. package/lib/components/views/ConfigurationView/index.js.map +1 -0
  56. package/lib/components/views/FilterView/index.d.ts +5 -0
  57. package/lib/components/views/FilterView/index.d.ts.map +1 -0
  58. package/lib/components/views/FilterView/index.js +463 -0
  59. package/lib/components/views/FilterView/index.js.map +1 -0
  60. package/lib/components/views/ImportView/index.d.ts +5 -0
  61. package/lib/components/views/ImportView/index.d.ts.map +1 -0
  62. package/lib/components/views/ImportView/index.js +514 -0
  63. package/lib/components/views/ImportView/index.js.map +1 -0
  64. package/lib/components/views/ResolutionView/EditableJsonView.d.ts +21 -0
  65. package/lib/components/views/ResolutionView/EditableJsonView.d.ts.map +1 -0
  66. package/lib/components/views/ResolutionView/EditableJsonView.js +109 -0
  67. package/lib/components/views/ResolutionView/EditableJsonView.js.map +1 -0
  68. package/lib/components/views/ResolutionView/ResolutionEditControls.d.ts +19 -0
  69. package/lib/components/views/ResolutionView/ResolutionEditControls.d.ts.map +1 -0
  70. package/lib/components/views/ResolutionView/ResolutionEditControls.js +82 -0
  71. package/lib/components/views/ResolutionView/ResolutionEditControls.js.map +1 -0
  72. package/lib/components/views/ResolutionView/index.d.ts +5 -0
  73. package/lib/components/views/ResolutionView/index.d.ts.map +1 -0
  74. package/lib/components/views/ResolutionView/index.js +255 -0
  75. package/lib/components/views/ResolutionView/index.js.map +1 -0
  76. package/lib/components/views/SourceView/index.d.ts +5 -0
  77. package/lib/components/views/SourceView/index.d.ts.map +1 -0
  78. package/lib/components/views/SourceView/index.js +316 -0
  79. package/lib/components/views/SourceView/index.js.map +1 -0
  80. package/lib/components/views/ZipLoaderView/index.d.ts +5 -0
  81. package/lib/components/views/ZipLoaderView/index.d.ts.map +1 -0
  82. package/lib/components/views/ZipLoaderView/index.js +313 -0
  83. package/lib/components/views/ZipLoaderView/index.js.map +1 -0
  84. package/lib/hooks/useConfigurationState.d.ts +46 -0
  85. package/lib/hooks/useConfigurationState.d.ts.map +1 -0
  86. package/lib/hooks/useConfigurationState.js +239 -0
  87. package/lib/hooks/useConfigurationState.js.map +1 -0
  88. package/lib/hooks/useFilterState.d.ts +7 -0
  89. package/lib/hooks/useFilterState.d.ts.map +1 -0
  90. package/lib/hooks/useFilterState.js +80 -0
  91. package/lib/hooks/useFilterState.js.map +1 -0
  92. package/lib/hooks/useResolutionState.d.ts +8 -0
  93. package/lib/hooks/useResolutionState.d.ts.map +1 -0
  94. package/lib/hooks/useResolutionState.js +253 -0
  95. package/lib/hooks/useResolutionState.js.map +1 -0
  96. package/lib/hooks/useResourceData.d.ts +19 -0
  97. package/lib/hooks/useResourceData.d.ts.map +1 -0
  98. package/lib/hooks/useResourceData.js +368 -0
  99. package/lib/hooks/useResourceData.js.map +1 -0
  100. package/lib/hooks/useViewState.d.ts +10 -0
  101. package/lib/hooks/useViewState.d.ts.map +1 -0
  102. package/lib/hooks/useViewState.js +29 -0
  103. package/lib/hooks/useViewState.js.map +1 -0
  104. package/lib/index.d.ts +27 -0
  105. package/lib/index.d.ts.map +1 -0
  106. package/lib/index.js +34 -0
  107. package/lib/index.js.map +1 -0
  108. package/lib/test/helpers/testDataLoader.d.ts +37 -0
  109. package/lib/test/helpers/testDataLoader.d.ts.map +1 -0
  110. package/lib/test/helpers/testDataLoader.js +171 -0
  111. package/lib/test/helpers/testDataLoader.js.map +1 -0
  112. package/lib/test/unit/utils/configurationUtils.test.d.ts +2 -0
  113. package/lib/test/unit/utils/configurationUtils.test.d.ts.map +1 -0
  114. package/lib/test/unit/utils/configurationUtils.test.js +497 -0
  115. package/lib/test/unit/utils/configurationUtils.test.js.map +1 -0
  116. package/lib/test/unit/utils/fileProcessing.test.d.ts +2 -0
  117. package/lib/test/unit/utils/fileProcessing.test.d.ts.map +1 -0
  118. package/lib/test/unit/utils/fileProcessing.test.js +321 -0
  119. package/lib/test/unit/utils/fileProcessing.test.js.map +1 -0
  120. package/lib/test/unit/utils/filterResources.test.d.ts +2 -0
  121. package/lib/test/unit/utils/filterResources.test.d.ts.map +1 -0
  122. package/lib/test/unit/utils/filterResources.test.js +403 -0
  123. package/lib/test/unit/utils/filterResources.test.js.map +1 -0
  124. package/lib/test/unit/utils/resolutionEditing.test.d.ts +2 -0
  125. package/lib/test/unit/utils/resolutionEditing.test.d.ts.map +1 -0
  126. package/lib/test/unit/utils/resolutionEditing.test.js +439 -0
  127. package/lib/test/unit/utils/resolutionEditing.test.js.map +1 -0
  128. package/lib/test/unit/utils/resolutionUtils.test.d.ts +2 -0
  129. package/lib/test/unit/utils/resolutionUtils.test.d.ts.map +1 -0
  130. package/lib/test/unit/utils/resolutionUtils.test.js +397 -0
  131. package/lib/test/unit/utils/resolutionUtils.test.js.map +1 -0
  132. package/lib/test/unit/utils/tsResIntegration.test.d.ts +2 -0
  133. package/lib/test/unit/utils/tsResIntegration.test.d.ts.map +1 -0
  134. package/lib/test/unit/utils/tsResIntegration.test.js +376 -0
  135. package/lib/test/unit/utils/tsResIntegration.test.js.map +1 -0
  136. package/lib/types/index.d.ts +251 -0
  137. package/lib/types/index.d.ts.map +1 -0
  138. package/lib/types/index.js +2 -0
  139. package/lib/types/index.js.map +1 -0
  140. package/lib/utils/configurationUtils.d.ts +74 -0
  141. package/lib/utils/configurationUtils.d.ts.map +1 -0
  142. package/lib/utils/configurationUtils.js +359 -0
  143. package/lib/utils/configurationUtils.js.map +1 -0
  144. package/lib/utils/fileProcessing.d.ts +18 -0
  145. package/lib/utils/fileProcessing.d.ts.map +1 -0
  146. package/lib/utils/fileProcessing.js +142 -0
  147. package/lib/utils/fileProcessing.js.map +1 -0
  148. package/lib/utils/filterResources.d.ts +38 -0
  149. package/lib/utils/filterResources.d.ts.map +1 -0
  150. package/lib/utils/filterResources.js +153 -0
  151. package/lib/utils/filterResources.js.map +1 -0
  152. package/lib/utils/resolutionEditing.d.ts +58 -0
  153. package/lib/utils/resolutionEditing.d.ts.map +1 -0
  154. package/lib/utils/resolutionEditing.js +246 -0
  155. package/lib/utils/resolutionEditing.js.map +1 -0
  156. package/lib/utils/resolutionUtils.d.ts +28 -0
  157. package/lib/utils/resolutionUtils.d.ts.map +1 -0
  158. package/lib/utils/resolutionUtils.js +216 -0
  159. package/lib/utils/resolutionUtils.js.map +1 -0
  160. package/lib/utils/tsResIntegration.d.ts +71 -0
  161. package/lib/utils/tsResIntegration.d.ts.map +1 -0
  162. package/lib/utils/tsResIntegration.js +294 -0
  163. package/lib/utils/tsResIntegration.js.map +1 -0
  164. package/lib/utils/zipLoader/browserZipLoader.d.ts +48 -0
  165. package/lib/utils/zipLoader/browserZipLoader.d.ts.map +1 -0
  166. package/lib/utils/zipLoader/browserZipLoader.js +247 -0
  167. package/lib/utils/zipLoader/browserZipLoader.js.map +1 -0
  168. package/lib/utils/zipLoader/index.d.ts +8 -0
  169. package/lib/utils/zipLoader/index.d.ts.map +1 -0
  170. package/lib/utils/zipLoader/index.js +13 -0
  171. package/lib/utils/zipLoader/index.js.map +1 -0
  172. package/lib/utils/zipLoader/nodeZipBuilder.d.ts +55 -0
  173. package/lib/utils/zipLoader/nodeZipBuilder.d.ts.map +1 -0
  174. package/lib/utils/zipLoader/nodeZipBuilder.js +98 -0
  175. package/lib/utils/zipLoader/nodeZipBuilder.js.map +1 -0
  176. package/lib/utils/zipLoader/types.d.ts +139 -0
  177. package/lib/utils/zipLoader/types.d.ts.map +1 -0
  178. package/lib/utils/zipLoader/types.js +2 -0
  179. package/lib/utils/zipLoader/types.js.map +1 -0
  180. package/lib/utils/zipLoader/zipUtils.d.ts +53 -0
  181. package/lib/utils/zipLoader/zipUtils.d.ts.map +1 -0
  182. package/lib/utils/zipLoader/zipUtils.js +229 -0
  183. package/lib/utils/zipLoader/zipUtils.js.map +1 -0
  184. package/package.json +69 -0
  185. package/rush-logs/ts-res-ui-components.build.cache.log +3 -0
  186. package/rush-logs/ts-res-ui-components.build.log +9 -0
  187. package/src/components/common/QualifierContextControl.tsx +151 -0
  188. package/src/components/common/ResourceListView.tsx +63 -0
  189. package/src/components/common/ResourceTreeView.tsx +271 -0
  190. package/src/components/forms/HierarchyEditor.tsx +204 -0
  191. package/src/components/forms/QualifierEditForm.tsx +355 -0
  192. package/src/components/forms/QualifierTypeEditForm.tsx +347 -0
  193. package/src/components/forms/ResourceTypeEditForm.tsx +331 -0
  194. package/src/components/forms/index.ts +11 -0
  195. package/src/components/orchestrator/ResourceOrchestrator.tsx +372 -0
  196. package/src/components/views/CompiledView/index.tsx +922 -0
  197. package/src/components/views/ConfigurationView/index.tsx +800 -0
  198. package/src/components/views/FilterView/index.tsx +825 -0
  199. package/src/components/views/ImportView/index.tsx +717 -0
  200. package/src/components/views/ResolutionView/EditableJsonView.tsx +214 -0
  201. package/src/components/views/ResolutionView/ResolutionEditControls.tsx +170 -0
  202. package/src/components/views/ResolutionView/index.tsx +591 -0
  203. package/src/components/views/SourceView/index.tsx +536 -0
  204. package/src/components/views/ZipLoaderView/index.tsx +485 -0
  205. package/src/hooks/useConfigurationState.ts +374 -0
  206. package/src/hooks/useFilterState.ts +97 -0
  207. package/src/hooks/useResolutionState.ts +355 -0
  208. package/src/hooks/useResourceData.ts +467 -0
  209. package/src/hooks/useViewState.ts +44 -0
  210. package/src/index.ts +45 -0
  211. package/src/test/helpers/testDataLoader.ts +195 -0
  212. package/src/test/unit/utils/configurationUtils.test.ts +630 -0
  213. package/src/test/unit/utils/fileProcessing.test.ts +391 -0
  214. package/src/test/unit/utils/filterResources.test.ts +574 -0
  215. package/src/test/unit/utils/resolutionEditing.test.ts +556 -0
  216. package/src/test/unit/utils/resolutionUtils.test.ts +521 -0
  217. package/src/test/unit/utils/tsResIntegration.test.ts +433 -0
  218. package/src/types/index.ts +322 -0
  219. package/src/utils/configurationUtils.ts +424 -0
  220. package/src/utils/fileProcessing.ts +160 -0
  221. package/src/utils/filterResources.ts +206 -0
  222. package/src/utils/resolutionEditing.ts +319 -0
  223. package/src/utils/resolutionUtils.ts +289 -0
  224. package/src/utils/tsResIntegration.ts +440 -0
  225. package/src/utils/zipLoader/browserZipLoader.ts +319 -0
  226. package/src/utils/zipLoader/index.ts +26 -0
  227. package/src/utils/zipLoader/nodeZipBuilder.ts +153 -0
  228. package/src/utils/zipLoader/types.ts +175 -0
  229. package/src/utils/zipLoader/zipUtils.ts +266 -0
  230. package/temp/build/typescript/ts_gZid87Hu.json +1 -0
  231. 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
+ }