@fgv/ts-res-browser 1.0.0

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 (95) hide show
  1. package/.rush/temp/5fc90bc7c2ccf812114ea099111568e5429be2a2.tar.log +15 -0
  2. package/.rush/temp/chunked-rush-logs/ts-res-browser.build.chunks.jsonl +35 -0
  3. package/.rush/temp/operation/build/all.log +35 -0
  4. package/.rush/temp/operation/build/log-chunks.jsonl +35 -0
  5. package/.rush/temp/operation/build/state.json +3 -0
  6. package/.rush/temp/shrinkwrap-deps.json +1217 -0
  7. package/README.md +95 -0
  8. package/TS_RES_BROWSER_PLAN.md +689 -0
  9. package/babel.config.js +17 -0
  10. package/config/jest.config.json +13 -0
  11. package/config/rig.json +16 -0
  12. package/dist/bundle.js +2 -0
  13. package/dist/bundle.js.LICENSE.txt +62 -0
  14. package/dist/index.html +1 -0
  15. package/package.json +78 -0
  16. package/postcss.config.js +3 -0
  17. package/public/index.html +15 -0
  18. package/rush-logs/ts-res-browser.build.cache.log +3 -0
  19. package/rush-logs/ts-res-browser.build.log +35 -0
  20. package/src/App.tsx +153 -0
  21. package/src/cli.ts +44 -0
  22. package/src/components/common/FileImporter.tsx +138 -0
  23. package/src/components/common/NavigationWarningModal.tsx +62 -0
  24. package/src/components/layout/AppLayout.tsx +46 -0
  25. package/src/components/layout/Header.tsx +22 -0
  26. package/src/components/layout/MessagesWindow.tsx +108 -0
  27. package/src/components/layout/Sidebar.tsx +92 -0
  28. package/src/components/tools/CompiledBrowser.tsx +949 -0
  29. package/src/components/tools/ConfigurationTool.tsx +1402 -0
  30. package/src/components/tools/FilterTool.tsx +1027 -0
  31. package/src/components/tools/ImportTool.tsx +246 -0
  32. package/src/components/tools/ResolutionViewer.tsx +1672 -0
  33. package/src/components/tools/SourceBrowser.tsx +496 -0
  34. package/src/hooks/useAppState.ts +107 -0
  35. package/src/hooks/useFileImport.ts +86 -0
  36. package/src/hooks/useNavigationWarning.ts +67 -0
  37. package/src/hooks/useResourceManager.ts +244 -0
  38. package/src/index.css +38 -0
  39. package/src/main.tsx +16 -0
  40. package/src/types/app.ts +62 -0
  41. package/src/types/fileSystemAccess.d.ts +94 -0
  42. package/src/utils/browserFileTreeAccessors.ts +245 -0
  43. package/src/utils/fileImport.ts +334 -0
  44. package/src/utils/filterResources.ts +677 -0
  45. package/src/utils/tsResIntegration.ts +467 -0
  46. package/tailwind.config.js +21 -0
  47. package/test-data/README.md +184 -0
  48. package/test-data/config-variations/README.md +64 -0
  49. package/test-data/config-variations/composition-terminology.json +112 -0
  50. package/test-data/config-variations/enterprise-complex.json +129 -0
  51. package/test-data/config-variations/gaming-app.json +87 -0
  52. package/test-data/config-variations/high-priority-language.json +65 -0
  53. package/test-data/config-variations/minimal-basic.json +37 -0
  54. package/test-data/config-variations/qualifier-default-values.json +71 -0
  55. package/test-data/config-variations/territory-first.json +68 -0
  56. package/test-data/config-variations/territory-hierarchy-test.json +31 -0
  57. package/test-data/config-variations/territory-hierarchy-unconstrained.json +32 -0
  58. package/test-data/default-matching-test/greeting.res.json +83 -0
  59. package/test-data/default-matching-test/only-defaults.res.json +48 -0
  60. package/test-data/sample-project/package.json +27 -0
  61. package/test-data/sample-project/src/resources/app-config.json +27 -0
  62. package/test-data/sample-project/src/resources/app.json +177 -0
  63. package/test-data/sample-project/src/resources/config/app.env=development.json +21 -0
  64. package/test-data/sample-project/src/resources/config/app.json +21 -0
  65. package/test-data/sample-project/src/resources/config/app.platform=mobile.json +23 -0
  66. package/test-data/sample-project/src/resources/feature-flags.json +23 -0
  67. package/test-data/sample-project/src/resources/home=CA/common.json +16 -0
  68. package/test-data/sample-project/src/resources/images/density=hdpi.json +29 -0
  69. package/test-data/sample-project/src/resources/images/home.json +29 -0
  70. package/test-data/sample-project/src/resources/images/logout.json +14 -0
  71. package/test-data/sample-project/src/resources/images/settings.json +14 -0
  72. package/test-data/sample-project/src/resources/images/ui-icons.json +29 -0
  73. package/test-data/sample-project/src/resources/images/user.density=hdpi.json +14 -0
  74. package/test-data/sample-project/src/resources/images/user.json +14 -0
  75. package/test-data/sample-project/src/resources/lang=en-US/common.json +15 -0
  76. package/test-data/sample-project/src/resources/language=en-CA/ui-terms.json +19 -0
  77. package/test-data/sample-project/src/resources/language=en-GB/ui-terms.json +30 -0
  78. package/test-data/sample-project/src/resources/language=fr-CA/ui-terms.json +25 -0
  79. package/test-data/sample-project/src/resources/language=nl-BE/ui-terms.json +17 -0
  80. package/test-data/sample-project/src/resources/region=europe/feature-flags.json +17 -0
  81. package/test-data/sample-project/src/resources/region=north-america/feature-flags.json +18 -0
  82. package/test-data/sample-project/src/resources/strings/dashboard.home=CA,language=fr.json +20 -0
  83. package/test-data/sample-project/src/resources/strings/dashboard.json +18 -0
  84. package/test-data/sample-project/src/resources/strings/dashboard.role=admin.json +23 -0
  85. package/test-data/sample-project/src/resources/strings/dashboard.role=user.json +21 -0
  86. package/test-data/sample-project/src/resources/strings/language=en-US.json +16 -0
  87. package/test-data/sample-project/src/resources/strings.json +78 -0
  88. package/test-data/sample-project/src/resources/territory=BE/app-config.json +19 -0
  89. package/test-data/sample-project/src/resources/territory=CA/app-config.json +21 -0
  90. package/test-data/sample-project/src/resources/territory=GB/app-config.json +25 -0
  91. package/test-data/sample-project/src/resources/territory=NL/app-config.json +25 -0
  92. package/test-data/sample-project/src/resources/ui-terms.json +96 -0
  93. package/test-data/sample-project/src/resources-config.json +111 -0
  94. package/tsconfig.json +27 -0
  95. package/webpack.config.js +54 -0
@@ -0,0 +1,1402 @@
1
+ import React, { useState, useMemo, useCallback } from 'react';
2
+ import {
3
+ CogIcon,
4
+ PlusIcon,
5
+ PencilIcon,
6
+ TrashIcon,
7
+ FolderOpenIcon,
8
+ DocumentArrowDownIcon,
9
+ CodeBracketIcon,
10
+ ChevronDownIcon,
11
+ ChevronUpIcon
12
+ } from '@heroicons/react/24/outline';
13
+ import { UseResourceManagerReturn } from '../../hooks/useResourceManager';
14
+ import { Message } from '../../types/app';
15
+ import { Config } from '@fgv/ts-res';
16
+ import {
17
+ DEFAULT_SYSTEM_CONFIGURATION,
18
+ getDefaultSystemConfiguration,
19
+ getAvailablePredefinedConfigurations,
20
+ getPredefinedConfiguration
21
+ } from '../../utils/tsResIntegration';
22
+ import { fileImporter } from '../../utils/fileImport';
23
+ import { Result } from '@fgv/ts-utils';
24
+
25
+ interface ConfigurationToolProps {
26
+ onMessage?: (type: Message['type'], message: string) => void;
27
+ resourceManager: UseResourceManagerReturn;
28
+ onUnsavedChanges?: (hasChanges: boolean) => void;
29
+ onSaveHandlerRef?: React.MutableRefObject<(() => void) | null>;
30
+ }
31
+
32
+ type ActivePanel = 'qualifiers' | 'qualifier-types' | 'resource-types';
33
+
34
+ const ConfigurationTool: React.FC<ConfigurationToolProps> = ({
35
+ onMessage,
36
+ resourceManager,
37
+ onUnsavedChanges,
38
+ onSaveHandlerRef
39
+ }) => {
40
+ const { state: resourceState } = resourceManager;
41
+ const [activePanel, setActivePanel] = useState<ActivePanel>('qualifiers');
42
+
43
+ // Check if we have processed resources
44
+ const hasProcessedResources = !!resourceState.processedResources;
45
+
46
+ // Check if we have an active configuration
47
+ const hasActiveConfiguration = !!resourceState.activeConfiguration;
48
+
49
+ // Get current system configuration
50
+ const systemConfiguration = useMemo(() => {
51
+ // Always use active configuration if available, regardless of processed resources
52
+ // This preserves the user's configuration context even after importing resources
53
+ return resourceState.activeConfiguration || DEFAULT_SYSTEM_CONFIGURATION;
54
+ }, [resourceState.activeConfiguration]);
55
+
56
+ // State for configuration edits
57
+ const [currentConfig, setCurrentConfig] = useState<Config.Model.ISystemConfiguration>(systemConfiguration);
58
+
59
+ // Update currentConfig when systemConfiguration changes
60
+ React.useEffect(() => {
61
+ setCurrentConfig(systemConfiguration);
62
+ }, [systemConfiguration]);
63
+
64
+ // State for editing items
65
+ const [editingQualifierType, setEditingQualifierType] = useState<string | null>(null);
66
+ const [editingQualifier, setEditingQualifier] = useState<string | null>(null);
67
+ const [editingMetadata, setEditingMetadata] = useState(false);
68
+
69
+ // Check if current config has changes compared to system configuration
70
+ const hasChanges = useMemo(() => {
71
+ return JSON.stringify(currentConfig) !== JSON.stringify(systemConfiguration);
72
+ }, [currentConfig, systemConfiguration]);
73
+
74
+ // Notify parent about unsaved changes
75
+ React.useEffect(() => {
76
+ onUnsavedChanges?.(hasChanges);
77
+ }, [hasChanges, onUnsavedChanges]);
78
+
79
+ // State for JSON view
80
+ const [showJsonView, setShowJsonView] = useState(false);
81
+
82
+ // Load configuration from file
83
+ const handleLoadConfiguration = useCallback(async () => {
84
+ try {
85
+ const fileResult = await fileImporter.pickFiles({
86
+ acceptedTypes: ['.json'],
87
+ multiple: false
88
+ });
89
+
90
+ if (fileResult.isFailure()) {
91
+ onMessage?.('error', `Failed to load configuration file: ${fileResult.message}`);
92
+ return;
93
+ }
94
+
95
+ const files = fileResult.value;
96
+ if (files.length === 0) {
97
+ onMessage?.('warning', 'No configuration file selected');
98
+ return;
99
+ }
100
+
101
+ const file = files[0];
102
+ let parsedConfig: Config.Model.ISystemConfiguration;
103
+
104
+ try {
105
+ parsedConfig = JSON.parse(file.content);
106
+ } catch (parseError) {
107
+ onMessage?.(
108
+ 'error',
109
+ `Failed to parse configuration file: ${
110
+ parseError instanceof Error ? parseError.message : String(parseError)
111
+ }`
112
+ );
113
+ return;
114
+ }
115
+
116
+ // Validate the configuration structure
117
+ if (!parsedConfig.qualifierTypes || !parsedConfig.qualifiers || !parsedConfig.resourceTypes) {
118
+ onMessage?.(
119
+ 'error',
120
+ 'Invalid configuration file: missing required sections (qualifierTypes, qualifiers, resourceTypes)'
121
+ );
122
+ return;
123
+ }
124
+
125
+ // Update the local config state for preview
126
+ setCurrentConfig(parsedConfig);
127
+ onMessage?.(
128
+ 'success',
129
+ `Configuration loaded successfully from ${file.name}${
130
+ parsedConfig.name ? ` (${parsedConfig.name})` : ''
131
+ }. Click "Apply Configuration" to activate it.`
132
+ );
133
+ } catch (error) {
134
+ onMessage?.(
135
+ 'error',
136
+ `Unexpected error loading configuration: ${error instanceof Error ? error.message : String(error)}`
137
+ );
138
+ }
139
+ }, [resourceManager.actions, onMessage]);
140
+
141
+ // Load predefined configuration
142
+ const handleLoadPredefinedConfiguration = useCallback(
143
+ async (configName: Config.PredefinedSystemConfiguration) => {
144
+ try {
145
+ const configResult = getPredefinedConfiguration(configName);
146
+
147
+ if (configResult.isFailure()) {
148
+ onMessage?.(
149
+ 'error',
150
+ `Failed to load predefined configuration '${configName}': ${configResult.message}`
151
+ );
152
+ return;
153
+ }
154
+
155
+ const config = configResult.value;
156
+
157
+ // Update the local config state for preview
158
+ setCurrentConfig(config);
159
+ onMessage?.(
160
+ 'success',
161
+ `Predefined configuration "${config.name}" loaded successfully. Click "Apply Configuration" to activate it.`
162
+ );
163
+ } catch (error) {
164
+ onMessage?.(
165
+ 'error',
166
+ `Unexpected error loading predefined configuration: ${
167
+ error instanceof Error ? error.message : String(error)
168
+ }`
169
+ );
170
+ }
171
+ },
172
+ [onMessage]
173
+ );
174
+
175
+ // Apply configuration handler
176
+ const handleApplyConfiguration = useCallback(
177
+ (newConfig: Config.Model.ISystemConfiguration) => {
178
+ try {
179
+ // Apply the configuration through the resource manager
180
+ resourceManager.actions.applyConfiguration(newConfig);
181
+
182
+ // Update local state
183
+ setCurrentConfig(newConfig);
184
+
185
+ // Provide user feedback
186
+ const configName = newConfig.name || 'Unnamed configuration';
187
+ onMessage?.(
188
+ 'success',
189
+ `Configuration "${configName}" applied successfully. Resources will use this configuration when loaded.`
190
+ );
191
+ } catch (error) {
192
+ onMessage?.(
193
+ 'error',
194
+ `Failed to apply configuration: ${error instanceof Error ? error.message : String(error)}`
195
+ );
196
+ }
197
+ },
198
+ [resourceManager.actions, onMessage]
199
+ );
200
+
201
+ // Handle save and navigate (called from navigation warning modal)
202
+ const handleSaveAndNavigate = useCallback(() => {
203
+ if (hasChanges) {
204
+ handleApplyConfiguration(currentConfig);
205
+ }
206
+ }, [hasChanges, currentConfig, handleApplyConfiguration]);
207
+
208
+ // Set the save handler reference for the parent component
209
+ React.useEffect(() => {
210
+ if (onSaveHandlerRef) {
211
+ onSaveHandlerRef.current = handleSaveAndNavigate;
212
+ }
213
+ return () => {
214
+ if (onSaveHandlerRef) {
215
+ onSaveHandlerRef.current = null;
216
+ }
217
+ };
218
+ }, [handleSaveAndNavigate, onSaveHandlerRef]);
219
+
220
+ // Restore defaults handler
221
+ const handleRestoreDefaults = useCallback(() => {
222
+ try {
223
+ // Get the latest default configuration
224
+ const defaultConfig = getDefaultSystemConfiguration();
225
+
226
+ // Apply the default configuration
227
+ resourceManager.actions.applyConfiguration(defaultConfig);
228
+
229
+ // Update local state
230
+ setCurrentConfig(defaultConfig);
231
+
232
+ // Provide user feedback
233
+ onMessage?.('success', 'Default configuration restored successfully.');
234
+ } catch (error) {
235
+ onMessage?.(
236
+ 'error',
237
+ `Failed to restore defaults: ${error instanceof Error ? error.message : String(error)}`
238
+ );
239
+ }
240
+ }, [resourceManager.actions, onMessage]);
241
+
242
+ // Handle metadata editing
243
+ const handleSaveMetadata = useCallback(
244
+ (name: string, description: string) => {
245
+ const updatedConfig = {
246
+ ...currentConfig,
247
+ name: name || undefined,
248
+ description: description || undefined
249
+ };
250
+ setCurrentConfig(updatedConfig);
251
+ setEditingMetadata(false);
252
+ onMessage?.('info', 'Configuration metadata updated');
253
+ },
254
+ [currentConfig, onMessage]
255
+ );
256
+
257
+ const handleCancelMetadataEdit = useCallback(() => {
258
+ setEditingMetadata(false);
259
+ }, []);
260
+
261
+ // Export configuration to JSON file
262
+ const handleExportConfiguration = useCallback(() => {
263
+ try {
264
+ const configJson = JSON.stringify(currentConfig, null, 2);
265
+ const blob = new Blob([configJson], { type: 'application/json' });
266
+ const url = URL.createObjectURL(blob);
267
+
268
+ const a = document.createElement('a');
269
+ a.href = url;
270
+ a.download = `${currentConfig.name || 'configuration'}.json`;
271
+ document.body.appendChild(a);
272
+ a.click();
273
+ document.body.removeChild(a);
274
+ URL.revokeObjectURL(url);
275
+
276
+ onMessage?.('success', 'Configuration exported successfully');
277
+ } catch (error) {
278
+ onMessage?.(
279
+ 'error',
280
+ `Failed to export configuration: ${error instanceof Error ? error.message : String(error)}`
281
+ );
282
+ }
283
+ }, [currentConfig, onMessage]);
284
+
285
+ const handleAddQualifierType = useCallback(() => {
286
+ const newQualifierType: Config.Model.ISystemConfiguration['qualifierTypes'][0] = {
287
+ name: `newType${currentConfig.qualifierTypes.length + 1}`,
288
+ systemType: 'literal',
289
+ configuration: {
290
+ allowContextList: true,
291
+ caseSensitive: true,
292
+ enumeratedValues: ['value1', 'value2']
293
+ }
294
+ };
295
+
296
+ const updatedConfig = {
297
+ ...currentConfig,
298
+ qualifierTypes: [...currentConfig.qualifierTypes, newQualifierType]
299
+ };
300
+
301
+ setCurrentConfig(updatedConfig);
302
+ onMessage?.('info', `Added qualifier type: ${newQualifierType.name}`);
303
+ }, [currentConfig, onMessage]);
304
+
305
+ const handleEditQualifierType = useCallback((typeName: string) => {
306
+ setEditingQualifierType(typeName);
307
+ }, []);
308
+
309
+ const handleSaveQualifierType = useCallback(
310
+ (typeName: string, updatedType: Config.Model.ISystemConfiguration['qualifierTypes'][0]) => {
311
+ const updatedConfig = {
312
+ ...currentConfig,
313
+ qualifierTypes: currentConfig.qualifierTypes.map((type) =>
314
+ type.name === typeName ? updatedType : type
315
+ )
316
+ };
317
+ setCurrentConfig(updatedConfig);
318
+ setEditingQualifierType(null);
319
+ onMessage?.('info', `Updated qualifier type: ${updatedType.name}`);
320
+ },
321
+ [currentConfig, onMessage]
322
+ );
323
+
324
+ const handleCancelEditQualifierType = useCallback(() => {
325
+ setEditingQualifierType(null);
326
+ }, []);
327
+
328
+ const handleDeleteQualifierType = useCallback(
329
+ (typeId: string) => {
330
+ onMessage?.('warning', `Deleting qualifier type ${typeId} will clear all loaded resources`);
331
+ // TODO: Implement qualifier type deletion with confirmation
332
+ },
333
+ [onMessage]
334
+ );
335
+
336
+ const renderQualifierTypesPanel = () => (
337
+ <div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
338
+ <div className="px-4 py-3 bg-gray-50 border-b border-gray-200">
339
+ <div className="flex items-center justify-between">
340
+ <h3 className="text-lg font-medium text-gray-900">Qualifier Types</h3>
341
+ <button
342
+ onClick={handleAddQualifierType}
343
+ className="inline-flex items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded text-blue-700 bg-blue-100 hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
344
+ >
345
+ <PlusIcon className="h-4 w-4 mr-1" />
346
+ Add Type
347
+ </button>
348
+ </div>
349
+ <p className="text-sm text-gray-600 mt-1">
350
+ {hasProcessedResources
351
+ ? 'Qualifier types from loaded configuration'
352
+ : 'Default qualifier types that will be used for resource loading'}
353
+ </p>
354
+ </div>
355
+ <div className="divide-y divide-gray-200">
356
+ {currentConfig.qualifierTypes.map((type) => (
357
+ <div key={type.name} className="p-4">
358
+ {editingQualifierType === type.name ? (
359
+ <QualifierTypeEditForm
360
+ qualifierType={type}
361
+ onSave={(updatedType) => handleSaveQualifierType(type.name, updatedType)}
362
+ onCancel={handleCancelEditQualifierType}
363
+ />
364
+ ) : (
365
+ <div className="flex items-center justify-between">
366
+ <div className="flex-1">
367
+ <div className="flex items-center space-x-3">
368
+ <h4 className="text-sm font-medium text-gray-900">{type.name}</h4>
369
+ <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
370
+ {type.systemType}
371
+ </span>
372
+ <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
373
+ Built-in
374
+ </span>
375
+ </div>
376
+ <div className="mt-2 text-sm text-gray-600">
377
+ <div className="grid grid-cols-2 gap-4">
378
+ <div>Context List: {type.configuration?.allowContextList === false ? 'No' : 'Yes'}</div>
379
+ <div>
380
+ Case Sensitive:{' '}
381
+ {type.systemType === 'literal' && type.configuration?.caseSensitive !== false
382
+ ? 'Yes'
383
+ : 'No'}
384
+ </div>
385
+ </div>
386
+ {type.systemType === 'literal' && type.configuration?.enumeratedValues && (
387
+ <div className="mt-1">Values: {type.configuration.enumeratedValues.join(', ')}</div>
388
+ )}
389
+ {type.systemType === 'territory' && type.configuration?.allowedTerritories && (
390
+ <div className="mt-1">
391
+ Territories: {type.configuration.allowedTerritories.join(', ')}
392
+ </div>
393
+ )}
394
+ {type.systemType === 'territory' && type.configuration?.acceptLowercase && (
395
+ <div className="mt-1">Accepts lowercase codes</div>
396
+ )}
397
+ {(type.systemType === 'literal' || type.systemType === 'territory') &&
398
+ type.configuration?.hierarchy && (
399
+ <div className="mt-1">
400
+ Hierarchy: {Object.keys(type.configuration.hierarchy).length} relationship(s)
401
+ </div>
402
+ )}
403
+ </div>
404
+ </div>
405
+ <div className="flex items-center space-x-2">
406
+ <button
407
+ onClick={() => handleEditQualifierType(type.name)}
408
+ className="p-1.5 text-gray-400 hover:text-blue-600"
409
+ title="Edit qualifier type"
410
+ >
411
+ <PencilIcon className="h-4 w-4" />
412
+ </button>
413
+ <button
414
+ onClick={() => handleDeleteQualifierType(type.name)}
415
+ className="p-1.5 text-gray-400 hover:text-red-600"
416
+ title="Delete qualifier type"
417
+ >
418
+ <TrashIcon className="h-4 w-4" />
419
+ </button>
420
+ </div>
421
+ </div>
422
+ )}
423
+ </div>
424
+ ))}
425
+ </div>
426
+ </div>
427
+ );
428
+
429
+ const handleAddQualifier = useCallback(() => {
430
+ // Find an available qualifier type for the new qualifier
431
+ const availableType = currentConfig.qualifierTypes[0]; // Default to first type
432
+ if (!availableType) {
433
+ onMessage?.('error', 'No qualifier types available. Add a qualifier type first.');
434
+ return;
435
+ }
436
+
437
+ const newQualifier: Config.Model.ISystemConfiguration['qualifiers'][0] = {
438
+ name: `newQualifier${currentConfig.qualifiers.length + 1}`,
439
+ typeName: availableType.name,
440
+ defaultPriority: 100 + currentConfig.qualifiers.length * 10
441
+ };
442
+
443
+ const updatedConfig = {
444
+ ...currentConfig,
445
+ qualifiers: [...currentConfig.qualifiers, newQualifier]
446
+ };
447
+
448
+ setCurrentConfig(updatedConfig);
449
+ onMessage?.('info', `Added qualifier: ${newQualifier.name}`);
450
+ }, [currentConfig, onMessage]);
451
+
452
+ const handleEditQualifier = useCallback((qualifierName: string) => {
453
+ setEditingQualifier(qualifierName);
454
+ }, []);
455
+
456
+ const handleSaveQualifier = useCallback(
457
+ (qualifierName: string, updatedQualifier: Config.Model.ISystemConfiguration['qualifiers'][0]) => {
458
+ const updatedConfig = {
459
+ ...currentConfig,
460
+ qualifiers: currentConfig.qualifiers.map((qualifier) =>
461
+ qualifier.name === qualifierName ? updatedQualifier : qualifier
462
+ )
463
+ };
464
+ setCurrentConfig(updatedConfig);
465
+ setEditingQualifier(null);
466
+ onMessage?.('info', `Updated qualifier: ${updatedQualifier.name}`);
467
+ },
468
+ [currentConfig, onMessage]
469
+ );
470
+
471
+ const handleCancelEditQualifier = useCallback(() => {
472
+ setEditingQualifier(null);
473
+ }, []);
474
+
475
+ const handleDeleteQualifier = useCallback(
476
+ (qualifierId: string) => {
477
+ onMessage?.('warning', `Deleting qualifier ${qualifierId} will clear all loaded resources`);
478
+ // TODO: Implement qualifier deletion with confirmation
479
+ },
480
+ [onMessage]
481
+ );
482
+
483
+ const renderQualifiersPanel = () => (
484
+ <div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
485
+ <div className="px-4 py-3 bg-gray-50 border-b border-gray-200">
486
+ <div className="flex items-center justify-between">
487
+ <h3 className="text-lg font-medium text-gray-900">Qualifiers</h3>
488
+ <button
489
+ onClick={handleAddQualifier}
490
+ className="inline-flex items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded text-blue-700 bg-blue-100 hover:bg-blue-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
491
+ >
492
+ <PlusIcon className="h-4 w-4 mr-1" />
493
+ Add Qualifier
494
+ </button>
495
+ </div>
496
+ <p className="text-sm text-gray-600 mt-1">
497
+ {hasProcessedResources
498
+ ? 'Qualifiers from loaded configuration'
499
+ : 'Default qualifiers that will be available for resource resolution'}
500
+ </p>
501
+ </div>
502
+ <div className="divide-y divide-gray-200">
503
+ {currentConfig.qualifiers
504
+ .slice()
505
+ .sort((a, b) => b.defaultPriority - a.defaultPriority)
506
+ .map((qualifier) => {
507
+ const qualifierType = currentConfig.qualifierTypes.find((qt) => qt.name === qualifier.typeName);
508
+ return (
509
+ <div key={qualifier.name} className="p-4">
510
+ {editingQualifier === qualifier.name ? (
511
+ <QualifierEditForm
512
+ qualifier={qualifier}
513
+ qualifierTypes={currentConfig.qualifierTypes}
514
+ onSave={(updatedQualifier) => handleSaveQualifier(qualifier.name, updatedQualifier)}
515
+ onCancel={handleCancelEditQualifier}
516
+ />
517
+ ) : (
518
+ <div className="flex items-center justify-between">
519
+ <div className="flex-1">
520
+ <div className="flex items-center space-x-3">
521
+ <h4 className="text-sm font-medium text-gray-900">{qualifier.name}</h4>
522
+ <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
523
+ {qualifier.typeName}
524
+ </span>
525
+ {qualifier.token && (
526
+ <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
527
+ token: {qualifier.token}
528
+ </span>
529
+ )}
530
+ </div>
531
+ <div className="mt-2 text-sm text-gray-600">
532
+ <div className="grid grid-cols-2 gap-4">
533
+ <div>Type: {qualifierType?.systemType || 'unknown'}</div>
534
+ <div>Priority: {qualifier.defaultPriority}</div>
535
+ </div>
536
+ {qualifier.defaultValue && (
537
+ <div className="mt-1">
538
+ <span className="text-xs text-gray-700">
539
+ <span className="font-medium">Default value:</span> {qualifier.defaultValue}
540
+ </span>
541
+ </div>
542
+ )}
543
+ {qualifier.tokenIsOptional && (
544
+ <div className="mt-1">
545
+ <span className="text-xs text-gray-500">Token is optional</span>
546
+ </div>
547
+ )}
548
+ </div>
549
+ </div>
550
+ <div className="flex items-center space-x-2">
551
+ <button
552
+ onClick={() => handleEditQualifier(qualifier.name)}
553
+ className="p-1.5 text-gray-400 hover:text-blue-600"
554
+ title="Edit qualifier"
555
+ >
556
+ <PencilIcon className="h-4 w-4" />
557
+ </button>
558
+ <button
559
+ onClick={() => handleDeleteQualifier(qualifier.name)}
560
+ className="p-1.5 text-gray-400 hover:text-red-600"
561
+ title="Delete qualifier"
562
+ >
563
+ <TrashIcon className="h-4 w-4" />
564
+ </button>
565
+ </div>
566
+ </div>
567
+ )}
568
+ </div>
569
+ );
570
+ })}
571
+ </div>
572
+ </div>
573
+ );
574
+
575
+ const renderResourceTypesPanel = () => {
576
+ const totalResources = resourceState.processedResources?.summary.totalResources || 0;
577
+ return (
578
+ <div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
579
+ <div className="px-4 py-3 bg-gray-50 border-b border-gray-200">
580
+ <h3 className="text-lg font-medium text-gray-900">Resource Types</h3>
581
+ <p className="text-sm text-gray-600 mt-1">
582
+ {hasProcessedResources
583
+ ? 'Resource types from loaded configuration'
584
+ : 'Default resource types available for use'}
585
+ </p>
586
+ </div>
587
+ <div className="divide-y divide-gray-200">
588
+ {currentConfig.resourceTypes.map((resourceType) => (
589
+ <div key={resourceType.name} className="p-4">
590
+ <div className="flex items-center justify-between">
591
+ <div className="flex-1">
592
+ <div className="flex items-center space-x-3">
593
+ <h4 className="text-sm font-medium text-gray-900">{resourceType.name}</h4>
594
+ <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
595
+ {resourceType.typeName}
596
+ </span>
597
+ {hasProcessedResources && (
598
+ <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
599
+ {totalResources} resources
600
+ </span>
601
+ )}
602
+ </div>
603
+ <div className="mt-2 text-sm text-gray-600">
604
+ JSON object resources for internationalization
605
+ </div>
606
+ </div>
607
+ </div>
608
+ </div>
609
+ ))}
610
+ </div>
611
+ </div>
612
+ );
613
+ };
614
+
615
+ const panels = [
616
+ { id: 'qualifiers', name: 'Qualifiers', component: renderQualifiersPanel },
617
+ { id: 'qualifier-types', name: 'Qualifier Types', component: renderQualifierTypesPanel },
618
+ { id: 'resource-types', name: 'Resource Types', component: renderResourceTypesPanel }
619
+ ];
620
+
621
+ return (
622
+ <div className="h-full flex flex-col">
623
+ {/* Header */}
624
+ <div className="flex-shrink-0 bg-white border-b border-gray-200 px-6 py-4">
625
+ <div className="flex items-center justify-between">
626
+ <div className="flex items-center space-x-3">
627
+ <CogIcon className="h-6 w-6 text-gray-400" />
628
+ <div className="flex flex-col">
629
+ <h1 className="text-xl font-semibold text-gray-900">Configuration</h1>
630
+ {(currentConfig.name || currentConfig.description) && (
631
+ <div className="text-sm text-gray-600">
632
+ {currentConfig.name && <span className="font-medium">{currentConfig.name}</span>}
633
+ {currentConfig.name && currentConfig.description && (
634
+ <span className="text-gray-400 mx-1">•</span>
635
+ )}
636
+ {currentConfig.description && <span>{currentConfig.description}</span>}
637
+ </div>
638
+ )}
639
+ </div>
640
+ </div>
641
+ <div className="flex items-center space-x-2">
642
+ <button
643
+ onClick={() => setEditingMetadata(true)}
644
+ className="inline-flex items-center px-3 py-1.5 border border-gray-300 text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
645
+ >
646
+ <PencilIcon className="h-4 w-4 mr-1" />
647
+ Edit Metadata
648
+ </button>
649
+ <div className="relative">
650
+ <select
651
+ onChange={(e) => {
652
+ if (e.target.value) {
653
+ handleLoadPredefinedConfiguration(e.target.value as Config.PredefinedSystemConfiguration);
654
+ e.target.value = ''; // Reset selection
655
+ }
656
+ }}
657
+ className="inline-flex items-center px-3 py-1.5 border border-gray-300 text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 pr-8"
658
+ defaultValue=""
659
+ >
660
+ <option value="" disabled>
661
+ Load Predefined...
662
+ </option>
663
+ {getAvailablePredefinedConfigurations().map((config) => (
664
+ <option key={config.name} value={config.name}>
665
+ {config.label}
666
+ </option>
667
+ ))}
668
+ </select>
669
+ </div>
670
+ <button
671
+ onClick={handleLoadConfiguration}
672
+ className="inline-flex items-center px-3 py-1.5 border border-gray-300 text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
673
+ >
674
+ <FolderOpenIcon className="h-4 w-4 mr-1" />
675
+ Load from File
676
+ </button>
677
+ <button
678
+ onClick={handleRestoreDefaults}
679
+ className="inline-flex items-center px-3 py-1.5 border border-gray-300 text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
680
+ >
681
+ Restore Defaults
682
+ </button>
683
+ <button
684
+ onClick={handleExportConfiguration}
685
+ className="inline-flex items-center px-3 py-1.5 border border-gray-300 text-xs font-medium rounded text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
686
+ >
687
+ <DocumentArrowDownIcon className="h-4 w-4 mr-1" />
688
+ Export JSON
689
+ </button>
690
+ <button
691
+ onClick={() => handleApplyConfiguration(currentConfig)}
692
+ disabled={!hasChanges}
693
+ className={`inline-flex items-center px-3 py-1.5 border border-transparent text-xs font-medium rounded focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ${
694
+ hasChanges
695
+ ? 'text-white bg-blue-600 hover:bg-blue-700'
696
+ : 'text-gray-400 bg-gray-300 cursor-not-allowed'
697
+ }`}
698
+ >
699
+ Apply Configuration
700
+ </button>
701
+ <span
702
+ className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
703
+ hasProcessedResources
704
+ ? 'bg-green-100 text-green-800'
705
+ : hasActiveConfiguration
706
+ ? 'bg-blue-100 text-blue-800'
707
+ : 'bg-yellow-100 text-yellow-800'
708
+ }`}
709
+ >
710
+ {hasProcessedResources
711
+ ? 'Loaded Configuration'
712
+ : hasActiveConfiguration
713
+ ? 'Active Configuration'
714
+ : 'Default Configuration'}
715
+ </span>
716
+ </div>
717
+ </div>
718
+ <p className="mt-1 text-sm text-gray-600">
719
+ {hasProcessedResources
720
+ ? 'Configuration from loaded resources - editing will clear loaded data'
721
+ : hasActiveConfiguration
722
+ ? `Custom configuration is active - resources will use this configuration when loaded${
723
+ hasChanges ? ' (unsaved changes)' : ''
724
+ }`
725
+ : `Default configuration - will be used when importing resources${
726
+ hasChanges ? ' (unsaved changes)' : ''
727
+ }`}
728
+ </p>
729
+ </div>
730
+
731
+ {/* Metadata Edit Modal */}
732
+ {editingMetadata && (
733
+ <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
734
+ <div className="bg-white rounded-lg shadow-xl p-6 max-w-md w-full mx-4">
735
+ <h3 className="text-lg font-medium text-gray-900 mb-4">Edit Configuration Metadata</h3>
736
+ <MetadataEditForm
737
+ name={currentConfig.name || ''}
738
+ description={currentConfig.description || ''}
739
+ onSave={handleSaveMetadata}
740
+ onCancel={handleCancelMetadataEdit}
741
+ />
742
+ </div>
743
+ </div>
744
+ )}
745
+
746
+ {/* JSON View Toggle */}
747
+ <div className="flex-shrink-0 bg-white border-b border-gray-200 px-6 py-2">
748
+ <button
749
+ onClick={() => setShowJsonView(!showJsonView)}
750
+ className="inline-flex items-center px-3 py-1.5 text-sm font-medium text-gray-700 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
751
+ >
752
+ <CodeBracketIcon className="h-4 w-4 mr-2" />
753
+ {showJsonView ? 'Hide' : 'Show'} JSON Configuration
754
+ {showJsonView ? (
755
+ <ChevronUpIcon className="h-4 w-4 ml-2" />
756
+ ) : (
757
+ <ChevronDownIcon className="h-4 w-4 ml-2" />
758
+ )}
759
+ </button>
760
+ </div>
761
+
762
+ {/* JSON View */}
763
+ {showJsonView && (
764
+ <div className="flex-shrink-0 bg-gray-50 border-b border-gray-200 px-6 py-4">
765
+ <div className="bg-white rounded-lg border border-gray-200 p-4">
766
+ <div className="flex items-center justify-between mb-2">
767
+ <h3 className="text-sm font-medium text-gray-900">Current Configuration (JSON)</h3>
768
+ <button
769
+ onClick={handleExportConfiguration}
770
+ className="inline-flex items-center px-2 py-1 text-xs font-medium text-gray-700 bg-gray-100 rounded hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
771
+ >
772
+ <DocumentArrowDownIcon className="h-3 w-3 mr-1" />
773
+ Export
774
+ </button>
775
+ </div>
776
+ <pre className="text-xs text-gray-800 bg-gray-50 p-3 rounded border overflow-x-auto max-h-64 overflow-y-auto">
777
+ {JSON.stringify(currentConfig, null, 2)}
778
+ </pre>
779
+ </div>
780
+ </div>
781
+ )}
782
+
783
+ {/* Panel Navigation */}
784
+ <div className="flex-shrink-0 bg-gray-50 border-b border-gray-200">
785
+ <nav className="flex space-x-8 px-6" aria-label="Tabs">
786
+ {panels.map((panel) => (
787
+ <button
788
+ key={panel.id}
789
+ onClick={() => setActivePanel(panel.id as ActivePanel)}
790
+ className={`py-4 px-1 border-b-2 font-medium text-sm ${
791
+ activePanel === panel.id
792
+ ? 'border-blue-500 text-blue-600'
793
+ : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
794
+ }`}
795
+ >
796
+ {panel.name}
797
+ </button>
798
+ ))}
799
+ </nav>
800
+ </div>
801
+
802
+ {/* Panel Content */}
803
+ <div className="flex-1 overflow-y-auto p-6">
804
+ {panels.find((p) => p.id === activePanel)?.component()}
805
+ </div>
806
+ </div>
807
+ );
808
+ };
809
+
810
+ // Hierarchy Editor Component
811
+ interface HierarchyEditorProps {
812
+ hierarchy: Record<string, string>;
813
+ onChange: (hierarchy: Record<string, string>) => void;
814
+ availableValues: string[];
815
+ }
816
+
817
+ const HierarchyEditor: React.FC<HierarchyEditorProps> = ({ hierarchy, onChange, availableValues }) => {
818
+ const [newChild, setNewChild] = useState('');
819
+ const [newParent, setNewParent] = useState('');
820
+
821
+ const handleAddRelationship = () => {
822
+ if (newChild && newParent && newChild !== newParent) {
823
+ const updatedHierarchy = { ...hierarchy, [newChild]: newParent };
824
+ onChange(updatedHierarchy);
825
+ setNewChild('');
826
+ setNewParent('');
827
+ }
828
+ };
829
+
830
+ const handleRemoveRelationship = (child: string) => {
831
+ const updatedHierarchy = { ...hierarchy };
832
+ delete updatedHierarchy[child];
833
+ onChange(updatedHierarchy);
834
+ };
835
+
836
+ const getHierarchyTree = () => {
837
+ const roots = new Set(availableValues);
838
+ const children = new Set(Object.keys(hierarchy));
839
+ const parents = new Set(Object.values(hierarchy));
840
+
841
+ // Remove children from roots (they have parents)
842
+ children.forEach((child) => roots.delete(child));
843
+
844
+ // Add parents that aren't in available values (for display purposes)
845
+ parents.forEach((parent) => {
846
+ if (!availableValues.includes(parent)) {
847
+ roots.add(parent);
848
+ }
849
+ });
850
+
851
+ const buildTree = (value: string, level = 0): any => {
852
+ const childrenOfValue = Object.entries(hierarchy).filter(([, parent]) => parent === value);
853
+ return {
854
+ value,
855
+ level,
856
+ children: childrenOfValue.map(([child]) => buildTree(child, level + 1))
857
+ };
858
+ };
859
+
860
+ return Array.from(roots).map((root) => buildTree(root));
861
+ };
862
+
863
+ const renderTree = (nodes: any[]): React.ReactNode => {
864
+ return nodes.map((node) => (
865
+ <div key={node.value} className="ml-4">
866
+ <div className="flex items-center space-x-2 py-1">
867
+ <span className="text-sm text-gray-700" style={{ marginLeft: `${node.level * 20}px` }}>
868
+ {node.level > 0 && '└─ '}
869
+ {node.value}
870
+ </span>
871
+ {hierarchy[node.value] && <span className="text-xs text-gray-500">→ {hierarchy[node.value]}</span>}
872
+ </div>
873
+ {node.children.length > 0 && renderTree(node.children)}
874
+ </div>
875
+ ));
876
+ };
877
+
878
+ return (
879
+ <div>
880
+ <label className="block text-sm font-medium text-gray-700 mb-2">Value Hierarchy</label>
881
+ <div className="border border-gray-300 rounded-md p-3 bg-white">
882
+ {/* Add new relationship form */}
883
+ <div className="mb-4 p-3 bg-gray-50 rounded border">
884
+ <div className="text-sm font-medium text-gray-700 mb-2">Add Parent-Child Relationship</div>
885
+ <div className="grid grid-cols-3 gap-2 items-end">
886
+ <div>
887
+ <label className="block text-xs text-gray-600">Child Value</label>
888
+ {availableValues.length > 0 ? (
889
+ <select
890
+ value={newChild}
891
+ onChange={(e) => setNewChild(e.target.value)}
892
+ className="w-full text-sm rounded border-gray-300 focus:border-blue-500 focus:ring-blue-500"
893
+ >
894
+ <option value="">Select child...</option>
895
+ {availableValues.map((value) => (
896
+ <option key={value} value={value}>
897
+ {value}
898
+ </option>
899
+ ))}
900
+ </select>
901
+ ) : (
902
+ <input
903
+ type="text"
904
+ value={newChild}
905
+ onChange={(e) => setNewChild(e.target.value)}
906
+ placeholder="Enter child value"
907
+ className="w-full text-sm rounded border-gray-300 focus:border-blue-500 focus:ring-blue-500"
908
+ />
909
+ )}
910
+ </div>
911
+ <div>
912
+ <label className="block text-xs text-gray-600">Parent Value</label>
913
+ <input
914
+ type="text"
915
+ value={newParent}
916
+ onChange={(e) => setNewParent(e.target.value)}
917
+ placeholder="Enter parent value"
918
+ className="w-full text-sm rounded border-gray-300 focus:border-blue-500 focus:ring-blue-500"
919
+ />
920
+ </div>
921
+ <div>
922
+ <button
923
+ onClick={handleAddRelationship}
924
+ disabled={!newChild || !newParent || newChild === newParent}
925
+ className="w-full px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed"
926
+ >
927
+ Add
928
+ </button>
929
+ </div>
930
+ </div>
931
+ <div className="text-xs text-gray-500 mt-1">
932
+ {availableValues.length > 0
933
+ ? "Define which values are children of other values. The parent doesn't need to be in the enumerated values list."
934
+ : 'Define which values are children of other values. Enter any valid values for this qualifier type.'}
935
+ </div>
936
+ </div>
937
+
938
+ {/* Current relationships */}
939
+ {Object.keys(hierarchy).length > 0 && (
940
+ <div className="mb-4">
941
+ <div className="text-sm font-medium text-gray-700 mb-2">Current Relationships</div>
942
+ <div className="space-y-1">
943
+ {Object.entries(hierarchy).map(([child, parent]) => (
944
+ <div key={child} className="flex items-center justify-between bg-gray-50 px-2 py-1 rounded">
945
+ <span className="text-sm">
946
+ <span className="font-medium">{child}</span> →{' '}
947
+ <span className="text-gray-600">{parent}</span>
948
+ </span>
949
+ <button
950
+ onClick={() => handleRemoveRelationship(child)}
951
+ className="text-red-600 hover:text-red-800 text-xs"
952
+ >
953
+ Remove
954
+ </button>
955
+ </div>
956
+ ))}
957
+ </div>
958
+ </div>
959
+ )}
960
+
961
+ {/* Hierarchy visualization */}
962
+ {(availableValues.length > 0 || Object.keys(hierarchy).length > 0) && (
963
+ <div>
964
+ <div className="text-sm font-medium text-gray-700 mb-2">Hierarchy Tree</div>
965
+ <div className="bg-gray-50 p-2 rounded border text-sm font-mono">
966
+ {getHierarchyTree().length > 0 ? (
967
+ renderTree(getHierarchyTree())
968
+ ) : (
969
+ <div className="text-gray-500 text-center py-2">
970
+ No hierarchy defined. Add relationships above to see the tree structure.
971
+ </div>
972
+ )}
973
+ </div>
974
+ </div>
975
+ )}
976
+ </div>
977
+ </div>
978
+ );
979
+ };
980
+
981
+ // Edit form components
982
+ interface QualifierTypeEditFormProps {
983
+ qualifierType: Config.Model.ISystemConfiguration['qualifierTypes'][0];
984
+ onSave: (updatedType: Config.Model.ISystemConfiguration['qualifierTypes'][0]) => void;
985
+ onCancel: () => void;
986
+ }
987
+
988
+ const QualifierTypeEditForm: React.FC<QualifierTypeEditFormProps> = ({ qualifierType, onSave, onCancel }) => {
989
+ const [name, setName] = useState(qualifierType.name);
990
+ const [systemType, setSystemType] = useState<'language' | 'territory' | 'literal'>(
991
+ qualifierType.systemType
992
+ );
993
+ const [allowContextList, setAllowContextList] = useState(
994
+ qualifierType.configuration?.allowContextList !== false
995
+ );
996
+ const [caseSensitive, setCaseSensitive] = useState(
997
+ qualifierType.systemType === 'literal' && qualifierType.configuration?.caseSensitive !== false
998
+ );
999
+ const [enumeratedValues, setEnumeratedValues] = useState(
1000
+ qualifierType.systemType === 'literal' && qualifierType.configuration?.enumeratedValues
1001
+ ? qualifierType.configuration.enumeratedValues.join(', ')
1002
+ : ''
1003
+ );
1004
+ const [allowedTerritories, setAllowedTerritories] = useState(
1005
+ qualifierType.systemType === 'territory' && qualifierType.configuration?.allowedTerritories
1006
+ ? qualifierType.configuration.allowedTerritories.join(', ')
1007
+ : ''
1008
+ );
1009
+ const [acceptLowercase, setAcceptLowercase] = useState(
1010
+ qualifierType.systemType === 'territory' && qualifierType.configuration?.acceptLowercase === true
1011
+ );
1012
+ const [hierarchy, setHierarchy] = useState<Record<string, string>>(
1013
+ (qualifierType.systemType === 'literal' || qualifierType.systemType === 'territory') &&
1014
+ qualifierType.configuration?.hierarchy
1015
+ ? (qualifierType.configuration.hierarchy as Record<string, string>)
1016
+ : {}
1017
+ );
1018
+
1019
+ const handleSave = () => {
1020
+ let updatedType: Config.Model.ISystemConfiguration['qualifierTypes'][0];
1021
+
1022
+ if (systemType === 'literal') {
1023
+ const configuration: any = {
1024
+ allowContextList,
1025
+ caseSensitive,
1026
+ enumeratedValues: enumeratedValues
1027
+ .split(',')
1028
+ .map((v) => v.trim())
1029
+ .filter((v) => v)
1030
+ };
1031
+
1032
+ // Add hierarchy if it has entries
1033
+ if (Object.keys(hierarchy).length > 0) {
1034
+ configuration.hierarchy = hierarchy;
1035
+ }
1036
+
1037
+ updatedType = {
1038
+ name,
1039
+ systemType: 'literal',
1040
+ configuration
1041
+ };
1042
+ } else if (systemType === 'territory') {
1043
+ const configuration: any = {
1044
+ allowContextList,
1045
+ acceptLowercase
1046
+ };
1047
+
1048
+ // Add allowedTerritories if specified
1049
+ const territoryValues = allowedTerritories
1050
+ .split(',')
1051
+ .map((v) => v.trim().toUpperCase())
1052
+ .filter((v) => v);
1053
+ if (territoryValues.length > 0) {
1054
+ configuration.allowedTerritories = territoryValues;
1055
+ }
1056
+
1057
+ // Add hierarchy if it has entries
1058
+ if (Object.keys(hierarchy).length > 0) {
1059
+ configuration.hierarchy = hierarchy;
1060
+ }
1061
+
1062
+ updatedType = {
1063
+ name,
1064
+ systemType: 'territory',
1065
+ configuration
1066
+ };
1067
+ } else {
1068
+ updatedType = {
1069
+ name,
1070
+ systemType,
1071
+ configuration: {
1072
+ allowContextList
1073
+ }
1074
+ };
1075
+ }
1076
+
1077
+ onSave(updatedType);
1078
+ };
1079
+
1080
+ return (
1081
+ <div className="space-y-4 p-4 bg-gray-50 rounded-lg">
1082
+ <div className="grid grid-cols-2 gap-4">
1083
+ <div>
1084
+ <label className="block text-sm font-medium text-gray-700">Name</label>
1085
+ <input
1086
+ type="text"
1087
+ value={name}
1088
+ onChange={(e) => setName(e.target.value)}
1089
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
1090
+ />
1091
+ </div>
1092
+ <div>
1093
+ <label className="block text-sm font-medium text-gray-700">System Type</label>
1094
+ <select
1095
+ value={systemType}
1096
+ onChange={(e) => setSystemType(e.target.value as 'language' | 'territory' | 'literal')}
1097
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
1098
+ >
1099
+ <option value="language">Language</option>
1100
+ <option value="territory">Territory</option>
1101
+ <option value="literal">Literal</option>
1102
+ </select>
1103
+ </div>
1104
+ </div>
1105
+
1106
+ <div className="flex items-center space-x-4">
1107
+ <label className="flex items-center">
1108
+ <input
1109
+ type="checkbox"
1110
+ checked={allowContextList}
1111
+ onChange={(e) => setAllowContextList(e.target.checked)}
1112
+ className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
1113
+ />
1114
+ <span className="ml-2 text-sm text-gray-700">Allow Context List</span>
1115
+ </label>
1116
+
1117
+ {systemType === 'literal' && (
1118
+ <label className="flex items-center">
1119
+ <input
1120
+ type="checkbox"
1121
+ checked={caseSensitive}
1122
+ onChange={(e) => setCaseSensitive(e.target.checked)}
1123
+ className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
1124
+ />
1125
+ <span className="ml-2 text-sm text-gray-700">Case Sensitive</span>
1126
+ </label>
1127
+ )}
1128
+ </div>
1129
+
1130
+ {systemType === 'literal' && (
1131
+ <div>
1132
+ <label className="block text-sm font-medium text-gray-700">
1133
+ Enumerated Values (comma-separated)
1134
+ </label>
1135
+ <input
1136
+ type="text"
1137
+ value={enumeratedValues}
1138
+ onChange={(e) => setEnumeratedValues(e.target.value)}
1139
+ placeholder="value1, value2, value3"
1140
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
1141
+ />
1142
+ </div>
1143
+ )}
1144
+
1145
+ {systemType === 'territory' && (
1146
+ <div>
1147
+ <label className="flex items-center mb-4">
1148
+ <input
1149
+ type="checkbox"
1150
+ checked={acceptLowercase}
1151
+ onChange={(e) => setAcceptLowercase(e.target.checked)}
1152
+ className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
1153
+ />
1154
+ <span className="ml-2 text-sm text-gray-700">Accept Lowercase Territory Codes</span>
1155
+ </label>
1156
+ </div>
1157
+ )}
1158
+
1159
+ {systemType === 'territory' && (
1160
+ <div>
1161
+ <label className="block text-sm font-medium text-gray-700">
1162
+ Allowed Territories (comma-separated, optional)
1163
+ </label>
1164
+ <input
1165
+ type="text"
1166
+ value={allowedTerritories}
1167
+ onChange={(e) => setAllowedTerritories(e.target.value)}
1168
+ placeholder="US, CA, GB, DE (leave empty to allow all valid territory codes)"
1169
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
1170
+ />
1171
+ <p className="mt-1 text-xs text-gray-500">
1172
+ Territory codes will be automatically converted to uppercase. Leave empty to allow any valid
1173
+ ISO-3166-2 Alpha-2 codes.
1174
+ </p>
1175
+ </div>
1176
+ )}
1177
+
1178
+ {systemType === 'literal' && (
1179
+ <HierarchyEditor
1180
+ hierarchy={hierarchy}
1181
+ onChange={setHierarchy}
1182
+ availableValues={enumeratedValues
1183
+ .split(',')
1184
+ .map((v) => v.trim())
1185
+ .filter((v) => v)}
1186
+ />
1187
+ )}
1188
+
1189
+ {systemType === 'territory' && (
1190
+ <HierarchyEditor
1191
+ hierarchy={hierarchy}
1192
+ onChange={setHierarchy}
1193
+ availableValues={allowedTerritories
1194
+ .split(',')
1195
+ .map((v) => v.trim().toUpperCase())
1196
+ .filter((v) => v)}
1197
+ />
1198
+ )}
1199
+
1200
+ <div className="flex justify-end space-x-2">
1201
+ <button
1202
+ onClick={onCancel}
1203
+ className="px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
1204
+ >
1205
+ Cancel
1206
+ </button>
1207
+ <button
1208
+ onClick={handleSave}
1209
+ className="px-3 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700"
1210
+ >
1211
+ Save
1212
+ </button>
1213
+ </div>
1214
+ </div>
1215
+ );
1216
+ };
1217
+
1218
+ interface QualifierEditFormProps {
1219
+ qualifier: Config.Model.ISystemConfiguration['qualifiers'][0];
1220
+ qualifierTypes: Config.Model.ISystemConfiguration['qualifierTypes'];
1221
+ onSave: (updatedQualifier: Config.Model.ISystemConfiguration['qualifiers'][0]) => void;
1222
+ onCancel: () => void;
1223
+ }
1224
+
1225
+ const QualifierEditForm: React.FC<QualifierEditFormProps> = ({
1226
+ qualifier,
1227
+ qualifierTypes,
1228
+ onSave,
1229
+ onCancel
1230
+ }) => {
1231
+ const [name, setName] = useState(qualifier.name);
1232
+ const [typeName, setTypeName] = useState(qualifier.typeName);
1233
+ const [defaultPriority, setDefaultPriority] = useState(qualifier.defaultPriority);
1234
+ const [token, setToken] = useState(qualifier.token || '');
1235
+ const [tokenIsOptional, setTokenIsOptional] = useState(qualifier.tokenIsOptional || false);
1236
+ const [defaultValue, setDefaultValue] = useState(qualifier.defaultValue || '');
1237
+
1238
+ const handleSave = () => {
1239
+ const updatedQualifier: Config.Model.ISystemConfiguration['qualifiers'][0] = {
1240
+ name,
1241
+ typeName,
1242
+ defaultPriority,
1243
+ ...(token && { token }),
1244
+ ...(token && { tokenIsOptional }),
1245
+ ...(defaultValue.trim() && { defaultValue: defaultValue.trim() })
1246
+ };
1247
+ onSave(updatedQualifier);
1248
+ };
1249
+
1250
+ return (
1251
+ <div className="space-y-4 p-4 bg-gray-50 rounded-lg">
1252
+ <div className="grid grid-cols-2 gap-4">
1253
+ <div>
1254
+ <label className="block text-sm font-medium text-gray-700">Name</label>
1255
+ <input
1256
+ type="text"
1257
+ value={name}
1258
+ onChange={(e) => setName(e.target.value)}
1259
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
1260
+ />
1261
+ </div>
1262
+ <div>
1263
+ <label className="block text-sm font-medium text-gray-700">Type</label>
1264
+ <select
1265
+ value={typeName}
1266
+ onChange={(e) => setTypeName(e.target.value)}
1267
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
1268
+ >
1269
+ {qualifierTypes.map((type) => (
1270
+ <option key={type.name} value={type.name}>
1271
+ {type.name} ({type.systemType})
1272
+ </option>
1273
+ ))}
1274
+ </select>
1275
+ </div>
1276
+ </div>
1277
+
1278
+ <div className="grid grid-cols-2 gap-4">
1279
+ <div>
1280
+ <label className="block text-sm font-medium text-gray-700">Default Priority</label>
1281
+ <input
1282
+ type="number"
1283
+ value={defaultPriority}
1284
+ onChange={(e) => setDefaultPriority(parseInt(e.target.value) || 0)}
1285
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
1286
+ />
1287
+ </div>
1288
+ <div>
1289
+ <label className="block text-sm font-medium text-gray-700">Token (optional)</label>
1290
+ <input
1291
+ type="text"
1292
+ value={token}
1293
+ onChange={(e) => setToken(e.target.value)}
1294
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
1295
+ />
1296
+ </div>
1297
+ </div>
1298
+
1299
+ {token && (
1300
+ <div>
1301
+ <label className="flex items-center">
1302
+ <input
1303
+ type="checkbox"
1304
+ checked={tokenIsOptional}
1305
+ onChange={(e) => setTokenIsOptional(e.target.checked)}
1306
+ className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
1307
+ />
1308
+ <span className="ml-2 text-sm text-gray-700">Token is optional</span>
1309
+ </label>
1310
+ </div>
1311
+ )}
1312
+
1313
+ <div>
1314
+ <label className="block text-sm font-medium text-gray-700">Default Value (optional)</label>
1315
+ <input
1316
+ type="text"
1317
+ value={defaultValue}
1318
+ onChange={(e) => setDefaultValue(e.target.value)}
1319
+ placeholder="Enter default value for this qualifier"
1320
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
1321
+ />
1322
+ <p className="mt-1 text-xs text-gray-500">
1323
+ The default value to use when this qualifier is not specified in a context. For list-capable
1324
+ qualifiers, use comma-separated values (e.g., "en-US, fr-FR").
1325
+ </p>
1326
+ </div>
1327
+
1328
+ <div className="flex justify-end space-x-2">
1329
+ <button
1330
+ onClick={onCancel}
1331
+ className="px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
1332
+ >
1333
+ Cancel
1334
+ </button>
1335
+ <button
1336
+ onClick={handleSave}
1337
+ className="px-3 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700"
1338
+ >
1339
+ Save
1340
+ </button>
1341
+ </div>
1342
+ </div>
1343
+ );
1344
+ };
1345
+
1346
+ // Metadata edit form component
1347
+ interface MetadataEditFormProps {
1348
+ name: string;
1349
+ description: string;
1350
+ onSave: (name: string, description: string) => void;
1351
+ onCancel: () => void;
1352
+ }
1353
+
1354
+ const MetadataEditForm: React.FC<MetadataEditFormProps> = ({ name, description, onSave, onCancel }) => {
1355
+ const [localName, setLocalName] = useState(name);
1356
+ const [localDescription, setLocalDescription] = useState(description);
1357
+
1358
+ const handleSave = () => {
1359
+ onSave(localName.trim(), localDescription.trim());
1360
+ };
1361
+
1362
+ return (
1363
+ <div className="space-y-4">
1364
+ <div>
1365
+ <label className="block text-sm font-medium text-gray-700 mb-1">Name</label>
1366
+ <input
1367
+ type="text"
1368
+ value={localName}
1369
+ onChange={(e) => setLocalName(e.target.value)}
1370
+ placeholder="Enter configuration name"
1371
+ className="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
1372
+ />
1373
+ </div>
1374
+ <div>
1375
+ <label className="block text-sm font-medium text-gray-700 mb-1">Description</label>
1376
+ <textarea
1377
+ value={localDescription}
1378
+ onChange={(e) => setLocalDescription(e.target.value)}
1379
+ placeholder="Enter configuration description"
1380
+ rows={3}
1381
+ className="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm"
1382
+ />
1383
+ </div>
1384
+ <div className="flex justify-end space-x-2">
1385
+ <button
1386
+ onClick={onCancel}
1387
+ className="px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
1388
+ >
1389
+ Cancel
1390
+ </button>
1391
+ <button
1392
+ onClick={handleSave}
1393
+ className="px-3 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700"
1394
+ >
1395
+ Save
1396
+ </button>
1397
+ </div>
1398
+ </div>
1399
+ );
1400
+ };
1401
+
1402
+ export default ConfigurationTool;