@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.
- package/cli.js +132 -21
- package/dist/bundle.js +1 -1
- package/dist/bundle.js.LICENSE.txt +11 -1
- package/package.json +42 -36
- package/.rush/temp/587277bfc395406ad444e989b6c309b9698f534c.tar.log +0 -15
- package/.rush/temp/chunked-rush-logs/ts-res-browser.build.chunks.jsonl +0 -35
- package/.rush/temp/operation/build/all.log +0 -35
- package/.rush/temp/operation/build/log-chunks.jsonl +0 -35
- package/.rush/temp/operation/build/state.json +0 -3
- package/.rush/temp/shrinkwrap-deps.json +0 -1225
- package/TS_RES_BROWSER_PLAN.md +0 -689
- package/babel.config.js +0 -17
- package/config/jest.config.json +0 -13
- package/config/rig.json +0 -16
- package/postcss.config.js +0 -3
- package/public/index.html +0 -15
- package/rush-logs/ts-res-browser.build.cache.log +0 -3
- package/rush-logs/ts-res-browser.build.log +0 -35
- package/src/App.tsx +0 -274
- package/src/cli.ts +0 -44
- package/src/components/common/FileImporter.tsx +0 -143
- package/src/components/common/NavigationWarningModal.tsx +0 -62
- package/src/components/layout/AppLayout.tsx +0 -46
- package/src/components/layout/Header.tsx +0 -22
- package/src/components/layout/MessagesWindow.tsx +0 -108
- package/src/components/layout/Sidebar.tsx +0 -99
- package/src/components/tools/CompiledBrowser.tsx +0 -949
- package/src/components/tools/ConfigurationTool.tsx +0 -1405
- package/src/components/tools/FilterTool.tsx +0 -1114
- package/src/components/tools/ImportTool.tsx +0 -249
- package/src/components/tools/ResolutionViewer.tsx +0 -1672
- package/src/components/tools/SourceBrowser.tsx +0 -496
- package/src/components/tools/ZipLoader.tsx +0 -302
- package/src/hooks/useAppState.ts +0 -111
- package/src/hooks/useFileImport.ts +0 -86
- package/src/hooks/useNavigationWarning.ts +0 -67
- package/src/hooks/useResourceManager.ts +0 -295
- package/src/hooks/useUrlParams.ts +0 -56
- package/src/index.css +0 -38
- package/src/main.tsx +0 -16
- package/src/types/app.ts +0 -70
- package/src/types/fileSystemAccess.d.ts +0 -94
- package/src/utils/browserFileTreeAccessors.ts +0 -245
- package/src/utils/fileImport.ts +0 -336
- package/src/utils/fileTreeConverter.ts +0 -160
- package/src/utils/filterResources.ts +0 -677
- package/src/utils/tsResIntegration.ts +0 -467
- package/src/utils/urlParams.ts +0 -238
- package/src/utils/zip/browserZipFileTreeAccessors.ts +0 -385
- package/tailwind.config.js +0 -21
- package/test-data/README.md +0 -184
- package/test-data/config-variations/README.md +0 -64
- package/test-data/config-variations/composition-terminology.json +0 -112
- package/test-data/config-variations/enterprise-complex.json +0 -129
- package/test-data/config-variations/gaming-app.json +0 -87
- package/test-data/config-variations/high-priority-language.json +0 -65
- package/test-data/config-variations/minimal-basic.json +0 -37
- package/test-data/config-variations/qualifier-default-values.json +0 -71
- package/test-data/config-variations/territory-first.json +0 -68
- package/test-data/config-variations/territory-hierarchy-test.json +0 -31
- package/test-data/config-variations/territory-hierarchy-unconstrained.json +0 -32
- package/test-data/default-matching-test/greeting.res.json +0 -83
- package/test-data/default-matching-test/only-defaults.res.json +0 -48
- package/test-data/sample-project/package.json +0 -27
- package/test-data/sample-project/src/resources/app-config.json +0 -27
- package/test-data/sample-project/src/resources/app.json +0 -177
- package/test-data/sample-project/src/resources/config/app.env=development.json +0 -21
- package/test-data/sample-project/src/resources/config/app.json +0 -21
- package/test-data/sample-project/src/resources/config/app.platform=mobile.json +0 -23
- package/test-data/sample-project/src/resources/feature-flags.json +0 -23
- package/test-data/sample-project/src/resources/home=CA/common.json +0 -16
- package/test-data/sample-project/src/resources/images/density=hdpi.json +0 -29
- package/test-data/sample-project/src/resources/images/home.json +0 -29
- package/test-data/sample-project/src/resources/images/logout.json +0 -14
- package/test-data/sample-project/src/resources/images/settings.json +0 -14
- package/test-data/sample-project/src/resources/images/ui-icons.json +0 -29
- package/test-data/sample-project/src/resources/images/user.density=hdpi.json +0 -14
- package/test-data/sample-project/src/resources/images/user.json +0 -14
- package/test-data/sample-project/src/resources/lang=en-US/common.json +0 -15
- package/test-data/sample-project/src/resources/language=en-CA/ui-terms.json +0 -19
- package/test-data/sample-project/src/resources/language=en-GB/ui-terms.json +0 -30
- package/test-data/sample-project/src/resources/language=fr-CA/ui-terms.json +0 -25
- package/test-data/sample-project/src/resources/language=nl-BE/ui-terms.json +0 -17
- package/test-data/sample-project/src/resources/region=europe/feature-flags.json +0 -17
- package/test-data/sample-project/src/resources/region=north-america/feature-flags.json +0 -18
- package/test-data/sample-project/src/resources/strings/dashboard.home=CA,language=fr.json +0 -20
- package/test-data/sample-project/src/resources/strings/dashboard.json +0 -18
- package/test-data/sample-project/src/resources/strings/dashboard.role=admin.json +0 -23
- package/test-data/sample-project/src/resources/strings/dashboard.role=user.json +0 -21
- package/test-data/sample-project/src/resources/strings/language=en-US.json +0 -16
- package/test-data/sample-project/src/resources/strings.json +0 -78
- package/test-data/sample-project/src/resources/territory=BE/app-config.json +0 -19
- package/test-data/sample-project/src/resources/territory=CA/app-config.json +0 -21
- package/test-data/sample-project/src/resources/territory=GB/app-config.json +0 -25
- package/test-data/sample-project/src/resources/territory=NL/app-config.json +0 -25
- package/test-data/sample-project/src/resources/ui-terms.json +0 -96
- package/test-data/sample-project/src/resources-config.json +0 -111
- package/tsconfig.json +0 -27
- 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;
|