This commit is contained in:
Jurgis Sakalauskas
2025-10-13 18:10:52 +03:00
parent ff18d0daeb
commit 7bc38e9a6e
7 changed files with 253 additions and 76 deletions
+1
View File
@@ -16,6 +16,7 @@
"description": "React frontend for the best-choice project", "description": "React frontend for the best-choice project",
"dependencies": { "dependencies": {
"@tanstack/react-query": "^5.90.2", "@tanstack/react-query": "^5.90.2",
"lucide-react": "^0.545.0",
"react": "^19.2.0", "react": "^19.2.0",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-router-dom": "^7.9.4" "react-router-dom": "^7.9.4"
+53
View File
@@ -88,6 +88,7 @@ export interface ImportedItemData {
model: string | null; model: string | null;
note: string | null; note: string | null;
attributes: Record<string, unknown>; attributes: Record<string, unknown>;
sourceUrl?: string | null;
} }
export const createItem = async ( export const createItem = async (
@@ -131,3 +132,55 @@ export const importItemFromUrl = async (
return response.data; return response.data;
}; };
export interface UpdateItemPayload {
manufacturer?: string | null;
model?: string;
status?: ItemStatus;
note?: string | null;
attributes?: Record<string, unknown>;
sourceUrl?: string | null;
sourceUrlId?: string | null;
}
export const updateItem = async (
itemId: string,
payload: UpdateItemPayload
): Promise<Item> => {
const body: Record<string, unknown> = {};
if ('manufacturer' in payload) {
body.manufacturer = payload.manufacturer ?? null;
}
if ('model' in payload) {
body.model = payload.model;
}
if ('status' in payload) {
body.status = payload.status;
}
if ('note' in payload) {
body.note = payload.note ?? null;
}
if ('attributes' in payload) {
body.attributes = payload.attributes ?? {};
}
if ('sourceUrl' in payload) {
body.sourceUrl = payload.sourceUrl ?? null;
}
if ('sourceUrlId' in payload) {
body.sourceUrlId = payload.sourceUrlId ?? null;
}
const response = await apiFetch<SingleItemResponse>(`/items/${itemId}`, {
method: 'PATCH',
body
});
return response.data;
};
+23 -1
View File
@@ -111,9 +111,31 @@ export const updateProject = async (
projectId: string, projectId: string,
payload: UpdateProjectPayload payload: UpdateProjectPayload
): Promise<Project> => { ): Promise<Project> => {
const body: Record<string, unknown> = {};
if (payload.name !== undefined) {
body.name = payload.name;
}
if (payload.description !== undefined) {
body.description = payload.description ?? null;
}
if (payload.status !== undefined) {
body.status = payload.status;
}
if (payload.attributes !== undefined) {
body.attributes = payload.attributes;
}
if (payload.priorityRules !== undefined) {
body.priorityRules = payload.priorityRules;
}
const response = await apiFetch<SingleProjectResponse>(`/projects/${projectId}`, { const response = await apiFetch<SingleProjectResponse>(`/projects/${projectId}`, {
method: 'PATCH', method: 'PATCH',
body: payload body
}); });
return response.data; return response.data;
+19 -7
View File
@@ -3,7 +3,7 @@ import type { CreateItemPayload, ImportedItemData } from '../api/items';
export interface ItemFormModalProps { export interface ItemFormModalProps {
isOpen: boolean; isOpen: boolean;
mode: 'url' | 'manual'; mode: 'url' | 'manual' | 'edit';
initialUrl: string | null; initialUrl: string | null;
onClose: () => void; onClose: () => void;
onSuccess: () => void; onSuccess: () => void;
@@ -67,7 +67,8 @@ export function ItemFormModal({
} }
setError(null); setError(null);
setSourceUrl(initialUrl ?? ''); const resolvedSourceUrl = initialData?.sourceUrl ?? initialUrl ?? '';
setSourceUrl(resolvedSourceUrl);
setManufacturer(initialData?.manufacturer ?? ''); setManufacturer(initialData?.manufacturer ?? '');
setModel(initialData?.model ?? ''); setModel(initialData?.model ?? '');
setNote(initialData?.note ?? ''); setNote(initialData?.note ?? '');
@@ -160,14 +161,19 @@ export function ItemFormModal({
} }
try { try {
await onSubmit({ const payload: CreateItemPayload = {
manufacturer: manufacturer.trim() ? manufacturer.trim() : null, manufacturer: manufacturer.trim() ? manufacturer.trim() : null,
model: model.trim(), model: model.trim(),
status: 'active',
note: note.trim() ? note.trim() : null, note: note.trim() ? note.trim() : null,
attributes: attributesPayload, attributes: attributesPayload,
sourceUrl: sourceUrl.trim() ? sourceUrl.trim() : null sourceUrl: sourceUrl.trim() ? sourceUrl.trim() : null
}); };
if (mode !== 'edit') {
payload.status = 'active';
}
await onSubmit(payload);
onSuccess(); onSuccess();
} catch (submitError) { } catch (submitError) {
setError((submitError as Error).message); setError((submitError as Error).message);
@@ -201,11 +207,17 @@ export function ItemFormModal({
<div className="flex-1 overflow-y-auto px-8 py-6"> <div className="flex-1 overflow-y-auto px-8 py-6">
<header className="mb-6"> <header className="mb-6">
<h2 className="text-xl font-semibold text-slate-900"> <h2 className="text-xl font-semibold text-slate-900">
{mode === 'url' ? 'Review Item Details' : 'Add Item Manually'} {mode === 'url'
? 'Review Item Details'
: mode === 'edit'
? 'Edit Item'
: 'Add Item Manually'}
</h2> </h2>
<p className="text-sm text-slate-500"> <p className="text-sm text-slate-500">
{mode === 'url' {mode === 'url'
? 'Double-check the imported information and fill in any gaps.' ? 'Double-check the imported information and fill in any gaps.'
: mode === 'edit'
? 'Update this item with the latest information.'
: 'Provide the specs and notes for this item.'} : 'Provide the specs and notes for this item.'}
</p> </p>
</header> </header>
@@ -249,7 +261,7 @@ export function ItemFormModal({
> >
<div className="flex flex-col gap-2 md:col-span-2"> <div className="flex flex-col gap-2 md:col-span-2">
<label className="text-sm font-medium text-slate-700" htmlFor="modal-item-source-url"> <label className="text-sm font-medium text-slate-700" htmlFor="modal-item-source-url">
Source URL {mode === 'manual' ? '(optional)' : ''} Source URL {mode === 'url' ? '' : '(optional)'}
</label> </label>
<input <input
id="modal-item-source-url" id="modal-item-source-url"
@@ -2,11 +2,18 @@ import { useMemo, useRef, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { import {
useCreateItemMutation, useCreateItemMutation,
useUpdateItemMutation,
useProjectItemsQuery, useProjectItemsQuery,
useProjectQuery, useProjectQuery,
useUpdateProjectMutation useUpdateProjectMutation
} from '../../query/projects'; } from '../../query/projects';
import { importItemFromUrl, type ImportedItemData } from '../../api/items'; import {
importItemFromUrl,
type CreateItemPayload,
type ImportedItemData,
type Item,
type UpdateItemPayload
} from '../../api/items';
import { ItemFormModal } from '../../components/item-form-modal'; import { ItemFormModal } from '../../components/item-form-modal';
import { ProjectHeader } from './project-header'; import { ProjectHeader } from './project-header';
import { ProjectDescriptionSection } from './project-description-section'; import { ProjectDescriptionSection } from './project-description-section';
@@ -18,18 +25,20 @@ export function ProjectDetailPage() {
const projectQuery = useProjectQuery(projectId); const projectQuery = useProjectQuery(projectId);
const itemsQuery = useProjectItemsQuery(projectId); const itemsQuery = useProjectItemsQuery(projectId);
const createItemMutation = useCreateItemMutation(projectId); const createItemMutation = useCreateItemMutation(projectId);
const updateItemMutation = useUpdateItemMutation(projectId);
const updateProjectMutation = useUpdateProjectMutation(projectId); const updateProjectMutation = useUpdateProjectMutation(projectId);
const [expandedItemId, setExpandedItemId] = useState<string | null>(null); const [expandedItemId, setExpandedItemId] = useState<string | null>(null);
const [quickUrl, setQuickUrl] = useState(''); const [quickUrl, setQuickUrl] = useState('');
const [quickError, setQuickError] = useState<string | null>(null); const [quickError, setQuickError] = useState<string | null>(null);
const [isItemModalOpen, setIsItemModalOpen] = useState(false); const [isItemModalOpen, setIsItemModalOpen] = useState(false);
const [itemModalMode, setItemModalMode] = useState<'url' | 'manual'>('manual'); const [itemModalMode, setItemModalMode] = useState<'url' | 'manual' | 'edit'>('manual');
const [modalInitialUrl, setModalInitialUrl] = useState<string | null>(null); const [modalInitialUrl, setModalInitialUrl] = useState<string | null>(null);
const [importedItemData, setImportedItemData] = useState<ImportedItemData | null>(null); const [importedItemData, setImportedItemData] = useState<ImportedItemData | null>(null);
const [isImportingItem, setIsImportingItem] = useState(false); const [isImportingItem, setIsImportingItem] = useState(false);
const [importError, setImportError] = useState<string | null>(null); const [importError, setImportError] = useState<string | null>(null);
const importRequestIdRef = useRef(0); const importRequestIdRef = useRef(0);
const [itemBeingEdited, setItemBeingEdited] = useState<Item | null>(null);
const project = projectQuery.data; const project = projectQuery.data;
const items = useMemo(() => itemsQuery.data?.data ?? [], [itemsQuery.data]); const items = useMemo(() => itemsQuery.data?.data ?? [], [itemsQuery.data]);
@@ -57,7 +66,7 @@ export function ProjectDetailPage() {
return Array.from(attributeSet); return Array.from(attributeSet);
}, [projectAttributes, items]); }, [projectAttributes, items]);
const openItemModal = (mode: 'url' | 'manual', initialUrl: string | null) => { const openItemModal = (mode: 'url' | 'manual' | 'edit', initialUrl: string | null) => {
setItemModalMode(mode); setItemModalMode(mode);
setModalInitialUrl(initialUrl); setModalInitialUrl(initialUrl);
setIsItemModalOpen(true); setIsItemModalOpen(true);
@@ -68,6 +77,8 @@ export function ProjectDetailPage() {
return; return;
} }
setItemBeingEdited(null);
const trimmed = quickUrl.trim(); const trimmed = quickUrl.trim();
if (!trimmed) { if (!trimmed) {
setQuickError('Enter a URL to import item details.'); setQuickError('Enter a URL to import item details.');
@@ -82,6 +93,7 @@ export function ProjectDetailPage() {
setQuickError(null); setQuickError(null);
setImportError(null); setImportError(null);
setImportedItemData(null); setImportedItemData(null);
setItemBeingEdited(null);
const requestId = importRequestIdRef.current + 1; const requestId = importRequestIdRef.current + 1;
importRequestIdRef.current = requestId; importRequestIdRef.current = requestId;
@@ -118,9 +130,20 @@ export function ProjectDetailPage() {
setImportedItemData(null); setImportedItemData(null);
setIsImportingItem(false); setIsImportingItem(false);
importRequestIdRef.current += 1; importRequestIdRef.current += 1;
setItemBeingEdited(null);
openItemModal('manual', quickUrl.trim() || null); openItemModal('manual', quickUrl.trim() || null);
}; };
const handleEditItem = (item: Item) => {
setQuickError(null);
setImportError(null);
setImportedItemData(null);
setIsImportingItem(false);
importRequestIdRef.current += 1;
setItemBeingEdited(item);
openItemModal('edit', item.sourceUrl ?? null);
};
const handleModalClose = () => { const handleModalClose = () => {
setIsItemModalOpen(false); setIsItemModalOpen(false);
setQuickError(null); setQuickError(null);
@@ -128,6 +151,7 @@ export function ProjectDetailPage() {
setImportedItemData(null); setImportedItemData(null);
setIsImportingItem(false); setIsImportingItem(false);
importRequestIdRef.current += 1; importRequestIdRef.current += 1;
setItemBeingEdited(null);
}; };
const handleModalSuccess = () => { const handleModalSuccess = () => {
@@ -140,6 +164,7 @@ export function ProjectDetailPage() {
setImportedItemData(null); setImportedItemData(null);
setIsImportingItem(false); setIsImportingItem(false);
importRequestIdRef.current += 1; importRequestIdRef.current += 1;
setItemBeingEdited(null);
}; };
const handleDescriptionUpdate = async (description: string | null) => { const handleDescriptionUpdate = async (description: string | null) => {
@@ -150,6 +175,26 @@ export function ProjectDetailPage() {
await updateProjectMutation.mutateAsync({ attributes }); await updateProjectMutation.mutateAsync({ attributes });
}; };
const handleUpdateItemSubmit = (payload: CreateItemPayload) => {
if (!itemBeingEdited) {
return Promise.reject(new Error('No item selected for editing'));
}
const updatePayload: UpdateItemPayload = {
manufacturer: payload.manufacturer,
model: payload.model,
note: payload.note,
attributes: payload.attributes,
sourceUrl: payload.sourceUrl,
sourceUrlId: payload.sourceUrlId
};
return updateItemMutation.mutateAsync({
itemId: itemBeingEdited.id,
payload: updatePayload
});
};
if (!projectId) { if (!projectId) {
return <p className="text-sm text-red-600">Project identifier is missing from the URL.</p>; return <p className="text-sm text-red-600">Project identifier is missing from the URL.</p>;
} }
@@ -177,6 +222,31 @@ export function ProjectDetailPage() {
setExpandedItemId((current) => (current === itemId ? null : itemId)); setExpandedItemId((current) => (current === itemId ? null : itemId));
}; };
const modalInitialData =
itemModalMode === 'url'
? importedItemData
: itemModalMode === 'edit' && itemBeingEdited
? {
manufacturer: itemBeingEdited.manufacturer,
model: itemBeingEdited.model,
note: itemBeingEdited.note,
attributes: itemBeingEdited.attributes ?? {},
sourceUrl: itemBeingEdited.sourceUrl ?? null
}
: null;
const itemModalSubmit =
itemModalMode === 'edit' ? handleUpdateItemSubmit : createItemMutation.mutateAsync;
const itemModalIsSubmitting = itemModalMode === 'edit'
? updateItemMutation.isPending
: createItemMutation.isPending;
const itemModalInitialDataLoading =
itemModalMode === 'url' ? isImportingItem : false;
const itemModalInitialDataError = itemModalMode === 'url' ? importError : null;
return ( return (
<div className="flex flex-col gap-10"> <div className="flex flex-col gap-10">
<section className="rounded-2xl border border-slate-200 bg-white p-8 shadow-sm"> <section className="rounded-2xl border border-slate-200 bg-white p-8 shadow-sm">
@@ -211,6 +281,7 @@ export function ProjectDetailPage() {
onAddUrl={handleAddUrlClick} onAddUrl={handleAddUrlClick}
onAddManual={handleAddManualClick} onAddManual={handleAddManualClick}
isImportingFromUrl={isImportingItem} isImportingFromUrl={isImportingItem}
onEditItem={handleEditItem}
/> />
<ItemFormModal <ItemFormModal
@@ -219,12 +290,12 @@ export function ProjectDetailPage() {
initialUrl={modalInitialUrl} initialUrl={modalInitialUrl}
onClose={handleModalClose} onClose={handleModalClose}
onSuccess={handleModalSuccess} onSuccess={handleModalSuccess}
onSubmit={createItemMutation.mutateAsync} onSubmit={itemModalSubmit}
isSubmitting={createItemMutation.isPending} isSubmitting={itemModalIsSubmitting}
projectAttributes={projectAttributes} projectAttributes={projectAttributes}
initialData={itemModalMode === 'url' ? importedItemData : null} initialData={modalInitialData}
isInitialDataLoading={itemModalMode === 'url' ? isImportingItem : false} isInitialDataLoading={itemModalInitialDataLoading}
initialDataError={itemModalMode === 'url' ? importError : null} initialDataError={itemModalInitialDataError}
/> />
</div> </div>
); );
@@ -1,4 +1,5 @@
import { Fragment, useEffect, useMemo, useState } from 'react'; import { Fragment, useEffect, useMemo, useState } from 'react';
import { ExternalLink, Pencil } from 'lucide-react';
import type { Item } from '../../api/items'; import type { Item } from '../../api/items';
import { ItemPricesPanel } from './item-prices-panel'; import { ItemPricesPanel } from './item-prices-panel';
@@ -15,6 +16,7 @@ interface ProjectItemsSectionProps {
onAddUrl: () => void; onAddUrl: () => void;
onAddManual: () => void; onAddManual: () => void;
isImportingFromUrl: boolean; isImportingFromUrl: boolean;
onEditItem: (item: Item) => void;
} }
const formatAttributeValue = (value: unknown): string => { const formatAttributeValue = (value: unknown): string => {
@@ -180,7 +182,8 @@ export function ProjectItemsSection({
onQuickUrlChange, onQuickUrlChange,
onAddUrl, onAddUrl,
onAddManual, onAddManual,
isImportingFromUrl isImportingFromUrl,
onEditItem
}: ProjectItemsSectionProps) { }: ProjectItemsSectionProps) {
const baseColumnCount = 2; // Item, Prices (status indicator lives inside item cell) const baseColumnCount = 2; // Item, Prices (status indicator lives inside item cell)
const [showDifferencesOnly, setShowDifferencesOnly] = useState(false); const [showDifferencesOnly, setShowDifferencesOnly] = useState(false);
@@ -309,31 +312,40 @@ export function ProjectItemsSection({
<tr className="hover:bg-slate-50"> <tr className="hover:bg-slate-50">
<td className="px-4 py-3 align-top"> <td className="px-4 py-3 align-top">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="inline-flex items-center gap-2 text-slate-900"> <div className="flex items-center gap-2">
<button
type="button"
onClick={() => onEditItem(item)}
className="group inline-flex items-center gap-2 rounded-md text-left text-slate-900 transition hover:text-blue-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white"
title="Edit item details"
>
<span <span
aria-hidden aria-hidden
className={`inline-block h-2.5 w-2.5 rounded-full ${ className={`inline-block h-2.5 w-2.5 rounded-full ${
item.status === 'active' ? 'bg-emerald-500' : 'bg-slate-300' item.status === 'active' ? 'bg-emerald-500' : 'bg-slate-300'
}`} }`}
/> />
<span className="font-semibold"> <span className="font-semibold group-hover:underline">
{item.manufacturer ? `${item.manufacturer} ${item.model}` : item.model} {item.manufacturer ? `${item.manufacturer} ${item.model}` : item.model}
</span> </span>
</span> </button>
{item.note ? (
<span className="text-xs text-slate-500">{item.note}</span>
) : null}
{item.sourceUrl ? ( {item.sourceUrl ? (
<a <a
href={item.sourceUrl} href={item.sourceUrl}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="text-xs font-medium text-blue-600 hover:text-blue-700" className="inline-flex h-8 w-8 items-center justify-center rounded-md border border-blue-200 text-blue-600 transition hover:bg-blue-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white"
title="Open source link"
> >
View source <ExternalLink aria-hidden className="h-4 w-4" />
<span className="sr-only">Open source link</span>
</a> </a>
) : null} ) : null}
</div> </div>
{item.note ? (
<span className="text-xs text-slate-500">{item.note}</span>
) : null}
</div>
</td> </td>
{visibleAttributes.map((attribute) => ( {visibleAttributes.map((attribute) => (
<td key={attribute} className="px-4 py-3 align-top text-xs text-slate-600"> <td key={attribute} className="px-4 py-3 align-top text-xs text-slate-600">
@@ -357,19 +369,7 @@ export function ProjectItemsSection({
aria-label={priceButtonLabel} aria-label={priceButtonLabel}
title={priceButtonLabel} title={priceButtonLabel}
> >
<svg <Pencil aria-hidden className="h-5 w-5 text-slate-400 transition hover:text-blue-600" />
aria-hidden="true"
className="h-4 w-4"
viewBox="0 0 20 20"
fill="currentColor"
>
<path d="M14.243 2.929a2 2 0 00-2.828 0L4 10.344V13h2.656l7.415-7.414a2 2 0 000-2.828z" />
<path
fillRule="evenodd"
d="M3.25 15a1.75 1.75 0 001.75 1.75h10A1.75 1.75 0 0016.75 15v-4a.75.75 0 00-1.5 0v4a.25.25 0 01-.25.25h-10a.25.25 0 01-.25-.25v-10a.25.25 0 01.25-.25h4a.75.75 0 000-1.5h-4A1.75 1.75 0 003.25 5v10z"
clipRule="evenodd"
/>
</svg>
<span className="sr-only">{priceButtonLabel}</span> <span className="sr-only">{priceButtonLabel}</span>
</button> </button>
</div> </div>
@@ -399,33 +399,40 @@ export function ProjectItemsSection({
return ( return (
<div key={item.id} className="rounded-xl border border-slate-200 p-4 shadow-sm"> <div key={item.id} className="rounded-xl border border-slate-200 p-4 shadow-sm">
<div className="flex items-start justify-between gap-3"> <div className="flex items-start gap-3">
<div className="flex items-center gap-2"> <button
type="button"
onClick={() => onEditItem(item)}
className="group flex flex-1 items-center gap-2 text-left text-slate-900 transition hover:text-blue-600 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white"
title="Edit item details"
>
<span <span
aria-hidden aria-hidden
className={`inline-block h-2.5 w-2.5 rounded-full ${ className={`inline-block h-2.5 w-2.5 rounded-full ${
item.status === 'active' ? 'bg-emerald-500' : 'bg-slate-300' item.status === 'active' ? 'bg-emerald-500' : 'bg-slate-300'
}`} }`}
/> />
<h3 className="text-lg font-semibold text-slate-900"> <span className="text-lg font-semibold group-hover:underline">
{item.manufacturer ? `${item.manufacturer} ${item.model}` : item.model} {item.manufacturer ? `${item.manufacturer} ${item.model}` : item.model}
</h3> </span>
</div> </button>
</div>
{item.note ? (
<p className="mt-1 text-sm text-slate-600">Note: {item.note}</p>
) : null}
{item.sourceUrl ? ( {item.sourceUrl ? (
<a <a
href={item.sourceUrl} href={item.sourceUrl}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
className="mt-1 inline-flex text-xs font-medium text-blue-600 hover:text-blue-700" className="inline-flex h-9 w-9 items-center justify-center rounded-md border border-blue-200 text-blue-600 transition hover:bg-blue-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white"
title="Open source link"
> >
View source <ExternalLink aria-hidden className="h-5 w-5" />
<span className="sr-only">Open source link</span>
</a> </a>
) : null} ) : null}
</div>
{item.note ? (
<p className="mt-1 text-sm text-slate-600">Note: {item.note}</p>
) : null}
<div className="mt-3 space-y-2"> <div className="mt-3 space-y-2">
{visibleAttributes.map((attribute) => ( {visibleAttributes.map((attribute) => (
@@ -450,14 +457,7 @@ export function ProjectItemsSection({
onClick={() => onToggleItem(item.id)} onClick={() => onToggleItem(item.id)}
className="mt-4 inline-flex w-full items-center justify-center gap-2 rounded-md border border-blue-200 px-3 py-2 text-xs font-semibold uppercase tracking-wide text-blue-600 transition hover:bg-blue-50" className="mt-4 inline-flex w-full items-center justify-center gap-2 rounded-md border border-blue-200 px-3 py-2 text-xs font-semibold uppercase tracking-wide text-blue-600 transition hover:bg-blue-50"
> >
<svg aria-hidden="true" className="h-4 w-4" viewBox="0 0 20 20" fill="currentColor"> <Pencil aria-hidden className="h-5 w-5 text-slate-400 transition hover:text-blue-600" />
<path d="M14.243 2.929a2 2 0 00-2.828 0L4 10.344V13h2.656l7.415-7.414a2 2 0 000-2.828z" />
<path
fillRule="evenodd"
d="M3.25 15a1.75 1.75 0 001.75 1.75h10A1.75 1.75 0 0016.75 15v-4a.75.75 0 00-1.5 0v4a.25.25 0 01-.25.25h-10a.25.25 0 01-.25-.25v-10a.25.25 0 01.25-.25h4a.75.75 0 000-1.5h-4A1.75 1.75 0 003.25 5v10z"
clipRule="evenodd"
/>
</svg>
{priceButtonLabel} {priceButtonLabel}
</button> </button>
+19 -1
View File
@@ -11,8 +11,10 @@ import {
import { import {
createItem, createItem,
fetchProjectItems, fetchProjectItems,
updateItem,
type CreateItemPayload, type CreateItemPayload,
type ProjectItemsResponse type ProjectItemsResponse,
type UpdateItemPayload
} from '../api/items'; } from '../api/items';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
@@ -98,3 +100,19 @@ export const useCreateItemMutation = (projectId: string | undefined) => {
} }
}); });
}; };
export const useUpdateItemMutation = (projectId: string | undefined) => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ itemId, payload }: { itemId: string; payload: UpdateItemPayload }) =>
updateItem(itemId, payload),
onSuccess: () => {
if (!projectId) {
return;
}
queryClient.invalidateQueries({ queryKey: projectsKeys.items(projectId) });
queryClient.invalidateQueries({ queryKey: projectsKeys.detail(projectId) });
}
});
};