mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-28 10:33:02 -05:00
Compare commits
5 Commits
react-quer
...
enhance/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
148d3759bf | ||
|
|
d480065924 | ||
|
|
8bf59fe1e5 | ||
|
|
24fd50ab3b | ||
|
|
dabad3258e |
@@ -31,6 +31,7 @@ import { LoadingIndicator } from './reports/LoadingIndicator';
|
|||||||
import { NarrowAlternate, WideComponent } from './responsive';
|
import { NarrowAlternate, WideComponent } from './responsive';
|
||||||
import { UserDirectoryPage } from './responsive/wide';
|
import { UserDirectoryPage } from './responsive/wide';
|
||||||
import { ScrollProvider } from './ScrollProvider';
|
import { ScrollProvider } from './ScrollProvider';
|
||||||
|
import { ScrollRestoreProvider } from './ScrollRestore';
|
||||||
import { useMultiuserEnabled } from './ServerContext';
|
import { useMultiuserEnabled } from './ServerContext';
|
||||||
import { Settings } from './settings';
|
import { Settings } from './settings';
|
||||||
import { FloatableSidebar } from './sidebar';
|
import { FloatableSidebar } from './sidebar';
|
||||||
@@ -202,10 +203,11 @@ export function FinancesApp() {
|
|||||||
width: '100%',
|
width: '100%',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ScrollProvider
|
<ScrollRestoreProvider>
|
||||||
isDisabled={!isNarrowWidth}
|
<ScrollProvider
|
||||||
scrollableRef={scrollableRef}
|
isDisabled={!isNarrowWidth}
|
||||||
>
|
scrollableRef={scrollableRef}
|
||||||
|
>
|
||||||
<View
|
<View
|
||||||
ref={scrollableRef}
|
ref={scrollableRef}
|
||||||
style={{
|
style={{
|
||||||
@@ -338,6 +340,7 @@ export function FinancesApp() {
|
|||||||
<Route path="*" element={null} />
|
<Route path="*" element={null} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</ScrollProvider>
|
</ScrollProvider>
|
||||||
|
</ScrollRestoreProvider>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
183
packages/desktop-client/src/components/ScrollRestore.tsx
Normal file
183
packages/desktop-client/src/components/ScrollRestore.tsx
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
import React, {
|
||||||
|
type ReactNode,
|
||||||
|
type RefObject,
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useCallback,
|
||||||
|
useRef,
|
||||||
|
useMemo,
|
||||||
|
} from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useNavigate } from '@desktop-client/hooks/useNavigate';
|
||||||
|
|
||||||
|
import debounce from 'debounce';
|
||||||
|
|
||||||
|
type ScrollPosition = {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ScrollRestoreContextValue = {
|
||||||
|
registerScrollElement: (key: string, element: HTMLElement) => () => void;
|
||||||
|
navigate: ReturnType<typeof useNavigate>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ScrollRestoreContext = createContext<ScrollRestoreContextValue | undefined>(undefined);
|
||||||
|
|
||||||
|
type ScrollRestoreProviderProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ScrollRestoreProvider({ children }: ScrollRestoreProviderProps) {
|
||||||
|
const location = useLocation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const scrollElements = useRef<Map<string, HTMLElement>>(new Map());
|
||||||
|
const originalNavigate = useRef(navigate);
|
||||||
|
|
||||||
|
// Update navigate ref when it changes
|
||||||
|
useEffect(() => {
|
||||||
|
originalNavigate.current = navigate;
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
// Create a custom navigate function that saves scroll positions before navigating
|
||||||
|
const enhancedNavigate = useCallback((to: any, options: any = {}) => {
|
||||||
|
// Get current scroll positions
|
||||||
|
const scrollPositions: Record<string, ScrollPosition> = {};
|
||||||
|
scrollElements.current.forEach((element, key) => {
|
||||||
|
scrollPositions[key] = {
|
||||||
|
x: element.scrollLeft,
|
||||||
|
y: element.scrollTop,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// If we have scroll positions, update the current location state
|
||||||
|
if (Object.keys(scrollPositions).length > 0) {
|
||||||
|
// First, update current location with scroll positions
|
||||||
|
originalNavigate.current(location.pathname + location.search, {
|
||||||
|
replace: true,
|
||||||
|
state: {
|
||||||
|
...location.state,
|
||||||
|
scrollPositions,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then navigate to the new location
|
||||||
|
originalNavigate.current(to, options);
|
||||||
|
}, [location, originalNavigate]);
|
||||||
|
|
||||||
|
// Restore scroll positions when coming to a page
|
||||||
|
useEffect(() => {
|
||||||
|
const savedPositions = location.state?.scrollPositions;
|
||||||
|
|
||||||
|
if (!savedPositions || typeof savedPositions !== 'object') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let attemptCount = 0;
|
||||||
|
const maxAttempts = 20; // Allow more attempts for loading states
|
||||||
|
|
||||||
|
const restoreScrollPositions = () => {
|
||||||
|
attemptCount++;
|
||||||
|
|
||||||
|
// Check if we have any registered elements that match the saved positions
|
||||||
|
const savedKeys = Object.keys(savedPositions as Record<string, ScrollPosition>);
|
||||||
|
const hasRegisteredElements = savedKeys.some(key => scrollElements.current.has(key));
|
||||||
|
|
||||||
|
if (!hasRegisteredElements) {
|
||||||
|
if (attemptCount < maxAttempts) {
|
||||||
|
// Elements not registered yet, try again with increasing delay for loading states
|
||||||
|
const delay = attemptCount < 5 ? 50 : attemptCount < 10 ? 150 : 300;
|
||||||
|
setTimeout(restoreScrollPositions, delay);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for next frame to ensure elements are fully rendered
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
Object.entries(savedPositions as Record<string, ScrollPosition>).forEach(([key, position]) => {
|
||||||
|
const element = scrollElements.current.get(key);
|
||||||
|
if (element && typeof position === 'object' && 'x' in position && 'y' in position) {
|
||||||
|
element.scrollTo(position.x, position.y);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start restoration after a small delay to allow initial mounting
|
||||||
|
const timeoutId = setTimeout(restoreScrollPositions, 10);
|
||||||
|
return () => clearTimeout(timeoutId);
|
||||||
|
}, [location.state?.scrollPositions]);
|
||||||
|
|
||||||
|
const registerScrollElement = useCallback((key: string, element: HTMLElement) => {
|
||||||
|
scrollElements.current.set(key, element);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
scrollElements.current.delete(key);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const contextValue = useMemo(
|
||||||
|
() => ({
|
||||||
|
registerScrollElement,
|
||||||
|
navigate: enhancedNavigate,
|
||||||
|
}),
|
||||||
|
[registerScrollElement, enhancedNavigate],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollRestoreContext.Provider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
</ScrollRestoreContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type ScrollRestoreProps = {
|
||||||
|
scrollKey?: string;
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ScrollRestore({ scrollKey = 'default', children }: ScrollRestoreProps) {
|
||||||
|
const context = useContext(ScrollRestoreContext);
|
||||||
|
const scrollElementRef = useRef<HTMLElement>(null);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('ScrollRestore must be used within a ScrollRestoreProvider');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { registerScrollElement } = context;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const element = scrollElementRef.current;
|
||||||
|
if (element) {
|
||||||
|
return registerScrollElement(scrollKey, element);
|
||||||
|
}
|
||||||
|
}, [registerScrollElement, scrollKey]);
|
||||||
|
|
||||||
|
// Clone the child element and add ref
|
||||||
|
const childElement = React.Children.only(children) as React.ReactElement<any>;
|
||||||
|
|
||||||
|
return React.cloneElement(childElement, {
|
||||||
|
innerRef: (ref: HTMLElement) => {
|
||||||
|
scrollElementRef.current = ref;
|
||||||
|
|
||||||
|
// Call original innerRef if it exists
|
||||||
|
const originalRef = childElement.props.innerRef;
|
||||||
|
if (typeof originalRef === 'function') {
|
||||||
|
originalRef(ref);
|
||||||
|
} else if (originalRef && typeof originalRef === 'object') {
|
||||||
|
(originalRef as RefObject<HTMLElement>).current = ref;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useScrollRestore() {
|
||||||
|
const context = useContext(ScrollRestoreContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useScrollRestore must be used within a ScrollRestoreProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ import { styles } from '@actual-app/components/styles';
|
|||||||
import { theme } from '@actual-app/components/theme';
|
import { theme } from '@actual-app/components/theme';
|
||||||
import { View } from '@actual-app/components/view';
|
import { View } from '@actual-app/components/view';
|
||||||
|
|
||||||
|
import { ScrollRestore } from '../ScrollRestore';
|
||||||
|
|
||||||
import { q } from 'loot-core/shared/query';
|
import { q } from 'loot-core/shared/query';
|
||||||
import {
|
import {
|
||||||
type CategoryEntity,
|
type CategoryEntity,
|
||||||
@@ -91,7 +93,7 @@ export function BudgetTable(props: BudgetTableProps) {
|
|||||||
);
|
);
|
||||||
const [categoryExpandedStatePref] = useGlobalPref('categoryExpandedState');
|
const [categoryExpandedStatePref] = useGlobalPref('categoryExpandedState');
|
||||||
const categoryExpandedState = categoryExpandedStatePref ?? 0;
|
const categoryExpandedState = categoryExpandedStatePref ?? 0;
|
||||||
const [editing, setEditing] = useState<{ id: string; cell: string } | null>(
|
const [editing, setEditing] = useState<{ id: string; cell: string } | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -229,6 +231,8 @@ export function BudgetTable(props: BudgetTableProps) {
|
|||||||
onCollapse(categoryGroups.map(g => g.id));
|
onCollapse(categoryGroups.map(g => g.id));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const schedulesQuery = useMemo(() => q('schedules').select('*'), []);
|
const schedulesQuery = useMemo(() => q('schedules').select('*'), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -280,15 +284,16 @@ export function BudgetTable(props: BudgetTableProps) {
|
|||||||
expandAllCategories={expandAllCategories}
|
expandAllCategories={expandAllCategories}
|
||||||
collapseAllCategories={collapseAllCategories}
|
collapseAllCategories={collapseAllCategories}
|
||||||
/>
|
/>
|
||||||
<View
|
<ScrollRestore scrollKey="budget-table">
|
||||||
style={{
|
<View
|
||||||
overflowY: 'scroll',
|
style={{
|
||||||
overflowAnchor: 'none',
|
overflowY: 'scroll',
|
||||||
flex: 1,
|
overflowAnchor: 'none',
|
||||||
paddingLeft: 5,
|
flex: 1,
|
||||||
paddingRight: 5,
|
paddingLeft: 5,
|
||||||
}}
|
paddingRight: 5,
|
||||||
>
|
}}
|
||||||
|
>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
@@ -315,7 +320,8 @@ export function BudgetTable(props: BudgetTableProps) {
|
|||||||
/>
|
/>
|
||||||
</SchedulesProvider>
|
</SchedulesProvider>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
</ScrollRestore>
|
||||||
</MonthsProvider>
|
</MonthsProvider>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -51,9 +51,9 @@ import {
|
|||||||
} from '@desktop-client/components/table';
|
} from '@desktop-client/components/table';
|
||||||
import { useCategoryScheduleGoalTemplateIndicator } from '@desktop-client/hooks/useCategoryScheduleGoalTemplateIndicator';
|
import { useCategoryScheduleGoalTemplateIndicator } from '@desktop-client/hooks/useCategoryScheduleGoalTemplateIndicator';
|
||||||
import { useContextMenu } from '@desktop-client/hooks/useContextMenu';
|
import { useContextMenu } from '@desktop-client/hooks/useContextMenu';
|
||||||
import { useNavigate } from '@desktop-client/hooks/useNavigate';
|
|
||||||
import { useUndo } from '@desktop-client/hooks/useUndo';
|
import { useUndo } from '@desktop-client/hooks/useUndo';
|
||||||
import { envelopeBudget } from '@desktop-client/queries/queries';
|
import { envelopeBudget } from '@desktop-client/queries/queries';
|
||||||
|
import { useScrollRestore } from '@desktop-client/components/ScrollRestore';
|
||||||
|
|
||||||
export function useEnvelopeSheetName<
|
export function useEnvelopeSheetName<
|
||||||
FieldName extends SheetFields<'envelope-budget'>,
|
FieldName extends SheetFields<'envelope-budget'>,
|
||||||
@@ -251,7 +251,7 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({
|
|||||||
|
|
||||||
const { showUndoNotification } = useUndo();
|
const { showUndoNotification } = useUndo();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const { navigate } = useScrollRestore();
|
||||||
|
|
||||||
const { schedule, scheduleStatus, isScheduleRecurring, description } =
|
const { schedule, scheduleStatus, isScheduleRecurring, description } =
|
||||||
useCategoryScheduleGoalTemplateIndicator({
|
useCategoryScheduleGoalTemplateIndicator({
|
||||||
@@ -414,6 +414,7 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Field name="spent" width="flex" style={{ textAlign: 'right' }}>
|
<Field name="spent" width="flex" style={{ textAlign: 'right' }}>
|
||||||
<View
|
<View
|
||||||
data-testid="category-month-spent"
|
data-testid="category-month-spent"
|
||||||
@@ -439,7 +440,7 @@ export const ExpenseCategoryMonth = memo(function ExpenseCategoryMonth({
|
|||||||
? theme.warningText
|
? theme.warningText
|
||||||
: theme.upcomingText,
|
: theme.upcomingText,
|
||||||
}}
|
}}
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
schedule._account
|
schedule._account
|
||||||
? navigate(`/accounts/${schedule._account}`)
|
? navigate(`/accounts/${schedule._account}`)
|
||||||
: navigate('/accounts')
|
: navigate('/accounts')
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
updateGroup,
|
updateGroup,
|
||||||
} from '@desktop-client/queries/queriesSlice';
|
} from '@desktop-client/queries/queriesSlice';
|
||||||
import { useDispatch } from '@desktop-client/redux';
|
import { useDispatch } from '@desktop-client/redux';
|
||||||
|
import { useScrollRestore } from '../ScrollRestore';
|
||||||
|
|
||||||
type TrackingReportComponents = {
|
type TrackingReportComponents = {
|
||||||
SummaryComponent: typeof trackingBudget.BudgetSummary;
|
SummaryComponent: typeof trackingBudget.BudgetSummary;
|
||||||
@@ -69,7 +70,7 @@ function BudgetInner(props: BudgetInnerProps) {
|
|||||||
const currentMonth = monthUtils.currentMonth();
|
const currentMonth = monthUtils.currentMonth();
|
||||||
const spreadsheet = useSpreadsheet();
|
const spreadsheet = useSpreadsheet();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const navigate = useNavigate();
|
const { navigate } = useScrollRestore();
|
||||||
const [summaryCollapsed, setSummaryCollapsedPref] = useLocalPref(
|
const [summaryCollapsed, setSummaryCollapsedPref] = useLocalPref(
|
||||||
'budget.summaryCollapsed',
|
'budget.summaryCollapsed',
|
||||||
);
|
);
|
||||||
@@ -280,6 +281,7 @@ function BudgetInner(props: BudgetInnerProps) {
|
|||||||
type: 'date',
|
type: 'date',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
navigate('/accounts', {
|
navigate('/accounts', {
|
||||||
state: {
|
state: {
|
||||||
goBack: true,
|
goBack: true,
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ import {
|
|||||||
type SheetCellProps,
|
type SheetCellProps,
|
||||||
} from '@desktop-client/components/table';
|
} from '@desktop-client/components/table';
|
||||||
import { useCategoryScheduleGoalTemplateIndicator } from '@desktop-client/hooks/useCategoryScheduleGoalTemplateIndicator';
|
import { useCategoryScheduleGoalTemplateIndicator } from '@desktop-client/hooks/useCategoryScheduleGoalTemplateIndicator';
|
||||||
import { useNavigate } from '@desktop-client/hooks/useNavigate';
|
import { useScrollRestore } from '@desktop-client/components/ScrollRestore';
|
||||||
import { useUndo } from '@desktop-client/hooks/useUndo';
|
import { useUndo } from '@desktop-client/hooks/useUndo';
|
||||||
import { trackingBudget } from '@desktop-client/queries/queries';
|
import { trackingBudget } from '@desktop-client/queries/queries';
|
||||||
|
|
||||||
@@ -242,7 +242,7 @@ export const CategoryMonth = memo(function CategoryMonth({
|
|||||||
|
|
||||||
const { showUndoNotification } = useUndo();
|
const { showUndoNotification } = useUndo();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const { navigate } = useScrollRestore();
|
||||||
|
|
||||||
const { schedule, scheduleStatus, isScheduleRecurring, description } =
|
const { schedule, scheduleStatus, isScheduleRecurring, description } =
|
||||||
useCategoryScheduleGoalTemplateIndicator({
|
useCategoryScheduleGoalTemplateIndicator({
|
||||||
@@ -417,7 +417,7 @@ export const CategoryMonth = memo(function CategoryMonth({
|
|||||||
? theme.warningText
|
? theme.warningText
|
||||||
: theme.upcomingText,
|
: theme.upcomingText,
|
||||||
}}
|
}}
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
schedule._account
|
schedule._account
|
||||||
? navigate(`/accounts/${schedule._account}`)
|
? navigate(`/accounts/${schedule._account}`)
|
||||||
: navigate('/accounts')
|
: navigate('/accounts')
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import { IncomeGroup } from './IncomeGroup';
|
|||||||
import { MOBILE_NAV_HEIGHT } from '@desktop-client/components/mobile/MobileNavTabs';
|
import { MOBILE_NAV_HEIGHT } from '@desktop-client/components/mobile/MobileNavTabs';
|
||||||
import { PullToRefresh } from '@desktop-client/components/mobile/PullToRefresh';
|
import { PullToRefresh } from '@desktop-client/components/mobile/PullToRefresh';
|
||||||
import { MobilePageHeader, Page } from '@desktop-client/components/Page';
|
import { MobilePageHeader, Page } from '@desktop-client/components/Page';
|
||||||
|
import { ScrollRestore, useScrollRestore } from '@desktop-client/components/ScrollRestore';
|
||||||
import { PrivacyFilter } from '@desktop-client/components/PrivacyFilter';
|
import { PrivacyFilter } from '@desktop-client/components/PrivacyFilter';
|
||||||
import { CellValue } from '@desktop-client/components/spreadsheet/CellValue';
|
import { CellValue } from '@desktop-client/components/spreadsheet/CellValue';
|
||||||
import { useFormat } from '@desktop-client/components/spreadsheet/useFormat';
|
import { useFormat } from '@desktop-client/components/spreadsheet/useFormat';
|
||||||
@@ -41,7 +42,6 @@ import { SchedulesProvider } from '@desktop-client/hooks/useCachedSchedules';
|
|||||||
import { useCategories } from '@desktop-client/hooks/useCategories';
|
import { useCategories } from '@desktop-client/hooks/useCategories';
|
||||||
import { useLocale } from '@desktop-client/hooks/useLocale';
|
import { useLocale } from '@desktop-client/hooks/useLocale';
|
||||||
import { useLocalPref } from '@desktop-client/hooks/useLocalPref';
|
import { useLocalPref } from '@desktop-client/hooks/useLocalPref';
|
||||||
import { useNavigate } from '@desktop-client/hooks/useNavigate';
|
|
||||||
import { useOverspentCategories } from '@desktop-client/hooks/useOverspentCategories';
|
import { useOverspentCategories } from '@desktop-client/hooks/useOverspentCategories';
|
||||||
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
|
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
|
||||||
import { useUndo } from '@desktop-client/hooks/useUndo';
|
import { useUndo } from '@desktop-client/hooks/useUndo';
|
||||||
@@ -331,6 +331,8 @@ export function BudgetTable({
|
|||||||
'mobile.showSpentColumn',
|
'mobile.showSpentColumn',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function toggleSpentColumn() {
|
function toggleSpentColumn() {
|
||||||
setShowSpentColumnPref(!showSpentColumn);
|
setShowSpentColumnPref(!showSpentColumn);
|
||||||
}
|
}
|
||||||
@@ -343,6 +345,8 @@ export function BudgetTable({
|
|||||||
|
|
||||||
const schedulesQuery = useMemo(() => q('schedules').select('*'), []);
|
const schedulesQuery = useMemo(() => q('schedules').select('*'), []);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page
|
<Page
|
||||||
padding={0}
|
padding={0}
|
||||||
@@ -400,27 +404,30 @@ export function BudgetTable({
|
|||||||
onShowBudgetSummary={onShowBudgetSummary}
|
onShowBudgetSummary={onShowBudgetSummary}
|
||||||
/>
|
/>
|
||||||
<PullToRefresh onRefresh={onRefresh}>
|
<PullToRefresh onRefresh={onRefresh}>
|
||||||
<View
|
<ScrollRestore scrollKey="mobile-budget-table">
|
||||||
data-testid="budget-table"
|
<View
|
||||||
style={{
|
data-testid="budget-table"
|
||||||
backgroundColor: theme.pageBackground,
|
style={{
|
||||||
paddingBottom: MOBILE_NAV_HEIGHT,
|
backgroundColor: theme.pageBackground,
|
||||||
}}
|
paddingBottom: MOBILE_NAV_HEIGHT,
|
||||||
>
|
overflowY: 'auto',
|
||||||
<SchedulesProvider query={schedulesQuery}>
|
}}
|
||||||
<BudgetGroups
|
>
|
||||||
type={budgetType}
|
<SchedulesProvider query={schedulesQuery}>
|
||||||
categoryGroups={categoryGroups}
|
<BudgetGroups
|
||||||
showBudgetedColumn={!showSpentColumn}
|
type={budgetType}
|
||||||
show3Columns={show3Columns}
|
categoryGroups={categoryGroups}
|
||||||
showHiddenCategories={showHiddenCategories}
|
showBudgetedColumn={!showSpentColumn}
|
||||||
month={month}
|
show3Columns={show3Columns}
|
||||||
onEditCategoryGroup={onEditCategoryGroup}
|
showHiddenCategories={showHiddenCategories}
|
||||||
onEditCategory={onEditCategory}
|
month={month}
|
||||||
onBudgetAction={onBudgetAction}
|
onEditCategoryGroup={onEditCategoryGroup}
|
||||||
/>
|
onEditCategory={onEditCategory}
|
||||||
</SchedulesProvider>
|
onBudgetAction={onBudgetAction}
|
||||||
</View>
|
/>
|
||||||
|
</SchedulesProvider>
|
||||||
|
</View>
|
||||||
|
</ScrollRestore>
|
||||||
</PullToRefresh>
|
</PullToRefresh>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
@@ -450,7 +457,7 @@ function Banner({ type = 'info', children }) {
|
|||||||
|
|
||||||
function UncategorizedTransactionsBanner(props) {
|
function UncategorizedTransactionsBanner(props) {
|
||||||
const count = useSheetValue(uncategorizedCount());
|
const count = useSheetValue(uncategorizedCount());
|
||||||
const navigate = useNavigate();
|
const { navigate } = useScrollRestore();
|
||||||
|
|
||||||
if (count === null || count <= 0) {
|
if (count === null || count <= 0) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ import { MobileBackButton } from '@desktop-client/components/mobile/MobileBackBu
|
|||||||
import { AddTransactionButton } from '@desktop-client/components/mobile/transactions/AddTransactionButton';
|
import { AddTransactionButton } from '@desktop-client/components/mobile/transactions/AddTransactionButton';
|
||||||
import { TransactionListWithBalances } from '@desktop-client/components/mobile/transactions/TransactionListWithBalances';
|
import { TransactionListWithBalances } from '@desktop-client/components/mobile/transactions/TransactionListWithBalances';
|
||||||
import { MobilePageHeader, Page } from '@desktop-client/components/Page';
|
import { MobilePageHeader, Page } from '@desktop-client/components/Page';
|
||||||
|
import { useScrollRestore } from '@desktop-client/components/ScrollRestore';
|
||||||
import { SchedulesProvider } from '@desktop-client/hooks/useCachedSchedules';
|
import { SchedulesProvider } from '@desktop-client/hooks/useCachedSchedules';
|
||||||
import { useCategoryPreviewTransactions } from '@desktop-client/hooks/useCategoryPreviewTransactions';
|
import { useCategoryPreviewTransactions } from '@desktop-client/hooks/useCategoryPreviewTransactions';
|
||||||
import { useDateFormat } from '@desktop-client/hooks/useDateFormat';
|
import { useDateFormat } from '@desktop-client/hooks/useDateFormat';
|
||||||
import { useLocale } from '@desktop-client/hooks/useLocale';
|
import { useLocale } from '@desktop-client/hooks/useLocale';
|
||||||
import { useNavigate } from '@desktop-client/hooks/useNavigate';
|
|
||||||
import { useTransactions } from '@desktop-client/hooks/useTransactions';
|
import { useTransactions } from '@desktop-client/hooks/useTransactions';
|
||||||
import { useTransactionsSearch } from '@desktop-client/hooks/useTransactionsSearch';
|
import { useTransactionsSearch } from '@desktop-client/hooks/useTransactionsSearch';
|
||||||
import * as queries from '@desktop-client/queries/queries';
|
import * as queries from '@desktop-client/queries/queries';
|
||||||
@@ -74,7 +74,7 @@ function TransactionListWithPreviews({
|
|||||||
month,
|
month,
|
||||||
}: TransactionListWithPreviewsProps) {
|
}: TransactionListWithPreviewsProps) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const navigate = useNavigate();
|
const { navigate } = useScrollRestore();
|
||||||
|
|
||||||
const baseTransactionsQuery = useCallback(
|
const baseTransactionsQuery = useCallback(
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { SpentCell } from './SpentCell';
|
|||||||
|
|
||||||
import { useSheetValue } from '@desktop-client/components/spreadsheet/useSheetValue';
|
import { useSheetValue } from '@desktop-client/components/spreadsheet/useSheetValue';
|
||||||
import { useCategories } from '@desktop-client/hooks/useCategories';
|
import { useCategories } from '@desktop-client/hooks/useCategories';
|
||||||
import { useNavigate } from '@desktop-client/hooks/useNavigate';
|
import { useScrollRestore } from '@desktop-client/components/ScrollRestore';
|
||||||
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
|
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
|
||||||
import { useUndo } from '@desktop-client/hooks/useUndo';
|
import { useUndo } from '@desktop-client/hooks/useUndo';
|
||||||
import { collapseModals, pushModal } from '@desktop-client/modals/modalsSlice';
|
import { collapseModals, pushModal } from '@desktop-client/modals/modalsSlice';
|
||||||
@@ -386,11 +386,12 @@ export function ExpenseCategoryListItem({
|
|||||||
onCover,
|
onCover,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const { navigate } = useScrollRestore();
|
||||||
const onShowActivity = useCallback(() => {
|
const onShowActivity = useCallback(() => {
|
||||||
if (!category) {
|
if (!category) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
navigate(`/categories/${category.id}?month=${month}`);
|
navigate(`/categories/${category.id}?month=${month}`);
|
||||||
}, [category, month, navigate]);
|
}, [category, month, navigate]);
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { BalanceCell } from './BalanceCell';
|
|||||||
import { BudgetCell } from './BudgetCell';
|
import { BudgetCell } from './BudgetCell';
|
||||||
import { getColumnWidth, ROW_HEIGHT } from './BudgetTable';
|
import { getColumnWidth, ROW_HEIGHT } from './BudgetTable';
|
||||||
|
|
||||||
import { useNavigate } from '@desktop-client/hooks/useNavigate';
|
import { useScrollRestore } from '@desktop-client/components/ScrollRestore';
|
||||||
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
|
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
|
||||||
import { collapseModals, pushModal } from '@desktop-client/modals/modalsSlice';
|
import { collapseModals, pushModal } from '@desktop-client/modals/modalsSlice';
|
||||||
import {
|
import {
|
||||||
@@ -185,7 +185,7 @@ export function IncomeCategoryListItem({
|
|||||||
}: IncomeCategoryListItemProps) {
|
}: IncomeCategoryListItemProps) {
|
||||||
const { value: category } = props;
|
const { value: category } = props;
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const navigate = useNavigate();
|
const { navigate } = useScrollRestore();
|
||||||
const [budgetType = 'envelope'] = useSyncedPref('budgetType');
|
const [budgetType = 'envelope'] = useSyncedPref('budgetType');
|
||||||
const balanceMenuModalName = `envelope-income-balance-menu`;
|
const balanceMenuModalName = `envelope-income-balance-menu`;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user