feat: implement optimistic updates for transaction handling and improve category selection in combobox components for enhanced user experience
This commit is contained in:
@@ -341,6 +341,20 @@ export default function TransactionsPage() {
|
||||
|
||||
const updatedTransaction = { ...transaction, categoryId };
|
||||
|
||||
// Optimistic update: update the cache immediately
|
||||
queryClient.setQueryData<typeof transactionsData>(
|
||||
["transactions", transactionParams],
|
||||
(oldData) => {
|
||||
if (!oldData) return oldData;
|
||||
return {
|
||||
...oldData,
|
||||
transactions: oldData.transactions.map((t) =>
|
||||
t.id === transactionId ? updatedTransaction : t,
|
||||
),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
await fetch("/api/banking/transactions", {
|
||||
method: "PUT",
|
||||
@@ -350,6 +364,8 @@ export default function TransactionsPage() {
|
||||
invalidateTransactions();
|
||||
} catch (error) {
|
||||
console.error("Failed to update transaction:", error);
|
||||
// Revert optimistic update on error
|
||||
invalidateTransactions();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -385,8 +401,23 @@ export default function TransactionsPage() {
|
||||
selectedTransactions.has(t.id),
|
||||
);
|
||||
|
||||
const transactionIds = transactionsToUpdate.map((t) => t.id);
|
||||
setSelectedTransactions(new Set());
|
||||
|
||||
// Optimistic update: update the cache immediately
|
||||
queryClient.setQueryData<typeof transactionsData>(
|
||||
["transactions", transactionParams],
|
||||
(oldData) => {
|
||||
if (!oldData) return oldData;
|
||||
return {
|
||||
...oldData,
|
||||
transactions: oldData.transactions.map((t) =>
|
||||
transactionIds.includes(t.id) ? { ...t, categoryId } : t,
|
||||
),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
transactionsToUpdate.map((t) =>
|
||||
@@ -400,6 +431,8 @@ export default function TransactionsPage() {
|
||||
invalidateTransactions();
|
||||
} catch (error) {
|
||||
console.error("Failed to update transactions:", error);
|
||||
// Revert optimistic update on error
|
||||
invalidateTransactions();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@ export function AccountFilterCombobox({
|
||||
<div key={folder.id}>
|
||||
{/* Folder row */}
|
||||
<CommandItem
|
||||
value={`folder-${currentPath}`}
|
||||
value={`folder-${folder.id}`}
|
||||
onSelect={() => handleSelectFolder(folder.id)}
|
||||
style={{ paddingLeft: `${paddingLeft}px` }}
|
||||
className="font-medium"
|
||||
@@ -182,12 +182,9 @@ export function AccountFilterCombobox({
|
||||
{isFolderPartiallySelected(folder.id) && (
|
||||
<div className="h-3 w-3 rounded-sm bg-primary/50 mr-1" />
|
||||
)}
|
||||
<Check
|
||||
className={cn(
|
||||
"h-4 w-4",
|
||||
isFolderSelected(folder.id) ? "opacity-100" : "opacity-0",
|
||||
{isFolderSelected(folder.id) && (
|
||||
<Check className="h-4 w-4" />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</CommandItem>
|
||||
|
||||
@@ -198,7 +195,7 @@ export function AccountFilterCombobox({
|
||||
return (
|
||||
<CommandItem
|
||||
key={account.id}
|
||||
value={`${currentPath} ${account.name}`}
|
||||
value={account.id}
|
||||
onSelect={() => handleSelect(account.id)}
|
||||
style={{ paddingLeft: `${paddingLeft + 16}px` }}
|
||||
className="min-w-0"
|
||||
@@ -210,12 +207,9 @@ export function AccountFilterCombobox({
|
||||
({formatCurrency(total)})
|
||||
</span>
|
||||
)}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4 shrink-0",
|
||||
value.includes(account.id) ? "opacity-100" : "opacity-0",
|
||||
{value.includes(account.id) && (
|
||||
<Check className="ml-auto h-4 w-4 shrink-0" />
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
@@ -292,7 +286,7 @@ export function AccountFilterCombobox({
|
||||
align="start"
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<Command>
|
||||
<Command value={isAll ? "all" : value.join(",")}>
|
||||
<CommandInput placeholder="Rechercher..." />
|
||||
<CommandList className="max-h-[300px]">
|
||||
<CommandEmpty>Aucun compte trouvé.</CommandEmpty>
|
||||
@@ -312,12 +306,9 @@ export function AccountFilterCombobox({
|
||||
)
|
||||
</span>
|
||||
)}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
isAll ? "opacity-100" : "opacity-0",
|
||||
{isAll && (
|
||||
<Check className="ml-auto h-4 w-4" />
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
|
||||
@@ -333,7 +324,7 @@ export function AccountFilterCombobox({
|
||||
return (
|
||||
<CommandItem
|
||||
key={account.id}
|
||||
value={`sans-dossier ${account.name}`}
|
||||
value={account.id}
|
||||
onSelect={() => handleSelect(account.id)}
|
||||
className="min-w-0"
|
||||
>
|
||||
@@ -346,14 +337,9 @@ export function AccountFilterCombobox({
|
||||
({formatCurrency(total)})
|
||||
</span>
|
||||
)}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4 shrink-0",
|
||||
value.includes(account.id)
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
{value.includes(account.id) && (
|
||||
<Check className="ml-auto h-4 w-4 shrink-0" />
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -105,7 +105,7 @@ export function CategoryCombobox({
|
||||
align={align}
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<Command>
|
||||
<Command value={value || ""}>
|
||||
<CommandInput placeholder="Rechercher une catégorie..." />
|
||||
<CommandList className="max-h-[250px]">
|
||||
<CommandEmpty>Aucune catégorie trouvée.</CommandEmpty>
|
||||
@@ -118,19 +118,16 @@ export function CategoryCombobox({
|
||||
<span className="text-muted-foreground">
|
||||
Aucune catégorie
|
||||
</span>
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
value === null ? "opacity-100" : "opacity-0",
|
||||
{value === null && (
|
||||
<Check className="ml-auto h-4 w-4" />
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
<CommandGroup>
|
||||
{parentCategories.map((parent) => (
|
||||
<div key={parent.id}>
|
||||
<CommandItem
|
||||
value={`${parent.name}`}
|
||||
value={parent.id}
|
||||
onSelect={() => handleSelect(parent.id)}
|
||||
>
|
||||
<CategoryIcon
|
||||
@@ -139,17 +136,14 @@ export function CategoryCombobox({
|
||||
size={16}
|
||||
/>
|
||||
<span className="font-medium">{parent.name}</span>
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
value === parent.id ? "opacity-100" : "opacity-0",
|
||||
{value === parent.id && (
|
||||
<Check className="ml-auto h-4 w-4" />
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
{childrenByParent[parent.id]?.map((child) => (
|
||||
<CommandItem
|
||||
key={child.id}
|
||||
value={`${parent.name} ${child.name}`}
|
||||
value={child.id}
|
||||
onSelect={() => handleSelect(child.id)}
|
||||
className="pl-8"
|
||||
>
|
||||
@@ -159,12 +153,9 @@ export function CategoryCombobox({
|
||||
size={16}
|
||||
/>
|
||||
<span>{child.name}</span>
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4",
|
||||
value === child.id ? "opacity-100" : "opacity-0",
|
||||
{value === child.id && (
|
||||
<Check className="ml-auto h-4 w-4" />
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</div>
|
||||
@@ -207,7 +198,7 @@ export function CategoryCombobox({
|
||||
align={align}
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<Command>
|
||||
<Command value={value || ""}>
|
||||
<CommandInput placeholder="Rechercher une catégorie..." />
|
||||
<CommandList className="max-h-[250px]">
|
||||
<CommandEmpty>Aucune catégorie trouvée.</CommandEmpty>
|
||||
@@ -227,7 +218,7 @@ export function CategoryCombobox({
|
||||
{parentCategories.map((parent) => (
|
||||
<div key={parent.id}>
|
||||
<CommandItem
|
||||
value={`${parent.name}`}
|
||||
value={parent.id}
|
||||
onSelect={() => handleSelect(parent.id)}
|
||||
>
|
||||
<CategoryIcon
|
||||
@@ -246,7 +237,7 @@ export function CategoryCombobox({
|
||||
{childrenByParent[parent.id]?.map((child) => (
|
||||
<CommandItem
|
||||
key={child.id}
|
||||
value={`${parent.name} ${child.name}`}
|
||||
value={child.id}
|
||||
onSelect={() => handleSelect(child.id)}
|
||||
className="pl-8"
|
||||
>
|
||||
|
||||
@@ -193,7 +193,7 @@ export function CategoryFilterCombobox({
|
||||
align="start"
|
||||
onOpenAutoFocus={(e) => e.preventDefault()}
|
||||
>
|
||||
<Command>
|
||||
<Command value={isAll ? "all" : isUncategorized ? "uncategorized" : value.join(",")}>
|
||||
<CommandInput placeholder="Rechercher..." />
|
||||
<CommandList className="max-h-[300px]">
|
||||
<CommandEmpty>Aucune catégorie trouvée.</CommandEmpty>
|
||||
@@ -212,15 +212,12 @@ export function CategoryFilterCombobox({
|
||||
({filteredTransactions.length})
|
||||
</span>
|
||||
)}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4 shrink-0",
|
||||
isAll ? "opacity-100" : "opacity-0",
|
||||
{isAll && (
|
||||
<Check className="ml-auto h-4 w-4 shrink-0" />
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
<CommandItem
|
||||
value="uncategorized non catégorisé"
|
||||
value="uncategorized"
|
||||
onSelect={() => handleSelect("uncategorized")}
|
||||
className="min-w-0"
|
||||
>
|
||||
@@ -231,19 +228,16 @@ export function CategoryFilterCombobox({
|
||||
({categoryCounts["uncategorized"]})
|
||||
</span>
|
||||
)}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4 shrink-0",
|
||||
isUncategorized ? "opacity-100" : "opacity-0",
|
||||
{isUncategorized && (
|
||||
<Check className="ml-auto h-4 w-4 shrink-0" />
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
<CommandGroup heading="Catégories">
|
||||
{parentCategories.map((parent) => (
|
||||
<div key={parent.id}>
|
||||
<CommandItem
|
||||
value={`${parent.name}`}
|
||||
value={parent.id}
|
||||
onSelect={() => handleSelect(parent.id)}
|
||||
className="min-w-0"
|
||||
>
|
||||
@@ -261,17 +255,14 @@ export function CategoryFilterCombobox({
|
||||
({categoryCounts[parent.id]})
|
||||
</span>
|
||||
)}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4 shrink-0",
|
||||
value.includes(parent.id) ? "opacity-100" : "opacity-0",
|
||||
{value.includes(parent.id) && (
|
||||
<Check className="ml-auto h-4 w-4 shrink-0" />
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
{childrenByParent[parent.id]?.map((child) => (
|
||||
<CommandItem
|
||||
key={child.id}
|
||||
value={`${parent.name} ${child.name}`}
|
||||
value={child.id}
|
||||
onSelect={() => handleSelect(child.id)}
|
||||
className="pl-8 min-w-0"
|
||||
>
|
||||
@@ -289,14 +280,9 @@ export function CategoryFilterCombobox({
|
||||
({categoryCounts[child.id]})
|
||||
</span>
|
||||
)}
|
||||
<Check
|
||||
className={cn(
|
||||
"ml-auto h-4 w-4 shrink-0",
|
||||
value.includes(child.id)
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
{value.includes(child.id) && (
|
||||
<Check className="ml-auto h-4 w-4 shrink-0" />
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user