From 887815c7da84a1b46d431efe098e8f919cc97ecf Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 3 Jul 2025 16:34:40 +0300 Subject: [PATCH] feat(ItemsList): enhance item saving logic with validation and error handling --- .gitignore | 1 + src/components/ItemsList.tsx | 181 +++++++++++++++++++---------------- 2 files changed, 101 insertions(+), 81 deletions(-) diff --git a/.gitignore b/.gitignore index 5c69304..45345b6 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,4 @@ prisma/*.sqlite3 /src/generated/prisma /.qodo +settings.json diff --git a/src/components/ItemsList.tsx b/src/components/ItemsList.tsx index 87d55d2..0afe4e1 100644 --- a/src/components/ItemsList.tsx +++ b/src/components/ItemsList.tsx @@ -60,15 +60,58 @@ async function loadItemsFromDb(url: string): Promise { async function saveItemToDb(url: string, item: Item): Promise { 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 { // Wikidata API functions async function fetchWikidataProperties(wikidataId: string): Promise> { - 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 = {}; - - 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> { - 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 = {}; - - 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 { 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;