mirror of
https://github.com/actualbudget/actual.git
synced 2026-10-12 02:10:19 +00:00
[AI] Add per-schedule custom upcoming length override (#7434)
* [AI] Add per-schedule custom upcoming length override * [AI] Add release notes for PR #7434 * [AI] Add custom length option to per-schedule upcoming length selector * [AI] Deduplicate preset values and guard against malformed custom upcoming length * [autofix.ci] apply automated fixes * [AI] Retrigger CI (flaky accounts E2E test) * [AI] Improve schedule editor layout for upcoming length field Restructure the custom upcoming length from a checkbox + conditional dropdown into a proper FormField with a single Select that includes "Use global default" as the null option. Place it in a responsive side-by-side row with the auto-post checkbox, consistent with the form's existing layout patterns. * [AI] Address CodeRabbit review feedback Tighten custom upcoming length validation with a proper regex instead of a loose hyphen check. Replace fixed height with minHeight on the auto-post checkbox row to avoid clipping translated labels. * Update VRT screenshots Auto-generated by VRT workflow PR: #7434 * [AI] Fix custom_upcoming_length not persisting on mobile The mobile schedule save handler was missing the custom_upcoming_length field from the payload sent to schedule/create and schedule/update. --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: youngcw <[email protected]>
This commit is contained in:
co-authored by
autofix-ci[bot]
github-actions[bot]
youngcw
parent
9d91da77ec
commit
c3717e7036
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 27 KiB |
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
@@ -101,6 +101,7 @@ export function MobileScheduleEditPage() {
|
||||
schedule: {
|
||||
id: state.schedule.id,
|
||||
posts_transaction: state.fields.posts_transaction,
|
||||
custom_upcoming_length: state.fields.custom_upcoming_length,
|
||||
name: state.fields.name,
|
||||
},
|
||||
conditions,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Button } from '@actual-app/components/button';
|
||||
import { useResponsive } from '@actual-app/components/hooks/useResponsive';
|
||||
import { InitialFocus } from '@actual-app/components/initial-focus';
|
||||
import { Input } from '@actual-app/components/input';
|
||||
import { Select } from '@actual-app/components/select';
|
||||
import { SpaceBetween } from '@actual-app/components/space-between';
|
||||
import { styles } from '@actual-app/components/styles';
|
||||
import { Text } from '@actual-app/components/text';
|
||||
@@ -20,6 +21,7 @@ import type {
|
||||
|
||||
import { Checkbox, FormField, FormLabel } from '#components/forms';
|
||||
import { OpSelect } from '#components/rules/RuleEditor';
|
||||
import { CustomUpcomingLength } from '#components/schedules/CustomUpcomingLength';
|
||||
import { DateSelect } from '#components/select/DateSelect';
|
||||
import { RecurringSchedulePicker } from '#components/select/RecurringSchedulePicker';
|
||||
import { SelectedItemsButton } from '#components/table';
|
||||
@@ -38,6 +40,7 @@ export type ScheduleFormFields = {
|
||||
amountOp: null | string;
|
||||
date: null | string | RecurConfig;
|
||||
posts_transaction: boolean;
|
||||
custom_upcoming_length: null | string;
|
||||
name: null | string;
|
||||
};
|
||||
|
||||
@@ -67,6 +70,11 @@ export type ScheduleEditFormDispatch =
|
||||
field: 'posts_transaction';
|
||||
value: boolean;
|
||||
}
|
||||
| {
|
||||
type: 'set-field';
|
||||
field: 'custom_upcoming_length';
|
||||
value: string | null;
|
||||
}
|
||||
| {
|
||||
type: 'set-repeats';
|
||||
repeats: boolean;
|
||||
@@ -309,57 +317,146 @@ export function ScheduleEditForm({
|
||||
</View>
|
||||
</SpaceBetween>
|
||||
|
||||
<SpaceBetween
|
||||
style={{
|
||||
marginTop: 20,
|
||||
display: isNarrowWidth ? 'grid' : 'flex',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
}}
|
||||
>
|
||||
<FormField style={{ flex: 1 }}>
|
||||
<FormLabel
|
||||
title={t('Upcoming length')}
|
||||
htmlFor="upcoming-length-field"
|
||||
/>
|
||||
{(() => {
|
||||
const presetValues = ['1', '7', '14', 'oneMonth', 'currentMonth'];
|
||||
const isPreset =
|
||||
fields.custom_upcoming_length != null &&
|
||||
presetValues.includes(fields.custom_upcoming_length);
|
||||
const isCustomValue =
|
||||
fields.custom_upcoming_length != null && !isPreset;
|
||||
const isValidCustomFormat =
|
||||
fields.custom_upcoming_length != null &&
|
||||
/^[1-9]\d*-(day|week|month|year)$/.test(
|
||||
fields.custom_upcoming_length,
|
||||
);
|
||||
const safeCustomValue =
|
||||
isCustomValue && isValidCustomFormat
|
||||
? fields.custom_upcoming_length
|
||||
: '1-day';
|
||||
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
id="upcoming-length-field"
|
||||
options={[
|
||||
['', t('Use global default')],
|
||||
['1', t('1 day')],
|
||||
['7', t('1 week')],
|
||||
['14', t('2 weeks')],
|
||||
['oneMonth', t('1 month')],
|
||||
['currentMonth', t('End of the current month')],
|
||||
['custom', t('Custom length')],
|
||||
]}
|
||||
value={
|
||||
fields.custom_upcoming_length == null
|
||||
? ''
|
||||
: isPreset
|
||||
? fields.custom_upcoming_length
|
||||
: 'custom'
|
||||
}
|
||||
onChange={value => {
|
||||
dispatch({
|
||||
type: 'set-field',
|
||||
field: 'custom_upcoming_length',
|
||||
value:
|
||||
value === ''
|
||||
? null
|
||||
: value === 'custom'
|
||||
? '1-day'
|
||||
: value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{isCustomValue && (
|
||||
<CustomUpcomingLength
|
||||
tempValue={safeCustomValue}
|
||||
onChange={value => {
|
||||
dispatch({
|
||||
type: 'set-field',
|
||||
field: 'custom_upcoming_length',
|
||||
value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
<Text
|
||||
style={{
|
||||
color: theme.pageTextLight,
|
||||
fontSize: 12,
|
||||
marginTop: 3,
|
||||
}}
|
||||
>
|
||||
<Trans>
|
||||
How far in advance this schedule appears as upcoming
|
||||
</Trans>
|
||||
</Text>
|
||||
</FormField>
|
||||
|
||||
<FormField style={{ flex: 1 }}>
|
||||
<FormLabel title=" " />
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
userSelect: 'none',
|
||||
minHeight: 28,
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
id="form_posts_transaction"
|
||||
checked={fields.posts_transaction}
|
||||
onChange={e => {
|
||||
dispatch({
|
||||
type: 'set-field',
|
||||
field: 'posts_transaction',
|
||||
value: e.target.checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<label
|
||||
htmlFor="form_posts_transaction"
|
||||
style={{ userSelect: 'none' }}
|
||||
>
|
||||
<Trans>Automatically add transaction</Trans>
|
||||
</label>
|
||||
</View>
|
||||
<Text
|
||||
style={{
|
||||
color: theme.pageTextLight,
|
||||
fontSize: 12,
|
||||
marginTop: 3,
|
||||
}}
|
||||
>
|
||||
<Trans>
|
||||
Automatically create transactions in the specified account
|
||||
</Trans>
|
||||
</Text>
|
||||
</FormField>
|
||||
</SpaceBetween>
|
||||
|
||||
<SpaceBetween
|
||||
gap={5}
|
||||
direction="vertical"
|
||||
align="end"
|
||||
style={{ marginTop: 10 }}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
marginTop: 5,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
userSelect: 'none',
|
||||
justifyContent: 'flex-end',
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
id="form_posts_transaction"
|
||||
checked={fields.posts_transaction}
|
||||
onChange={e => {
|
||||
dispatch({
|
||||
type: 'set-field',
|
||||
field: 'posts_transaction',
|
||||
value: e.target.checked,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<label
|
||||
htmlFor="form_posts_transaction"
|
||||
style={{ userSelect: 'none' }}
|
||||
>
|
||||
<Trans>Automatically add transaction</Trans>
|
||||
</label>
|
||||
</View>
|
||||
|
||||
<Text
|
||||
style={{
|
||||
maxWidth: 350,
|
||||
textAlign: 'right',
|
||||
color: theme.pageTextLight,
|
||||
fontSize: 13,
|
||||
lineHeight: '1.4em',
|
||||
}}
|
||||
>
|
||||
<Trans>
|
||||
If checked, the schedule will automatically create transactions
|
||||
for you in the specified account
|
||||
</Trans>
|
||||
</Text>
|
||||
|
||||
{!adding && schedule.rule && (
|
||||
<SpaceBetween style={{ marginTop: 20, alignItems: 'center' }}>
|
||||
<SpaceBetween style={{ marginTop: 10, alignItems: 'center' }}>
|
||||
{isCustom && (
|
||||
<Text
|
||||
style={{
|
||||
|
||||
@@ -127,6 +127,7 @@ export function ScheduleEditModal({ id, transaction }: ScheduleEditModalProps) {
|
||||
schedule: {
|
||||
id: state.schedule.id,
|
||||
posts_transaction: state.fields.posts_transaction,
|
||||
custom_upcoming_length: state.fields.custom_upcoming_length,
|
||||
name: state.fields.name,
|
||||
},
|
||||
conditions,
|
||||
|
||||
@@ -59,6 +59,11 @@ type ScheduleEditAction =
|
||||
field: 'posts_transaction';
|
||||
value: boolean;
|
||||
}
|
||||
| {
|
||||
type: 'set-field';
|
||||
field: 'custom_upcoming_length';
|
||||
value: string | null;
|
||||
}
|
||||
| {
|
||||
type: 'set-transactions';
|
||||
transactions: TransactionEntity[];
|
||||
@@ -109,6 +114,8 @@ function createScheduleEditReducer(useGetScheduledAmount: boolean = false) {
|
||||
amountOp: schedule._amountOp || 'isapprox',
|
||||
date: schedule._date ?? null,
|
||||
posts_transaction: action.schedule.posts_transaction ?? false,
|
||||
custom_upcoming_length:
|
||||
action.schedule.custom_upcoming_length ?? null,
|
||||
name: schedule.name ?? null,
|
||||
},
|
||||
};
|
||||
@@ -255,6 +262,7 @@ export function useScheduleEdit({
|
||||
amountOp: null,
|
||||
date: null,
|
||||
posts_transaction: false,
|
||||
custom_upcoming_length: null,
|
||||
name: null,
|
||||
},
|
||||
transactions: [],
|
||||
|
||||
@@ -42,7 +42,7 @@ function loadStatuses(
|
||||
s.next_date,
|
||||
s.completed,
|
||||
hasTrans.has(s.id),
|
||||
upcomingLength,
|
||||
s.custom_upcoming_length ?? upcomingLength,
|
||||
),
|
||||
]),
|
||||
) as ScheduleStatuses;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
ALTER TABLE schedules ADD COLUMN custom_upcoming_length TEXT DEFAULT NULL;
|
||||
|
||||
COMMIT;
|
||||
@@ -104,6 +104,7 @@ export const schema = {
|
||||
next_date: f('date'),
|
||||
completed: f('boolean'),
|
||||
posts_transaction: f('boolean'),
|
||||
custom_upcoming_length: f('string'),
|
||||
tombstone: f('boolean'),
|
||||
|
||||
// These are special fields that are actually pulled from the
|
||||
|
||||
@@ -120,6 +120,7 @@ export type DbSchedule = {
|
||||
active: 1 | 0;
|
||||
completed: 1 | 0;
|
||||
posts_transaction: 1 | 0;
|
||||
custom_upcoming_length: string | null;
|
||||
tombstone: 1 | 0;
|
||||
};
|
||||
|
||||
@@ -323,6 +324,7 @@ export type DbViewSchedule = {
|
||||
active: DbSchedule['active'];
|
||||
completed: DbSchedule['completed'];
|
||||
posts_transaction: DbSchedule['posts_transaction'];
|
||||
custom_upcoming_length: DbSchedule['custom_upcoming_length'];
|
||||
tombstone: DbSchedule['tombstone'];
|
||||
_payee: DbPayeeMapping['targetId'];
|
||||
_account: DbAccount['id'];
|
||||
|
||||
@@ -539,7 +539,7 @@ async function advanceSchedulesService(syncSuccess) {
|
||||
schedule.next_date,
|
||||
schedule.completed,
|
||||
hasTrans.has(schedule.id),
|
||||
upcomingLength[0]?.value ?? '7',
|
||||
schedule.custom_upcoming_length ?? upcomingLength[0]?.value ?? '7',
|
||||
);
|
||||
|
||||
if (status === 'paid') {
|
||||
|
||||
@@ -495,14 +495,16 @@ export function computeSchedulePreviewTransactions(
|
||||
|
||||
const today = d.startOfDay(monthUtils.parseDate(monthUtils.currentDay()));
|
||||
|
||||
const upcomingPeriodEnd = d.startOfDay(
|
||||
monthUtils.parseDate(
|
||||
monthUtils.addDays(today, getUpcomingDays(upcomingLength)),
|
||||
),
|
||||
);
|
||||
|
||||
return schedulesForPreview
|
||||
.flatMap(schedule => {
|
||||
const effectiveUpcomingLength =
|
||||
schedule.custom_upcoming_length ?? upcomingLength;
|
||||
const upcomingPeriodEnd = d.startOfDay(
|
||||
monthUtils.parseDate(
|
||||
monthUtils.addDays(today, getUpcomingDays(effectiveUpcomingLength)),
|
||||
),
|
||||
);
|
||||
|
||||
const { date: dateConditions } = extractScheduleConds(
|
||||
schedule._conditions,
|
||||
);
|
||||
|
||||
@@ -26,6 +26,7 @@ export type ScheduleEntity = {
|
||||
next_date: string;
|
||||
completed: boolean;
|
||||
posts_transaction: boolean;
|
||||
custom_upcoming_length?: string | null;
|
||||
tombstone: boolean;
|
||||
|
||||
// These are special fields that are actually pulled from the
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [jreniel]
|
||||
---
|
||||
|
||||
Add per-schedule custom upcoming length override, allowing each schedule to have its own "upcoming" notification window instead of using only the global setting.
|
||||
Reference in New Issue
Block a user