Skip to content

Commit 4884f1b

Browse files
committed
Fix data type conversion for custom NIPs in relay info settings
- Update RelayInfoSettings.tsx to use tags mode for custom NIP support - Add number conversion logic to ensure NIPs are sent as numbers to backend - Enhance useGenericSettings.ts with comprehensive field mapping for all groups - Remove query_cache from settings types as it's no longer used
1 parent f3905e3 commit 4884f1b

3 files changed

Lines changed: 296 additions & 14 deletions

File tree

src/components/settings/RelayInfoSettings.tsx

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,26 @@ const RelayInfoSettings: React.FC = () => {
205205
}
206206
>
207207
<Select
208-
mode="multiple"
209-
placeholder="Select supported NIPs"
208+
mode="tags"
209+
placeholder="Select or type custom NIP numbers (e.g. 1, 42, 999)"
210210
style={{ width: '100%' }}
211+
tokenSeparators={[',', ' ']}
212+
filterOption={(input, option) => {
213+
if (!option?.children) return false;
214+
return option.children.toString().toLowerCase().includes(input.toLowerCase());
215+
}}
216+
onChange={(values: (string | number)[]) => {
217+
// Convert all values to numbers, filtering out invalid ones
218+
const numberValues = values
219+
.map((val: string | number) => {
220+
const num = Number(val);
221+
return isNaN(num) ? null : num;
222+
})
223+
.filter((val: number | null): val is number => val !== null);
224+
225+
// Update the form field with number values
226+
form.setFieldsValue({ relaysupportednips: numberValues });
227+
}}
211228
>
212229
{nipOptions.map(option => (
213230
<Option key={option.value} value={option.value}>

src/hooks/useGenericSettings.ts

Lines changed: 277 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,269 @@ import { readToken } from '@app/services/localStorage.service';
44
import { useHandleLogout } from './authUtils';
55
import { SettingsGroupName, SettingsGroupType } from '@app/types/settings.types';
66

7+
// Helper function to extract the correct nested data for each settings group
8+
const extractSettingsForGroup = (settings: any, groupName: string) => {
9+
console.log(`Extracting settings for group: ${groupName}`, settings);
10+
11+
let rawData: any = {};
12+
13+
switch (groupName) {
14+
case 'image_moderation':
15+
rawData = settings?.content_filtering?.image_moderation || {};
16+
break;
17+
18+
case 'content_filter':
19+
rawData = settings?.content_filtering?.text_filter || {};
20+
break;
21+
22+
case 'nest_feeder':
23+
rawData = settings?.external_services?.nest_feeder || {};
24+
break;
25+
26+
case 'ollama':
27+
rawData = settings?.external_services?.ollama || {};
28+
break;
29+
30+
case 'wallet':
31+
rawData = settings?.external_services?.wallet || {};
32+
break;
33+
34+
case 'relay_info':
35+
rawData = settings?.relay || {};
36+
break;
37+
38+
case 'general':
39+
rawData = settings?.server || {};
40+
break;
41+
42+
default:
43+
console.warn(`Unknown settings group: ${groupName}`);
44+
return {};
45+
}
46+
47+
// Handle the prefixed field name issue
48+
// The backend returns both prefixed and unprefixed fields, but forms expect prefixed ones
49+
if (groupName === 'image_moderation' && rawData) {
50+
const processedData: any = {};
51+
52+
// Map unprefixed fields to prefixed ones that the form expects
53+
const imageModerationMappings: Record<string, string[]> = {
54+
'image_moderation_api': ['image_moderation_api', 'api'],
55+
'image_moderation_check_interval': ['image_moderation_check_interval_seconds', 'check_interval_seconds'],
56+
'image_moderation_concurrency': ['image_moderation_concurrency', 'concurrency'],
57+
'image_moderation_enabled': ['image_moderation_enabled', 'enabled'],
58+
'image_moderation_mode': ['image_moderation_mode', 'mode'],
59+
'image_moderation_temp_dir': ['image_moderation_temp_dir', 'temp_dir'],
60+
'image_moderation_threshold': ['image_moderation_threshold', 'threshold'],
61+
'image_moderation_timeout': ['image_moderation_timeout_seconds', 'timeout_seconds']
62+
};
63+
64+
// Map fields, prioritizing prefixed versions if they exist
65+
Object.entries(imageModerationMappings).forEach(([formField, possibleBackendFields]) => {
66+
for (const backendField of possibleBackendFields) {
67+
if (rawData[backendField] !== undefined) {
68+
processedData[formField] = rawData[backendField];
69+
break; // Use the first found value
70+
}
71+
}
72+
});
73+
74+
console.log(`Processed ${groupName} data:`, processedData);
75+
return processedData;
76+
}
77+
78+
if (groupName === 'content_filter' && rawData) {
79+
const processedData: any = {};
80+
81+
// Handle content filter prefixed fields
82+
const contentFilterMappings: Record<string, string> = {
83+
'content_filter_cache_size': 'cache_size',
84+
'content_filter_cache_ttl': 'cache_ttl_seconds',
85+
'content_filter_enabled': 'enabled',
86+
'full_text_kinds': 'full_text_search_kinds' // Special mapping
87+
};
88+
89+
// Start with raw data
90+
Object.keys(rawData).forEach(key => {
91+
processedData[key] = rawData[key];
92+
});
93+
94+
// Apply prefixed field mappings
95+
Object.entries(contentFilterMappings).forEach(([prefixedKey, rawKey]) => {
96+
if (rawData[rawKey] !== undefined) {
97+
processedData[prefixedKey] = rawData[rawKey];
98+
}
99+
});
100+
101+
console.log(`Processed ${groupName} data:`, processedData);
102+
return processedData;
103+
}
104+
105+
// Add more mappings for other services that might need prefixed fields
106+
if (groupName === 'nest_feeder' && rawData) {
107+
const processedData: any = {};
108+
109+
// Handle nest_feeder prefixed fields based on the prefixedSettingsMap
110+
const nestFeederMappings: Record<string, string> = {
111+
'nest_feeder_cache_size': 'cache_size',
112+
'nest_feeder_cache_ttl': 'cache_ttl',
113+
'nest_feeder_enabled': 'enabled',
114+
'nest_feeder_timeout': 'timeout',
115+
'nest_feeder_url': 'url'
116+
};
117+
118+
// Start with raw data
119+
Object.keys(rawData).forEach(key => {
120+
processedData[key] = rawData[key];
121+
});
122+
123+
// Apply prefixed field mappings
124+
Object.entries(nestFeederMappings).forEach(([prefixedKey, rawKey]) => {
125+
if (rawData[rawKey] !== undefined) {
126+
processedData[prefixedKey] = rawData[rawKey];
127+
}
128+
});
129+
130+
console.log(`Processed ${groupName} data:`, processedData);
131+
return processedData;
132+
}
133+
134+
// Handle relay info field name mapping
135+
if (groupName === 'relay_info' && rawData) {
136+
const processedData: any = {};
137+
138+
// Map backend field names to frontend field names
139+
const relayInfoMappings: Record<string, string> = {
140+
'relayname': 'name',
141+
'relaydescription': 'description',
142+
'relaycontact': 'contact',
143+
'relaypubkey': 'pubkey', // This might not exist in backend, will be empty
144+
'relaydhtkey': 'dht_key',
145+
'relaysoftware': 'software',
146+
'relayversion': 'version',
147+
'relaysupportednips': 'supported_nips'
148+
};
149+
150+
// Apply field mappings
151+
Object.entries(relayInfoMappings).forEach(([frontendKey, backendKey]) => {
152+
if (rawData[backendKey] !== undefined) {
153+
processedData[frontendKey] = rawData[backendKey];
154+
} else {
155+
// Set default values for missing fields
156+
if (frontendKey === 'relaypubkey') {
157+
processedData[frontendKey] = ''; // Default empty for pubkey
158+
} else if (frontendKey === 'relaysupportednips') {
159+
processedData[frontendKey] = []; // Default empty array
160+
}
161+
}
162+
});
163+
164+
console.log(`Processed ${groupName} data:`, processedData);
165+
return processedData;
166+
}
167+
168+
return rawData;
169+
};
170+
171+
// Helper function to build the nested update structure for the new API
172+
const buildNestedUpdate = (groupName: string, data: any) => {
173+
switch (groupName) {
174+
case 'image_moderation':
175+
return {
176+
settings: {
177+
content_filtering: {
178+
image_moderation: data
179+
}
180+
}
181+
};
182+
183+
case 'content_filter':
184+
return {
185+
settings: {
186+
content_filtering: {
187+
text_filter: data
188+
}
189+
}
190+
};
191+
192+
case 'nest_feeder':
193+
return {
194+
settings: {
195+
external_services: {
196+
nest_feeder: data
197+
}
198+
}
199+
};
200+
201+
case 'ollama':
202+
return {
203+
settings: {
204+
external_services: {
205+
ollama: data
206+
}
207+
}
208+
};
209+
210+
case 'wallet':
211+
return {
212+
settings: {
213+
external_services: {
214+
wallet: data
215+
}
216+
}
217+
};
218+
219+
case 'relay_info':
220+
// Reverse the field mapping for saving
221+
const backendRelayData: any = {};
222+
const relayFieldMappings: Record<string, string> = {
223+
'name': 'relayname',
224+
'description': 'relaydescription',
225+
'contact': 'relaycontact',
226+
'dht_key': 'relaydhtkey',
227+
'software': 'relaysoftware',
228+
'version': 'relayversion',
229+
'supported_nips': 'relaysupportednips'
230+
// Note: not mapping pubkey since it doesn't exist in backend
231+
};
232+
233+
Object.entries(relayFieldMappings).forEach(([backendKey, frontendKey]) => {
234+
if (data[frontendKey] !== undefined) {
235+
// Special handling for supported_nips to ensure they're numbers
236+
if (backendKey === 'supported_nips') {
237+
const nips = data[frontendKey];
238+
if (Array.isArray(nips)) {
239+
backendRelayData[backendKey] = nips.map((nip: any) => Number(nip)).filter((nip: number) => !isNaN(nip));
240+
} else {
241+
backendRelayData[backendKey] = [];
242+
}
243+
} else {
244+
backendRelayData[backendKey] = data[frontendKey];
245+
}
246+
}
247+
});
248+
249+
return {
250+
settings: {
251+
relay: backendRelayData
252+
}
253+
};
254+
255+
case 'general':
256+
return {
257+
settings: {
258+
server: data
259+
}
260+
};
261+
262+
default:
263+
console.warn(`Unknown settings group for save: ${groupName}`);
264+
return {
265+
settings: {}
266+
};
267+
}
268+
};
269+
7270
interface UseGenericSettingsResult<T> {
8271
settings: T | null;
9272
loading: boolean;
@@ -35,7 +298,7 @@ const useGenericSettings = <T extends SettingsGroupName>(
35298

36299
console.log(`Fetching ${groupName} settings...`);
37300

38-
const response = await fetch(`${config.baseURL}/api/settings/${groupName}`, {
301+
const response = await fetch(`${config.baseURL}/api/settings`, {
39302
headers: {
40303
'Authorization': `Bearer ${token}`,
41304
},
@@ -52,10 +315,10 @@ const useGenericSettings = <T extends SettingsGroupName>(
52315
}
53316

54317
const data = await response.json();
55-
console.log(`Raw ${groupName} settings data:`, data);
318+
console.log(`Raw settings data:`, data);
56319

57-
// The API returns data in the format { [groupName]: settings }
58-
const settingsData = data[groupName] as SettingsGroupType<T>;
320+
// Extract the correct nested data based on groupName
321+
const settingsData = extractSettingsForGroup(data.settings, groupName) as SettingsGroupType<T>;
59322

60323
if (!settingsData) {
61324
console.warn(`No settings data found for group: ${groupName}`);
@@ -185,9 +448,9 @@ const useGenericSettings = <T extends SettingsGroupName>(
185448
console.log(`Settings from state for ${groupName}:`, settings);
186449
const { prefix, formKeys } = prefixedSettingsMap[groupName];
187450

188-
// First fetch current settings to preserve values not in the form
189-
console.log(`Fetching current ${groupName} settings before saving...`);
190-
const fetchResponse = await fetch(`${config.baseURL}/api/settings/${groupName}`, {
451+
// First fetch complete settings structure to preserve all values
452+
console.log(`Fetching complete settings before saving ${groupName}...`);
453+
const fetchResponse = await fetch(`${config.baseURL}/api/settings`, {
191454
headers: {
192455
'Authorization': `Bearer ${token}`,
193456
},
@@ -198,7 +461,7 @@ const useGenericSettings = <T extends SettingsGroupName>(
198461
}
199462

200463
const currentData = await fetchResponse.json();
201-
const currentSettings = currentData[groupName] || {};
464+
const currentSettings = extractSettingsForGroup(currentData.settings, groupName) || {};
202465
console.log(`Current ${groupName} settings from API:`, currentSettings);
203466

204467
// Create a properly prefixed object for the API
@@ -231,13 +494,17 @@ const useGenericSettings = <T extends SettingsGroupName>(
231494

232495
console.log(`Saving ${groupName} settings:`, dataToSave);
233496

234-
const response = await fetch(`${config.baseURL}/api/settings/${groupName}`, {
497+
// Construct the nested update structure for the new API
498+
const nestedUpdate = buildNestedUpdate(groupName, dataToSave);
499+
console.log(`Nested update structure:`, nestedUpdate);
500+
501+
const response = await fetch(`${config.baseURL}/api/settings`, {
235502
method: 'POST',
236503
headers: {
237504
'Content-Type': 'application/json',
238505
'Authorization': `Bearer ${token}`,
239506
},
240-
body: JSON.stringify({ [groupName]: dataToSave }),
507+
body: JSON.stringify(nestedUpdate),
241508
});
242509

243510
if (response.status === 401) {

src/types/settings.types.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ export type SettingsGroupName =
9898
| 'relay_info'
9999
| 'wallet'
100100
| 'general'
101-
| 'query_cache'
102101
| 'relay_settings';
103102

104103
export type SettingsGroupType<T extends SettingsGroupName> =
@@ -109,6 +108,5 @@ export type SettingsGroupType<T extends SettingsGroupName> =
109108
T extends 'relay_info' ? RelayInfoSettings :
110109
T extends 'wallet' ? WalletSettings :
111110
T extends 'general' ? GeneralSettings :
112-
T extends 'query_cache' ? QueryCacheSettings :
113111
T extends 'relay_settings' ? any : // Using any for relay_settings as it's already defined elsewhere
114112
never;

0 commit comments

Comments
 (0)