mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-09 03:32:54 -05:00
Add limit/refill automation types (#6692)
* Add limit/refill automation components * Add release note * Fix typecheck * Rabbit PR feedback * Review
This commit is contained in:
committed by
GitHub
parent
a68b2acac3
commit
cfc18c240a
@@ -3,6 +3,7 @@ import { useMemo, useReducer, useRef, useState } from 'react';
|
||||
import { SpaceBetween } from '@actual-app/components/space-between';
|
||||
import type { CSSProperties } from '@actual-app/components/styles';
|
||||
|
||||
import { firstDayOfMonth } from 'loot-core/shared/months';
|
||||
import type {
|
||||
CategoryGroupEntity,
|
||||
ScheduleEntity,
|
||||
@@ -11,6 +12,7 @@ import type { Template } from 'loot-core/types/models/templates';
|
||||
|
||||
import { BudgetAutomationEditor } from './BudgetAutomationEditor';
|
||||
import { BudgetAutomationReadOnly } from './BudgetAutomationReadOnly';
|
||||
import type { DisplayTemplateType } from './constants';
|
||||
import { DEFAULT_PRIORITY, getInitialState, templateReducer } from './reducer';
|
||||
|
||||
import { useEffectAfterMount } from '@desktop-client/hooks/useEffectAfterMount';
|
||||
@@ -19,17 +21,24 @@ type BudgetAutomationProps = {
|
||||
categories: CategoryGroupEntity[];
|
||||
schedules: readonly ScheduleEntity[];
|
||||
template?: Template;
|
||||
onSave?: (template: Template) => void;
|
||||
onSave?: (template: Template, displayType: DisplayTemplateType) => void;
|
||||
onDelete?: () => void;
|
||||
style?: CSSProperties;
|
||||
readOnlyStyle?: CSSProperties;
|
||||
inline?: boolean;
|
||||
hasLimitAutomation?: boolean;
|
||||
onAddLimitAutomation?: () => void;
|
||||
};
|
||||
|
||||
const DEFAULT_TEMPLATE: Template = {
|
||||
directive: 'template',
|
||||
type: 'simple',
|
||||
monthly: 0,
|
||||
type: 'periodic',
|
||||
amount: 0,
|
||||
period: {
|
||||
period: 'month',
|
||||
amount: 1,
|
||||
},
|
||||
starting: firstDayOfMonth(new Date()),
|
||||
priority: DEFAULT_PRIORITY,
|
||||
};
|
||||
|
||||
@@ -42,18 +51,19 @@ export const BudgetAutomation = ({
|
||||
style,
|
||||
template,
|
||||
inline = false,
|
||||
hasLimitAutomation,
|
||||
onAddLimitAutomation,
|
||||
}: BudgetAutomationProps) => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const [state, dispatch] = useReducer(
|
||||
templateReducer,
|
||||
const [state, dispatch] = useReducer(templateReducer, null, () =>
|
||||
getInitialState(template ?? DEFAULT_TEMPLATE),
|
||||
);
|
||||
|
||||
const onSaveRef = useRef(onSave);
|
||||
onSaveRef.current = onSave;
|
||||
useEffectAfterMount(() => {
|
||||
onSaveRef.current?.(state.template);
|
||||
onSaveRef.current?.(state.template, state.displayType);
|
||||
}, [state]);
|
||||
|
||||
const categoryNameMap = useMemo(() => {
|
||||
@@ -91,6 +101,8 @@ export const BudgetAutomation = ({
|
||||
dispatch={dispatch}
|
||||
schedules={schedules}
|
||||
categories={categories}
|
||||
hasLimitAutomation={hasLimitAutomation}
|
||||
onAddLimitAutomation={onAddLimitAutomation}
|
||||
/>
|
||||
)}
|
||||
</SpaceBetween>
|
||||
|
||||
@@ -18,9 +18,10 @@ import type { Action } from './actions';
|
||||
import { displayTemplateTypes } from './constants';
|
||||
import type { ReducerState } from './constants';
|
||||
import { HistoricalAutomation } from './editor/HistoricalAutomation';
|
||||
import { LimitAutomation } from './editor/LimitAutomation';
|
||||
import { PercentageAutomation } from './editor/PercentageAutomation';
|
||||
import { RefillAutomation } from './editor/RefillAutomation';
|
||||
import { ScheduleAutomation } from './editor/ScheduleAutomation';
|
||||
import { SimpleAutomation } from './editor/SimpleAutomation';
|
||||
import { WeekAutomation } from './editor/WeekAutomation';
|
||||
|
||||
import {
|
||||
@@ -35,10 +36,25 @@ type BudgetAutomationEditorProps = {
|
||||
dispatch: (action: Action) => void;
|
||||
schedules: readonly ScheduleEntity[];
|
||||
categories: CategoryGroupEntity[];
|
||||
hasLimitAutomation?: boolean;
|
||||
onAddLimitAutomation?: () => void;
|
||||
};
|
||||
|
||||
const displayTypeToDescription = {
|
||||
simple: <Trans>Add a fixed amount to this category each month.</Trans>,
|
||||
limit: (
|
||||
<Trans>
|
||||
Set a cap for all budget contributions to this category across all
|
||||
automations. The maximum can be set on a monthly, weekly, or daily basis.
|
||||
For example, a $100 weekly cap would result in a $400 monthly cap ($500
|
||||
depending on the month).
|
||||
</Trans>
|
||||
),
|
||||
refill: (
|
||||
<Trans>
|
||||
Refill the category up to the balance limit set by the balance limit
|
||||
automation.
|
||||
</Trans>
|
||||
),
|
||||
week: (
|
||||
<Trans>
|
||||
Add a fixed amount to this category for each week in the month. For
|
||||
@@ -77,14 +93,24 @@ export function BudgetAutomationEditor({
|
||||
dispatch,
|
||||
schedules,
|
||||
categories,
|
||||
hasLimitAutomation = false,
|
||||
onAddLimitAutomation,
|
||||
}: BudgetAutomationEditorProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
let automationEditor: ReactNode;
|
||||
switch (state.displayType) {
|
||||
case 'simple':
|
||||
case 'limit':
|
||||
automationEditor = (
|
||||
<SimpleAutomation template={state.template} dispatch={dispatch} />
|
||||
<LimitAutomation template={state.template} dispatch={dispatch} />
|
||||
);
|
||||
break;
|
||||
case 'refill':
|
||||
automationEditor = (
|
||||
<RefillAutomation
|
||||
hasLimitAutomation={hasLimitAutomation}
|
||||
onAddLimitAutomation={onAddLimitAutomation}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case 'week':
|
||||
@@ -116,6 +142,7 @@ export function BudgetAutomationEditor({
|
||||
);
|
||||
break;
|
||||
default:
|
||||
state satisfies never;
|
||||
automationEditor = (
|
||||
<Text>
|
||||
<Trans>Unrecognized automation type.</Trans>
|
||||
|
||||
@@ -15,9 +15,10 @@ import { View } from '@actual-app/components/view';
|
||||
|
||||
import type { ReducerState } from './constants';
|
||||
import { HistoricalAutomationReadOnly } from './editor/HistoricalAutomationReadOnly';
|
||||
import { LimitAutomationReadOnly } from './editor/LimitAutomationReadOnly';
|
||||
import { PercentageAutomationReadOnly } from './editor/PercentageAutomationReadOnly';
|
||||
import { RefillAutomationReadOnly } from './editor/RefillAutomationReadOnly';
|
||||
import { ScheduleAutomationReadOnly } from './editor/ScheduleAutomationReadOnly';
|
||||
import { SimpleAutomationReadOnly } from './editor/SimpleAutomationReadOnly';
|
||||
import { WeekAutomationReadOnly } from './editor/WeekAutomationReadOnly';
|
||||
|
||||
type BudgetAutomationReadOnlyProps = {
|
||||
@@ -43,11 +44,14 @@ export function BudgetAutomationReadOnly({
|
||||
|
||||
let automationReadOnly;
|
||||
switch (state.displayType) {
|
||||
case 'simple':
|
||||
case 'limit':
|
||||
automationReadOnly = (
|
||||
<SimpleAutomationReadOnly template={state.template} />
|
||||
<LimitAutomationReadOnly template={state.template} />
|
||||
);
|
||||
break;
|
||||
case 'refill':
|
||||
automationReadOnly = <RefillAutomationReadOnly />;
|
||||
break;
|
||||
case 'week':
|
||||
automationReadOnly = <WeekAutomationReadOnly template={state.template} />;
|
||||
break;
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import type {
|
||||
AverageTemplate,
|
||||
CopyTemplate,
|
||||
LimitTemplate,
|
||||
PercentageTemplate,
|
||||
PeriodicTemplate,
|
||||
RefillTemplate,
|
||||
ScheduleTemplate,
|
||||
SimpleTemplate,
|
||||
} from 'loot-core/types/models/templates';
|
||||
|
||||
export const displayTemplateTypes = [
|
||||
['simple', 'Fixed (monthly)'] as const,
|
||||
['limit', 'Balance limit'] as const,
|
||||
['refill', 'Refill'] as const,
|
||||
['week', 'Fixed (weekly)'] as const,
|
||||
['schedule', 'Schedule'] as const,
|
||||
['schedule', 'Existing schedule'] as const,
|
||||
['percentage', 'Percent of category'] as const,
|
||||
['historical', 'Copy past budgets'] as const,
|
||||
];
|
||||
@@ -19,8 +21,12 @@ export type DisplayTemplateType = (typeof displayTemplateTypes)[number][0];
|
||||
|
||||
export type ReducerState =
|
||||
| {
|
||||
template: SimpleTemplate;
|
||||
displayType: 'simple';
|
||||
template: LimitTemplate;
|
||||
displayType: 'limit';
|
||||
}
|
||||
| {
|
||||
template: RefillTemplate;
|
||||
displayType: 'refill';
|
||||
}
|
||||
| {
|
||||
template: PeriodicTemplate;
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Select } from '@actual-app/components/select';
|
||||
import { SpaceBetween } from '@actual-app/components/space-between';
|
||||
import { theme } from '@actual-app/components/theme';
|
||||
import { View } from '@actual-app/components/view';
|
||||
import { css } from '@emotion/css';
|
||||
import { getDay } from 'date-fns/getDay';
|
||||
import { setDay } from 'date-fns/setDay';
|
||||
|
||||
import { currentDate, dayFromDate, parseDate } from 'loot-core/shared/months';
|
||||
import { amountToInteger, integerToAmount } from 'loot-core/shared/util';
|
||||
import type { LimitTemplate } from 'loot-core/types/models/templates';
|
||||
|
||||
import { updateTemplate } from '@desktop-client/components/budget/goals/actions';
|
||||
import type { Action } from '@desktop-client/components/budget/goals/actions';
|
||||
import { FormField, FormLabel } from '@desktop-client/components/forms';
|
||||
import { AmountInput } from '@desktop-client/components/util/AmountInput';
|
||||
import { useDaysOfWeek } from '@desktop-client/hooks/useDaysOfWeek';
|
||||
import { useFormat } from '@desktop-client/hooks/useFormat';
|
||||
|
||||
type LimitAutomationProps = {
|
||||
template: LimitTemplate;
|
||||
dispatch: (action: Action) => void;
|
||||
};
|
||||
|
||||
export const LimitAutomation = ({
|
||||
template,
|
||||
dispatch,
|
||||
}: LimitAutomationProps) => {
|
||||
const { t } = useTranslation();
|
||||
const format = useFormat();
|
||||
const daysOfWeek = useDaysOfWeek();
|
||||
|
||||
const period = template.period;
|
||||
const amount = amountToInteger(
|
||||
template.amount,
|
||||
format.currency.decimalPlaces,
|
||||
);
|
||||
const start = template.start;
|
||||
const dayOfWeek = start ? getDay(parseDate(start)) : 0;
|
||||
const hold = template.hold;
|
||||
|
||||
const selectButtonClassName = css({
|
||||
'&[data-hovered]': {
|
||||
backgroundColor: theme.buttonNormalBackgroundHover,
|
||||
},
|
||||
});
|
||||
|
||||
const weekdayField = (
|
||||
<FormField style={{ flex: 1 }}>
|
||||
<FormLabel title={t('Weekday')} htmlFor="weekday-field" />
|
||||
|
||||
<Select
|
||||
id="weekday-field"
|
||||
value={dayOfWeek.toString()}
|
||||
onChange={value =>
|
||||
dispatch(
|
||||
updateTemplate({
|
||||
type: 'limit',
|
||||
start: dayFromDate(setDay(currentDate(), Number(value))),
|
||||
}),
|
||||
)
|
||||
}
|
||||
options={Object.entries(daysOfWeek)}
|
||||
className={selectButtonClassName}
|
||||
/>
|
||||
</FormField>
|
||||
);
|
||||
|
||||
const amountField = (
|
||||
<FormField key="amount-field" style={{ flex: 1 }}>
|
||||
<FormLabel title={t('Amount')} htmlFor="amount-field" />
|
||||
<AmountInput
|
||||
id="amount-field"
|
||||
value={amount}
|
||||
zeroSign="+"
|
||||
onUpdate={(value: number) =>
|
||||
dispatch(
|
||||
updateTemplate({
|
||||
type: 'limit',
|
||||
amount: integerToAmount(value, format.currency.decimalPlaces),
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SpaceBetween align="center" gap={10} style={{ marginTop: 10 }}>
|
||||
<FormField key="cadence-field" style={{ flex: 1 }}>
|
||||
<FormLabel title={t('Cadence')} htmlFor="cadence-field" />
|
||||
|
||||
<Select
|
||||
id="cadence-field"
|
||||
value={period}
|
||||
onChange={cadence =>
|
||||
dispatch(updateTemplate({ type: 'limit', period: cadence }))
|
||||
}
|
||||
options={[
|
||||
['daily', t('Daily')],
|
||||
['weekly', t('Weekly')],
|
||||
['monthly', t('Monthly')],
|
||||
]}
|
||||
className={selectButtonClassName}
|
||||
/>
|
||||
</FormField>
|
||||
{period === 'weekly' ? weekdayField : amountField}
|
||||
</SpaceBetween>
|
||||
|
||||
<SpaceBetween align="center" gap={10} style={{ marginTop: 10 }}>
|
||||
{period === 'weekly' && amountField}
|
||||
<FormField key="excess-funds-field" style={{ flex: 1 }}>
|
||||
<FormLabel
|
||||
title={t('Excess funds mode')}
|
||||
htmlFor="excess-funds-field"
|
||||
/>
|
||||
|
||||
<Select
|
||||
id="excess-funds-field"
|
||||
value={hold}
|
||||
onChange={value =>
|
||||
dispatch(updateTemplate({ type: 'limit', hold: value }))
|
||||
}
|
||||
options={[
|
||||
[false, t('Remove all funds over the limit')],
|
||||
[true, t('Retain any funds over the limit')],
|
||||
]}
|
||||
className={selectButtonClassName}
|
||||
/>
|
||||
</FormField>
|
||||
{period !== 'weekly' && <View style={{ flex: 1 }} />}
|
||||
</SpaceBetween>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
import { amountToInteger } from 'loot-core/shared/util';
|
||||
import type { LimitTemplate } from 'loot-core/types/models/templates';
|
||||
|
||||
import { useFormat } from '@desktop-client/hooks/useFormat';
|
||||
|
||||
type LimitAutomationReadOnlyProps = {
|
||||
template: LimitTemplate;
|
||||
};
|
||||
|
||||
export const LimitAutomationReadOnly = ({
|
||||
template,
|
||||
}: LimitAutomationReadOnlyProps) => {
|
||||
const format = useFormat();
|
||||
|
||||
const period = template.period;
|
||||
const amount = format(
|
||||
amountToInteger(template.amount, format.currency.decimalPlaces),
|
||||
'financial',
|
||||
);
|
||||
const hold = template.hold;
|
||||
|
||||
switch (period) {
|
||||
case 'daily':
|
||||
return hold ? (
|
||||
<Trans>Set a balance limit of {{ daily: amount }}/day (soft cap)</Trans>
|
||||
) : (
|
||||
<Trans>Set a balance limit of {{ daily: amount }}/day (hard cap)</Trans>
|
||||
);
|
||||
case 'weekly':
|
||||
return hold ? (
|
||||
<Trans>
|
||||
Set a balance limit of {{ weekly: amount }}/week (soft cap)
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
Set a balance limit of {{ weekly: amount }}/week (hard cap)
|
||||
</Trans>
|
||||
);
|
||||
case 'monthly':
|
||||
return hold ? (
|
||||
<Trans>
|
||||
Set a balance limit of {{ monthly: amount }}/month (soft cap)
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
Set a balance limit of {{ monthly: amount }}/month (hard cap)
|
||||
</Trans>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import { Button } from '@actual-app/components/button';
|
||||
import { SpaceBetween } from '@actual-app/components/space-between';
|
||||
import { Text } from '@actual-app/components/text';
|
||||
import { View } from '@actual-app/components/view';
|
||||
|
||||
import { Warning } from '@desktop-client/components/alerts';
|
||||
|
||||
type RefillAutomationProps = {
|
||||
hasLimitAutomation: boolean;
|
||||
onAddLimitAutomation?: () => void;
|
||||
};
|
||||
|
||||
export function RefillAutomation({
|
||||
hasLimitAutomation,
|
||||
onAddLimitAutomation,
|
||||
}: RefillAutomationProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<SpaceBetween direction="vertical" gap={10} style={{ marginTop: 10 }}>
|
||||
<Text>
|
||||
<Trans>Uses the balance limit automation for this category.</Trans>
|
||||
</Text>
|
||||
{!hasLimitAutomation && (
|
||||
<Warning>
|
||||
<SpaceBetween gap={10} align="center" style={{ flexWrap: 'wrap' }}>
|
||||
<View>
|
||||
<Trans>
|
||||
Add a balance limit automation to set the refill target.
|
||||
</Trans>
|
||||
</View>
|
||||
{onAddLimitAutomation && (
|
||||
<Button
|
||||
variant="bare"
|
||||
onPress={onAddLimitAutomation}
|
||||
aria-label={t('Add balance limit automation')}
|
||||
>
|
||||
<Trans>Add balance limit</Trans>
|
||||
</Button>
|
||||
)}
|
||||
</SpaceBetween>
|
||||
</Warning>
|
||||
)}
|
||||
</SpaceBetween>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
export const RefillAutomationReadOnly = () => {
|
||||
return <Trans>Refill to balance limit</Trans>;
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { SimpleTemplate } from 'loot-core/types/models/templates';
|
||||
|
||||
import { updateTemplate } from '@desktop-client/components/budget/goals/actions';
|
||||
import type { Action } from '@desktop-client/components/budget/goals/actions';
|
||||
import { FormField, FormLabel } from '@desktop-client/components/forms';
|
||||
import { AmountInput } from '@desktop-client/components/util/AmountInput';
|
||||
|
||||
type SimpleAutomationProps = {
|
||||
template: SimpleTemplate;
|
||||
dispatch: (action: Action) => void;
|
||||
};
|
||||
|
||||
export const SimpleAutomation = ({
|
||||
template,
|
||||
dispatch,
|
||||
}: SimpleAutomationProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<FormField>
|
||||
<FormLabel title={t('Amount')} htmlFor="amount-field" />
|
||||
<AmountInput
|
||||
id="amount-field"
|
||||
key="amount-input"
|
||||
value={template.monthly ?? 0}
|
||||
zeroSign="+"
|
||||
onUpdate={(value: number) =>
|
||||
dispatch(
|
||||
updateTemplate({
|
||||
type: 'simple',
|
||||
monthly: value,
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
);
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
import type { SimpleTemplate } from 'loot-core/types/models/templates';
|
||||
import type { TransObjectLiteral } from 'loot-core/types/util';
|
||||
|
||||
import { FinancialText } from '@desktop-client/components/FinancialText';
|
||||
import { useFormat } from '@desktop-client/hooks/useFormat';
|
||||
|
||||
type SimpleAutomationReadOnlyProps = {
|
||||
template: SimpleTemplate;
|
||||
};
|
||||
|
||||
export const SimpleAutomationReadOnly = ({
|
||||
template,
|
||||
}: SimpleAutomationReadOnlyProps) => {
|
||||
const format = useFormat();
|
||||
return (
|
||||
<Trans>
|
||||
Budget{' '}
|
||||
<FinancialText>
|
||||
{
|
||||
{
|
||||
amount: format(template.monthly ?? 0, 'financial'),
|
||||
} as TransObjectLiteral
|
||||
}
|
||||
</FinancialText>{' '}
|
||||
each month
|
||||
</Trans>
|
||||
);
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { firstDayOfMonth } from 'loot-core/shared/months';
|
||||
import type { Template } from 'loot-core/types/models/templates';
|
||||
|
||||
import type { Action } from './actions';
|
||||
@@ -13,8 +14,18 @@ export const getInitialState = (template: Template | null): ReducerState => {
|
||||
switch (type) {
|
||||
case 'simple':
|
||||
return {
|
||||
template,
|
||||
displayType: 'simple',
|
||||
template: {
|
||||
type: 'periodic',
|
||||
amount: template.monthly ?? 0,
|
||||
period: {
|
||||
period: 'month',
|
||||
amount: 1,
|
||||
},
|
||||
starting: firstDayOfMonth(new Date()),
|
||||
priority: template.priority,
|
||||
directive: template.directive,
|
||||
},
|
||||
displayType: 'week',
|
||||
};
|
||||
case 'percentage':
|
||||
return {
|
||||
@@ -37,9 +48,15 @@ export const getInitialState = (template: Template | null): ReducerState => {
|
||||
case 'remainder':
|
||||
throw new Error('Remainder is not yet supported');
|
||||
case 'limit':
|
||||
throw new Error('Limit is not yet supported');
|
||||
return {
|
||||
template,
|
||||
displayType: 'limit',
|
||||
};
|
||||
case 'refill':
|
||||
throw new Error('Refill is not yet supported');
|
||||
return {
|
||||
template,
|
||||
displayType: 'refill',
|
||||
};
|
||||
case 'average':
|
||||
case 'copy':
|
||||
return {
|
||||
@@ -60,16 +77,30 @@ const changeType = (
|
||||
visualType: DisplayTemplateType,
|
||||
): ReducerState => {
|
||||
switch (visualType) {
|
||||
case 'simple':
|
||||
if (prevState.template.type === 'simple') {
|
||||
case 'limit':
|
||||
if (prevState.template.type === 'limit') {
|
||||
return prevState;
|
||||
}
|
||||
return {
|
||||
displayType: visualType,
|
||||
template: {
|
||||
directive: 'template',
|
||||
type: 'simple',
|
||||
monthly: 5,
|
||||
type: 'limit',
|
||||
amount: 500,
|
||||
period: 'monthly',
|
||||
hold: false,
|
||||
priority: null,
|
||||
},
|
||||
};
|
||||
case 'refill':
|
||||
if (prevState.template.type === 'refill') {
|
||||
return prevState;
|
||||
}
|
||||
return {
|
||||
displayType: visualType,
|
||||
template: {
|
||||
directive: 'template',
|
||||
type: 'refill',
|
||||
priority: DEFAULT_PRIORITY,
|
||||
},
|
||||
};
|
||||
@@ -187,13 +218,10 @@ function mapTemplateTypesForUpdate(
|
||||
}
|
||||
|
||||
if (state.template.type === template.type) {
|
||||
const { type: _1, directive: _2, ...rest } = template;
|
||||
const mergedTemplate = Object.assign({}, state.template, template);
|
||||
return {
|
||||
...state,
|
||||
...getInitialState({
|
||||
...state.template,
|
||||
...rest,
|
||||
}),
|
||||
...getInitialState(mergedTemplate),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -207,7 +235,8 @@ export const templateReducer = (
|
||||
state: ReducerState,
|
||||
action: Action,
|
||||
): ReducerState => {
|
||||
switch (action.type) {
|
||||
const type = action.type;
|
||||
switch (type) {
|
||||
case 'set-type':
|
||||
return {
|
||||
...state,
|
||||
@@ -221,6 +250,7 @@ export const templateReducer = (
|
||||
case 'update-template':
|
||||
return mapTemplateTypesForUpdate(state, action.payload);
|
||||
default:
|
||||
return state;
|
||||
// Make sure we're not missing any cases
|
||||
throw new Error(`Unknown display type: ${type satisfies never}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -17,28 +17,9 @@ import { Column, Setting } from './UI';
|
||||
import { Checkbox } from '@desktop-client/components/forms';
|
||||
import { useSidebar } from '@desktop-client/components/sidebar/SidebarProvider';
|
||||
import { useDateFormat } from '@desktop-client/hooks/useDateFormat';
|
||||
import { useDaysOfWeek } from '@desktop-client/hooks/useDaysOfWeek';
|
||||
import { useSyncedPref } from '@desktop-client/hooks/useSyncedPref';
|
||||
|
||||
// Follows Pikaday 'firstDay' numbering
|
||||
// https://github.com/Pikaday/Pikaday
|
||||
function useDaysOfWeek() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const daysOfWeek: {
|
||||
value: SyncedPrefs['firstDayOfWeekIdx'];
|
||||
label: string;
|
||||
}[] = [
|
||||
{ value: '0', label: t('Sunday') },
|
||||
{ value: '1', label: t('Monday') },
|
||||
{ value: '2', label: t('Tuesday') },
|
||||
{ value: '3', label: t('Wednesday') },
|
||||
{ value: '4', label: t('Thursday') },
|
||||
{ value: '5', label: t('Friday') },
|
||||
{ value: '6', label: t('Saturday') },
|
||||
] as const;
|
||||
|
||||
return { daysOfWeek };
|
||||
}
|
||||
const dateFormats: { value: SyncedPrefs['dateFormat']; label: string }[] = [
|
||||
{ value: 'MM/dd/yyyy', label: 'MM/DD/YYYY' },
|
||||
{ value: 'dd/MM/yyyy', label: 'DD/MM/YYYY' },
|
||||
@@ -61,7 +42,7 @@ export function FormatSettings() {
|
||||
const numberFormat = _numberFormat || 'comma-dot';
|
||||
const [hideFraction, setHideFractionPref] = useSyncedPref('hideFraction');
|
||||
|
||||
const { daysOfWeek } = useDaysOfWeek();
|
||||
const daysOfWeek = useDaysOfWeek();
|
||||
|
||||
const selectButtonClassName = css({
|
||||
'&[data-hovered]': {
|
||||
@@ -125,7 +106,7 @@ export function FormatSettings() {
|
||||
<Select
|
||||
value={firstDayOfWeekIdx}
|
||||
onChange={idx => setFirstDayOfWeekIdxPref(idx)}
|
||||
options={daysOfWeek.map(f => [f.value, f.label])}
|
||||
options={Object.entries(daysOfWeek)}
|
||||
className={selectButtonClassName}
|
||||
/>
|
||||
</Column>
|
||||
|
||||
19
packages/desktop-client/src/hooks/useDaysOfWeek.ts
Normal file
19
packages/desktop-client/src/hooks/useDaysOfWeek.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
// Follows Pikaday 'firstDay' numbering
|
||||
// https://github.com/Pikaday/Pikaday
|
||||
export function useDaysOfWeek() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const daysOfWeek = {
|
||||
0: t('Sunday'),
|
||||
1: t('Monday'),
|
||||
2: t('Tuesday'),
|
||||
3: t('Wednesday'),
|
||||
4: t('Thursday'),
|
||||
5: t('Friday'),
|
||||
6: t('Saturday'),
|
||||
} satisfies Record<number, string>;
|
||||
|
||||
return daysOfWeek;
|
||||
}
|
||||
6
upcoming-release-notes/6692.md
Normal file
6
upcoming-release-notes/6692.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [jfdoming]
|
||||
---
|
||||
|
||||
Add limit/refill automation editors
|
||||
Reference in New Issue
Block a user