feat(ItemsList): enhance item saving logic with validation and error handling

This commit is contained in:
ryan 2025-07-03 16:34:40 +03:00
parent d480495715
commit 887815c7da
2 changed files with 101 additions and 81 deletions

1
.gitignore vendored
View file

@ -52,3 +52,4 @@ prisma/*.sqlite3
/src/generated/prisma /src/generated/prisma
/.qodo /.qodo
settings.json

View file

@ -60,15 +60,58 @@ async function loadItemsFromDb(url: string): Promise<ItemsResponse> {
async function saveItemToDb(url: string, item: Item): Promise<void> { async function saveItemToDb(url: string, item: Item): Promise<void> {
console.log('[FRONTEND] Saving item - ID:', item.id, 'Name:', item.name); console.log('[FRONTEND] Saving item - ID:', item.id, 'Name:', item.name);
const encodedUrl = encodeURIComponent(url); // Validate item before saving
const response = await fetch(`/api/urls/${encodedUrl}/items`, { if (!item.id) {
method: 'POST', throw new Error('Item ID is required');
headers: { 'Content-Type': 'application/json' }, }
body: JSON.stringify(item)
});
if (!response.ok) { // Ensure name and description are strings
throw new Error(`Failed to save item: ${response.status}`); const sanitizedItem = {
...item,
name: item.name || '',
description: item.description || '',
customProperties: item.customProperties || {}
};
const encodedUrl = encodeURIComponent(url);
try {
const response = await fetch(`/api/urls/${encodedUrl}/items`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(sanitizedItem)
});
if (!response.ok) {
const errorText = await response.text();
console.error('[ERROR] Failed to save item:', response.status, errorText);
// Handle specific database constraint errors
if (response.status === 400 && errorText.includes('Unique constraint failed')) {
console.log('[INFO] Item already exists, attempting update...');
// Try to update instead of create
const updateResponse = await fetch(`/api/urls/${encodedUrl}/items/${item.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(sanitizedItem)
});
if (!updateResponse.ok) {
const updateErrorText = await updateResponse.text();
throw new Error(`Failed to update item (${updateResponse.status}): ${updateErrorText}`);
}
console.log('[SUCCESS] Item updated successfully');
return;
}
throw new Error(`Failed to save item (${response.status}): ${errorText}`);
}
console.log('[SUCCESS] Item saved successfully');
} catch (error) {
console.error('[ERROR] Error in saveItemToDb:', error);
throw error;
} }
} }
@ -109,44 +152,15 @@ async function addPropertyToDb(url: string, property: string): Promise<void> {
// Wikidata API functions // Wikidata API functions
async function fetchWikidataProperties(wikidataId: string): Promise<Record<string, string>> { async function fetchWikidataProperties(wikidataId: string): Promise<Record<string, string>> {
const sparqlQuery = `
SELECT ?prop ?propLabel ?value ?valueLabel WHERE {
wd:${wikidataId} ?prop ?statement.
?statement ?ps ?value.
?property wikibase:claim ?prop.
?property wikibase:statementProperty ?ps.
SERVICE wikibase:label {
bd:serviceParam wikibase:language "en".
?prop rdfs:label ?propLabel.
?value rdfs:label ?valueLabel.
}
}
`;
const url = `https://query.wikidata.org/sparql?query=${encodeURIComponent(sparqlQuery)}&format=json`;
try { try {
const response = await fetch(url, { const response = await fetch(`/api/wikidata/properties?wikidataId=${encodeURIComponent(wikidataId)}`);
headers: { 'Accept': 'application/json' }
});
if (!response.ok) return {}; if (!response.ok) {
console.error('Wikidata properties API error:', response.status);
const data = await response.json(); return {};
const result: Record<string, string> = {};
if (data.results?.bindings) {
for (const binding of data.results.bindings) {
const propUri = binding.prop?.value;
const value = binding.valueLabel?.value || binding.value?.value || '';
if (propUri) {
const propId = propUri.split('/').pop() || '';
result[propId] = value;
}
}
} }
const result = await response.json();
return result; return result;
} catch (error) { } catch (error) {
console.error('Error fetching Wikidata properties:', error); console.error('Error fetching Wikidata properties:', error);
@ -155,40 +169,21 @@ async function fetchWikidataProperties(wikidataId: string): Promise<Record<strin
} }
async function fetchPropertyLabels(propertyIds: string[]): Promise<Record<string, string>> { async function fetchPropertyLabels(propertyIds: string[]): Promise<Record<string, string>> {
const cleanIds = propertyIds.map(id => id.replace('http://www.wikidata.org/prop/', '')); if (propertyIds.length === 0) return {};
const propertyIdsStr = cleanIds.join(' wd:');
const sparqlQuery = `
SELECT ?prop ?propLabel WHERE {
VALUES ?prop { wd:${propertyIdsStr} }
SERVICE wikibase:label { bd:serviceParam wikibase:language "en". }
}
`;
const url = `https://query.wikidata.org/sparql?query=${encodeURIComponent(sparqlQuery)}&format=json`;
try { try {
const response = await fetch(url, { const response = await fetch('/api/wikidata/labels', {
headers: { 'Accept': 'application/json' } method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ propertyIds })
}); });
if (!response.ok) return {}; if (!response.ok) {
console.error('Property labels API error:', response.status);
const data = await response.json(); return {};
const result: Record<string, string> = {};
if (data.results?.bindings) {
for (const binding of data.results.bindings) {
const propUri = binding.prop?.value;
const label = binding.propLabel?.value || '';
if (propUri) {
const propId = propUri.split('/').pop() || '';
result[propId] = label;
}
}
} }
const result = await response.json();
return result; return result;
} catch (error) { } catch (error) {
console.error('Error fetching property labels:', error); console.error('Error fetching property labels:', error);
@ -199,14 +194,16 @@ async function fetchPropertyLabels(propertyIds: string[]): Promise<Record<string
async function fetchWikidataSuggestions(query: string): Promise<WikidataSuggestion[]> { async function fetchWikidataSuggestions(query: string): Promise<WikidataSuggestion[]> {
if (!query.trim()) return []; if (!query.trim()) return [];
const url = `https://www.wikidata.org/w/api.php?action=wbsearchentities&search=${encodeURIComponent(query)}&language=en&limit=5&format=json&origin=*`;
try { try {
const response = await fetch(url); const response = await fetch(`/api/wikidata/suggestions?query=${encodeURIComponent(query)}`);
if (!response.ok) return [];
const data = await response.json(); if (!response.ok) {
return data.search || []; console.error('Wikidata suggestions API error:', response.status);
return [];
}
const suggestions = await response.json();
return suggestions;
} catch (error) { } catch (error) {
console.error('Error fetching Wikidata suggestions:', error); console.error('Error fetching Wikidata suggestions:', error);
return []; return [];
@ -317,14 +314,27 @@ export function ItemsList({ url }: ItemsListProps) {
if (field === 'name') { if (field === 'name') {
item.name = value; item.name = value;
// Fetch Wikidata suggestions // Fetch Wikidata suggestions only if value is not empty
if (value.trim()) { if (value.trim().length >= 2) {
fetchWikidataSuggestions(value).then(suggestions => { fetchWikidataSuggestions(value).then(suggestions => {
setWikidataSuggestions(prev => ({ setWikidataSuggestions(prev => ({
...prev, ...prev,
[`name-${index}`]: suggestions [`name-${index}`]: suggestions
})); }));
}).catch(error => {
console.error('Error fetching Wikidata suggestions:', error);
// Clear suggestions on error
setWikidataSuggestions(prev => ({
...prev,
[`name-${index}`]: []
}));
}); });
} else {
// Clear suggestions if value is too short
setWikidataSuggestions(prev => ({
...prev,
[`name-${index}`]: []
}));
} }
} else if (field === 'description') { } else if (field === 'description') {
item.description = value; item.description = value;
@ -334,8 +344,13 @@ export function ItemsList({ url }: ItemsListProps) {
newItems[index] = item; newItems[index] = item;
// Auto-save // Auto-save with error handling
saveItemMutation.mutate(item); saveItemMutation.mutate(item, {
onError: (error) => {
console.error('Failed to save item:', error);
// You could add a toast notification here
}
});
// Add new row if editing last row and value is not empty // Add new row if editing last row and value is not empty
if (index === newItems.length - 1 && value.trim()) { if (index === newItems.length - 1 && value.trim()) {
@ -347,7 +362,11 @@ export function ItemsList({ url }: ItemsListProps) {
customProperties: {} customProperties: {}
}; };
newItems.push(newItem); newItems.push(newItem);
saveItemMutation.mutate(newItem); saveItemMutation.mutate(newItem, {
onError: (error) => {
console.error('Failed to save new item:', error);
}
});
} }
return newItems; return newItems;