feat(ItemsList): enhance item saving logic with validation and error handling
This commit is contained in:
parent
d480495715
commit
887815c7da
2 changed files with 101 additions and 81 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -52,3 +52,4 @@ prisma/*.sqlite3
|
|||
|
||||
/src/generated/prisma
|
||||
/.qodo
|
||||
settings.json
|
||||
|
|
|
@ -60,15 +60,58 @@ async function loadItemsFromDb(url: string): Promise<ItemsResponse> {
|
|||
async function saveItemToDb(url: string, item: Item): Promise<void> {
|
||||
console.log('[FRONTEND] Saving item - ID:', item.id, 'Name:', item.name);
|
||||
|
||||
const encodedUrl = encodeURIComponent(url);
|
||||
const response = await fetch(`/api/urls/${encodedUrl}/items`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(item)
|
||||
});
|
||||
// Validate item before saving
|
||||
if (!item.id) {
|
||||
throw new Error('Item ID is required');
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to save item: ${response.status}`);
|
||||
// Ensure name and description are strings
|
||||
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
|
||||
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 {
|
||||
const response = await fetch(url, {
|
||||
headers: { 'Accept': 'application/json' }
|
||||
});
|
||||
const response = await fetch(`/api/wikidata/properties?wikidataId=${encodeURIComponent(wikidataId)}`);
|
||||
|
||||
if (!response.ok) return {};
|
||||
|
||||
const data = await response.json();
|
||||
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;
|
||||
}
|
||||
}
|
||||
if (!response.ok) {
|
||||
console.error('Wikidata properties API error:', response.status);
|
||||
return {};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result;
|
||||
} catch (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>> {
|
||||
const cleanIds = propertyIds.map(id => id.replace('http://www.wikidata.org/prop/', ''));
|
||||
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`;
|
||||
if (propertyIds.length === 0) return {};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
headers: { 'Accept': 'application/json' }
|
||||
const response = await fetch('/api/wikidata/labels', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ propertyIds })
|
||||
});
|
||||
|
||||
if (!response.ok) return {};
|
||||
|
||||
const data = await response.json();
|
||||
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;
|
||||
}
|
||||
}
|
||||
if (!response.ok) {
|
||||
console.error('Property labels API error:', response.status);
|
||||
return {};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return result;
|
||||
} catch (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[]> {
|
||||
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 {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) return [];
|
||||
const response = await fetch(`/api/wikidata/suggestions?query=${encodeURIComponent(query)}`);
|
||||
|
||||
const data = await response.json();
|
||||
return data.search || [];
|
||||
if (!response.ok) {
|
||||
console.error('Wikidata suggestions API error:', response.status);
|
||||
return [];
|
||||
}
|
||||
|
||||
const suggestions = await response.json();
|
||||
return suggestions;
|
||||
} catch (error) {
|
||||
console.error('Error fetching Wikidata suggestions:', error);
|
||||
return [];
|
||||
|
@ -317,14 +314,27 @@ export function ItemsList({ url }: ItemsListProps) {
|
|||
|
||||
if (field === 'name') {
|
||||
item.name = value;
|
||||
// Fetch Wikidata suggestions
|
||||
if (value.trim()) {
|
||||
// Fetch Wikidata suggestions only if value is not empty
|
||||
if (value.trim().length >= 2) {
|
||||
fetchWikidataSuggestions(value).then(suggestions => {
|
||||
setWikidataSuggestions(prev => ({
|
||||
...prev,
|
||||
[`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') {
|
||||
item.description = value;
|
||||
|
@ -334,8 +344,13 @@ export function ItemsList({ url }: ItemsListProps) {
|
|||
|
||||
newItems[index] = item;
|
||||
|
||||
// Auto-save
|
||||
saveItemMutation.mutate(item);
|
||||
// Auto-save with error handling
|
||||
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
|
||||
if (index === newItems.length - 1 && value.trim()) {
|
||||
|
@ -347,7 +362,11 @@ export function ItemsList({ url }: ItemsListProps) {
|
|||
customProperties: {}
|
||||
};
|
||||
newItems.push(newItem);
|
||||
saveItemMutation.mutate(newItem);
|
||||
saveItemMutation.mutate(newItem, {
|
||||
onError: (error) => {
|
||||
console.error('Failed to save new item:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return newItems;
|
||||
|
|
Loading…
Add table
Reference in a new issue