Load more transactions button
This commit is contained in:
parent
d13ecc2722
commit
7f243bbc1e
|
|
@ -27,3 +27,4 @@ class AccountSyncController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -28,3 +28,4 @@ class BankSyncController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -25,3 +25,4 @@ class CategorySyncController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export function CreateAutomationRuleDialog({
|
|||
const [open, setOpen] = useState(false);
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [title, setTitle] = useState('');
|
||||
const [priority, setPriority] = useState('0');
|
||||
const [priority, setPriority] = useState('10');
|
||||
const [ruleStructure, setRuleStructure] = useState<RuleStructure>({
|
||||
groups: [createEmptyGroup()],
|
||||
groupOperator: 'and',
|
||||
|
|
@ -123,7 +123,7 @@ export function CreateAutomationRuleDialog({
|
|||
onSuccess: async () => {
|
||||
setOpen(false);
|
||||
setTitle('');
|
||||
setPriority('0');
|
||||
setPriority('10');
|
||||
setRuleStructure({
|
||||
groups: [createEmptyGroup()],
|
||||
groupOperator: 'and',
|
||||
|
|
@ -186,7 +186,7 @@ export function CreateAutomationRuleDialog({
|
|||
min="0"
|
||||
value={priority}
|
||||
onChange={(e) => setPriority(e.target.value)}
|
||||
placeholder="0"
|
||||
placeholder="10"
|
||||
required
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
|
|
|
|||
|
|
@ -75,3 +75,4 @@ export function SyncStatusButton() {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -105,3 +105,4 @@ export function CategorySelect({
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -5,15 +5,17 @@ interface DataTablePaginationProps {
|
|||
rowCountLabel?: string;
|
||||
displayedCount?: number;
|
||||
total?: number;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function DataTablePagination({
|
||||
displayedCount = undefined,
|
||||
total = undefined,
|
||||
rowCountLabel = 'row(s) total',
|
||||
children,
|
||||
}: DataTablePaginationProps) {
|
||||
return (
|
||||
<div className="flex px-5 items-center justify-end space-x-2">
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
{displayedCount && total && (
|
||||
<div className="text-muted-foreground flex-1 text-sm">
|
||||
{Math.min(total, displayedCount)} of {total} {rowCountLabel}
|
||||
|
|
@ -24,15 +26,7 @@ export function DataTablePagination({
|
|||
{displayedCount} {rowCountLabel}
|
||||
</div>
|
||||
)}
|
||||
{((displayedCount ?? 0) < (total ?? 0)) && <div className="space-x-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled
|
||||
>
|
||||
Loading <RefreshCw className="ml-2 h-4 w-4 animate-spin" />
|
||||
</Button>
|
||||
</div>}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,3 +22,4 @@ export function useOnlineStatus() {
|
|||
return isOnline;
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -108,7 +108,12 @@ export default function AutomationRules() {
|
|||
})),
|
||||
[rawRules],
|
||||
);
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [sorting, setSorting] = useState<SortingState>([
|
||||
{
|
||||
id: 'priority',
|
||||
desc: false,
|
||||
},
|
||||
]);
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
|
||||
{},
|
||||
|
|
@ -116,23 +121,22 @@ export default function AutomationRules() {
|
|||
|
||||
const columns: ColumnDef<AutomationRule>[] = [
|
||||
{
|
||||
accessorKey: 'title',
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === 'asc')
|
||||
}
|
||||
>
|
||||
Title
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
accessorKey: 'priority',
|
||||
header: 'Priority',
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="pl-3 font-medium">
|
||||
<div className="font-medium">
|
||||
{row.getValue('priority')}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'title',
|
||||
header: 'Title',
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="font-medium">
|
||||
{row.getValue('title')}
|
||||
</div>
|
||||
);
|
||||
|
|
@ -256,12 +260,12 @@ export default function AutomationRules() {
|
|||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header
|
||||
.column
|
||||
.columnDef
|
||||
.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
header
|
||||
.column
|
||||
.columnDef
|
||||
.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
} from '@tanstack/react-table';
|
||||
import { isWithinInterval, parseISO } from 'date-fns';
|
||||
import { useLiveQuery } from 'dexie-react-hooks';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import { index as transactionsIndex } from '@/actions/App/Http/Controllers/TransactionController';
|
||||
|
|
@ -30,10 +30,12 @@ import {
|
|||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { DataTable } from '@/components/ui/data-table';
|
||||
import { DataTablePagination } from '@/components/ui/data-table-pagination';
|
||||
import { DataTableViewOptions } from '@/components/ui/data-table-view-options';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Spinner } from '@/components/ui/spinner';
|
||||
import { useEncryptionKey } from '@/contexts/encryption-key-context';
|
||||
import AppSidebarLayout from '@/layouts/app/app-sidebar-layout';
|
||||
import { decrypt, encrypt, importKey } from '@/lib/crypto';
|
||||
|
|
@ -50,6 +52,7 @@ import {
|
|||
type DecryptedTransaction,
|
||||
type TransactionFilters as Filters,
|
||||
} from '@/types/transaction';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
|
||||
const breadcrumbs: BreadcrumbItem[] = [
|
||||
{
|
||||
|
|
@ -125,7 +128,7 @@ export default function Transactions({ categories, accounts, banks }: Props) {
|
|||
const [isBulkUpdating, setIsBulkUpdating] = useState(false);
|
||||
const [isReEvaluating, setIsReEvaluating] = useState(false);
|
||||
const [displayedCount, setDisplayedCount] = useState(25);
|
||||
const observerTarget = useRef<HTMLDivElement>(null);
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||
|
||||
const updateTransaction = useCallback(
|
||||
(updatedTransaction: DecryptedTransaction) => {
|
||||
|
|
@ -552,7 +555,7 @@ export default function Transactions({ categories, accounts, banks }: Props) {
|
|||
|
||||
const selectedCategory = result.categoryId
|
||||
? categories.find((c) => c.id === result.categoryId) ||
|
||||
null
|
||||
null
|
||||
: null;
|
||||
|
||||
let decryptedNotes = transaction.decryptedNotes;
|
||||
|
|
@ -696,7 +699,7 @@ export default function Transactions({ categories, accounts, banks }: Props) {
|
|||
|
||||
const selectedCategory = result.categoryId
|
||||
? categories.find((c) => c.id === result.categoryId) ||
|
||||
null
|
||||
null
|
||||
: null;
|
||||
|
||||
let decryptedNotes = transaction.decryptedNotes;
|
||||
|
|
@ -805,34 +808,18 @@ export default function Transactions({ categories, accounts, banks }: Props) {
|
|||
});
|
||||
|
||||
const loadMore = useCallback(() => {
|
||||
if (displayedCount < sortedTransactions.length) {
|
||||
setDisplayedCount((prev) =>
|
||||
Math.min(prev + 25, sortedTransactions.length),
|
||||
);
|
||||
if (displayedCount < sortedTransactions.length && !isLoadingMore) {
|
||||
setIsLoadingMore(true);
|
||||
requestAnimationFrame(() => {
|
||||
setDisplayedCount((prev) =>
|
||||
Math.min(prev + 25, sortedTransactions.length),
|
||||
);
|
||||
requestAnimationFrame(() => {
|
||||
setIsLoadingMore(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [displayedCount, sortedTransactions.length]);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting) {
|
||||
loadMore();
|
||||
}
|
||||
},
|
||||
{ threshold: 0.1 },
|
||||
);
|
||||
|
||||
const currentTarget = observerTarget.current;
|
||||
if (currentTarget) {
|
||||
observer.observe(currentTarget);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (currentTarget) {
|
||||
observer.unobserve(currentTarget);
|
||||
}
|
||||
};
|
||||
}, [loadMore]);
|
||||
}, [displayedCount, sortedTransactions.length, isLoadingMore]);
|
||||
|
||||
useEffect(() => {
|
||||
setDisplayedCount(25);
|
||||
|
|
@ -1031,18 +1018,25 @@ export default function Transactions({ categories, accounts, banks }: Props) {
|
|||
displayedCount={displayedCount}
|
||||
total={sortedTransactions.length}
|
||||
rowCountLabel="transactions total"
|
||||
/>
|
||||
|
||||
{displayedCount < sortedTransactions.length && (
|
||||
<div
|
||||
ref={observerTarget}
|
||||
className="h-4 flex items-center justify-center"
|
||||
>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Loading more...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{displayedCount < sortedTransactions.length && (
|
||||
<Button
|
||||
onClick={loadMore}
|
||||
disabled={isLoadingMore}
|
||||
variant="outline"
|
||||
>
|
||||
{isLoadingMore ? (
|
||||
<>
|
||||
<Spinner />
|
||||
Loading
|
||||
</>
|
||||
) : (<>
|
||||
Load more
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</DataTablePagination>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -46,3 +46,4 @@ class AccountSyncService {
|
|||
|
||||
export const accountSyncService = new AccountSyncService();
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -42,3 +42,4 @@ class BankSyncService {
|
|||
|
||||
export const bankSyncService = new BankSyncService();
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -46,3 +46,4 @@ class CategorySyncService {
|
|||
|
||||
export const categorySyncService = new CategorySyncService();
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -61,3 +61,4 @@ export function formatRuleActions(rule: AutomationRule): string {
|
|||
return 'Add note';
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -79,3 +79,4 @@ it('requires authentication', function () {
|
|||
$response->assertUnauthorized();
|
||||
});
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -53,3 +53,4 @@ it('requires authentication', function () {
|
|||
$response->assertUnauthorized();
|
||||
});
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue