@fgv/ts-res-browser 5.0.0-2 → 5.0.0-21

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