Skip to content

Commit 46ba056

Browse files
authored
Merge pull request #357 from kenlefeb/sc-4216-link-schedule-names
[AI] fix(schedules): show schedule names in the phone Link schedule dialog [sc-4216]
2 parents 6fc2e4e + 0065f2e commit 46ba056

5 files changed

Lines changed: 293 additions & 41 deletions

File tree

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { render, screen } from '@testing-library/react';
2+
import { describe, expect, it, vi } from 'vitest';
3+
4+
import { TestProviders } from '#mocks';
5+
6+
import { ScheduleLink } from './ScheduleLink';
7+
8+
const state = vi.hoisted(() => ({ isNarrowWidth: true }));
9+
10+
vi.mock('@actual-app/components/hooks/useResponsive', () => ({
11+
useResponsive: () => ({
12+
isNarrowWidth: state.isNarrowWidth,
13+
isWideWidth: !state.isNarrowWidth,
14+
}),
15+
}));
16+
17+
vi.mock('#components/common/Modal', () => ({
18+
Modal: ({ children }: { children: (a: { state: unknown }) => never }) =>
19+
children({ state: { close: vi.fn() } }),
20+
ModalHeader: () => null,
21+
ModalCloseButton: () => null,
22+
}));
23+
24+
vi.mock('#components/common/Search', async () => {
25+
const { forwardRef } = await import('react');
26+
return {
27+
Search: forwardRef<HTMLInputElement, { width: number | string }>(
28+
({ width }, ref) => (
29+
<input ref={ref} data-testid="search" data-width={String(width)} />
30+
),
31+
),
32+
};
33+
});
34+
35+
vi.mock('#hooks/useSchedules', () => ({
36+
useSchedules: () => ({
37+
schedules: [],
38+
statuses: new Map(),
39+
isLoading: false,
40+
}),
41+
}));
42+
43+
vi.mock('./SchedulesTable', () => ({
44+
ROW_HEIGHT: 43,
45+
NARROW_ROW_HEIGHT: 76,
46+
SchedulesTable: () => null,
47+
}));
48+
49+
function renderLink() {
50+
render(
51+
<TestProviders>
52+
<ScheduleLink
53+
transactionIds={['t1']}
54+
getTransaction={vi.fn()}
55+
accountName=""
56+
/>
57+
</TestProviders>,
58+
);
59+
}
60+
61+
describe('ScheduleLink header', () => {
62+
it('makes the search full width and stacks the header on a phone', () => {
63+
state.isNarrowWidth = true;
64+
renderLink();
65+
const search = screen.getByTestId('search');
66+
expect(search).toHaveAttribute('data-width', '100%');
67+
expect(search.parentElement).toHaveStyle({
68+
flexDirection: 'column',
69+
});
70+
});
71+
72+
it('keeps the 300px search in a row on desktop', () => {
73+
state.isNarrowWidth = false;
74+
renderLink();
75+
const search = screen.getByTestId('search');
76+
expect(search).toHaveAttribute('data-width', '300');
77+
expect(search.parentElement).toHaveStyle({
78+
flexDirection: 'row',
79+
});
80+
});
81+
});

‎packages/desktop-client/src/components/schedules/ScheduleLink.tsx‎

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import React, { useMemo, useRef, useState } from 'react';
33
import { Trans, useTranslation } from 'react-i18next';
44

55
import { Button } from '@actual-app/components/button';
6+
import { useResponsive } from '@actual-app/components/hooks/useResponsive';
67
import { SvgAdd } from '@actual-app/components/icons/v0';
78
import { InitialFocus } from '@actual-app/components/initial-focus';
89
import { Text } from '@actual-app/components/text';
@@ -17,7 +18,11 @@ import { pushModal } from '#modals/modalsSlice';
1718
import type { Modal as ModalType } from '#modals/modalsSlice';
1819
import { useDispatch } from '#redux';
1920

20-
import { ROW_HEIGHT, SchedulesTable } from './SchedulesTable';
21+
import {
22+
NARROW_ROW_HEIGHT,
23+
ROW_HEIGHT,
24+
SchedulesTable,
25+
} from './SchedulesTable';
2126

2227
type ScheduleLinkProps = Extract<
2328
ModalType,
@@ -31,6 +36,8 @@ export function ScheduleLink({
3136
onScheduleLinked,
3237
}: ScheduleLinkProps) {
3338
const { t } = useTranslation();
39+
const { isNarrowWidth } = useResponsive();
40+
const rowHeight = isNarrowWidth ? NARROW_ROW_HEIGHT : ROW_HEIGHT;
3441

3542
const dispatch = useDispatch();
3643
const [filter, setFilter] = useState(accountName || '');
@@ -86,10 +93,10 @@ export function ScheduleLink({
8693
/>
8794
<View
8895
style={{
89-
flexDirection: 'row',
90-
gap: 4,
96+
flexDirection: isNarrowWidth ? 'column' : 'row',
97+
gap: isNarrowWidth ? 8 : 4,
9198
marginBottom: 20,
92-
alignItems: 'center',
99+
alignItems: isNarrowWidth ? 'stretch' : 'center',
93100
}}
94101
>
95102
<Text>
@@ -106,7 +113,7 @@ export function ScheduleLink({
106113
searchInput.current = r;
107114
}}
108115
isInModal
109-
width={300}
116+
width={isNarrowWidth ? '100%' : 300}
110117
placeholder={t('Filter schedules…')}
111118
value={filter}
112119
onChange={setFilter}
@@ -116,7 +123,10 @@ export function ScheduleLink({
116123
{ids.length === 1 && (
117124
<Button
118125
variant="primary"
119-
style={{ marginLeft: 15, padding: '4px 10px' }}
126+
style={{
127+
marginLeft: isNarrowWidth ? 0 : 15,
128+
padding: '4px 10px',
129+
}}
120130
onPress={() => {
121131
state.close();
122132
void onCreate();
@@ -131,7 +141,7 @@ export function ScheduleLink({
131141
<View
132142
style={{
133143
flex: `1 1 ${
134-
(ROW_HEIGHT - 1) * (Math.max(schedules.length, 1) + 1)
144+
(rowHeight - 1) * (Math.max(schedules.length, 1) + 1)
135145
}px`,
136146
marginTop: 15,
137147
maxHeight: '50vh',
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { render, screen } from '@testing-library/react';
2+
import { describe, expect, it, vi } from 'vitest';
3+
4+
import { TestProviders } from '#mocks';
5+
6+
import { SchedulesTable } from './SchedulesTable';
7+
8+
const state = vi.hoisted(() => ({ isNarrowWidth: true }));
9+
10+
vi.mock('@actual-app/components/hooks/useResponsive', () => ({
11+
useResponsive: () => ({
12+
isNarrowWidth: state.isNarrowWidth,
13+
isWideWidth: !state.isNarrowWidth,
14+
}),
15+
}));
16+
17+
vi.mock('#hooks/useScheduleDateLookup', () => ({
18+
useScheduleDateLookup: () => ({ schedules: [], isLoading: false }),
19+
}));
20+
21+
vi.mock('#hooks/usePayees', () => ({
22+
usePayees: () => ({ data: [{ id: 'p1', name: 'Acme Corp' }] }),
23+
}));
24+
25+
vi.mock('#hooks/usePayee', () => ({
26+
usePayee: () => ({ data: { id: 'p1', name: 'Acme Corp' } }),
27+
}));
28+
29+
vi.mock('#hooks/useAccounts', () => ({
30+
useAccounts: () => ({ data: [{ id: 'a1', name: 'Joint Checking' }] }),
31+
}));
32+
33+
vi.mock('#hooks/useAccount', () => ({
34+
useAccount: () => ({ id: 'a1', name: 'Joint Checking' }),
35+
}));
36+
37+
const schedules = [
38+
{
39+
id: 's1',
40+
name: 'Payroll deposit',
41+
_payee: 'p1',
42+
_account: 'a1',
43+
_amount: 187711,
44+
_amountOp: 'is',
45+
_date: '2026-11-13',
46+
next_date: '2026-11-13',
47+
completed: false,
48+
},
49+
] as never;
50+
51+
function renderTable() {
52+
render(
53+
<TestProviders>
54+
<SchedulesTable
55+
schedules={schedules}
56+
statuses={new Map([['s1', 'due']])}
57+
filter=""
58+
allowCompleted={false}
59+
minimal
60+
onSelect={vi.fn()}
61+
style={{}}
62+
/>
63+
</TestProviders>,
64+
);
65+
}
66+
67+
describe('SchedulesTable in the minimal (link) dialog', () => {
68+
it('stacks the name, payee and account on a phone-width screen', () => {
69+
state.isNarrowWidth = true;
70+
renderTable();
71+
expect(screen.getByText('Payroll deposit')).toBeInTheDocument();
72+
expect(screen.getByText(/Acme Corp/)).toBeInTheDocument();
73+
expect(screen.getByText(/Joint Checking/)).toBeInTheDocument();
74+
// The wide column headers are replaced by the stacked row
75+
expect(screen.queryByText('Payee')).not.toBeInTheDocument();
76+
});
77+
78+
it('keeps the column layout on desktop', () => {
79+
state.isNarrowWidth = false;
80+
renderTable();
81+
expect(screen.getByText('Payee')).toBeInTheDocument();
82+
expect(screen.getByText('Account')).toBeInTheDocument();
83+
});
84+
});

0 commit comments

Comments
 (0)