add image

This commit is contained in:
Jurgis Sakalauskas
2025-10-16 09:33:41 +03:00
parent cea323ec3b
commit 9cdcd23fd9
16 changed files with 906 additions and 47 deletions
+36
View File
@@ -0,0 +1,36 @@
import type { ItemImage } from '@shared/models/item-image';
import { apiFetch } from './http-client';
interface ItemImagesResponse {
data: ItemImage[];
}
interface SingleImageResponse {
data: ItemImage;
}
export const fetchItemImages = async (itemId: string): Promise<ItemImage[]> => {
const response = await apiFetch<ItemImagesResponse>(`/items/${itemId}/images`);
return response.data;
};
export const createItemImage = async (
itemId: string,
url: string
): Promise<ItemImage> => {
const response = await apiFetch<SingleImageResponse>(
`/items/${itemId}/images`,
{
method: 'POST',
body: { url }
}
);
return response.data;
};
export const deleteItemImage = async (imageId: string): Promise<void> => {
await apiFetch(`/images/${imageId}`, {
method: 'DELETE'
});
};
+16 -1
View File
@@ -45,6 +45,7 @@ export interface CreateItemPayload {
attributes?: Record<string, unknown>;
sourceUrl?: string | null;
sourceUrlId?: string | null;
defaultImageId?: string | null;
}
interface SingleItemResponse {
@@ -57,6 +58,8 @@ export interface ImportedItemData {
note: string | null;
attributes: Record<string, unknown>;
sourceUrl?: string | null;
images?: Array<{ id?: string; url: string }>;
defaultImageId?: string | null;
}
export const createItem = async (
@@ -74,7 +77,9 @@ export const createItem = async (
note: payload.note ?? null,
attributes: payload.attributes ?? {},
sourceUrl: payload.sourceUrl ?? null,
sourceUrlId: payload.sourceUrlId ?? null
sourceUrlId: payload.sourceUrlId ?? null,
defaultImageId:
payload.defaultImageId === undefined ? null : payload.defaultImageId
}
}
);
@@ -109,6 +114,7 @@ export interface UpdateItemPayload {
attributes?: Record<string, unknown>;
sourceUrl?: string | null;
sourceUrlId?: string | null;
defaultImageId?: string | null;
}
export const updateItem = async (
@@ -145,6 +151,10 @@ export const updateItem = async (
body.sourceUrlId = payload.sourceUrlId ?? null;
}
if ('defaultImageId' in payload) {
body.defaultImageId = payload.defaultImageId ?? null;
}
const response = await apiFetch<SingleItemResponse>(`/items/${itemId}`, {
method: 'PATCH',
body
@@ -152,3 +162,8 @@ export const updateItem = async (
return response.data;
};
export const fetchItem = async (itemId: string): Promise<Item> => {
const response = await apiFetch<SingleItemResponse>(`/items/${itemId}`);
return response.data;
};
+327 -6
View File
@@ -1,15 +1,54 @@
import { FormEvent, useCallback, useEffect, useMemo, useState } from 'react';
import { FormEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import type { CreateItemPayload, ImportedItemData } from '../api/items';
import type { AttributeScalarType } from '../utils/attribute-values';
import { inferScalarValue } from '../utils/attribute-values';
interface ImageEntry {
id: string;
existingId: string | null;
url: string;
hasPreviewError: boolean;
}
const IMAGE_URL_MAX_LENGTH = 2048;
const createUniqueId = () => Math.random().toString(36).slice(2);
const createBlankImageEntry = (): ImageEntry => ({
id: createUniqueId(),
existingId: null,
url: '',
hasPreviewError: false
});
const isValidImageUrl = (value: string): boolean => {
try {
const parsed = new URL(value);
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
} catch {
return false;
}
};
export interface ItemFormSubmitPayload {
itemPayload: CreateItemPayload;
imageChanges: {
toCreate: Array<{ id: string; url: string }>;
toDelete: string[];
defaultSelection:
| { type: 'existing'; id: string }
| { type: 'new'; id: string }
| null;
};
}
export interface ItemFormModalProps {
isOpen: boolean;
mode: 'url' | 'manual' | 'edit';
initialUrl: string | null;
onClose: () => void;
onSuccess: () => void;
onSubmit: (payload: CreateItemPayload) => Promise<unknown>;
onSubmit: (payload: ItemFormSubmitPayload) => Promise<unknown>;
isSubmitting: boolean;
projectAttributes: string[];
initialData: ImportedItemData | null;
@@ -26,8 +65,6 @@ interface AttributeEntry {
isArrayMember: boolean;
}
const createUniqueId = () => Math.random().toString(36).slice(2);
const createBlankEntry = (key = '', isArrayMember = false): AttributeEntry => ({
id: createUniqueId(),
key,
@@ -308,7 +345,11 @@ export function ItemFormModal({
const [note, setNote] = useState('');
const [sourceUrl, setSourceUrl] = useState('');
const [attributeEntries, setAttributeEntries] = useState<AttributeEntry[]>([]);
const [imageEntries, setImageEntries] = useState<ImageEntry[]>([]);
const [selectedDefaultImageId, setSelectedDefaultImageId] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const initialImageIdsRef = useRef<Set<string>>(new Set());
const initialImageUrlsRef = useRef<Map<string, string>>(new Map());
const isFormDisabled = isSubmitting || isInitialDataLoading;
const trackedAttributeMetadata = useMemo(
() => computeTrackedKeyMetadata(projectAttributes),
@@ -324,6 +365,13 @@ export function ItemFormModal({
[trackedAttributeMetadata]
);
const updateImageEntries = useCallback(
(updater: (previous: ImageEntry[]) => ImageEntry[]) => {
setImageEntries((previous) => updater(previous));
},
[]
);
useEffect(() => {
if (!isOpen) {
return;
@@ -374,6 +422,37 @@ export function ItemFormModal({
trackedAttributeMetadata
)
);
const existingImageIds = new Set<string>();
const existingImageUrlMap = new Map<string, string>();
const initialImages = initialData?.images ?? [];
const nextImageEntries: ImageEntry[] = initialImages.map((image) => {
const existingId = image.id ?? null;
const entryId = existingId ?? createUniqueId();
if (existingId) {
existingImageIds.add(existingId);
existingImageUrlMap.set(existingId, image.url.trim());
}
return {
id: entryId,
existingId,
url: image.url,
hasPreviewError: false
};
});
initialImageIdsRef.current = existingImageIds;
initialImageUrlsRef.current = existingImageUrlMap;
setImageEntries(nextImageEntries);
if (initialData?.defaultImageId) {
const entryWithDefault = nextImageEntries.find(
(entry) => entry.existingId === initialData.defaultImageId
);
setSelectedDefaultImageId(entryWithDefault ? entryWithDefault.id : null);
} else {
setSelectedDefaultImageId(null);
}
}, [isOpen, initialUrl, initialData, projectAttributes, trackedAttributeMetadata]);
useEffect(() => {
@@ -389,6 +468,9 @@ export function ItemFormModal({
return () => window.removeEventListener('keydown', handleKeyDown);
}, [isOpen, onClose]);
const initialDataStatusLabel =
mode === 'edit' ? 'Loading item details…' : 'Importing item details…';
if (!isOpen) {
return null;
}
@@ -476,6 +558,70 @@ export function ItemFormModal({
}
});
const normalizedImages = imageEntries.map((entry) => ({
...entry,
url: entry.url.trim()
}));
const imagesToCreateMap = new Map<string, string>();
const imagesToDeleteSet = new Set(initialImageIdsRef.current);
const existingImageUrlMap = initialImageUrlsRef.current;
for (const image of normalizedImages) {
if (!image.url.length) {
continue;
}
if (image.url.length > IMAGE_URL_MAX_LENGTH) {
setError('Image URL must be 2048 characters or fewer.');
return;
}
if (!isValidImageUrl(image.url)) {
setError('Each image must use a valid http(s) URL.');
return;
}
if (image.existingId) {
const initialUrl = existingImageUrlMap.get(image.existingId) ?? '';
if (image.url === initialUrl) {
imagesToDeleteSet.delete(image.existingId);
} else {
imagesToCreateMap.set(image.id, image.url);
}
} else {
imagesToCreateMap.set(image.id, image.url);
}
}
let defaultSelection: ItemFormSubmitPayload['imageChanges']['defaultSelection'] =
null;
if (selectedDefaultImageId) {
const selectedEntry = normalizedImages.find(
(entry) => entry.id === selectedDefaultImageId
);
if (selectedEntry) {
if (
selectedEntry.existingId &&
!imagesToDeleteSet.has(selectedEntry.existingId) &&
selectedEntry.url.length
) {
defaultSelection = { type: 'existing', id: selectedEntry.existingId };
} else if (selectedEntry.url.length) {
imagesToCreateMap.set(selectedEntry.id, selectedEntry.url);
defaultSelection = { type: 'new', id: selectedEntry.id };
} else {
defaultSelection = null;
}
}
}
const imagesToCreate = Array.from(imagesToCreateMap.entries()).map(
([id, url]) => ({ id, url })
);
const imagesToDelete = Array.from(imagesToDeleteSet);
try {
const payload: CreateItemPayload = {
manufacturer: manufacturer.trim() ? manufacturer.trim() : null,
@@ -489,7 +635,14 @@ export function ItemFormModal({
payload.status = 'active';
}
await onSubmit(payload);
await onSubmit({
itemPayload: payload,
imageChanges: {
toCreate: imagesToCreate,
toDelete: imagesToDelete,
defaultSelection
}
});
onSuccess();
} catch (submitError) {
setError((submitError as Error).message);
@@ -560,7 +713,7 @@ export function ItemFormModal({
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"
/>
</svg>
<span>Importing item details</span>
<span>{initialDataStatusLabel}</span>
</div>
) : null}
@@ -837,6 +990,174 @@ export function ItemFormModal({
</div>
</div>
<div className="md:col-span-2 flex flex-col gap-4">
<div className="flex items-center justify-between gap-2">
<span className="text-sm font-medium text-slate-700">Images</span>
<button
type="button"
onClick={() => {
updateImageEntries((previous) => [
...previous,
createBlankImageEntry()
]);
setError(null);
}}
disabled={isFormDisabled}
className="rounded-md border border-blue-600 px-4 py-2 text-xs font-semibold uppercase tracking-wide text-blue-600 transition hover:bg-blue-50 disabled:cursor-not-allowed disabled:border-slate-200 disabled:text-slate-400 disabled:hover:bg-transparent"
>
Add Image
</button>
</div>
{imageEntries.length ? (
<div className="flex flex-col gap-4">
{imageEntries.map((image) => {
const trimmedUrl = image.url.trim();
const isDefault = selectedDefaultImageId === image.id;
const cardHighlight = isDefault
? 'border-blue-400 shadow-md shadow-blue-100'
: 'border-slate-200';
return (
<div
key={image.id}
className={`rounded-lg border ${cardHighlight} bg-white p-4 shadow-sm`}
>
<div className="flex flex-col gap-3 md:flex-row md:items-start md:gap-5">
<div className="flex h-28 w-full items-center justify-center overflow-hidden rounded-md border border-slate-200 bg-slate-50 md:h-24 md:w-24">
{trimmedUrl ? (
image.hasPreviewError ? (
<span className="px-2 text-center text-xs text-slate-500">
Preview unavailable
</span>
) : (
<img
src={trimmedUrl}
alt="Item preview"
className="h-full w-full object-cover"
loading="lazy"
onError={() => {
updateImageEntries((previous) =>
previous.map((entry) =>
entry.id === image.id
? { ...entry, hasPreviewError: true }
: entry
)
);
}}
onLoad={() => {
updateImageEntries((previous) =>
previous.map((entry) =>
entry.id === image.id && entry.hasPreviewError
? { ...entry, hasPreviewError: false }
: entry
)
);
}}
/>
)
) : (
<span className="px-3 text-center text-xs text-slate-500">
Add an image URL to preview it here.
</span>
)}
</div>
<div className="flex-1 space-y-3">
<input
type="url"
value={image.url}
disabled={isFormDisabled}
onChange={(event) => {
const nextValue = event.target.value;
updateImageEntries((previous) =>
previous.map((entry) =>
entry.id === image.id
? {
...entry,
url: nextValue,
hasPreviewError: false
}
: entry
)
);
if (
selectedDefaultImageId === image.id &&
!nextValue.trim()
) {
setSelectedDefaultImageId(null);
}
setError(null);
}}
placeholder="https://example.com/photo.jpg"
className="w-full rounded-md border border-slate-300 px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 disabled:cursor-not-allowed disabled:border-slate-200 disabled:bg-slate-100"
/>
<div className="flex flex-wrap items-center justify-between gap-3">
<label className="flex items-center gap-2 text-xs font-medium text-slate-600">
<input
type="radio"
name="default-image"
value={image.id}
checked={isDefault}
disabled={isFormDisabled || !trimmedUrl.length}
onChange={() => {
if (!trimmedUrl.length) {
setSelectedDefaultImageId(null);
return;
}
setSelectedDefaultImageId(image.id);
setError(null);
}}
className="h-4 w-4 border-slate-300 text-blue-600 focus:ring-blue-500"
/>
<span>Use as default image</span>
</label>
<button
type="button"
onClick={() => {
updateImageEntries((previous) =>
previous.filter((entry) => entry.id !== image.id)
);
if (selectedDefaultImageId === image.id) {
setSelectedDefaultImageId(null);
}
setError(null);
}}
disabled={isFormDisabled}
className="rounded-md border border-red-200 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-red-600 transition hover:bg-red-50 disabled:cursor-not-allowed disabled:border-slate-200 disabled:text-slate-400 disabled:hover:bg-transparent"
>
Remove
</button>
</div>
</div>
</div>
</div>
);
})}
</div>
) : (
<p className="text-sm text-slate-400">
No images added yet. Use the button above to attach product photos.
</p>
)}
{selectedDefaultImageId ? (
<div className="flex justify-end">
<button
type="button"
onClick={() => {
setSelectedDefaultImageId(null);
setError(null);
}}
disabled={isFormDisabled}
className="rounded-md border border-slate-300 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-slate-600 transition hover:bg-slate-100 disabled:cursor-not-allowed disabled:border-slate-200 disabled:text-slate-400 disabled:hover:bg-transparent"
>
Clear Default Image
</button>
</div>
) : null}
<p className="text-xs text-slate-500">
Previews load directly from the provided URLs. Broken previews will not block saving.
</p>
</div>
<div className="md:col-span-2 flex items-center justify-between">
<div className="text-xs text-slate-500">
Fields marked with * are required. Remove any attribute row you do not need.
@@ -1,7 +1,9 @@
import { useMemo, useRef, useState } from 'react';
import { useParams } from 'react-router-dom';
import type { Item } from '@shared/models/item';
import { useQueryClient } from '@tanstack/react-query';
import {
projectsKeys,
useCreateItemMutation,
useUpdateItemMutation,
useProjectItemsQuery,
@@ -9,12 +11,13 @@ import {
useUpdateProjectMutation
} from '../../query/projects';
import {
fetchItem,
importItemFromUrl,
type CreateItemPayload,
type ImportedItemData,
type UpdateItemPayload
} from '../../api/items';
import { ItemFormModal } from '../../components/item-form-modal';
import { createItemImage, deleteItemImage } from '../../api/images';
import { ItemFormModal, type ItemFormSubmitPayload } from '../../components/item-form-modal';
import { ProjectHeader } from './project-header';
import { ProjectDescriptionSection } from './project-description-section';
import { TrackedAttributesSection } from './tracked-attributes-section';
@@ -27,6 +30,7 @@ export function ProjectDetailPage() {
const createItemMutation = useCreateItemMutation(projectId);
const updateItemMutation = useUpdateItemMutation(projectId);
const updateProjectMutation = useUpdateProjectMutation(projectId);
const queryClient = useQueryClient();
const [quickUrl, setQuickUrl] = useState('');
const [quickError, setQuickError] = useState<string | null>(null);
@@ -37,7 +41,12 @@ export function ProjectDetailPage() {
const [isImportingItem, setIsImportingItem] = useState(false);
const [importError, setImportError] = useState<string | null>(null);
const importRequestIdRef = useRef(0);
const editRequestIdRef = useRef(0);
const [itemBeingEdited, setItemBeingEdited] = useState<Item | null>(null);
const [editItemInitialData, setEditItemInitialData] = useState<ImportedItemData | null>(null);
const [isLoadingItemDetails, setIsLoadingItemDetails] = useState(false);
const [editItemError, setEditItemError] = useState<string | null>(null);
const [isSavingItem, setIsSavingItem] = useState(false);
const project = projectQuery.data;
const items = useMemo(() => itemsQuery.data?.data ?? [], [itemsQuery.data]);
@@ -97,7 +106,11 @@ export function ProjectDetailPage() {
setQuickError(null);
setImportError(null);
setImportedItemData(null);
setEditItemInitialData(null);
setEditItemError(null);
setIsLoadingItemDetails(false);
setItemBeingEdited(null);
editRequestIdRef.current += 1;
const requestId = importRequestIdRef.current + 1;
importRequestIdRef.current = requestId;
@@ -134,18 +147,74 @@ export function ProjectDetailPage() {
setImportedItemData(null);
setIsImportingItem(false);
importRequestIdRef.current += 1;
editRequestIdRef.current += 1;
setItemBeingEdited(null);
setEditItemInitialData(null);
setEditItemError(null);
setIsLoadingItemDetails(false);
setIsSavingItem(false);
openItemModal('manual', quickUrl.trim() || null);
};
const handleEditItem = (item: Item) => {
const handleEditItem = async (item: Item) => {
setQuickError(null);
setImportError(null);
setImportedItemData(null);
setIsImportingItem(false);
importRequestIdRef.current += 1;
editRequestIdRef.current += 1;
setEditItemError(null);
setIsSavingItem(false);
setIsLoadingItemDetails(true);
const initialImages =
item.images?.map((image) => ({ id: image.id, url: image.url })) ?? [];
setEditItemInitialData({
manufacturer: item.manufacturer,
model: item.model,
note: item.note,
attributes: item.attributes ?? {},
sourceUrl: item.sourceUrl ?? null,
images: initialImages,
defaultImageId: item.defaultImageId ?? null
});
setItemBeingEdited(item);
openItemModal('edit', item.sourceUrl ?? null);
const requestId = editRequestIdRef.current;
try {
const fullItem = await fetchItem(item.id);
if (editRequestIdRef.current !== requestId) {
return;
}
setItemBeingEdited(fullItem);
setEditItemInitialData({
manufacturer: fullItem.manufacturer,
model: fullItem.model,
note: fullItem.note,
attributes: fullItem.attributes ?? {},
sourceUrl: fullItem.sourceUrl ?? null,
images:
fullItem.images?.map((image) => ({
id: image.id,
url: image.url
})) ?? [],
defaultImageId: fullItem.defaultImageId ?? null
});
} catch (error) {
if (editRequestIdRef.current !== requestId) {
return;
}
const message =
error instanceof Error && error.message.trim().length
? error.message.trim()
: 'Failed to load item details.';
setEditItemError(message);
} finally {
if (editRequestIdRef.current === requestId) {
setIsLoadingItemDetails(false);
}
}
};
const handleModalClose = () => {
@@ -155,7 +224,12 @@ export function ProjectDetailPage() {
setImportedItemData(null);
setIsImportingItem(false);
importRequestIdRef.current += 1;
editRequestIdRef.current += 1;
setItemBeingEdited(null);
setEditItemInitialData(null);
setEditItemError(null);
setIsLoadingItemDetails(false);
setIsSavingItem(false);
};
const handleModalSuccess = () => {
@@ -168,7 +242,12 @@ export function ProjectDetailPage() {
setImportedItemData(null);
setIsImportingItem(false);
importRequestIdRef.current += 1;
editRequestIdRef.current += 1;
setItemBeingEdited(null);
setEditItemInitialData(null);
setEditItemError(null);
setIsLoadingItemDetails(false);
setIsSavingItem(false);
};
const handleDescriptionUpdate = async (description: string | null) => {
@@ -179,24 +258,126 @@ export function ProjectDetailPage() {
await updateProjectMutation.mutateAsync({ attributes });
};
const handleUpdateItemSubmit = (payload: CreateItemPayload) => {
if (!itemBeingEdited) {
return Promise.reject(new Error('No item selected for editing'));
const handleCreateItemSubmit = async (payload: ItemFormSubmitPayload) => {
if (!projectId) {
throw new Error('Project identifier is missing. Reload the page and try again.');
}
const updatePayload: UpdateItemPayload = {
manufacturer: payload.manufacturer,
model: payload.model,
note: payload.note,
attributes: payload.attributes,
sourceUrl: payload.sourceUrl,
sourceUrlId: payload.sourceUrlId
};
setIsSavingItem(true);
return updateItemMutation.mutateAsync({
itemId: itemBeingEdited.id,
payload: updatePayload
});
try {
const createdItem = await createItemMutation.mutateAsync(payload.itemPayload);
const createdImageIds = new Map<string, string>();
if (payload.imageChanges.toCreate.length) {
await Promise.all(
payload.imageChanges.toCreate.map(async (image) => {
const created = await createItemImage(createdItem.id, image.url);
createdImageIds.set(image.id, created.id);
})
);
}
const { defaultSelection } = payload.imageChanges;
if (defaultSelection) {
let resolvedDefaultId: string | null = null;
if (defaultSelection.type === 'existing') {
resolvedDefaultId = defaultSelection.id;
} else {
const mappedId = createdImageIds.get(defaultSelection.id);
if (!mappedId) {
throw new Error('Failed to determine the default image.');
}
resolvedDefaultId = mappedId;
}
if (resolvedDefaultId) {
await updateItemMutation.mutateAsync({
itemId: createdItem.id,
payload: { defaultImageId: resolvedDefaultId }
});
}
}
await queryClient.invalidateQueries({ queryKey: projectsKeys.items(projectId) });
await queryClient.invalidateQueries({ queryKey: projectsKeys.detail(projectId) });
} finally {
setIsSavingItem(false);
}
};
const handleUpdateItemSubmit = async (payload: ItemFormSubmitPayload) => {
if (!itemBeingEdited) {
throw new Error('No item selected for editing');
}
setIsSavingItem(true);
try {
const createdImageIds = new Map<string, string>();
if (payload.imageChanges.toCreate.length) {
await Promise.all(
payload.imageChanges.toCreate.map(async (image) => {
const created = await createItemImage(itemBeingEdited.id, image.url);
createdImageIds.set(image.id, created.id);
})
);
}
const { defaultSelection, toDelete } = payload.imageChanges;
let resolvedDefaultId: string | null | undefined;
if (defaultSelection) {
if (defaultSelection.type === 'existing') {
resolvedDefaultId = defaultSelection.id;
} else {
const mappedId = createdImageIds.get(defaultSelection.id);
if (!mappedId) {
throw new Error('Failed to determine the default image.');
}
resolvedDefaultId = mappedId;
}
} else if (itemBeingEdited.defaultImageId) {
resolvedDefaultId = null;
}
const updatePayload: UpdateItemPayload = {
manufacturer: payload.itemPayload.manufacturer,
model: payload.itemPayload.model,
note: payload.itemPayload.note,
attributes: payload.itemPayload.attributes,
sourceUrl: payload.itemPayload.sourceUrl,
sourceUrlId: payload.itemPayload.sourceUrlId
};
if (resolvedDefaultId !== undefined) {
updatePayload.defaultImageId = resolvedDefaultId;
}
await updateItemMutation.mutateAsync({
itemId: itemBeingEdited.id,
payload: updatePayload
});
if (toDelete.length) {
const protectedId =
typeof resolvedDefaultId === 'string' ? resolvedDefaultId : null;
const targets = protectedId
? toDelete.filter((id) => id !== protectedId)
: toDelete;
if (targets.length) {
await Promise.all(targets.map((id) => deleteItemImage(id)));
}
}
if (projectId) {
await queryClient.invalidateQueries({ queryKey: projectsKeys.items(projectId) });
await queryClient.invalidateQueries({ queryKey: projectsKeys.detail(projectId) });
}
} finally {
setIsSavingItem(false);
}
};
if (!projectId) {
@@ -225,27 +406,32 @@ export function ProjectDetailPage() {
const modalInitialData =
itemModalMode === 'url'
? importedItemData
: itemModalMode === 'edit' && itemBeingEdited
? {
manufacturer: itemBeingEdited.manufacturer,
model: itemBeingEdited.model,
note: itemBeingEdited.note,
attributes: itemBeingEdited.attributes ?? {},
sourceUrl: itemBeingEdited.sourceUrl ?? null
}
: itemModalMode === 'edit'
? editItemInitialData
: null;
const itemModalSubmit =
itemModalMode === 'edit' ? handleUpdateItemSubmit : createItemMutation.mutateAsync;
itemModalMode === 'edit' ? handleUpdateItemSubmit : handleCreateItemSubmit;
const itemModalIsSubmitting = itemModalMode === 'edit'
? updateItemMutation.isPending
: createItemMutation.isPending;
const itemModalIsSubmitting =
isSavingItem ||
(itemModalMode === 'edit'
? updateItemMutation.isPending
: createItemMutation.isPending);
const itemModalInitialDataLoading =
itemModalMode === 'url' ? isImportingItem : false;
itemModalMode === 'url'
? isImportingItem
: itemModalMode === 'edit'
? isLoadingItemDetails
: false;
const itemModalInitialDataError = itemModalMode === 'url' ? importError : null;
const itemModalInitialDataError =
itemModalMode === 'url'
? importError
: itemModalMode === 'edit'
? editItemError
: null;
return (
<div className="flex flex-col gap-10">