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
|
/src/generated/prisma
|
||||||
/.qodo
|
/.qodo
|
||||||
|
settings.json
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Add table
Reference in a new issue