Separate useSchedules and useSchedulesStatus because not all callers of useSchedules use the status. This saves us some unnecessary queries.

This commit is contained in:
Joel Jeremy Marquez
2026-01-23 15:59:35 -08:00
parent 11a4eb65a0
commit 9e317b0a71
29 changed files with 204 additions and 234 deletions

View File

@@ -131,9 +131,7 @@ export function ManageRules({
const [filter, setFilter] = useState('');
const dispatch = useDispatch();
const {
data: { schedules },
} = useSchedules({
const { data: schedules = [] } = useSchedules({
query: q('schedules').select('*'),
});
const { data: { list: categories } = { list: [] } } = useCategories();

View File

@@ -58,7 +58,6 @@ import { useDateFormat } from '@desktop-client/hooks/useDateFormat';
import { useFailedAccounts } from '@desktop-client/hooks/useFailedAccounts';
import { useLocalPref } from '@desktop-client/hooks/useLocalPref';
import { usePayees } from '@desktop-client/hooks/usePayees';
import { getSchedulesQuery } from '@desktop-client/hooks/useSchedules';
import { SelectedProviderWithItems } from '@desktop-client/hooks/useSelected';
import type { Actions } from '@desktop-client/hooks/useSelected';
import {
@@ -82,6 +81,7 @@ import { pagedQuery } from '@desktop-client/queries/pagedQuery';
import type { PagedQuery } from '@desktop-client/queries/pagedQuery';
import { useDispatch, useSelector } from '@desktop-client/redux';
import type { AppDispatch } from '@desktop-client/redux/store';
import { schedulesViewQuery } from '@desktop-client/schedules';
import { updateNewTransactions } from '@desktop-client/transactions/transactionsSlice';
type ConditionEntity = Partial<RuleConditionEntity> | TransactionFilterEntity;
@@ -1993,7 +1993,7 @@ export function Account() {
const savedFiters = useTransactionFilters();
const schedulesQuery = useMemo(
() => getSchedulesQuery(params.id),
() => schedulesViewQuery(params.id),
[params.id],
);

View File

@@ -94,10 +94,7 @@ function SelectedBalance({ selectedItems, account }: SelectedBalanceProps) {
let scheduleBalance = 0;
const {
data: { schedules },
isFetching,
} = useCachedSchedules();
const { data: schedules = [], isFetching } = useCachedSchedules();
if (isFetching) {
return null;

View File

@@ -14,7 +14,6 @@ import { useAccountPreviewTransactions } from '@desktop-client/hooks/useAccountP
import { SchedulesProvider } from '@desktop-client/hooks/useCachedSchedules';
import { useDateFormat } from '@desktop-client/hooks/useDateFormat';
import { useNavigate } from '@desktop-client/hooks/useNavigate';
import { getSchedulesQuery } from '@desktop-client/hooks/useSchedules';
import { useSheetValue } from '@desktop-client/hooks/useSheetValue';
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
import {
@@ -25,6 +24,7 @@ import { useTransactionsSearch } from '@desktop-client/hooks/useTransactionsSear
import { collapseModals, pushModal } from '@desktop-client/modals/modalsSlice';
import * as queries from '@desktop-client/queries';
import { useDispatch } from '@desktop-client/redux';
import { schedulesViewQuery } from '@desktop-client/schedules';
import * as bindings from '@desktop-client/spreadsheet/bindings';
export function AccountTransactions({
@@ -33,7 +33,7 @@ export function AccountTransactions({
readonly account: AccountEntity;
}) {
const schedulesQuery = useMemo(
() => getSchedulesQuery(account.id),
() => schedulesViewQuery(account.id),
[account.id],
);

View File

@@ -11,16 +11,16 @@ import { SchedulesProvider } from '@desktop-client/hooks/useCachedSchedules';
import { useDateFormat } from '@desktop-client/hooks/useDateFormat';
import { useNavigate } from '@desktop-client/hooks/useNavigate';
import { usePreviewTransactions } from '@desktop-client/hooks/usePreviewTransactions';
import { getSchedulesQuery } from '@desktop-client/hooks/useSchedules';
import { useTransactions } from '@desktop-client/hooks/useTransactions';
import { useTransactionsSearch } from '@desktop-client/hooks/useTransactionsSearch';
import { collapseModals, pushModal } from '@desktop-client/modals/modalsSlice';
import * as queries from '@desktop-client/queries';
import { useDispatch } from '@desktop-client/redux';
import { schedulesViewQuery } from '@desktop-client/schedules';
import * as bindings from '@desktop-client/spreadsheet/bindings';
export function AllAccountTransactions() {
const schedulesQuery = useMemo(() => getSchedulesQuery(), []);
const schedulesQuery = useMemo(() => schedulesViewQuery(), []);
return (
<SchedulesProvider query={schedulesQuery}>

View File

@@ -12,16 +12,16 @@ import { useDateFormat } from '@desktop-client/hooks/useDateFormat';
import { useNavigate } from '@desktop-client/hooks/useNavigate';
import { useOffBudgetAccounts } from '@desktop-client/hooks/useOffBudgetAccounts';
import { usePreviewTransactions } from '@desktop-client/hooks/usePreviewTransactions';
import { getSchedulesQuery } from '@desktop-client/hooks/useSchedules';
import { useTransactions } from '@desktop-client/hooks/useTransactions';
import { useTransactionsSearch } from '@desktop-client/hooks/useTransactionsSearch';
import { collapseModals, pushModal } from '@desktop-client/modals/modalsSlice';
import * as queries from '@desktop-client/queries';
import { useDispatch } from '@desktop-client/redux';
import { schedulesViewQuery } from '@desktop-client/schedules';
import * as bindings from '@desktop-client/spreadsheet/bindings';
export function OffBudgetAccountTransactions() {
const schedulesQuery = useMemo(() => getSchedulesQuery('offbudget'), []);
const schedulesQuery = useMemo(() => schedulesViewQuery('offbudget'), []);
return (
<SchedulesProvider query={schedulesQuery}>

View File

@@ -12,16 +12,16 @@ import { useDateFormat } from '@desktop-client/hooks/useDateFormat';
import { useNavigate } from '@desktop-client/hooks/useNavigate';
import { useOnBudgetAccounts } from '@desktop-client/hooks/useOnBudgetAccounts';
import { usePreviewTransactions } from '@desktop-client/hooks/usePreviewTransactions';
import { getSchedulesQuery } from '@desktop-client/hooks/useSchedules';
import { useTransactions } from '@desktop-client/hooks/useTransactions';
import { useTransactionsSearch } from '@desktop-client/hooks/useTransactionsSearch';
import { collapseModals, pushModal } from '@desktop-client/modals/modalsSlice';
import * as queries from '@desktop-client/queries';
import { useDispatch } from '@desktop-client/redux';
import { schedulesViewQuery } from '@desktop-client/schedules';
import * as bindings from '@desktop-client/spreadsheet/bindings';
export function OnBudgetAccountTransactions() {
const schedulesQuery = useMemo(() => getSchedulesQuery('onbudget'), []);
const schedulesQuery = useMemo(() => schedulesViewQuery('onbudget'), []);
return (
<SchedulesProvider query={schedulesQuery}>

View File

@@ -31,10 +31,7 @@ export function MobileRuleEditPage() {
const [rule, setRule] = useState<RuleEntity | null>(null);
const [isLoading, setIsLoading] = useState(false);
const {
data: { schedules },
isSuccess,
} = useSchedules({
const { data: schedules = [], isSuccess } = useSchedules({
query: rule?.id
? q('schedules').filter({ rule: rule.id, completed: false }).select('*')
: q('schedules').filter({ id: null }).select('*'), // Return empty result when no rule,

View File

@@ -37,9 +37,7 @@ export function MobileRulesPage() {
const [isLoading, setIsLoading] = useState(true);
const [filter, setFilter] = useState('');
const {
data: { schedules },
} = useSchedules({
const { data: schedules = [] } = useSchedules({
query: q('schedules').select('*'),
});
const { data: { list: categories } = { list: [] } } = useCategories();

View File

@@ -23,7 +23,10 @@ import { useDateFormat } from '@desktop-client/hooks/useDateFormat';
import { useFormat } from '@desktop-client/hooks/useFormat';
import { useNavigate } from '@desktop-client/hooks/useNavigate';
import { usePayees } from '@desktop-client/hooks/usePayees';
import { useSchedules } from '@desktop-client/hooks/useSchedules';
import {
useSchedules,
useScheduleStatus,
} from '@desktop-client/hooks/useSchedules';
import { useUndo } from '@desktop-client/hooks/useUndo';
import { addNotification } from '@desktop-client/notifications/notificationsSlice';
import { useDispatch } from '@desktop-client/redux';
@@ -38,10 +41,12 @@ export function MobileSchedulesPage() {
const format = useFormat();
const dateFormat = useDateFormat() || 'MM/dd/yyyy';
const { isFetching: isSchedulesLoading, data: schedules = [] } = useSchedules(
{ query: q('schedules').select('*') },
);
const {
isFetching: isSchedulesLoading,
data: { schedules, scheduleStatusMap },
} = useSchedules({ query: q('schedules').select('*') });
data: { statusLookup = {} },
} = useScheduleStatus({ schedules });
const payees = usePayees();
const accounts = useAccounts();
@@ -67,7 +72,7 @@ export function MobileSchedulesPage() {
const dateStr = schedule.next_date
? monthUtilFormat(schedule.next_date, dateFormat)
: null;
const statusLabel = scheduleStatusMap.get(schedule.id);
const statusLabel = statusLookup[schedule.id];
return (
filterIncludes(schedule.name) ||
@@ -156,7 +161,7 @@ export function MobileSchedulesPage() {
<SchedulesList
schedules={filteredSchedules}
isLoading={isSchedulesLoading}
scheduleStatusMap={scheduleStatusMap}
statusLookup={statusLookup}
onSchedulePress={handleSchedulePress}
onScheduleDelete={handleScheduleDelete}
hasCompletedSchedules={hasCompletedSchedules}

View File

@@ -6,7 +6,7 @@ import { Text } from '@actual-app/components/text';
import { theme } from '@actual-app/components/theme';
import { View } from '@actual-app/components/view';
import type { ScheduleStatusMap } from 'loot-core/shared/schedules';
import type { ScheduleStatusLookup } from 'loot-core/shared/schedules';
import type { ScheduleEntity } from 'loot-core/types/models';
import { SchedulesListItem } from './SchedulesListItem';
@@ -20,7 +20,7 @@ type SchedulesListEntry = ScheduleEntity | CompletedSchedulesItem;
type SchedulesListProps = {
schedules: readonly ScheduleEntity[];
isLoading: boolean;
scheduleStatusMap: ScheduleStatusMap;
statusLookup: ScheduleStatusLookup;
onSchedulePress: (schedule: ScheduleEntity) => void;
onScheduleDelete: (schedule: ScheduleEntity) => void;
hasCompletedSchedules?: boolean;
@@ -31,7 +31,7 @@ type SchedulesListProps = {
export function SchedulesList({
schedules,
isLoading,
scheduleStatusMap,
statusLookup,
onSchedulePress,
onScheduleDelete,
hasCompletedSchedules = false,
@@ -125,7 +125,7 @@ export function SchedulesList({
) : (
<SchedulesListItem
value={item}
status={scheduleStatusMap.get(item.id) || 'scheduled'}
status={statusLookup[item.id] || 'scheduled'}
onAction={() => onSchedulePress(item)}
onDelete={() => onScheduleDelete(item)}
/>

View File

@@ -315,10 +315,8 @@ type PayeeIconsProps = {
function PayeeIcons({ transaction, transferAccount }: PayeeIconsProps) {
const { id, schedule: scheduleId } = transaction;
const {
isFetching: isSchedulesLoading,
data: { schedules },
} = useCachedSchedules();
const { isFetching: isSchedulesLoading, data: schedules = [] } =
useCachedSchedules();
const isPreview = isPreviewId(id);
const schedule = schedules.find(s => s.id === scheduleId);
const isScheduleRecurring =

View File

@@ -189,9 +189,7 @@ export function BudgetAutomationsModal({ categoryId }: { categoryId: string }) {
onLoaded: setAutomations,
});
const {
data: { schedules },
} = useSchedules({
const { data: schedules = [] } = useSchedules({
query: q('schedules').select('*'),
});

View File

@@ -44,12 +44,11 @@ export function ScheduledTransactionMenuModal({
borderTop: `1px solid ${theme.pillBorder}`,
};
const scheduleId = transactionId?.split('/')?.[1];
const {
isFetching: isSchedulesLoading,
data: { schedules },
} = useSchedules({
query: q('schedules').filter({ id: scheduleId }).select('*'),
});
const { isFetching: isSchedulesLoading, data: schedules = [] } = useSchedules(
{
query: q('schedules').filter({ id: scheduleId }).select('*'),
},
);
if (isSchedulesLoading) {
return null;

View File

@@ -58,7 +58,10 @@ import { GenericInput } from '@desktop-client/components/util/GenericInput';
import { useDateFormat } from '@desktop-client/hooks/useDateFormat';
import { useFeatureFlag } from '@desktop-client/hooks/useFeatureFlag';
import { useFormat } from '@desktop-client/hooks/useFormat';
import { useSchedules } from '@desktop-client/hooks/useSchedules';
import {
useSchedules,
useScheduleStatus,
} from '@desktop-client/hooks/useSchedules';
import {
SelectedProvider,
useSelected,
@@ -366,10 +369,13 @@ function ScheduleDescription({ id }) {
const { isNarrowWidth } = useResponsive();
const dateFormat = useDateFormat() || 'MM/dd/yyyy';
const format = useFormat();
const { data: schedules = [], isFetching: isSchedulesLoading } = useSchedules(
{ query: q('schedules').filter({ id }).select('*') },
);
const {
data: { schedules, scheduleStatusLabelMap: statusLabels } = {},
isFetching: isSchedulesLoading,
} = useSchedules({ query: q('schedules').filter({ id }).select('*') });
data: { statusLabelLookup = {} },
} = useScheduleStatus({ schedules });
if (isSchedulesLoading) {
return null;
@@ -381,7 +387,7 @@ function ScheduleDescription({ id }) {
return <View style={{ flex: 1 }}>{id}</View>;
}
const status = statusLabels.get(schedule.id) as ScheduleStatusLabel;
const status = statusLabelLookup[schedule.id] as ScheduleStatusLabel;
return (
<View

View File

@@ -22,10 +22,7 @@ export function ScheduleValue({ value }: ScheduleValueProps) {
const { t } = useTranslation();
const payees = usePayees();
const byId = getPayeesById(payees);
const {
data: { schedules },
isFetching,
} = useSchedules({
const { data: schedules = [], isFetching } = useSchedules({
query: q('schedules').select('*'),
});

View File

@@ -20,7 +20,10 @@ import {
ModalHeader,
} from '@desktop-client/components/common/Modal';
import { Search } from '@desktop-client/components/common/Search';
import { useSchedules } from '@desktop-client/hooks/useSchedules';
import {
useSchedules,
useScheduleStatus,
} from '@desktop-client/hooks/useSchedules';
import { pushModal } from '@desktop-client/modals/modalsSlice';
import type { Modal as ModalType } from '@desktop-client/modals/modalsSlice';
import { useDispatch } from '@desktop-client/redux';
@@ -40,12 +43,15 @@ export function ScheduleLink({
const dispatch = useDispatch();
const [filter, setFilter] = useState(accountName || '');
const { isFetching: isSchedulesLoading, data: schedules = [] } = useSchedules(
{
query: q('schedules').filter({ completed: false }).select('*'),
},
);
const {
isFetching: isSchedulesLoading,
data: { schedules, scheduleStatusMap },
} = useSchedules({
query: q('schedules').filter({ completed: false }).select('*'),
});
data: { statusLookup = {} },
} = useScheduleStatus({ schedules });
const searchInput = useRef<HTMLInputElement | null>(null);
@@ -150,7 +156,7 @@ export function ScheduleLink({
close();
}}
schedules={schedules}
scheduleStatusMap={scheduleStatusMap}
statusLookup={statusLookup}
style={null}
/>
</View>

View File

@@ -18,7 +18,7 @@ import { getNormalisedString } from 'loot-core/shared/normalisation';
import { getScheduledAmount } from 'loot-core/shared/schedules';
import type {
ScheduleStatus,
ScheduleStatusMap,
ScheduleStatusLookup,
} from 'loot-core/shared/schedules';
import type { ScheduleEntity } from 'loot-core/types/models';
@@ -43,7 +43,7 @@ import { usePayees } from '@desktop-client/hooks/usePayees';
type SchedulesTableProps = {
isLoading?: boolean;
schedules: readonly ScheduleEntity[];
scheduleStatusMap: ScheduleStatusMap;
statusLookup: ScheduleStatusLookup;
filter: string;
allowCompleted: boolean;
onSelect: (schedule: ScheduleEntity) => void;
@@ -204,11 +204,11 @@ function ScheduleRow({
onAction,
onSelect,
minimal,
scheduleStatusMap,
statusLookup,
dateFormat,
}: {
schedule: ScheduleEntity;
scheduleStatusMap: ScheduleStatusMap;
statusLookup: ScheduleStatusLookup;
dateFormat: string;
} & Pick<SchedulesTableProps, 'onSelect' | 'onAction' | 'minimal'>) {
const { t } = useTranslation();
@@ -250,7 +250,7 @@ function ScheduleRow({
>
<OverflowMenu
schedule={schedule}
status={scheduleStatusMap.get(schedule.id)}
status={statusLookup[schedule.id]}
onAction={(action, id) => {
onAction(action, id);
resetPosition();
@@ -283,7 +283,7 @@ function ScheduleRow({
: null}
</Field>
<Field width={120} name="status" style={{ alignItems: 'flex-start' }}>
<StatusBadge status={scheduleStatusMap.get(schedule.id)} />
<StatusBadge status={statusLookup[schedule.id]} />
</Field>
<ScheduleAmountCell amount={schedule._amount} op={schedule._amountOp} />
{!minimal && (
@@ -323,7 +323,7 @@ function ScheduleRow({
export function SchedulesTable({
isLoading,
schedules,
scheduleStatusMap,
statusLookup,
filter,
minimal,
allowCompleted,
@@ -370,19 +370,11 @@ export function SchedulesTable({
filterIncludes(payee && payee.name) ||
filterIncludes(account && account.name) ||
filterIncludes(amountStr) ||
filterIncludes(scheduleStatusMap.get(schedule.id)) ||
filterIncludes(statusLookup[schedule.id]) ||
filterIncludes(dateStr)
);
});
}, [
payees,
accounts,
schedules,
filter,
scheduleStatusMap,
format,
dateFormat,
]);
}, [payees, accounts, schedules, filter, statusLookup, format, dateFormat]);
const items: readonly SchedulesTableItem[] = useMemo(() => {
const unCompletedSchedules = filteredSchedules.filter(s => !s.completed);
@@ -430,7 +422,7 @@ export function SchedulesTable({
return (
<ScheduleRow
schedule={item as ScheduleEntity}
{...{ scheduleStatusMap, dateFormat, onSelect, onAction, minimal }}
{...{ statusLookup, dateFormat, onSelect, onAction, minimal }}
/>
);
}

View File

@@ -14,7 +14,10 @@ import type { ScheduleItemAction } from './SchedulesTable';
import { Search } from '@desktop-client/components/common/Search';
import { Page } from '@desktop-client/components/Page';
import { useSchedules } from '@desktop-client/hooks/useSchedules';
import {
useSchedules,
useScheduleStatus,
} from '@desktop-client/hooks/useSchedules';
import { pushModal } from '@desktop-client/modals/modalsSlice';
import { useDispatch } from '@desktop-client/redux';
@@ -80,10 +83,13 @@ export function Schedules() {
[],
);
const { isLoading: isSchedulesLoading, data: schedules = [] } = useSchedules({
query: q('schedules').select('*'),
});
const {
isLoading: isSchedulesLoading,
data: { schedules, scheduleStatusMap },
} = useSchedules({ query: q('schedules').select('*') });
data: { statusLookup = {} },
} = useScheduleStatus({ schedules });
return (
<Page header={t('Schedules')}>
@@ -113,7 +119,7 @@ export function Schedules() {
isLoading={isSchedulesLoading}
schedules={schedules}
filter={filter}
scheduleStatusMap={scheduleStatusMap}
statusLookup={statusLookup}
allowCompleted
onSelect={onEdit}
onAction={onAction}

View File

@@ -78,9 +78,7 @@ export function SelectedTransactionsButton({
.map(id => id.split('/')[1]);
}, [selectedIds]);
const {
data: { schedules: selectedSchedules },
} = useSchedules({
const { data: selectedSchedules = [] } = useSchedules({
query: q('schedules')
.filter({ id: { $oneof: scheduleIds } })
.select('*'),

View File

@@ -67,9 +67,7 @@ export function TransactionMenu({
.map(id => id.split('/')[1]);
}, [selectedIds]);
const {
data: { schedules: selectedSchedules },
} = useSchedules({
const { data: selectedSchedules = [] } = useSchedules({
query: q('schedules')
.filter({ id: { $oneof: scheduleIds } })
.select('*'),

View File

@@ -764,10 +764,7 @@ function PayeeIcons({
const { t } = useTranslation();
const scheduleId = transaction.schedule;
const {
isFetching,
data: { schedules },
} = useCachedSchedules();
const { isFetching, data: schedules = [] } = useCachedSchedules();
if (isFetching) {
return null;
@@ -1095,7 +1092,7 @@ const Transaction = memo(function Transaction({
_unmatched = false,
} = transaction;
const { data: { schedules = [] } = {} } = useCachedSchedules();
const { data: schedules = [] } = useCachedSchedules();
const schedule = transaction.schedule
? schedules.find(s => s.id === transaction.schedule)
: null;

View File

@@ -42,15 +42,14 @@ export function useCategoryScheduleGoalTemplateIndicator({
'upcomingScheduledTransactionLength',
);
const upcomingDays = getUpcomingDays(upcomingScheduledTransactionLength);
const { schedules, statuses: scheduleStatuses } =
useCategoryScheduleGoalTemplates({
category,
});
const { schedules, statusLookup } = useCategoryScheduleGoalTemplates({
category,
});
return useMemo<UseCategoryScheduleGoalTemplateResult>(() => {
const schedulesToDisplay = schedules
.filter(schedule => {
const status = scheduleStatuses.get(schedule.id);
const status = statusLookup[schedule.id];
return status === 'upcoming' || status === 'due' || status === 'missed';
})
.filter(schedule => {
@@ -66,8 +65,8 @@ export function useCategoryScheduleGoalTemplateIndicator({
})
.sort((a, b) => {
// Display missed schedules first, then due, then upcoming.
const aStatus = scheduleStatuses.get(a.id);
const bStatus = scheduleStatuses.get(b.id);
const aStatus = statusLookup[a.id];
const bStatus = statusLookup[b.id];
if (aStatus === 'missed' && bStatus !== 'missed') return -1;
if (bStatus === 'missed' && aStatus !== 'missed') return 1;
if (aStatus === 'due' && bStatus !== 'due') return -1;
@@ -80,7 +79,7 @@ export function useCategoryScheduleGoalTemplateIndicator({
return getScheduleStatusDescription({
t,
schedule: s,
scheduleStatus: scheduleStatuses.get(s.id),
scheduleStatus: statusLookup[s.id],
locale,
});
})
@@ -88,7 +87,7 @@ export function useCategoryScheduleGoalTemplateIndicator({
const schedule = schedulesToDisplay[0] || null;
const scheduleStatus =
(schedule ? scheduleStatuses.get(schedule.id) : null) || null;
(schedule ? statusLookup[schedule.id] : null) || null;
return {
schedule,
@@ -98,7 +97,7 @@ export function useCategoryScheduleGoalTemplateIndicator({
),
description,
};
}, [locale, month, scheduleStatuses, schedules, t, upcomingDays]);
}, [locale, month, statusLookup, schedules, t, upcomingDays]);
}
function getScheduleStatusDescription({

View File

@@ -1,13 +1,12 @@
import { useMemo } from 'react';
import type {
ScheduleStatusLabelMap,
ScheduleStatusMap,
} from 'loot-core/shared/schedules';
import type { CategoryEntity, ScheduleEntity } from 'loot-core/types/models';
import { useCachedSchedules } from './useCachedSchedules';
import { useFeatureFlag } from './useFeatureFlag';
import { useScheduleStatus } from './useSchedules';
import type { ScheduleStatusData } from '@desktop-client/schedules';
type ScheduleGoalDefinition = {
type: 'schedule';
@@ -18,30 +17,25 @@ type UseCategoryScheduleGoalTemplatesProps = {
category?: CategoryEntity | undefined;
};
type UseCategoryScheduleGoalTemplatesResult = {
schedules: ScheduleEntity[];
statuses: ScheduleStatusMap;
statusLabels: ScheduleStatusLabelMap;
type UseCategoryScheduleGoalTemplatesResult = ScheduleStatusData & {
schedules: readonly ScheduleEntity[];
};
export function useCategoryScheduleGoalTemplates({
category,
}: UseCategoryScheduleGoalTemplatesProps): UseCategoryScheduleGoalTemplatesResult {
const isGoalTemplatesEnabled = useFeatureFlag('goalTemplatesEnabled');
const { data: allSchedules = [] } = useCachedSchedules();
const {
data: {
schedules: allSchedules,
scheduleStatusMap: allStatuses,
scheduleStatusLabelMap: allStatusLabels,
},
} = useCachedSchedules();
data: { statusLookup = {}, statusLabelLookup = {} },
} = useScheduleStatus({ schedules: allSchedules });
return useMemo(() => {
if (!isGoalTemplatesEnabled || !category || !category.goal_def) {
return {
schedules: [],
statuses: new Map(),
statusLabels: new Map(),
statusLookup: {},
statusLabelLookup: {},
};
}
@@ -52,8 +46,8 @@ export function useCategoryScheduleGoalTemplates({
console.error('Failed to parse category goal_def:', e);
return {
schedules: [],
statuses: new Map(),
statusLabels: new Map(),
statusLookup: {},
statusLabelLookup: {},
};
}
@@ -64,8 +58,8 @@ export function useCategoryScheduleGoalTemplates({
if (!scheduleGoalDefinitions.length) {
return {
schedules: [],
statuses: new Map(),
statusLabels: new Map(),
statusLookup: {},
statusLabelLookup: {},
};
}
@@ -75,22 +69,22 @@ export function useCategoryScheduleGoalTemplates({
const scheduleIds = new Set(schedules.map(s => s.id));
const statuses = new Map(
[...allStatuses].filter(([id]) => scheduleIds.has(id)),
const filteredStatusLookup = Object.fromEntries(
Object.entries(statusLookup).filter(([id]) => scheduleIds.has(id)),
);
const statusLabels = new Map(
[...allStatusLabels].filter(([id]) => scheduleIds.has(id)),
const filteredStatusLabelLookup = Object.fromEntries(
Object.entries(statusLabelLookup).filter(([id]) => scheduleIds.has(id)),
);
return {
schedules,
statuses,
statusLabels,
statusLookup: filteredStatusLookup,
statusLabelLookup: filteredStatusLabelLookup,
};
}, [
allSchedules,
allStatusLabels,
allStatuses,
statusLabelLookup,
statusLookup,
category,
isGoalTemplatesEnabled,
]);

View File

@@ -7,6 +7,7 @@ import type { IntegerAmount } from 'loot-core/shared/util';
import type { ScheduleEntity, TransactionEntity } from 'loot-core/types/models';
import { useCachedSchedules } from './useCachedSchedules';
import { useScheduleStatus } from './useSchedules';
import { useSyncedPref } from './useSyncedPref';
import { calculateRunningBalancesBottomUp } from './useTransactions';
@@ -42,8 +43,11 @@ export function usePreviewTransactions({
const {
isFetching: isSchedulesLoading,
error: scheduleQueryError,
data: { schedules, scheduleStatusMap },
data: schedules = [],
} = useCachedSchedules();
const {
data: { statusLookup },
} = useScheduleStatus({ schedules });
const [isLoading, setIsLoading] = useState(isSchedulesLoading);
const [error, setError] = useState<Error | undefined>(undefined);
const [runningBalances, setRunningBalances] = useState<
@@ -65,17 +69,11 @@ export function usePreviewTransactions({
return computeSchedulePreviewTransactions(
schedules,
scheduleStatusMap,
statusLookup,
upcomingLength,
filter,
);
}, [
filter,
isSchedulesLoading,
schedules,
scheduleStatusMap,
upcomingLength,
]);
}, [filter, isSchedulesLoading, schedules, statusLookup, upcomingLength]);
useEffect(() => {
let isUnmounted = false;
@@ -100,10 +98,7 @@ export function usePreviewTransactions({
if (!isUnmounted) {
const withDefaults = newTrans.map(t => ({
...t,
category:
t.schedule != null
? scheduleStatusMap.get(t.schedule)
: undefined,
category: t.schedule != null ? statusLookup[t.schedule] : undefined,
schedule: t.schedule,
subtransactions: t.subtransactions?.map(
(st: TransactionEntity) => ({
@@ -145,7 +140,7 @@ export function usePreviewTransactions({
return () => {
isUnmounted = true;
};
}, [scheduleTransactions, schedules, scheduleStatusMap, upcomingLength]);
}, [scheduleTransactions, schedules, statusLookup, upcomingLength]);
const returnError = error || scheduleQueryError;
return {

View File

@@ -1,54 +1,39 @@
import { useQuery } from '@tanstack/react-query';
import type { UseQueryResult } from '@tanstack/react-query';
import { q } from 'loot-core/shared/query';
import type { Query } from 'loot-core/shared/query';
import type { AccountEntity } from 'loot-core/types/models';
import type { ScheduleEntity } from 'loot-core/types/models';
import { useSyncedPref } from './useSyncedPref';
import { accountFilter } from '@desktop-client/queries';
import { scheduleQueries } from '@desktop-client/schedules';
import type { ScheduleData } from '@desktop-client/schedules';
import type { ScheduleStatusData } from '@desktop-client/schedules';
export type UseSchedulesProps = {
query?: Query;
};
export type UseSchedulesResult = UseQueryResult<ScheduleData>;
export type UseSchedulesResult = UseQueryResult<ScheduleEntity[]>;
export function useSchedules({
query,
}: UseSchedulesProps = {}): UseSchedulesResult {
return useQuery(scheduleQueries.aql({ query }));
}
type UseScheduleStatusProps = {
schedules: ScheduleEntity[];
};
type UseScheduleStatusResult = UseQueryResult<ScheduleStatusData>;
export function useScheduleStatus({
schedules,
}: UseScheduleStatusProps): UseScheduleStatusResult {
const [upcomingLength] = useSyncedPref('upcomingScheduledTransactionLength');
return useQuery(
scheduleQueries.aql({
query,
statusOptions: { enabled: true, upcomingLength },
scheduleQueries.statuses({
schedules,
upcomingLength,
}),
);
}
export function getSchedulesQuery(
view?: AccountEntity['id'] | 'onbudget' | 'offbudget' | 'uncategorized',
) {
const filterByAccount = accountFilter(view, '_account');
const filterByPayee = accountFilter(view, '_payee.transfer_acct');
let query = q('schedules')
.select('*')
.filter({
$and: [{ '_account.closed': false }],
});
if (view) {
if (view === 'uncategorized') {
query = query.filter({ next_date: null });
} else {
query = query.filter({
$or: [filterByAccount, filterByPayee],
});
}
}
return query.orderBy({ next_date: 'desc' });
}

View File

@@ -1,5 +1,6 @@
import { queryOptions } from '@tanstack/react-query';
import { q } from 'loot-core/shared/query';
import type { Query } from 'loot-core/shared/query';
import {
getHasTransactionsQuery,
@@ -7,11 +8,16 @@ import {
getStatusLabel,
} from 'loot-core/shared/schedules';
import type {
ScheduleStatusLabelMap,
ScheduleStatusMap,
ScheduleStatusLabelLookup,
ScheduleStatusLookup,
} from 'loot-core/shared/schedules';
import type { ScheduleEntity, TransactionEntity } from 'loot-core/types/models';
import type {
AccountEntity,
ScheduleEntity,
TransactionEntity,
} from 'loot-core/types/models';
import { accountFilter } from '@desktop-client/queries';
import { aqlQuery } from '@desktop-client/queries/aqlQuery';
export type ScheduleData = ScheduleStatusData & {
@@ -19,25 +25,21 @@ export type ScheduleData = ScheduleStatusData & {
};
export type ScheduleStatusData = {
scheduleStatusMap: ScheduleStatusMap;
scheduleStatusLabelMap: ScheduleStatusLabelMap;
statusLookup: ScheduleStatusLookup;
statusLabelLookup: ScheduleStatusLabelLookup;
};
type AqlOptions = {
query?: Query;
statusOptions?: {
enabled: boolean;
upcomingLength: string;
};
};
export const scheduleQueries = {
all: () => ['schedules'],
lists: () => [...scheduleQueries.all(), 'lists'],
aql: ({ query, statusOptions }: AqlOptions) =>
queryOptions<ScheduleData>({
aql: ({ query }: AqlOptions) =>
queryOptions<ScheduleEntity[]>({
queryKey: [...scheduleQueries.lists(), query],
queryFn: async ({ client }) => {
queryFn: async () => {
if (!query) {
// Shouldn't happen because of the enabled flag, but needed to satisfy TS
throw new Error('No query provided.');
@@ -45,32 +47,10 @@ export const scheduleQueries = {
const { data: schedules }: { data: ScheduleEntity[] } =
await aqlQuery(query);
if (statusOptions?.enabled) {
const statuses = await client.ensureQueryData(
scheduleQueries.statuses({
schedules,
upcomingLength: statusOptions.upcomingLength,
}),
);
return {
schedules,
scheduleStatusMap: statuses.scheduleStatusMap,
scheduleStatusLabelMap: statuses.scheduleStatusLabelMap,
};
}
return {
schedules,
scheduleStatusMap: new Map(),
scheduleStatusLabelMap: new Map(),
};
return schedules;
},
enabled: !!query,
placeholderData: {
schedules: [],
scheduleStatusMap: new Map(),
scheduleStatusLabelMap: new Map(),
},
placeholderData: [],
}),
statuses: ({
schedules,
@@ -89,7 +69,7 @@ export const scheduleQueries = {
transactions.filter(Boolean).map(trans => trans.schedule),
);
const scheduleStatusMap: ScheduleStatusMap = new Map(
const statusLookup: ScheduleStatusLookup = Object.fromEntries(
schedules.map(s => [
s.id,
getStatus(
@@ -101,15 +81,40 @@ export const scheduleQueries = {
]),
);
const scheduleStatusLabelMap: ScheduleStatusLabelMap = new Map(
[...scheduleStatusMap.keys()].map(key => [
const statusLabelLookup: ScheduleStatusLabelLookup = Object.fromEntries(
Object.keys(statusLookup).map(key => [
key,
getStatusLabel(scheduleStatusMap.get(key) || ''),
getStatusLabel(statusLookup[key] || ''),
]),
);
return { scheduleStatusMap, scheduleStatusLabelMap };
return { statusLookup, statusLabelLookup };
},
enabled: schedules.length > 0,
}),
};
export function schedulesViewQuery(
view?: AccountEntity['id'] | 'onbudget' | 'offbudget' | 'uncategorized',
) {
const filterByAccount = accountFilter(view, '_account');
const filterByPayee = accountFilter(view, '_payee.transfer_acct');
let query = q('schedules')
.select('*')
.filter({
$and: [{ '_account.closed': false }],
});
if (view) {
if (view === 'uncategorized') {
query = query.filter({ next_date: null });
} else {
query = query.filter({
$or: [filterByAccount, filterByPayee],
});
}
}
return query.orderBy({ next_date: 'desc' });
}

View File

@@ -11,7 +11,7 @@ import {
getStatus,
getUpcomingDays,
} from './schedules';
import type { ScheduleStatusMap } from './schedules';
import type { ScheduleStatusLookup } from './schedules';
i18next.init({
lng: 'en',
@@ -483,7 +483,7 @@ describe('schedules', () => {
],
});
const statuses: ScheduleStatusMap = new Map([['sched-1', 'missed']]);
const statuses: ScheduleStatusLookup = new Map([['sched-1', 'missed']]);
const result = computeSchedulePreviewTransactions(
[schedule],
statuses,
@@ -508,7 +508,7 @@ describe('schedules', () => {
],
});
const statuses: ScheduleStatusMap = new Map([['sched-1', 'missed']]);
const statuses: ScheduleStatusLookup = new Map([['sched-1', 'missed']]);
const result = computeSchedulePreviewTransactions(
[schedule],
statuses,
@@ -527,7 +527,9 @@ describe('schedules', () => {
_conditions: [{ field: 'date', op: 'is', value: '2017-01-03' }],
});
const statuses: ScheduleStatusMap = new Map([['sched-1', 'upcoming']]);
const statuses: ScheduleStatusLookup = new Map([
['sched-1', 'upcoming'],
]);
const result = computeSchedulePreviewTransactions(
[schedule],
statuses,
@@ -551,7 +553,7 @@ describe('schedules', () => {
],
});
const statuses: ScheduleStatusMap = new Map([['sched-1', 'paid']]);
const statuses: ScheduleStatusLookup = new Map([['sched-1', 'paid']]);
const result = computeSchedulePreviewTransactions(
[schedule],
statuses,

View File

@@ -474,17 +474,17 @@ export function scheduleIsRecurring(dateCond: Condition | null) {
return value.type === 'recur';
}
export type ScheduleStatusMap = Map<ScheduleEntity['id'], ScheduleStatus>;
export type ScheduleStatusLabelMap = Map<
export type ScheduleStatusLookup = Record<ScheduleEntity['id'], ScheduleStatus>;
export type ScheduleStatusLabelLookup = Record<
ScheduleEntity['id'],
ScheduleStatusLabel
>;
export function isForPreview(
schedule: ScheduleEntity,
statusMap: ScheduleStatusMap,
statusMap: ScheduleStatusLookup,
) {
const status = statusMap.get(schedule.id);
const status = statusMap[schedule.id];
return (
!schedule.completed &&
['due', 'upcoming', 'missed', 'paid'].includes(status!)
@@ -493,7 +493,7 @@ export function isForPreview(
export function computeSchedulePreviewTransactions(
schedules: readonly ScheduleEntity[],
statuses: ScheduleStatusMap,
statuses: ScheduleStatusLookup,
upcomingLength?: string,
filter?: (schedule: ScheduleEntity) => boolean,
) {
@@ -515,7 +515,7 @@ export function computeSchedulePreviewTransactions(
schedule._conditions,
);
const status = statuses.get(schedule.id);
const status = statuses[schedule.id];
const isRecurring = scheduleIsRecurring(dateConditions);
const dates = [schedule.next_date];