diff --git a/packages/desktop-client/e2e/page-models/account-page.js b/packages/desktop-client/e2e/page-models/account-page.js index c4fb469323d9d9373f74fd726afa5b12f1bc056a..aa23d041867c2c68816d37b343ca4dab18793e43 100644 --- a/packages/desktop-client/e2e/page-models/account-page.js +++ b/packages/desktop-client/e2e/page-models/account-page.js @@ -106,7 +106,7 @@ export class AccountPage { await this.menuButton.click(); await this.page.getByRole('button', { name: 'Close Account' }).click(); return new CloseAccountModal( - this.page.locator('css=[aria-modal]'), + this.page.getByTestId('close-account-modal'), this.page, ); } diff --git a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-cash-flow-graph-and-checks-visuals-1-chromium-linux.png b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-cash-flow-graph-and-checks-visuals-1-chromium-linux.png index 171ae0751a2e5a7e649672bbc41d46f13d599d69..7ee3aa5afa31bb300e22542905fba7cbc6769e66 100644 Binary files a/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-cash-flow-graph-and-checks-visuals-1-chromium-linux.png and b/packages/desktop-client/e2e/reports.test.js-snapshots/Reports-loads-cash-flow-graph-and-checks-visuals-1-chromium-linux.png differ diff --git a/packages/desktop-client/src/components/Modals.tsx b/packages/desktop-client/src/components/Modals.tsx index 01f1505f40f1fbec7ccde907965ff20422477090..07711a2d2a3ce25be823134e3cddf7c8ac21753c 100644 --- a/packages/desktop-client/src/components/Modals.tsx +++ b/packages/desktop-client/src/components/Modals.tsx @@ -1,17 +1,17 @@ // @ts-strict-ignore import React, { useEffect } from 'react'; -import { useSelector } from 'react-redux'; +import { useDispatch } from 'react-redux'; import { useLocation } from 'react-router-dom'; -import { type State } from 'loot-core/src/client/state-types'; +import { closeModal } from 'loot-core/client/actions'; import { type PopModalAction } from 'loot-core/src/client/state-types/modals'; import { send } from 'loot-core/src/platform/client/fetch'; import * as monthUtils from 'loot-core/src/shared/months'; -import { useActions } from '../hooks/useActions'; +import { useModalState } from '../hooks/useModalState'; import { useSyncServerStatus } from '../hooks/useSyncServerStatus'; -import { ModalTitle } from './common/Modal'; +import { ModalTitle, ModalHeader } from './common/Modal2'; import { AccountAutocompleteModal } from './modals/AccountAutocompleteModal'; import { AccountMenuModal } from './modals/AccountMenuModal'; import { BudgetListModal } from './modals/BudgetListModal'; @@ -71,67 +71,43 @@ export type CommonModalProps = { }; export function Modals() { - const modalStack = useSelector((state: State) => state.modals.modalStack); - const isHidden = useSelector((state: State) => state.modals.isHidden); - const actions = useActions(); const location = useLocation(); + const dispatch = useDispatch(); + const { modalStack } = useModalState(); useEffect(() => { if (modalStack.length > 0) { - actions.closeModal(); + dispatch(closeModal()); } }, [location]); const syncServerStatus = useSyncServerStatus(); const modals = modalStack - .map(({ name, options }, idx) => { - const modalProps: CommonModalProps = { - onClose: actions.popModal, - onBack: actions.popModal, - showBack: idx > 0, - isCurrent: idx === modalStack.length - 1, - isHidden, - stackIndex: idx, - }; - + .map(({ name, options }) => { switch (name) { case 'keyboard-shortcuts': - return <KeyboardShortcutModal modalProps={modalProps} />; + return <KeyboardShortcutModal />; case 'import-transactions': - return ( - <ImportTransactions - key={name} - modalProps={modalProps} - options={options} - /> - ); + return <ImportTransactions key={name} options={options} />; case 'add-account': return ( <CreateAccountModal key={name} - modalProps={modalProps} syncServerStatus={syncServerStatus} upgradingAccountId={options?.upgradingAccountId} /> ); case 'add-local-account': - return ( - <CreateLocalAccountModal - key={name} - modalProps={modalProps} - actions={actions} - /> - ); + return <CreateLocalAccountModal key={name} />; case 'close-account': return ( <CloseAccountModal key={name} - modalProps={modalProps} account={options.account} balance={options.balance} canDelete={options.canDelete} @@ -142,10 +118,8 @@ export function Modals() { return ( <SelectLinkedAccounts key={name} - modalProps={modalProps} externalAccounts={options.accounts} requisitionId={options.requisitionId} - actions={actions} syncSource={options.syncSource} /> ); @@ -154,7 +128,6 @@ export function Modals() { return ( <ConfirmCategoryDelete key={name} - modalProps={modalProps} category={options.category} group={options.group} onDelete={options.onDelete} @@ -165,7 +138,6 @@ export function Modals() { return ( <ConfirmUnlinkAccount key={name} - modalProps={modalProps} accountName={options.accountName} onUnlink={options.onUnlink} /> @@ -175,7 +147,6 @@ export function Modals() { return ( <ConfirmTransactionEdit key={name} - modalProps={modalProps} onCancel={options.onCancel} onConfirm={options.onConfirm} confirmReason={options.confirmReason} @@ -186,7 +157,6 @@ export function Modals() { return ( <ConfirmTransactionDelete key={name} - modalProps={modalProps} onConfirm={options.onConfirm} /> ); @@ -197,26 +167,17 @@ export function Modals() { key={name} watchUpdates budgetId={options.budgetId} - modalProps={modalProps} - actions={actions} backupDisabled={false} /> ); case 'manage-rules': - return ( - <ManageRulesModal - key={name} - modalProps={modalProps} - payeeId={options?.payeeId} - /> - ); + return <ManageRulesModal key={name} payeeId={options?.payeeId} />; case 'edit-rule': return ( <EditRule key={name} - modalProps={modalProps} defaultRule={options.rule} onSave={options.onSave} /> @@ -226,7 +187,6 @@ export function Modals() { return ( <MergeUnusedPayees key={name} - modalProps={modalProps} payeeIds={options.payeeIds} targetPayeeId={options.targetPayeeId} /> @@ -234,27 +194,18 @@ export function Modals() { case 'gocardless-init': return ( - <GoCardlessInitialise - key={name} - modalProps={modalProps} - onSuccess={options.onSuccess} - /> + <GoCardlessInitialise key={name} onSuccess={options.onSuccess} /> ); case 'simplefin-init': return ( - <SimpleFinInitialise - key={name} - modalProps={modalProps} - onSuccess={options.onSuccess} - /> + <SimpleFinInitialise key={name} onSuccess={options.onSuccess} /> ); case 'gocardless-external-msg': return ( <GoCardlessExternalMsg key={name} - modalProps={modalProps} onMoveExternal={options.onMoveExternal} onClose={() => { options.onClose?.(); @@ -265,28 +216,15 @@ export function Modals() { ); case 'create-encryption-key': - return ( - <CreateEncryptionKeyModal - key={name} - modalProps={modalProps} - options={options} - /> - ); + return <CreateEncryptionKeyModal key={name} options={options} />; case 'fix-encryption-key': - return ( - <FixEncryptionKeyModal - key={name} - modalProps={modalProps} - options={options} - /> - ); + return <FixEncryptionKeyModal key={name} options={options} />; case 'edit-field': return ( <EditField key={name} - modalProps={modalProps} name={options.name} onSubmit={options.onSubmit} onClose={options.onClose} @@ -297,7 +235,6 @@ export function Modals() { return ( <CategoryAutocompleteModal key={name} - modalProps={modalProps} autocompleteProps={{ value: null, onSelect: options.onSelect, @@ -313,7 +250,6 @@ export function Modals() { return ( <AccountAutocompleteModal key={name} - modalProps={modalProps} autocompleteProps={{ value: null, onSelect: options.onSelect, @@ -327,7 +263,6 @@ export function Modals() { return ( <PayeeAutocompleteModal key={name} - modalProps={modalProps} autocompleteProps={{ value: null, onSelect: options.onSelect, @@ -340,8 +275,13 @@ export function Modals() { return ( <SingleInputModal key={name} - modalProps={modalProps} - title={<ModalTitle title="New Category" shrinkOnOverflow />} + name={name} + Header={props => ( + <ModalHeader + {...props} + title={<ModalTitle title="New Category" shrinkOnOverflow />} + /> + )} inputPlaceholder="Category name" buttonText="Add" onValidate={options.onValidate} @@ -353,8 +293,15 @@ export function Modals() { return ( <SingleInputModal key={name} - modalProps={modalProps} - title={<ModalTitle title="New Category Group" shrinkOnOverflow />} + name={name} + Header={props => ( + <ModalHeader + {...props} + title={ + <ModalTitle title="New Category Group" shrinkOnOverflow /> + } + /> + )} inputPlaceholder="Category group name" buttonText="Add" onValidate={options.onValidate} @@ -370,7 +317,6 @@ export function Modals() { > <RolloverBudgetSummaryModal key={name} - modalProps={modalProps} month={options.month} onBudgetAction={options.onBudgetAction} /> @@ -378,21 +324,13 @@ export function Modals() { ); case 'report-budget-summary': - return ( - <ReportBudgetSummaryModal - key={name} - modalProps={modalProps} - month={options.month} - /> - ); + return <ReportBudgetSummaryModal key={name} month={options.month} />; case 'schedule-edit': return ( <ScheduleDetails key={name} - modalProps={modalProps} id={options?.id || null} - actions={actions} transaction={options?.transaction || null} /> ); @@ -401,36 +339,21 @@ export function Modals() { return ( <ScheduleLink key={name} - modalProps={modalProps} - actions={actions} transactionIds={options?.transactionIds} getTransaction={options?.getTransaction} /> ); case 'schedules-discover': - return ( - <DiscoverSchedules - key={name} - modalProps={modalProps} - actions={actions} - /> - ); + return <DiscoverSchedules key={name} />; case 'schedule-posts-offline-notification': - return ( - <PostsOfflineNotification - key={name} - modalProps={modalProps} - actions={actions} - /> - ); + return <PostsOfflineNotification key={name} />; case 'account-menu': return ( <AccountMenuModal key={name} - modalProps={modalProps} accountId={options.accountId} onSave={options.onSave} onEditNotes={options.onEditNotes} @@ -444,11 +367,11 @@ export function Modals() { return ( <CategoryMenuModal key={name} - modalProps={modalProps} categoryId={options.categoryId} onSave={options.onSave} onEditNotes={options.onEditNotes} onDelete={options.onDelete} + onToggleVisibility={options.onToggleVisibility} onClose={options.onClose} /> ); @@ -460,7 +383,6 @@ export function Modals() { value={monthUtils.sheetForMonth(options.month)} > <RolloverBudgetMenuModal - modalProps={modalProps} categoryId={options.categoryId} onUpdateBudget={options.onUpdateBudget} onCopyLastMonthAverage={options.onCopyLastMonthAverage} @@ -477,7 +399,6 @@ export function Modals() { value={monthUtils.sheetForMonth(options.month)} > <ReportBudgetMenuModal - modalProps={modalProps} categoryId={options.categoryId} onUpdateBudget={options.onUpdateBudget} onCopyLastMonthAverage={options.onCopyLastMonthAverage} @@ -491,13 +412,13 @@ export function Modals() { return ( <CategoryGroupMenuModal key={name} - modalProps={modalProps} groupId={options.groupId} onSave={options.onSave} onAddCategory={options.onAddCategory} onEditNotes={options.onEditNotes} onSaveNotes={options.onSaveNotes} onDelete={options.onDelete} + onToggleVisibility={options.onToggleVisibility} onClose={options.onClose} /> ); @@ -506,7 +427,6 @@ export function Modals() { return ( <NotesModal key={name} - modalProps={modalProps} id={options.id} name={options.name} onSave={options.onSave} @@ -520,7 +440,6 @@ export function Modals() { value={monthUtils.sheetForMonth(options.month)} > <RolloverBalanceMenuModal - modalProps={modalProps} categoryId={options.categoryId} onCarryover={options.onCarryover} onTransfer={options.onTransfer} @@ -536,7 +455,6 @@ export function Modals() { value={monthUtils.sheetForMonth(options.month)} > <RolloverToBudgetMenuModal - modalProps={modalProps} onTransfer={options.onTransfer} onCover={options.onCover} onHoldBuffer={options.onHoldBuffer} @@ -552,7 +470,6 @@ export function Modals() { value={monthUtils.sheetForMonth(options.month)} > <HoldBufferModal - modalProps={modalProps} month={options.month} onSubmit={options.onSubmit} /> @@ -566,7 +483,6 @@ export function Modals() { value={monthUtils.sheetForMonth(options.month)} > <ReportBalanceMenuModal - modalProps={modalProps} categoryId={options.categoryId} onCarryover={options.onCarryover} /> @@ -577,7 +493,6 @@ export function Modals() { return ( <TransferModal key={name} - modalProps={modalProps} title={options.title} month={options.month} amount={options.amount} @@ -590,7 +505,6 @@ export function Modals() { return ( <CoverModal key={name} - modalProps={modalProps} title={options.title} month={options.month} showToBeBudgeted={options.showToBeBudgeted} @@ -602,7 +516,6 @@ export function Modals() { return ( <ScheduledTransactionMenuModal key={name} - modalProps={modalProps} transactionId={options.transactionId} onPost={options.onPost} onSkip={options.onSkip} @@ -613,7 +526,6 @@ export function Modals() { return ( <BudgetPageMenuModal key={name} - modalProps={modalProps} onAddCategoryGroup={options.onAddCategoryGroup} onToggleHiddenCategories={options.onToggleHiddenCategories} onSwitchBudgetFile={options.onSwitchBudgetFile} @@ -627,7 +539,6 @@ export function Modals() { value={monthUtils.sheetForMonth(options.month)} > <RolloverBudgetMonthMenuModal - modalProps={modalProps} month={options.month} onBudgetAction={options.onBudgetAction} onEditNotes={options.onEditNotes} @@ -642,7 +553,6 @@ export function Modals() { value={monthUtils.sheetForMonth(options.month)} > <ReportBudgetMonthMenuModal - modalProps={modalProps} month={options.month} onBudgetAction={options.onBudgetAction} onEditNotes={options.onEditNotes} @@ -651,7 +561,7 @@ export function Modals() { ); case 'budget-list': - return <BudgetListModal key={name} modalProps={modalProps} />; + return <BudgetListModal key={name} />; default: console.error('Unknown modal:', name); diff --git a/packages/desktop-client/src/components/common/Modal.tsx b/packages/desktop-client/src/components/common/Modal.tsx index 802bac03d159a75534c21d85e2d68e273d5ea114..e3a65d694b2adef0384767acd945c5d114c287c6 100644 --- a/packages/desktop-client/src/components/common/Modal.tsx +++ b/packages/desktop-client/src/components/common/Modal.tsx @@ -322,7 +322,8 @@ type ModalButtonsProps = { children: ReactNode; }; -export const ModalButtons = ({ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const ModalButtons = ({ style, leftContent, focusButton = false, @@ -367,7 +368,7 @@ type ModalTitleProps = { shrinkOnOverflow?: boolean; }; -export function ModalTitle({ +function ModalTitle({ title, isEditable, getStyle, @@ -466,7 +467,7 @@ type ModalCloseButtonProps = { style?: CSSProperties; }; -export function ModalCloseButton({ onClick, style }: ModalCloseButtonProps) { +function ModalCloseButton({ onClick, style }: ModalCloseButtonProps) { return ( <Button type="bare" diff --git a/packages/desktop-client/src/components/common/Modal2.tsx b/packages/desktop-client/src/components/common/Modal2.tsx new file mode 100644 index 0000000000000000000000000000000000000000..84fab17c4c2f6c7919c1a854fa53c8e89e4a386b --- /dev/null +++ b/packages/desktop-client/src/components/common/Modal2.tsx @@ -0,0 +1,460 @@ +import React, { + useEffect, + useRef, + useLayoutEffect, + useState, + type ReactNode, + type ComponentPropsWithoutRef, + type ComponentPropsWithRef, +} from 'react'; +import { + ModalOverlay as ReactAriaModalOverlay, + Modal as ReactAriaModal, + Dialog, +} from 'react-aria-components'; +import { useHotkeysContext } from 'react-hotkeys-hook'; + +import { AutoTextSize } from 'auto-text-size'; + +import { useModalState } from '../../hooks/useModalState'; +import { AnimatedLoading } from '../../icons/AnimatedLoading'; +import { SvgLogo } from '../../icons/logo'; +import { SvgDelete } from '../../icons/v0'; +import { type CSSProperties, styles, theme } from '../../style'; +import { tokens } from '../../tokens'; + +import { Button } from './Button'; +import { Input } from './Input'; +import { Text } from './Text'; +import { TextOneLine } from './TextOneLine'; +import { View } from './View'; + +type ModalProps = ComponentPropsWithRef<typeof ReactAriaModal> & { + name: string; + isLoading?: boolean; + noAnimation?: boolean; + style?: CSSProperties; + onClose?: () => void; + containerProps?: { + style?: CSSProperties; + }; +}; + +export const Modal = ({ + name, + isLoading = false, + noAnimation = false, + style, + children, + onClose, + containerProps, + ...props +}: ModalProps) => { + const { enableScope, disableScope } = useHotkeysContext(); + + // This deactivates any key handlers in the "app" scope + useEffect(() => { + enableScope(name); + return () => disableScope(name); + }, [enableScope, disableScope, name]); + + const { isHidden, isActive, onClose: closeModal } = useModalState(); + + const handleOnClose = () => { + closeModal(); + onClose?.(); + }; + + return ( + <ReactAriaModalOverlay + data-testid={`${name}-modal`} + isDismissable + defaultOpen={true} + onOpenChange={isOpen => !isOpen && handleOnClose?.()} + style={{ + position: 'fixed', + inset: 0, + zIndex: 3000, + overflowY: 'auto', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontSize: 14, + ...style, + }} + {...props} + > + <ReactAriaModal> + {modalProps => ( + <Dialog aria-label="Modal dialog"> + <ModalContentContainer + noAnimation={noAnimation} + isActive={isActive(name)} + {...containerProps} + style={{ + flex: 1, + padding: 10, + willChange: 'opacity, transform', + maxWidth: '90vw', + minWidth: '90vw', + maxHeight: '90vh', + minHeight: 0, + borderRadius: 6, + //border: '1px solid ' + theme.modalBorder, + color: theme.pageText, + backgroundColor: theme.modalBackground, + opacity: isHidden ? 0 : 1, + [`@media (min-width: ${tokens.breakpoint_small})`]: { + minWidth: tokens.breakpoint_small, + }, + ...styles.shadowLarge, + ...styles.lightScrollbar, + ...containerProps?.style, + }} + > + <View style={{ paddingTop: 0, flex: 1 }}> + {typeof children === 'function' + ? children(modalProps) + : children} + </View> + {isLoading && ( + <View + style={{ + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: theme.pageBackground, + alignItems: 'center', + justifyContent: 'center', + zIndex: 1000, + }} + > + <AnimatedLoading + style={{ width: 20, height: 20 }} + color={theme.pageText} + /> + </View> + )} + </ModalContentContainer> + </Dialog> + )} + </ReactAriaModal> + </ReactAriaModalOverlay> + ); +}; + +type ModalContentContainerProps = { + style?: CSSProperties; + noAnimation?: boolean; + isActive?: boolean; + children: ReactNode; +}; + +const ModalContentContainer = ({ + style, + noAnimation, + isActive, + children, +}: ModalContentContainerProps) => { + const contentRef = useRef<HTMLDivElement>(null); + const mounted = useRef(false); + const rotateFactor = useRef(Math.random() * 10 - 5); + + useLayoutEffect(() => { + if (!contentRef.current) { + return; + } + + function setProps() { + if (!contentRef.current) { + return; + } + + if (isActive) { + contentRef.current.style.transform = 'translateY(0px) scale(1)'; + contentRef.current.style.pointerEvents = 'auto'; + } else { + contentRef.current.style.transform = `translateY(-40px) scale(.95) rotate(${rotateFactor.current}deg)`; + contentRef.current.style.pointerEvents = 'none'; + } + } + + if (!mounted.current) { + if (noAnimation) { + contentRef.current.style.opacity = '1'; + contentRef.current.style.transform = 'translateY(0px) scale(1)'; + + setTimeout(() => { + if (contentRef.current) { + contentRef.current.style.transition = + 'opacity .1s, transform .1s cubic-bezier(.42, 0, .58, 1)'; + } + }, 0); + } else { + contentRef.current.style.opacity = '0'; + contentRef.current.style.transform = 'translateY(10px) scale(1)'; + + setTimeout(() => { + if (contentRef.current) { + mounted.current = true; + contentRef.current.style.transition = + 'opacity .1s, transform .1s cubic-bezier(.42, 0, .58, 1)'; + contentRef.current.style.opacity = '1'; + setProps(); + } + }, 0); + } + } else { + setProps(); + } + }, [noAnimation, isActive]); + + return ( + <View + innerRef={contentRef} + style={{ + ...style, + ...(noAnimation && !isActive && { display: 'none' }), + }} + > + {children} + </View> + ); +}; + +type ModalButtonsProps = { + style?: CSSProperties; + leftContent?: ReactNode; + focusButton?: boolean; + children: ReactNode; +}; + +export const ModalButtons = ({ + style, + leftContent, + focusButton = false, + children, +}: ModalButtonsProps) => { + const containerRef = useRef<HTMLDivElement>(null); + + useEffect(() => { + if (focusButton && containerRef.current) { + const button = containerRef.current.querySelector<HTMLButtonElement>( + 'button:not([data-hidden])', + ); + + if (button) { + button.focus(); + } + } + }, [focusButton]); + + return ( + <View + innerRef={containerRef} + style={{ + flexDirection: 'row', + marginTop: 30, + ...style, + }} + > + {leftContent} + <View style={{ flex: 1 }} /> + {children} + </View> + ); +}; + +type ModalHeaderProps = { + leftContent?: ReactNode; + showLogo?: boolean; + title?: ReactNode; + rightContent?: ReactNode; +}; + +export function ModalHeader({ + leftContent, + showLogo, + title, + rightContent, +}: ModalHeaderProps) { + return ( + <View + aria-label="Modal header" + style={{ + justifyContent: 'center', + alignItems: 'center', + position: 'relative', + height: 60, + }} + > + <View + style={{ + position: 'absolute', + left: 0, + }} + > + {leftContent} + </View> + + {(title || showLogo) && ( + <View + style={{ + textAlign: 'center', + // We need to force a width for the text-overflow + // ellipses to work because we are aligning center. + width: 'calc(100% - 60px)', + }} + > + {showLogo && ( + <SvgLogo + width={30} + height={30} + style={{ justifyContent: 'center', alignSelf: 'center' }} + /> + )} + {title && + (typeof title === 'string' || typeof title === 'number' ? ( + <ModalTitle title={`${title}`} /> + ) : ( + title + ))} + </View> + )} + + {rightContent && ( + <View + style={{ + position: 'absolute', + right: 0, + }} + > + {rightContent} + </View> + )} + </View> + ); +} + +type ModalTitleProps = { + title: string; + isEditable?: boolean; + getStyle?: (isEditing: boolean) => CSSProperties; + onEdit?: (isEditing: boolean) => void; + onTitleUpdate?: (newName: string) => void; + shrinkOnOverflow?: boolean; +}; + +export function ModalTitle({ + title, + isEditable, + getStyle, + onTitleUpdate, + shrinkOnOverflow = false, +}: ModalTitleProps) { + const [isEditing, setIsEditing] = useState(false); + + const onTitleClick = () => { + if (isEditable) { + setIsEditing(true); + } + }; + + const _onTitleUpdate = (newTitle: string) => { + if (newTitle !== title) { + onTitleUpdate?.(newTitle); + } + setIsEditing(false); + }; + + const inputRef = useRef<HTMLInputElement>(null); + useEffect(() => { + if (isEditing) { + if (inputRef.current) { + inputRef.current.scrollLeft = 0; + } + } + }, [isEditing]); + + const style = getStyle?.(isEditing); + + return isEditing ? ( + <Input + inputRef={inputRef} + style={{ + fontSize: 25, + fontWeight: 700, + textAlign: 'center', + ...style, + }} + focused={isEditing} + defaultValue={title} + onUpdate={_onTitleUpdate} + onKeyDown={e => { + if (e.key === 'Enter') { + e.preventDefault(); + _onTitleUpdate?.(e.currentTarget.value); + } + }} + /> + ) : ( + <View + style={{ + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + }} + > + {shrinkOnOverflow ? ( + <AutoTextSize + as={Text} + minFontSizePx={15} + maxFontSizePx={25} + onClick={onTitleClick} + style={{ + fontSize: 25, + fontWeight: 700, + textAlign: 'center', + ...(isEditable && styles.underlinedText), + ...style, + }} + > + {title} + </AutoTextSize> + ) : ( + <TextOneLine + onClick={onTitleClick} + style={{ + fontSize: 25, + fontWeight: 700, + textAlign: 'center', + ...(isEditable && styles.underlinedText), + ...style, + }} + > + {title} + </TextOneLine> + )} + </View> + ); +} + +type ModalCloseButtonProps = { + onClick: ComponentPropsWithoutRef<typeof Button>['onClick']; + style?: CSSProperties; +}; + +export function ModalCloseButton({ onClick, style }: ModalCloseButtonProps) { + return ( + <Button + type="bare" + onClick={onClick} + style={{ padding: '10px 10px' }} + aria-label="Close" + > + <SvgDelete width={10} style={style} /> + </Button> + ); +} diff --git a/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx b/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx index 7da6005d2e90aeb8590efcfc40fbfea00d529e35..4bac2bee7a7711c6ea53c2f1570d84a761cd4ee4 100644 --- a/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx +++ b/packages/desktop-client/src/components/mobile/budget/BudgetTable.jsx @@ -70,7 +70,10 @@ function ToBudget({ toBudget, onClick, show3Cols }) { <Button type="bare" style={{ maxWidth: sidebarColumnWidth }} - onClick={onClick} + onPointerUp={e => { + e.stopPropagation(); + onClick?.(); + }} > <View> <Label @@ -140,7 +143,10 @@ function Saved({ projected, onClick, show3Cols }) { <Button type="bare" style={{ maxWidth: sidebarColumnWidth }} - onClick={onClick} + onPointerUp={e => { + e.stopPropagation(); + onClick?.(); + }} > <View> <View> @@ -267,7 +273,10 @@ function BudgetCell({ type="financial" getStyle={makeAmountGrey} data-testid={name} - onClick={onOpenCategoryBudgetMenu} + onPointerUp={e => { + e.stopPropagation(); + onOpenCategoryBudgetMenu(); + }} {...props} /> ); @@ -436,7 +445,10 @@ const ExpenseCategory = memo(function ExpenseCategory({ style={{ maxWidth: sidebarColumnWidth, }} - onClick={() => onEdit?.(category.id)} + onPointerUp={e => { + e.stopPropagation(); + onEdit?.(category.id); + }} > <View style={{ @@ -525,7 +537,10 @@ const ExpenseCategory = memo(function ExpenseCategory({ binding={spent} getStyle={makeAmountGrey} type="financial" - onClick={onShowActivity} + onPointerUp={e => { + e.stopPropagation(); + onShowActivity(); + }} formatter={value => ( <Button type="bare" @@ -560,7 +575,13 @@ const ExpenseCategory = memo(function ExpenseCategory({ width: columnWidth, }} > - <span role="button" onClick={() => onOpenBalanceMenu?.()}> + <span + role="button" + onPointerUp={e => { + e.stopPropagation(); + onOpenBalanceMenu(); + }} + > <BalanceWithCarryover carryover={carryover} balance={balance} @@ -710,7 +731,10 @@ const ExpenseGroupHeader = memo(function ExpenseGroupHeader({ hoveredStyle={{ backgroundColor: 'transparent', }} - onClick={() => onToggleCollapse?.(group.id)} + onPointerUp={e => { + e.stopPropagation(); + onToggleCollapse?.(group.id); + }} > <SvgExpandArrow width={8} @@ -727,7 +751,10 @@ const ExpenseGroupHeader = memo(function ExpenseGroupHeader({ style={{ maxWidth: sidebarColumnWidth, }} - onClick={() => onEdit?.(group.id)} + onPointerUp={e => { + e.stopPropagation(); + onEdit?.(group.id); + }} > <View style={{ @@ -862,7 +889,7 @@ const ExpenseGroupHeader = memo(function ExpenseGroupHeader({ {/* {editMode && ( <View> <Button - onClick={() => onAddCategory(group.id, group.is_income)} + onPointerUp={() => onAddCategory(group.id, group.is_income)} style={{ padding: 10 }} > <Add width={15} height={15} /> @@ -937,7 +964,10 @@ const IncomeGroupHeader = memo(function IncomeGroupHeader({ hoveredStyle={{ backgroundColor: 'transparent', }} - onClick={() => onToggleCollapse?.(group.id)} + onPointerUp={e => { + e.stopPropagation(); + onToggleCollapse?.(group.id); + }} > <SvgExpandArrow width={8} @@ -954,7 +984,10 @@ const IncomeGroupHeader = memo(function IncomeGroupHeader({ style={{ maxWidth: sidebarColumnWidth, }} - onClick={() => onEdit?.(group.id)} + onPointerUp={e => { + e.stopPropagation(); + onEdit?.(group.id); + }} > <View style={{ @@ -1100,7 +1133,10 @@ const IncomeCategory = memo(function IncomeCategory({ style={{ maxWidth: sidebarColumnWidth, }} - onClick={() => onEdit?.(category.id)} + onPointerUp={e => { + e.stopPropagation(); + onEdit?.(category.id); + }} > <View style={{ @@ -1225,20 +1261,20 @@ const IncomeCategory = memo(function IncomeCategory({ // <MathOperations emitter={emitter} /> // <View style={{ flex: 1 }} /> // <Button -// onClick={() => emitter.emit('moveUp')} +// onPointerUp={() => emitter.emit('moveUp')} // style={{ marginRight: 5 }} // data-testid="up" // > // <ArrowThinUp width={13} height={13} /> // </Button> // <Button -// onClick={() => emitter.emit('moveDown')} +// onPointerUp={() => emitter.emit('moveDown')} // style={{ marginRight: 5 }} // data-testid="down" // > // <ArrowThinDown width={13} height={13} /> // </Button> -// <Button onClick={() => emitter.emit('done')} data-testid="done"> +// <Button onPointerUp={() => emitter.emit('done')} data-testid="done"> // Done // </Button> // </View> @@ -1627,7 +1663,10 @@ export function BudgetTable({ }} hoveredStyle={noBackgroundColorStyle} activeStyle={noBackgroundColorStyle} - onClick={() => onOpenBudgetPageMenu?.()} + onPointerUp={e => { + e.stopPropagation(); + onOpenBudgetPageMenu?.(); + }} > <SvgLogo width="20" height="20" /> <SvgCheveronRight @@ -1751,7 +1790,10 @@ function BudgetTableHeader({ <Button type="bare" disabled={show3Cols} - onClick={toggleSpentColumn} + onPointerUp={e => { + e.stopPropagation(); + toggleSpentColumn(); + }} style={buttonStyle} > <View style={{ alignItems: 'flex-end' }}> @@ -1812,7 +1854,10 @@ function BudgetTableHeader({ <Button type="bare" disabled={show3Cols} - onClick={toggleSpentColumn} + onPointerUp={e => { + e.stopPropagation(); + toggleSpentColumn(); + }} style={buttonStyle} > <View style={{ alignItems: 'flex-end' }}> @@ -1926,7 +1971,12 @@ function MonthSelector({ > <Button type="bare" - onClick={prevEnabled && onPrevMonth} + onPointerUp={e => { + e.stopPropagation(); + if (prevEnabled) { + onPrevMonth(); + } + }} style={{ ...styles.noTapHighlight, ...arrowButtonStyle, @@ -1949,13 +1999,21 @@ function MonthSelector({ margin: '0 5px', ...styles.underlinedText, }} - onClick={() => onOpenMonthMenu?.(month)} + onPointerUp={e => { + e.stopPropagation(); + onOpenMonthMenu?.(month); + }} > {monthUtils.format(month, 'MMMM ‘yy')} </Text> <Button type="bare" - onClick={nextEnabled && onNextMonth} + onPointerUp={e => { + e.stopPropagation(); + if (nextEnabled) { + onNextMonth(); + } + }} style={{ ...styles.noTapHighlight, ...arrowButtonStyle, diff --git a/packages/desktop-client/src/components/mobile/budget/index.tsx b/packages/desktop-client/src/components/mobile/budget/index.tsx index 9fcc4ab9ab34a26a9040798d5647a1e13848b939..91e84611503f6fd20a2ab24efa766c377ad03dec 100644 --- a/packages/desktop-client/src/components/mobile/budget/index.tsx +++ b/packages/desktop-client/src/components/mobile/budget/index.tsx @@ -169,6 +169,15 @@ function BudgetInner(props: BudgetInnerProps) { } }; + const onToggleGroupVisibility = groupId => { + const group = categoryGroups.find(g => g.id === groupId); + onSaveGroup({ + ...group, + hidden: !!!group.hidden, + }); + dispatch(collapseModals('category-group-menu')); + }; + const onSaveCategory = category => { dispatch(updateCategory(category)); }; @@ -196,6 +205,15 @@ function BudgetInner(props: BudgetInnerProps) { } }; + const onToggleCategoryVisibility = categoryId => { + const category = categories.find(c => c.id === categoryId); + onSaveCategory({ + ...category, + hidden: !!!category.hidden, + }); + dispatch(collapseModals('category-menu')); + }; + const onReorderCategory = (id, { inGroup, aroundCategory }) => { let groupId, targetId; @@ -323,6 +341,7 @@ function BudgetInner(props: BudgetInnerProps) { onAddCategory: onOpenNewCategoryModal, onEditNotes: onOpenCategoryGroupNotesModal, onDelete: onDeleteGroup, + onToggleVisibility: onToggleGroupVisibility, }), ); }; @@ -335,6 +354,7 @@ function BudgetInner(props: BudgetInnerProps) { onSave: onSaveCategory, onEditNotes: onOpenCategoryNotesModal, onDelete: onDeleteCategory, + onToggleVisibility: onToggleCategoryVisibility, onBudgetAction, }), ); diff --git a/packages/desktop-client/src/components/modals/AccountAutocompleteModal.tsx b/packages/desktop-client/src/components/modals/AccountAutocompleteModal.tsx index 025c9a2658a3ca5d45354a8a21a82f1bf39ecb37..0c1d06a008037210d58a81d770559a8c6671c904 100644 --- a/packages/desktop-client/src/components/modals/AccountAutocompleteModal.tsx +++ b/packages/desktop-client/src/components/modals/AccountAutocompleteModal.tsx @@ -3,27 +3,24 @@ import React, { type ComponentPropsWithoutRef } from 'react'; import { useResponsive } from '../../ResponsiveProvider'; import { theme } from '../../style'; import { AccountAutocomplete } from '../autocomplete/AccountAutocomplete'; -import { ModalCloseButton, Modal, ModalTitle } from '../common/Modal'; +import { + ModalCloseButton, + Modal, + ModalTitle, + ModalHeader, +} from '../common/Modal2'; import { View } from '../common/View'; import { SectionLabel } from '../forms'; -import { type CommonModalProps } from '../Modals'; type AccountAutocompleteModalProps = { - modalProps: CommonModalProps; autocompleteProps: ComponentPropsWithoutRef<typeof AccountAutocomplete>; onClose: () => void; }; export function AccountAutocompleteModal({ - modalProps, autocompleteProps, onClose, }: AccountAutocompleteModalProps) { - const _onClose = () => { - modalProps.onClose(); - onClose?.(); - }; - const { isNarrowWidth } = useResponsive(); const defaultAutocompleteProps = { containerProps: { style: { height: isNarrowWidth ? '90vh' : 275 } }, @@ -31,51 +28,57 @@ export function AccountAutocompleteModal({ return ( <Modal - title={ - <ModalTitle - title="Account" - getStyle={() => ({ color: theme.menuAutoCompleteText })} - /> - } + name="account-autocomplete" noAnimation={!isNarrowWidth} - showHeader={isNarrowWidth} - focusAfterClose={false} - {...modalProps} - onClose={_onClose} - style={{ - height: isNarrowWidth ? '85vh' : 275, - backgroundColor: theme.menuAutoCompleteBackground, + onClose={onClose} + containerProps={{ + style: { + height: isNarrowWidth ? '85vh' : 275, + backgroundColor: theme.menuAutoCompleteBackground, + }, }} - CloseButton={props => ( - <ModalCloseButton - {...props} - style={{ color: theme.menuAutoCompleteText }} - /> - )} > - {() => ( - <View> - {!isNarrowWidth && ( - <SectionLabel - title="Account" - style={{ - alignSelf: 'center', - color: theme.menuAutoCompleteText, - marginBottom: 10, - }} + {({ state: { close } }) => ( + <> + {isNarrowWidth && ( + <ModalHeader + title={ + <ModalTitle + title="Account" + getStyle={() => ({ color: theme.menuAutoCompleteText })} + /> + } + rightContent={ + <ModalCloseButton + onClick={close} + style={{ color: theme.menuAutoCompleteText }} + /> + } /> )} - <View style={{ flex: 1 }}> - <AccountAutocomplete - focused={true} - embedded={true} - closeOnBlur={false} - onClose={_onClose} - {...defaultAutocompleteProps} - {...autocompleteProps} - /> + <View> + {!isNarrowWidth && ( + <SectionLabel + title="Account" + style={{ + alignSelf: 'center', + color: theme.menuAutoCompleteText, + marginBottom: 10, + }} + /> + )} + <View style={{ flex: 1 }}> + <AccountAutocomplete + focused={true} + embedded={true} + closeOnBlur={false} + onClose={close} + {...defaultAutocompleteProps} + {...autocompleteProps} + /> + </View> </View> - </View> + </> )} </Modal> ); diff --git a/packages/desktop-client/src/components/modals/AccountMenuModal.tsx b/packages/desktop-client/src/components/modals/AccountMenuModal.tsx index 44fbafb33354ecbbaf1207681c86738ffebf9356..2c4299e2952e51d22577f7b0d0cd192da67e37aa 100644 --- a/packages/desktop-client/src/components/modals/AccountMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/AccountMenuModal.tsx @@ -9,14 +9,17 @@ import { SvgNotesPaper } from '../../icons/v2'; import { type CSSProperties, styles, theme } from '../../style'; import { Button } from '../common/Button2'; import { Menu } from '../common/Menu'; -import { Modal, ModalTitle } from '../common/Modal'; +import { + Modal, + ModalCloseButton, + ModalHeader, + ModalTitle, +} from '../common/Modal2'; import { Popover } from '../common/Popover'; import { View } from '../common/View'; -import { type CommonModalProps } from '../Modals'; import { Notes } from '../Notes'; type AccountMenuModalProps = { - modalProps: CommonModalProps; accountId: string; onSave: (account: AccountEntity) => void; onCloseAccount: (accountId: string) => void; @@ -26,7 +29,6 @@ type AccountMenuModalProps = { }; export function AccountMenuModal({ - modalProps, accountId, onSave, onCloseAccount, @@ -37,11 +39,6 @@ export function AccountMenuModal({ const account = useAccount(accountId); const originalNotes = useNotes(`account-${accountId}`); - const _onClose = () => { - modalProps?.onClose(); - onClose?.(); - }; - const onRename = (newName: string) => { if (!account) { return; @@ -77,69 +74,84 @@ export function AccountMenuModal({ return ( <Modal - title={ - <ModalTitle isEditable title={account.name} onTitleUpdate={onRename} /> - } - showHeader - focusAfterClose={false} - {...modalProps} - onClose={_onClose} - style={{ - height: '45vh', + name="account-menu" + onClose={onClose} + containerProps={{ + style: { + height: '45vh', + }, }} - leftHeaderContent={ - <AdditionalAccountMenu - account={account} - onClose={onCloseAccount} - onReopen={onReopenAccount} - /> - } > - <View - style={{ - flex: 1, - flexDirection: 'column', - }} - > - <View - style={{ - overflowY: 'auto', - flex: 1, - }} - > - <Notes - notes={ - originalNotes && originalNotes.length > 0 - ? originalNotes - : 'No notes' + {({ state: { close } }) => ( + <> + <ModalHeader + leftContent={ + <AdditionalAccountMenu + account={account} + onClose={onCloseAccount} + onReopen={onReopenAccount} + /> + } + title={ + <ModalTitle + isEditable + title={account.name} + onTitleUpdate={onRename} + /> } - editable={false} - focused={false} - getStyle={() => ({ - borderRadius: 6, - ...((!originalNotes || originalNotes.length === 0) && { - justifySelf: 'center', - alignSelf: 'center', - color: theme.pageTextSubdued, - }), - })} + rightContent={<ModalCloseButton onClick={close} />} /> - </View> - <View - style={{ - flexDirection: 'row', - flexWrap: 'wrap', - justifyContent: 'space-between', - alignContent: 'space-between', - paddingTop: 10, - }} - > - <Button style={buttonStyle} onPress={_onEditNotes}> - <SvgNotesPaper width={20} height={20} style={{ paddingRight: 5 }} /> - Edit notes - </Button> - </View> - </View> + <View + style={{ + flex: 1, + flexDirection: 'column', + }} + > + <View + style={{ + overflowY: 'auto', + flex: 1, + }} + > + <Notes + notes={ + originalNotes && originalNotes.length > 0 + ? originalNotes + : 'No notes' + } + editable={false} + focused={false} + getStyle={() => ({ + borderRadius: 6, + ...((!originalNotes || originalNotes.length === 0) && { + justifySelf: 'center', + alignSelf: 'center', + color: theme.pageTextSubdued, + }), + })} + /> + </View> + <View + style={{ + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'space-between', + alignContent: 'space-between', + paddingTop: 10, + }} + > + <Button style={buttonStyle} onPress={_onEditNotes}> + <SvgNotesPaper + width={20} + height={20} + style={{ paddingRight: 5 }} + /> + Edit notes + </Button> + </View> + </View> + </> + )} </Modal> ); } diff --git a/packages/desktop-client/src/components/modals/BudgetListModal.tsx b/packages/desktop-client/src/components/modals/BudgetListModal.tsx index 5d5906dfe75230159ff033e7ce42d953e090bf6f..af44d7960c59486da90b357c2e9b8e869a011099 100644 --- a/packages/desktop-client/src/components/modals/BudgetListModal.tsx +++ b/packages/desktop-client/src/components/modals/BudgetListModal.tsx @@ -2,42 +2,42 @@ import React from 'react'; import { useSelector } from 'react-redux'; import { useLocalPref } from '../../hooks/useLocalPref'; -import { Modal } from '../common/Modal'; +import { Modal, ModalHeader, ModalCloseButton } from '../common/Modal2'; import { Text } from '../common/Text'; import { View } from '../common/View'; import { BudgetList } from '../manager/BudgetList'; -import { type CommonModalProps } from '../Modals'; -type BudgetListModalProps = { - modalProps: CommonModalProps; -}; - -export function BudgetListModal({ modalProps }: BudgetListModalProps) { +export function BudgetListModal() { const [id] = useLocalPref('id'); const currentFile = useSelector(state => state.budgets.allFiles?.find(f => 'id' in f && f.id === id), ); return ( - <Modal - title="Switch Budget File" - showHeader - focusAfterClose={false} - {...modalProps} - > - <View - style={{ - justifyContent: 'center', - alignItems: 'center', - margin: '20px 0', - }} - > - <Text style={{ fontSize: 17, fontWeight: 400 }}>Switching from:</Text> - <Text style={{ fontSize: 17, fontWeight: 700 }}> - {currentFile?.name} - </Text> - </View> - <BudgetList showHeader={false} quickSwitchMode={true} /> + <Modal name="budget-list"> + {({ state: { close } }) => ( + <> + <ModalHeader + title="Switch Budget File" + rightContent={<ModalCloseButton onClick={close} />} + /> + <View + style={{ + justifyContent: 'center', + alignItems: 'center', + margin: '20px 0', + }} + > + <Text style={{ fontSize: 17, fontWeight: 400 }}> + Switching from: + </Text> + <Text style={{ fontSize: 17, fontWeight: 700 }}> + {currentFile?.name} + </Text> + </View> + <BudgetList showHeader={false} quickSwitchMode={true} /> + </> + )} </Modal> ); } diff --git a/packages/desktop-client/src/components/modals/BudgetPageMenuModal.tsx b/packages/desktop-client/src/components/modals/BudgetPageMenuModal.tsx index b6b164e52d88fe692489a2a502fded5e86b5e35e..43f943f41ff71069b3079474685019ce4339c262 100644 --- a/packages/desktop-client/src/components/modals/BudgetPageMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/BudgetPageMenuModal.tsx @@ -3,17 +3,11 @@ import React, { type ComponentPropsWithoutRef } from 'react'; import { useLocalPref } from '../../hooks/useLocalPref'; import { type CSSProperties, theme, styles } from '../../style'; import { Menu } from '../common/Menu'; -import { Modal } from '../common/Modal'; -import { type CommonModalProps } from '../Modals'; +import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2'; -type BudgetPageMenuModalProps = ComponentPropsWithoutRef< - typeof BudgetPageMenu -> & { - modalProps: CommonModalProps; -}; +type BudgetPageMenuModalProps = ComponentPropsWithoutRef<typeof BudgetPageMenu>; export function BudgetPageMenuModal({ - modalProps, onAddCategoryGroup, onToggleHiddenCategories, onSwitchBudgetFile, @@ -26,13 +20,21 @@ export function BudgetPageMenuModal({ }; return ( - <Modal showHeader focusAfterClose={false} {...modalProps}> - <BudgetPageMenu - getItemStyle={() => defaultMenuItemStyle} - onAddCategoryGroup={onAddCategoryGroup} - onToggleHiddenCategories={onToggleHiddenCategories} - onSwitchBudgetFile={onSwitchBudgetFile} - /> + <Modal name="budget-page-menu"> + {({ state: { close } }) => ( + <> + <ModalHeader + showLogo + rightContent={<ModalCloseButton onClick={close} />} + /> + <BudgetPageMenu + getItemStyle={() => defaultMenuItemStyle} + onAddCategoryGroup={onAddCategoryGroup} + onToggleHiddenCategories={onToggleHiddenCategories} + onSwitchBudgetFile={onSwitchBudgetFile} + /> + </> + )} </Modal> ); } diff --git a/packages/desktop-client/src/components/modals/CategoryAutocompleteModal.tsx b/packages/desktop-client/src/components/modals/CategoryAutocompleteModal.tsx index e158b63f13754de893bb0dc381116723cf4a83d0..cfdab70bfb5cd2ab2e4b7c847240f6ecb332933b 100644 --- a/packages/desktop-client/src/components/modals/CategoryAutocompleteModal.tsx +++ b/packages/desktop-client/src/components/modals/CategoryAutocompleteModal.tsx @@ -5,30 +5,27 @@ import * as monthUtils from 'loot-core/src/shared/months'; import { useResponsive } from '../../ResponsiveProvider'; import { theme } from '../../style'; import { CategoryAutocomplete } from '../autocomplete/CategoryAutocomplete'; -import { ModalCloseButton, Modal, ModalTitle } from '../common/Modal'; +import { + ModalCloseButton, + Modal, + ModalTitle, + ModalHeader, +} from '../common/Modal2'; import { View } from '../common/View'; import { SectionLabel } from '../forms'; -import { type CommonModalProps } from '../Modals'; import { NamespaceContext } from '../spreadsheet/NamespaceContext'; type CategoryAutocompleteModalProps = { - modalProps: CommonModalProps; autocompleteProps: ComponentPropsWithoutRef<typeof CategoryAutocomplete>; onClose: () => void; month?: string; }; export function CategoryAutocompleteModal({ - modalProps, autocompleteProps, month, onClose, }: CategoryAutocompleteModalProps) { - const _onClose = () => { - modalProps.onClose(); - onClose?.(); - }; - const { isNarrowWidth } = useResponsive(); const defaultAutocompleteProps = { @@ -37,56 +34,62 @@ export function CategoryAutocompleteModal({ return ( <Modal - title={ - <ModalTitle - title="Category" - getStyle={() => ({ color: theme.menuAutoCompleteText })} - /> - } + name="category-autocomplete" noAnimation={!isNarrowWidth} - showHeader={isNarrowWidth} - focusAfterClose={false} - {...modalProps} - onClose={_onClose} - style={{ - height: isNarrowWidth ? '85vh' : 275, - backgroundColor: theme.menuAutoCompleteBackground, + onClose={onClose} + containerProps={{ + style: { + height: isNarrowWidth ? '85vh' : 275, + backgroundColor: theme.menuAutoCompleteBackground, + }, }} - CloseButton={props => ( - <ModalCloseButton - {...props} - style={{ color: theme.menuAutoCompleteText }} - /> - )} > - {() => ( - <View> - {!isNarrowWidth && ( - <SectionLabel - title="Category" - style={{ - alignSelf: 'center', - color: theme.menuAutoCompleteText, - marginBottom: 10, - }} + {({ state: { close } }) => ( + <> + {isNarrowWidth && ( + <ModalHeader + title={ + <ModalTitle + title="Category" + getStyle={() => ({ color: theme.menuAutoCompleteText })} + /> + } + rightContent={ + <ModalCloseButton + onClick={close} + style={{ color: theme.menuAutoCompleteText }} + /> + } /> )} - <View style={{ flex: 1 }}> - <NamespaceContext.Provider - value={month ? monthUtils.sheetForMonth(month) : ''} - > - <CategoryAutocomplete - focused={true} - embedded={true} - closeOnBlur={false} - showSplitOption={false} - onClose={_onClose} - {...defaultAutocompleteProps} - {...autocompleteProps} + <View> + {!isNarrowWidth && ( + <SectionLabel + title="Category" + style={{ + alignSelf: 'center', + color: theme.menuAutoCompleteText, + marginBottom: 10, + }} /> - </NamespaceContext.Provider> + )} + <View style={{ flex: 1 }}> + <NamespaceContext.Provider + value={month ? monthUtils.sheetForMonth(month) : ''} + > + <CategoryAutocomplete + focused={true} + embedded={true} + closeOnBlur={false} + showSplitOption={false} + onClose={close} + {...defaultAutocompleteProps} + {...autocompleteProps} + /> + </NamespaceContext.Provider> + </View> </View> - </View> + </> )} </Modal> ); diff --git a/packages/desktop-client/src/components/modals/CategoryGroupMenuModal.tsx b/packages/desktop-client/src/components/modals/CategoryGroupMenuModal.tsx index 4989e6a0c01040bc4a274806e0963077972b1cf0..d155b21d84f1148df98a0c780e6e07e2dda0683b 100644 --- a/packages/desktop-client/src/components/modals/CategoryGroupMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/CategoryGroupMenuModal.tsx @@ -10,41 +10,40 @@ import { SvgNotesPaper, SvgViewHide, SvgViewShow } from '../../icons/v2'; import { type CSSProperties, styles, theme } from '../../style'; import { Button } from '../common/Button2'; import { Menu } from '../common/Menu'; -import { Modal, ModalTitle } from '../common/Modal'; +import { + Modal, + ModalCloseButton, + ModalHeader, + ModalTitle, +} from '../common/Modal2'; import { Popover } from '../common/Popover'; import { View } from '../common/View'; -import { type CommonModalProps } from '../Modals'; import { Notes } from '../Notes'; type CategoryGroupMenuModalProps = { - modalProps: CommonModalProps; groupId: string; onSave: (group: CategoryGroupEntity) => void; onAddCategory: (groupId: string, isIncome: boolean) => void; onEditNotes: (id: string) => void; onSaveNotes: (id: string, notes: string) => void; onDelete: (groupId: string) => void; + onToggleVisibility: (groupId: string) => void; onClose?: () => void; }; export function CategoryGroupMenuModal({ - modalProps, groupId, onSave, onAddCategory, onEditNotes, onDelete, + onToggleVisibility, onClose, }: CategoryGroupMenuModalProps) { const { grouped: categoryGroups } = useCategories(); const group = categoryGroups.find(g => g.id === groupId); const notes = useNotes(group.id); - const _onClose = () => { - modalProps?.onClose(); - onClose?.(); - }; - const onRename = newName => { if (newName !== group.name) { onSave?.({ @@ -62,18 +61,14 @@ export function CategoryGroupMenuModal({ onEditNotes?.(group.id); }; - const _onToggleVisibility = () => { - onSave?.({ - ...group, - hidden: !!!group.hidden, - }); - _onClose(); - }; - const _onDelete = () => { onDelete?.(group.id); }; + const _onToggleVisibility = () => { + onToggleVisibility?.(group.id); + }; + const buttonStyle: CSSProperties = { ...styles.mediumText, height: styles.mobileMinHeight, @@ -86,70 +81,85 @@ export function CategoryGroupMenuModal({ return ( <Modal - title={ - <ModalTitle isEditable title={group.name} onTitleUpdate={onRename} /> - } - showHeader - focusAfterClose={false} - {...modalProps} - onClose={_onClose} - style={{ - height: '45vh', + name="category-group-menu" + onClose={onClose} + containerProps={{ + style: { + height: '45vh', + }, }} - leftHeaderContent={ - <AdditionalCategoryGroupMenu - group={group} - onDelete={_onDelete} - onToggleVisibility={_onToggleVisibility} - /> - } > - <View - style={{ - flex: 1, - flexDirection: 'column', - }} - > - <View - style={{ - overflowY: 'auto', - flex: 1, - }} - > - <Notes - notes={notes?.length > 0 ? notes : 'No notes'} - editable={false} - focused={false} - getStyle={() => ({ - ...styles.mediumText, - borderRadius: 6, - ...((!notes || notes.length === 0) && { - justifySelf: 'center', - alignSelf: 'center', - color: theme.pageTextSubdued, - }), - })} + {({ state: { close } }) => ( + <> + <ModalHeader + leftContent={ + <AdditionalCategoryGroupMenu + group={group} + onDelete={_onDelete} + onToggleVisibility={_onToggleVisibility} + /> + } + title={ + <ModalTitle + isEditable + title={group.name} + onTitleUpdate={onRename} + /> + } + rightContent={<ModalCloseButton onClick={close} />} /> - </View> - <View - style={{ - flexDirection: 'row', - flexWrap: 'wrap', - justifyContent: 'space-between', - alignContent: 'space-between', - paddingTop: 10, - }} - > - <Button style={buttonStyle} onPress={_onAddCategory}> - <SvgAdd width={17} height={17} style={{ paddingRight: 5 }} /> - Add category - </Button> - <Button style={buttonStyle} onPress={_onEditNotes}> - <SvgNotesPaper width={20} height={20} style={{ paddingRight: 5 }} /> - Edit notes - </Button> - </View> - </View> + <View + style={{ + flex: 1, + flexDirection: 'column', + }} + > + <View + style={{ + overflowY: 'auto', + flex: 1, + }} + > + <Notes + notes={notes?.length > 0 ? notes : 'No notes'} + editable={false} + focused={false} + getStyle={() => ({ + ...styles.mediumText, + borderRadius: 6, + ...((!notes || notes.length === 0) && { + justifySelf: 'center', + alignSelf: 'center', + color: theme.pageTextSubdued, + }), + })} + /> + </View> + <View + style={{ + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'space-between', + alignContent: 'space-between', + paddingTop: 10, + }} + > + <Button style={buttonStyle} onPress={_onAddCategory}> + <SvgAdd width={17} height={17} style={{ paddingRight: 5 }} /> + Add category + </Button> + <Button style={buttonStyle} onPress={_onEditNotes}> + <SvgNotesPaper + width={20} + height={20} + style={{ paddingRight: 5 }} + /> + Edit notes + </Button> + </View> + </View> + </> + )} </Modal> ); } diff --git a/packages/desktop-client/src/components/modals/CategoryMenuModal.tsx b/packages/desktop-client/src/components/modals/CategoryMenuModal.tsx index 9451de7cb4a359c0f525ef5d26698d2f536bcd1b..c6991962751f6bcc960fda9721f7f86aab529ddd 100644 --- a/packages/desktop-client/src/components/modals/CategoryMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/CategoryMenuModal.tsx @@ -11,36 +11,36 @@ import { SvgNotesPaper, SvgViewHide, SvgViewShow } from '../../icons/v2'; import { type CSSProperties, styles, theme } from '../../style'; import { Button } from '../common/Button2'; import { Menu } from '../common/Menu'; -import { Modal, ModalTitle } from '../common/Modal'; +import { + Modal, + ModalCloseButton, + ModalHeader, + ModalTitle, +} from '../common/Modal2'; import { Popover } from '../common/Popover'; import { View } from '../common/View'; -import { type CommonModalProps } from '../Modals'; import { Notes } from '../Notes'; type CategoryMenuModalProps = { - modalProps: CommonModalProps; categoryId: string; onSave: (category: CategoryEntity) => void; - onEditNotes: (id: string) => void; + onEditNotes: (categoryId: string) => void; onDelete: (categoryId: string) => void; + onToggleVisibility: (categoryId: string) => void; onClose?: () => void; }; export function CategoryMenuModal({ - modalProps, categoryId, onSave, onEditNotes, onDelete, + onToggleVisibility, onClose, }: CategoryMenuModalProps) { const category = useCategory(categoryId); const categoryGroup = useCategoryGroup(category?.cat_group); const originalNotes = useNotes(category.id); - const _onClose = () => { - modalProps?.onClose(); - onClose?.(); - }; const onRename = newName => { if (newName !== category.name) { @@ -52,11 +52,7 @@ export function CategoryMenuModal({ }; const _onToggleVisibility = () => { - onSave?.({ - ...category, - hidden: !category.hidden, - }); - _onClose(); + onToggleVisibility?.(category.id); }; const _onEditNotes = () => { @@ -77,66 +73,79 @@ export function CategoryMenuModal({ return ( <Modal - title={ - <ModalTitle isEditable title={category.name} onTitleUpdate={onRename} /> - } - showHeader - focusAfterClose={false} - {...modalProps} - onClose={_onClose} - style={{ - height: '45vh', + name="category-menu" + onClose={onClose} + containerProps={{ + style: { height: '45vh' }, }} - leftHeaderContent={ - <AdditionalCategoryMenu - category={category} - categoryGroup={categoryGroup} - onDelete={_onDelete} - onToggleVisibility={_onToggleVisibility} - /> - } > - <View - style={{ - flex: 1, - flexDirection: 'column', - }} - > - <View - style={{ - overflowY: 'auto', - flex: 1, - }} - > - <Notes - notes={originalNotes?.length > 0 ? originalNotes : 'No notes'} - editable={false} - focused={false} - getStyle={() => ({ - borderRadius: 6, - ...((!originalNotes || originalNotes.length === 0) && { - justifySelf: 'center', - alignSelf: 'center', - color: theme.pageTextSubdued, - }), - })} + {({ state: { close } }) => ( + <> + <ModalHeader + leftContent={ + <AdditionalCategoryMenu + category={category} + categoryGroup={categoryGroup} + onDelete={_onDelete} + onToggleVisibility={_onToggleVisibility} + /> + } + title={ + <ModalTitle + isEditable + title={category.name} + onTitleUpdate={onRename} + /> + } + rightContent={<ModalCloseButton onClick={close} />} /> - </View> - <View - style={{ - flexDirection: 'row', - flexWrap: 'wrap', - justifyContent: 'space-between', - alignContent: 'space-between', - paddingTop: 10, - }} - > - <Button style={buttonStyle} onPress={_onEditNotes}> - <SvgNotesPaper width={20} height={20} style={{ paddingRight: 5 }} /> - Edit notes - </Button> - </View> - </View> + <View + style={{ + flex: 1, + flexDirection: 'column', + }} + > + <View + style={{ + overflowY: 'auto', + flex: 1, + }} + > + <Notes + notes={originalNotes?.length > 0 ? originalNotes : 'No notes'} + editable={false} + focused={false} + getStyle={() => ({ + borderRadius: 6, + ...((!originalNotes || originalNotes.length === 0) && { + justifySelf: 'center', + alignSelf: 'center', + color: theme.pageTextSubdued, + }), + })} + /> + </View> + <View + style={{ + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'space-between', + alignContent: 'space-between', + paddingTop: 10, + }} + > + <Button style={buttonStyle} onPress={_onEditNotes}> + <SvgNotesPaper + width={20} + height={20} + style={{ paddingRight: 5 }} + /> + Edit notes + </Button> + </View> + </View> + </> + )} </Modal> ); } diff --git a/packages/desktop-client/src/components/modals/CloseAccountModal.tsx b/packages/desktop-client/src/components/modals/CloseAccountModal.tsx index 93c87019fbdbfb19df4a18feef3daf118ce64dec..c1668a70105217f8da6455c7f9436b4ac85b535b 100644 --- a/packages/desktop-client/src/components/modals/CloseAccountModal.tsx +++ b/packages/desktop-client/src/components/modals/CloseAccountModal.tsx @@ -20,11 +20,10 @@ import { CategoryAutocomplete } from '../autocomplete/CategoryAutocomplete'; import { Button } from '../common/Button2'; import { FormError } from '../common/FormError'; import { Link } from '../common/Link'; -import { Modal } from '../common/Modal'; +import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2'; import { Paragraph } from '../common/Paragraph'; import { Text } from '../common/Text'; import { View } from '../common/View'; -import { type CommonModalProps } from '../Modals'; function needsCategory( account: AccountEntity, @@ -43,14 +42,12 @@ type CloseAccountModalProps = { account: AccountEntity; balance: number; canDelete: boolean; - modalProps: CommonModalProps; }; export function CloseAccountModal({ account, balance, canDelete, - modalProps, }: CloseAccountModalProps) { const accounts = useAccounts().filter(a => a.closed === 0); const { grouped: categoryGroups, list: categories } = useCategories(); @@ -103,163 +100,173 @@ export function CloseAccountModal({ dispatch( closeAccount(account.id, transferAccountId || null, categoryId || null), ); - modalProps.onClose(); } }; return ( <Modal - title="Close Account" - {...modalProps} - style={{ flex: 0 }} - loading={loading} + name="close-account" + isLoading={loading} + containerProps={{ style: { width: '30vw' } }} > - {() => ( - <View> - <Paragraph> - Are you sure you want to close <strong>{account.name}</strong>?{' '} - {canDelete ? ( - <span> - This account has no transactions so it will be permanently - deleted. - </span> - ) : ( - <span> - This account has transactions so we can’t permanently delete it. - </span> - )} - </Paragraph> - <Form onSubmit={onSubmit}> - {balance !== 0 && ( - <View> - <Paragraph> - This account has a balance of{' '} - <strong>{integerToCurrency(balance)}</strong>. To close this - account, select a different account to transfer this balance - to: - </Paragraph> - - <View style={{ marginBottom: 15 }}> - <AccountAutocomplete - includeClosedAccounts={false} - value={transferAccountId} - inputProps={{ - placeholder: 'Select account...', - ...(isNarrowWidth && { - value: transferAccount?.name || '', - style: { - ...narrowStyle, - }, - onClick: () => { - dispatch( - pushModal('account-autocomplete', { - includeClosedAccounts: false, - onSelect: onSelectAccount, - }), - ); - }, - }), - }} - onSelect={onSelectAccount} - /> - </View> - - {transferError && ( - <FormError style={{ marginBottom: 15 }}> - Transfer is required - </FormError> - )} + {({ state: { close } }) => ( + <> + <ModalHeader + title="Close Account" + rightContent={<ModalCloseButton onClick={close} />} + /> + <View> + <Paragraph> + Are you sure you want to close <strong>{account.name}</strong>?{' '} + {canDelete ? ( + <span> + This account has no transactions so it will be permanently + deleted. + </span> + ) : ( + <span> + This account has transactions so we can’t permanently delete + it. + </span> + )} + </Paragraph> + <Form + onSubmit={e => { + onSubmit(e); + close(); + }} + > + {balance !== 0 && ( + <View> + <Paragraph> + This account has a balance of{' '} + <strong>{integerToCurrency(balance)}</strong>. To close this + account, select a different account to transfer this balance + to: + </Paragraph> - {needsCategory(account, transferAccountId, accounts) && ( <View style={{ marginBottom: 15 }}> - <Paragraph> - Since you are transferring the balance from a budgeted - account to an off-budget account, this transaction must be - categorized. Select a category: - </Paragraph> - - <CategoryAutocomplete - categoryGroups={categoryGroups} - value={categoryId} + <AccountAutocomplete + includeClosedAccounts={false} + value={transferAccountId} inputProps={{ - placeholder: 'Select category...', + placeholder: 'Select account...', ...(isNarrowWidth && { - value: category?.name || '', + value: transferAccount?.name || '', style: { ...narrowStyle, }, onClick: () => { dispatch( - pushModal('category-autocomplete', { - categoryGroups, - showHiddenCategories: true, - onSelect: onSelectCategory, + pushModal('account-autocomplete', { + includeClosedAccounts: false, + onSelect: onSelectAccount, }), ); }, }), }} - onSelect={onSelectCategory} + onSelect={onSelectAccount} /> - - {categoryError && ( - <FormError>Category is required</FormError> - )} </View> - )} - </View> - )} - {!canDelete && ( - <View style={{ marginBottom: 15 }}> - <Text style={{ fontSize: 12 }}> - You can also{' '} - <Link - variant="text" - onClick={() => { - setLoading(true); + {transferError && ( + <FormError style={{ marginBottom: 15 }}> + Transfer is required + </FormError> + )} - dispatch(forceCloseAccount(account.id)); - modalProps.onClose(); - }} - style={{ color: theme.errorText }} - > - force close - </Link>{' '} - the account which will delete it and all its transactions - permanently. Doing so may change your budget unexpectedly - since money in it may vanish. - </Text> - </View> - )} + {needsCategory(account, transferAccountId, accounts) && ( + <View style={{ marginBottom: 15 }}> + <Paragraph> + Since you are transferring the balance from a budgeted + account to an off-budget account, this transaction must + be categorized. Select a category: + </Paragraph> - <View - style={{ - flexDirection: 'row', - justifyContent: 'flex-end', - }} - > - <Button - style={{ - marginRight: 10, - height: isNarrowWidth ? styles.mobileMinHeight : undefined, - }} - onPress={modalProps.onClose} - > - Cancel - </Button> - <Button - type="submit" - variant="primary" + <CategoryAutocomplete + categoryGroups={categoryGroups} + value={categoryId} + inputProps={{ + placeholder: 'Select category...', + ...(isNarrowWidth && { + value: category?.name || '', + style: { + ...narrowStyle, + }, + onClick: () => { + dispatch( + pushModal('category-autocomplete', { + categoryGroups, + showHiddenCategories: true, + onSelect: onSelectCategory, + }), + ); + }, + }), + }} + onSelect={onSelectCategory} + /> + + {categoryError && ( + <FormError>Category is required</FormError> + )} + </View> + )} + </View> + )} + + {!canDelete && ( + <View style={{ marginBottom: 15 }}> + <Text style={{ fontSize: 12 }}> + You can also{' '} + <Link + variant="text" + onClick={() => { + setLoading(true); + + dispatch(forceCloseAccount(account.id)); + close(); + }} + style={{ color: theme.errorText }} + > + force close + </Link>{' '} + the account which will delete it and all its transactions + permanently. Doing so may change your budget unexpectedly + since money in it may vanish. + </Text> + </View> + )} + + <View style={{ - height: isNarrowWidth ? styles.mobileMinHeight : undefined, + flexDirection: 'row', + justifyContent: 'flex-end', }} > - Close Account - </Button> - </View> - </Form> - </View> + <Button + style={{ + marginRight: 10, + height: isNarrowWidth ? styles.mobileMinHeight : undefined, + }} + onPress={close} + > + Cancel + </Button> + <Button + type="submit" + variant="primary" + style={{ + height: isNarrowWidth ? styles.mobileMinHeight : undefined, + }} + > + Close Account + </Button> + </View> + </Form> + </View> + </> )} </Modal> ); diff --git a/packages/desktop-client/src/components/modals/ConfirmCategoryDelete.tsx b/packages/desktop-client/src/components/modals/ConfirmCategoryDelete.tsx index a3726e0b98b109d94087aa86d32d5b85150e75b6..7713a6a09ffdbbe8f421ae6d2bdd1624a3a4dc00 100644 --- a/packages/desktop-client/src/components/modals/ConfirmCategoryDelete.tsx +++ b/packages/desktop-client/src/components/modals/ConfirmCategoryDelete.tsx @@ -6,20 +6,17 @@ import { theme } from '../../style'; import { CategoryAutocomplete } from '../autocomplete/CategoryAutocomplete'; import { Block } from '../common/Block'; import { Button } from '../common/Button2'; -import { Modal } from '../common/Modal'; +import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2'; import { Text } from '../common/Text'; import { View } from '../common/View'; -import { type CommonModalProps } from '../Modals'; type ConfirmCategoryDeleteProps = { - modalProps: CommonModalProps; category: string; group: string; onDelete: (categoryId: string) => void; }; export function ConfirmCategoryDelete({ - modalProps, group: groupId, category: categoryId, onDelete, @@ -56,82 +53,92 @@ export function ConfirmCategoryDelete({ const isIncome = !!(category || group).is_income; return ( - <Modal title="Confirm Delete" {...modalProps} style={{ flex: 0 }}> - {() => ( - <View style={{ lineHeight: 1.5 }}> - {group ? ( - <Block> - Categories in the group <strong>{group.name}</strong> are used by - existing transaction - {!isIncome && - ' or it has a positive leftover balance currently'}.{' '} - <strong>Are you sure you want to delete it?</strong> If so, you - must select another category to transfer existing transactions and - balance to. - </Block> - ) : ( - <Block> - <strong>{category.name}</strong> is used by existing transactions - {!isIncome && - ' or it has a positive leftover balance currently'}.{' '} - <strong>Are you sure you want to delete it?</strong> If so, you - must select another category to transfer existing transactions and - balance to. - </Block> - )} + <Modal + name="confirm-category-delete" + containerProps={{ style: { width: '30vw' } }} + > + {({ state: { close } }) => ( + <> + <ModalHeader + title="Confirm Delete" + rightContent={<ModalCloseButton onClick={close} />} + /> + <View style={{ lineHeight: 1.5 }}> + {group ? ( + <Block> + Categories in the group <strong>{group.name}</strong> are used + by existing transaction + {!isIncome && + ' or it has a positive leftover balance currently'} + . <strong>Are you sure you want to delete it?</strong> If so, + you must select another category to transfer existing + transactions and balance to. + </Block> + ) : ( + <Block> + <strong>{category.name}</strong> is used by existing + transactions + {!isIncome && + ' or it has a positive leftover balance currently'} + . <strong>Are you sure you want to delete it?</strong> If so, + you must select another category to transfer existing + transactions and balance to. + </Block> + )} - {error && renderError(error)} + {error && renderError(error)} - <View - style={{ - marginTop: 20, - flexDirection: 'row', - justifyContent: 'flex-start', - alignItems: 'center', - }} - > - <Text>Transfer to:</Text> + <View + style={{ + marginTop: 20, + flexDirection: 'row', + justifyContent: 'flex-start', + alignItems: 'center', + }} + > + <Text>Transfer to:</Text> + + <View style={{ flex: 1, marginLeft: 10, marginRight: 30 }}> + <CategoryAutocomplete + categoryGroups={ + group + ? categoryGroups.filter( + g => g.id !== group.id && !!g.is_income === isIncome, + ) + : categoryGroups + .filter(g => !!g.is_income === isIncome) + .map(g => ({ + ...g, + categories: g.categories.filter( + c => c.id !== category.id, + ), + })) + } + value={transferCategory} + inputProps={{ + placeholder: 'Select category...', + }} + onSelect={category => setTransferCategory(category)} + showHiddenCategories={true} + /> + </View> - <View style={{ flex: 1, marginLeft: 10, marginRight: 30 }}> - <CategoryAutocomplete - categoryGroups={ - group - ? categoryGroups.filter( - g => g.id !== group.id && !!g.is_income === isIncome, - ) - : categoryGroups - .filter(g => !!g.is_income === isIncome) - .map(g => ({ - ...g, - categories: g.categories.filter( - c => c.id !== category.id, - ), - })) - } - value={transferCategory} - inputProps={{ - placeholder: 'Select category...', + <Button + variant="primary" + onPress={() => { + if (!transferCategory) { + setError('required-transfer'); + } else { + onDelete(transferCategory); + close(); + } }} - onSelect={category => setTransferCategory(category)} - showHiddenCategories={true} - /> + > + Delete + </Button> </View> - - <Button - variant="primary" - onPress={() => { - if (!transferCategory) { - setError('required-transfer'); - } else { - onDelete(transferCategory); - modalProps.onClose(); - } - }} - > - Delete - </Button> </View> - </View> + </> )} </Modal> ); diff --git a/packages/desktop-client/src/components/modals/ConfirmTransactionDelete.tsx b/packages/desktop-client/src/components/modals/ConfirmTransactionDelete.tsx index f66ea339c43e2dc76735a814d0d0214274812d74..3c1ceaba0255bafca5435dc4497c4ce068fa8693 100644 --- a/packages/desktop-client/src/components/modals/ConfirmTransactionDelete.tsx +++ b/packages/desktop-client/src/components/modals/ConfirmTransactionDelete.tsx @@ -3,18 +3,15 @@ import React from 'react'; import { useResponsive } from '../../ResponsiveProvider'; import { styles } from '../../style'; import { Button } from '../common/Button2'; -import { Modal } from '../common/Modal'; +import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2'; import { Paragraph } from '../common/Paragraph'; import { View } from '../common/View'; -import { type CommonModalProps } from '../Modals'; type ConfirmTransactionDeleteProps = { - modalProps: CommonModalProps; onConfirm: () => void; }; export function ConfirmTransactionDelete({ - modalProps, onConfirm, }: ConfirmTransactionDeleteProps) { const { isNarrowWidth } = useResponsive(); @@ -25,36 +22,46 @@ export function ConfirmTransactionDelete({ : {}; return ( - <Modal title="Confirm Delete" {...modalProps}> - <View style={{ lineHeight: 1.5 }}> - <Paragraph>Are you sure you want to delete the transaction?</Paragraph> - <View - style={{ - flexDirection: 'row', - justifyContent: 'flex-end', - }} - > - <Button - style={{ - marginRight: 10, - ...narrowButtonStyle, - }} - onPress={modalProps.onClose} - > - Cancel - </Button> - <Button - variant="primary" - style={narrowButtonStyle} - onPress={() => { - onConfirm(); - modalProps.onClose(); - }} - > - Delete - </Button> - </View> - </View> + <Modal name="confirm-transaction-delete"> + {({ state: { close } }) => ( + <> + <ModalHeader + title="Confirm Delete" + rightContent={<ModalCloseButton onClick={close} />} + /> + <View style={{ lineHeight: 1.5 }}> + <Paragraph> + Are you sure you want to delete the transaction? + </Paragraph> + <View + style={{ + flexDirection: 'row', + justifyContent: 'flex-end', + }} + > + <Button + style={{ + marginRight: 10, + ...narrowButtonStyle, + }} + onPress={close} + > + Cancel + </Button> + <Button + variant="primary" + style={narrowButtonStyle} + onPress={() => { + onConfirm(); + close(); + }} + > + Delete + </Button> + </View> + </View> + </> + )} </Modal> ); } diff --git a/packages/desktop-client/src/components/modals/ConfirmTransactionEdit.tsx b/packages/desktop-client/src/components/modals/ConfirmTransactionEdit.tsx index 0d88859c5eed296c0b0392e78b8c3b8b6c0898a7..6b445bda669085eec147f9d2289130fdf192d071 100644 --- a/packages/desktop-client/src/components/modals/ConfirmTransactionEdit.tsx +++ b/packages/desktop-client/src/components/modals/ConfirmTransactionEdit.tsx @@ -3,97 +3,105 @@ import React from 'react'; import { Block } from '../common/Block'; import { Button } from '../common/Button2'; -import { Modal } from '../common/Modal'; +import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2'; import { View } from '../common/View'; -import { type CommonModalProps } from '../Modals'; type ConfirmTransactionEditProps = { - modalProps: Partial<CommonModalProps>; onCancel?: () => void; onConfirm: () => void; confirmReason: string; }; export function ConfirmTransactionEdit({ - modalProps, onCancel, onConfirm, confirmReason, }: ConfirmTransactionEditProps) { return ( - <Modal title="Reconciled Transaction" {...modalProps} style={{ flex: 0 }}> - {() => ( - <View style={{ lineHeight: 1.5 }}> - {confirmReason === 'batchDeleteWithReconciled' ? ( - <Block> - Deleting reconciled transactions may bring your reconciliation out - of balance. - </Block> - ) : confirmReason === 'batchEditWithReconciled' ? ( - <Block> - Editing reconciled transactions may bring your reconciliation out - of balance. - </Block> - ) : confirmReason === 'batchDuplicateWithReconciled' ? ( - <Block> - Duplicating reconciled transactions may bring your reconciliation - out of balance. - </Block> - ) : confirmReason === 'editReconciled' ? ( - <Block> - Saving your changes to this reconciled transaction may bring your - reconciliation out of balance. - </Block> - ) : confirmReason === 'unlockReconciled' ? ( - <Block> - Unlocking this transaction means you won‘t be warned about changes - that can impact your reconciled balance. (Changes to amount, - account, payee, etc). - </Block> - ) : confirmReason === 'deleteReconciled' ? ( - <Block> - Deleting this reconciled transaction may bring your reconciliation - out of balance. - </Block> - ) : ( - <Block>Are you sure you want to edit this transaction?</Block> - )} + <Modal + name="confirm-transaction-edit" + containerProps={{ style: { width: '30vw' } }} + > + {({ state: { close } }) => ( + <> + <ModalHeader + title="Reconciled Transaction" + rightContent={<ModalCloseButton onClick={close} />} + /> + <View style={{ lineHeight: 1.5 }}> + {confirmReason === 'batchDeleteWithReconciled' ? ( + <Block> + Deleting reconciled transactions may bring your reconciliation + out of balance. + </Block> + ) : confirmReason === 'batchEditWithReconciled' ? ( + <Block> + Editing reconciled transactions may bring your reconciliation + out of balance. + </Block> + ) : confirmReason === 'batchDuplicateWithReconciled' ? ( + <Block> + Duplicating reconciled transactions may bring your + reconciliation out of balance. + </Block> + ) : confirmReason === 'editReconciled' ? ( + <Block> + Saving your changes to this reconciled transaction may bring + your reconciliation out of balance. + </Block> + ) : confirmReason === 'unlockReconciled' ? ( + <Block> + Unlocking this transaction means you won‘t be warned about + changes that can impact your reconciled balance. (Changes to + amount, account, payee, etc). + </Block> + ) : confirmReason === 'deleteReconciled' ? ( + <Block> + Deleting this reconciled transaction may bring your + reconciliation out of balance. + </Block> + ) : ( + <Block>Are you sure you want to edit this transaction?</Block> + )} - <View - style={{ - marginTop: 20, - flexDirection: 'row', - justifyContent: 'flex-start', - alignItems: 'center', - }} - > <View style={{ + marginTop: 20, flexDirection: 'row', - justifyContent: 'flex-end', + justifyContent: 'flex-start', + alignItems: 'center', }} > - <Button - style={{ marginRight: 10 }} - onPress={() => { - modalProps.onClose(); - onCancel(); + <View + style={{ + flexDirection: 'row', + justifyContent: 'flex-end', }} > - Cancel - </Button> - <Button - variant="primary" - onPress={() => { - modalProps.onClose(); - onConfirm(); - }} - > - Confirm - </Button> + <Button + aria-label="Cancel" + style={{ marginRight: 10 }} + onPress={() => { + close(); + onCancel(); + }} + > + Cancel + </Button> + <Button + aria-label="Confirm" + variant="primary" + onPress={() => { + close(); + onConfirm(); + }} + > + Confirm + </Button> + </View> </View> </View> - </View> + </> )} </Modal> ); diff --git a/packages/desktop-client/src/components/modals/ConfirmUnlinkAccount.tsx b/packages/desktop-client/src/components/modals/ConfirmUnlinkAccount.tsx index 88fca3fe758af16a8791e53dc151d91baba6f47e..e3d5fb53595935c4ecfcf93cd0fed864028fa59d 100644 --- a/packages/desktop-client/src/components/modals/ConfirmUnlinkAccount.tsx +++ b/packages/desktop-client/src/components/modals/ConfirmUnlinkAccount.tsx @@ -1,55 +1,61 @@ import React from 'react'; import { Button } from '../common/Button2'; -import { Modal } from '../common/Modal'; +import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2'; import { Paragraph } from '../common/Paragraph'; import { View } from '../common/View'; -import { type CommonModalProps } from '../Modals'; type ConfirmUnlinkAccountProps = { - modalProps: CommonModalProps; accountName: string; onUnlink: () => void; }; export function ConfirmUnlinkAccount({ - modalProps, accountName, onUnlink, }: ConfirmUnlinkAccountProps) { return ( - <Modal title="Confirm Unlink" {...modalProps} style={{ flex: 0 }}> - {() => ( - <View style={{ lineHeight: 1.5 }}> - <Paragraph> - Are you sure you want to unlink <strong>{accountName}</strong>? - </Paragraph> + <Modal + name="confirm-unlink-account" + containerProps={{ style: { width: '30vw' } }} + > + {({ state: { close } }) => ( + <> + <ModalHeader + title="Confirm Unlink" + rightContent={<ModalCloseButton onClick={close} />} + /> + <View style={{ lineHeight: 1.5 }}> + <Paragraph> + Are you sure you want to unlink <strong>{accountName}</strong>? + </Paragraph> - <Paragraph> - Transactions will no longer be synchronized with this account and - must be manually entered. - </Paragraph> + <Paragraph> + Transactions will no longer be synchronized with this account and + must be manually entered. + </Paragraph> - <View - style={{ - flexDirection: 'row', - justifyContent: 'flex-end', - }} - > - <Button style={{ marginRight: 10 }} onPress={modalProps.onClose}> - Cancel - </Button> - <Button - variant="primary" - onPress={() => { - onUnlink(); - modalProps.onClose(); + <View + style={{ + flexDirection: 'row', + justifyContent: 'flex-end', }} > - Unlink - </Button> + <Button style={{ marginRight: 10 }} onPress={close}> + Cancel + </Button> + <Button + variant="primary" + onPress={() => { + onUnlink(); + close(); + }} + > + Unlink + </Button> + </View> </View> - </View> + </> )} </Modal> ); diff --git a/packages/desktop-client/src/components/modals/CoverModal.tsx b/packages/desktop-client/src/components/modals/CoverModal.tsx index 490f0f4f0575cd9038fd28479adfb05c0146b51e..fbf9e8f14104dd408d9cc616b4ae1eeb70430b94 100644 --- a/packages/desktop-client/src/components/modals/CoverModal.tsx +++ b/packages/desktop-client/src/components/modals/CoverModal.tsx @@ -8,13 +8,11 @@ import { useInitialMount } from '../../hooks/useInitialMount'; import { styles } from '../../style'; import { addToBeBudgetedGroup } from '../budget/util'; import { Button } from '../common/Button2'; -import { Modal } from '../common/Modal'; +import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2'; import { View } from '../common/View'; import { FieldLabel, TapField } from '../mobile/MobileForms'; -import { type CommonModalProps } from '../Modals'; type CoverModalProps = { - modalProps: CommonModalProps; title: string; month: string; showToBeBudgeted?: boolean; @@ -22,7 +20,6 @@ type CoverModalProps = { }; export function CoverModal({ - modalProps, title, month, showToBeBudgeted = true, @@ -57,8 +54,6 @@ export function CoverModal({ if (categoryId) { onSubmit?.(categoryId); } - - modalProps.onClose(); }; const initialMount = useInitialMount(); @@ -72,31 +67,42 @@ export function CoverModal({ const fromCategory = categories.find(c => c.id === fromCategoryId); return ( - <Modal title={title} showHeader focusAfterClose={false} {...modalProps}> - <View> - <FieldLabel title="Cover from category:" /> - <TapField value={fromCategory?.name} onClick={onCategoryClick} /> - </View> + <Modal name="cover"> + {({ state: { close } }) => ( + <> + <ModalHeader + title={title} + rightContent={<ModalCloseButton onClick={close} />} + /> + <View> + <FieldLabel title="Cover from category:" /> + <TapField value={fromCategory?.name} onClick={onCategoryClick} /> + </View> - <View - style={{ - justifyContent: 'center', - alignItems: 'center', - paddingTop: 10, - }} - > - <Button - variant="primary" - style={{ - height: styles.mobileMinHeight, - marginLeft: styles.mobileEditingPadding, - marginRight: styles.mobileEditingPadding, - }} - onPress={() => _onSubmit(fromCategoryId)} - > - Transfer - </Button> - </View> + <View + style={{ + justifyContent: 'center', + alignItems: 'center', + paddingTop: 10, + }} + > + <Button + variant="primary" + style={{ + height: styles.mobileMinHeight, + marginLeft: styles.mobileEditingPadding, + marginRight: styles.mobileEditingPadding, + }} + onPress={() => { + _onSubmit(fromCategoryId); + close(); + }} + > + Transfer + </Button> + </View> + </> + )} </Modal> ); } diff --git a/packages/desktop-client/src/components/modals/CreateAccountModal.tsx b/packages/desktop-client/src/components/modals/CreateAccountModal.tsx index b9cd815ef4b822de5f0122cf780169fb583cb09f..c23e67aaf2e3d001502f70e9e610c807fd0c58e2 100644 --- a/packages/desktop-client/src/components/modals/CreateAccountModal.tsx +++ b/packages/desktop-client/src/components/modals/CreateAccountModal.tsx @@ -14,21 +14,18 @@ import { theme } from '../../style'; import { Button, ButtonWithLoading } from '../common/Button2'; import { Link } from '../common/Link'; import { Menu } from '../common/Menu'; -import { Modal } from '../common/Modal'; +import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2'; import { Paragraph } from '../common/Paragraph'; import { Popover } from '../common/Popover'; import { Text } from '../common/Text'; import { View } from '../common/View'; -import { type CommonModalProps } from '../Modals'; type CreateAccountProps = { - modalProps: CommonModalProps; syncServerStatus: SyncServerStatus; upgradingAccountId?: string; }; export function CreateAccountModal({ - modalProps, syncServerStatus, upgradingAccountId, }: CreateAccountProps) { @@ -179,204 +176,211 @@ export function CreateAccountModal({ const simpleFinSyncFeatureFlag = useFeatureFlag('simpleFinSync'); return ( - <Modal title={title} {...modalProps}> - {() => ( - <View style={{ maxWidth: 500, gap: 30, color: theme.pageText }}> - {upgradingAccountId == null && ( - <View style={{ gap: 10 }}> - <Button - variant="primary" - style={{ - padding: '10px 0', - fontSize: 15, - fontWeight: 600, - }} - onPress={onCreateLocalAccount} - > - Create local account - </Button> - <View style={{ lineHeight: '1.4em', fontSize: 15 }}> - <Text> - <strong>Create a local account</strong> if you want to add - transactions manually. You can also{' '} - <Link - variant="external" - to="https://actualbudget.org/docs/transactions/importing" - linkColor="muted" - > - import QIF/OFX/QFX files into a local account - </Link> - . - </Text> - </View> - </View> - )} - <View style={{ gap: 10 }}> - {syncServerStatus === 'online' ? ( - <> - <View + <Modal name="add-account"> + {({ state: { close } }) => ( + <> + <ModalHeader + title={title} + rightContent={<ModalCloseButton onClick={close} />} + /> + <View style={{ maxWidth: 500, gap: 30, color: theme.pageText }}> + {upgradingAccountId == null && ( + <View style={{ gap: 10 }}> + <Button + variant="primary" style={{ - flexDirection: 'row', - gap: 10, - alignItems: 'center', + padding: '10px 0', + fontSize: 15, + fontWeight: 600, }} + onPress={onCreateLocalAccount} > - <ButtonWithLoading - isDisabled={syncServerStatus !== 'online'} + Create local account + </Button> + <View style={{ lineHeight: '1.4em', fontSize: 15 }}> + <Text> + <strong>Create a local account</strong> if you want to add + transactions manually. You can also{' '} + <Link + variant="external" + to="https://actualbudget.org/docs/transactions/importing" + linkColor="muted" + > + import QIF/OFX/QFX files into a local account + </Link> + . + </Text> + </View> + </View> + )} + <View style={{ gap: 10 }}> + {syncServerStatus === 'online' ? ( + <> + <View style={{ - padding: '10px 0', - fontSize: 15, - fontWeight: 600, - flex: 1, + flexDirection: 'row', + gap: 10, + alignItems: 'center', }} - onPress={onConnectGoCardless} > - {isGoCardlessSetupComplete - ? 'Link bank account with GoCardless' - : 'Set up GoCardless for bank sync'} - </ButtonWithLoading> - {isGoCardlessSetupComplete && ( - <> - <Button - ref={triggerRef} - variant="bare" - onPress={() => setGoCardlessMenuOpen(true)} - aria-label="GoCardless menu" - > - <SvgDotsHorizontalTriple - width={15} - height={15} - style={{ transform: 'rotateZ(90deg)' }} - /> - </Button> - - <Popover - triggerRef={triggerRef} - isOpen={menuGoCardlessOpen} - onOpenChange={() => setGoCardlessMenuOpen(false)} - > - <Menu - onMenuSelect={item => { - if (item === 'reconfigure') { - onGoCardlessReset(); - } - }} - items={[ - { - name: 'reconfigure', - text: 'Reset GoCardless credentials', - }, - ]} - /> - </Popover> - </> - )} - </View> - <Text style={{ lineHeight: '1.4em', fontSize: 15 }}> - <strong> - Link a <em>European</em> bank account - </strong>{' '} - to automatically download transactions. GoCardless provides - reliable, up-to-date information from hundreds of banks. - </Text> - {simpleFinSyncFeatureFlag === true && ( - <> - <View + <ButtonWithLoading + isDisabled={syncServerStatus !== 'online'} style={{ - flexDirection: 'row', - gap: 10, - marginTop: '18px', - alignItems: 'center', + padding: '10px 0', + fontSize: 15, + fontWeight: 600, + flex: 1, }} + onPress={onConnectGoCardless} > - <ButtonWithLoading - isDisabled={syncServerStatus !== 'online'} - isLoading={loadingSimpleFinAccounts} + {isGoCardlessSetupComplete + ? 'Link bank account with GoCardless' + : 'Set up GoCardless for bank sync'} + </ButtonWithLoading> + {isGoCardlessSetupComplete && ( + <> + <Button + ref={triggerRef} + variant="bare" + onPress={() => setGoCardlessMenuOpen(true)} + aria-label="GoCardless menu" + > + <SvgDotsHorizontalTriple + width={15} + height={15} + style={{ transform: 'rotateZ(90deg)' }} + /> + </Button> + + <Popover + triggerRef={triggerRef} + isOpen={menuGoCardlessOpen} + onOpenChange={() => setGoCardlessMenuOpen(false)} + > + <Menu + onMenuSelect={item => { + if (item === 'reconfigure') { + onGoCardlessReset(); + } + }} + items={[ + { + name: 'reconfigure', + text: 'Reset GoCardless credentials', + }, + ]} + /> + </Popover> + </> + )} + </View> + <Text style={{ lineHeight: '1.4em', fontSize: 15 }}> + <strong> + Link a <em>European</em> bank account + </strong>{' '} + to automatically download transactions. GoCardless provides + reliable, up-to-date information from hundreds of banks. + </Text> + {simpleFinSyncFeatureFlag === true && ( + <> + <View style={{ - padding: '10px 0', - fontSize: 15, - fontWeight: 600, - flex: 1, + flexDirection: 'row', + gap: 10, + marginTop: '18px', + alignItems: 'center', }} - onPress={onConnectSimpleFin} > - {isSimpleFinSetupComplete - ? 'Link bank account with SimpleFIN' - : 'Set up SimpleFIN for bank sync'} - </ButtonWithLoading> - {isSimpleFinSetupComplete && ( - <> - <Button - ref={triggerRef} - variant="bare" - onPress={() => setSimplefinMenuOpen(true)} - aria-label="SimpleFIN menu" - > - <SvgDotsHorizontalTriple - width={15} - height={15} - style={{ transform: 'rotateZ(90deg)' }} - /> - </Button> - <Popover - triggerRef={triggerRef} - isOpen={menuSimplefinOpen} - onOpenChange={() => setSimplefinMenuOpen(false)} - > - <Menu - onMenuSelect={item => { - if (item === 'reconfigure') { - onSimpleFinReset(); - } - }} - items={[ - { - name: 'reconfigure', - text: 'Reset SimpleFIN credentials', - }, - ]} - /> - </Popover> - </> - )} - </View> - <Text style={{ lineHeight: '1.4em', fontSize: 15 }}> - <strong> - Link a <em>North American</em> bank account - </strong>{' '} - to automatically download transactions. SimpleFIN provides - reliable, up-to-date information from hundreds of banks. - </Text> - </> - )} - </> - ) : ( - <> - <Button - isDisabled - style={{ - padding: '10px 0', - fontSize: 15, - fontWeight: 600, - }} - > - Set up bank sync - </Button> - <Paragraph style={{ fontSize: 15 }}> - Connect to an Actual server to set up{' '} - <Link - variant="external" - to="https://actualbudget.org/docs/advanced/bank-sync" - linkColor="muted" + <ButtonWithLoading + isDisabled={syncServerStatus !== 'online'} + isLoading={loadingSimpleFinAccounts} + style={{ + padding: '10px 0', + fontSize: 15, + fontWeight: 600, + flex: 1, + }} + onPress={onConnectSimpleFin} + > + {isSimpleFinSetupComplete + ? 'Link bank account with SimpleFIN' + : 'Set up SimpleFIN for bank sync'} + </ButtonWithLoading> + {isSimpleFinSetupComplete && ( + <> + <Button + ref={triggerRef} + variant="bare" + onPress={() => setSimplefinMenuOpen(true)} + aria-label="SimpleFIN menu" + > + <SvgDotsHorizontalTriple + width={15} + height={15} + style={{ transform: 'rotateZ(90deg)' }} + /> + </Button> + <Popover + triggerRef={triggerRef} + isOpen={menuSimplefinOpen} + onOpenChange={() => setSimplefinMenuOpen(false)} + > + <Menu + onMenuSelect={item => { + if (item === 'reconfigure') { + onSimpleFinReset(); + } + }} + items={[ + { + name: 'reconfigure', + text: 'Reset SimpleFIN credentials', + }, + ]} + /> + </Popover> + </> + )} + </View> + <Text style={{ lineHeight: '1.4em', fontSize: 15 }}> + <strong> + Link a <em>North American</em> bank account + </strong>{' '} + to automatically download transactions. SimpleFIN + provides reliable, up-to-date information from hundreds + of banks. + </Text> + </> + )} + </> + ) : ( + <> + <Button + isDisabled + style={{ + padding: '10px 0', + fontSize: 15, + fontWeight: 600, + }} > - automatic syncing - </Link> - . - </Paragraph> - </> - )} + Set up bank sync + </Button> + <Paragraph style={{ fontSize: 15 }}> + Connect to an Actual server to set up{' '} + <Link + variant="external" + to="https://actualbudget.org/docs/advanced/bank-sync" + linkColor="muted" + > + automatic syncing + </Link> + . + </Paragraph> + </> + )} + </View> </View> - </View> + </> )} </Modal> ); diff --git a/packages/desktop-client/src/components/modals/CreateEncryptionKeyModal.tsx b/packages/desktop-client/src/components/modals/CreateEncryptionKeyModal.tsx index 1c39b5aa9fe9ef7cf8773c15f788bc75d3017f36..8844739a4900f2cb53dd64c854988eac5ab53209 100644 --- a/packages/desktop-client/src/components/modals/CreateEncryptionKeyModal.tsx +++ b/packages/desktop-client/src/components/modals/CreateEncryptionKeyModal.tsx @@ -1,5 +1,6 @@ // @ts-strict-ignore import React, { useState } from 'react'; +import { Form } from 'react-aria-components'; import { useDispatch } from 'react-redux'; import { css } from 'glamor'; @@ -14,21 +15,23 @@ import { ButtonWithLoading } from '../common/Button2'; import { InitialFocus } from '../common/InitialFocus'; import { Input } from '../common/Input'; import { Link } from '../common/Link'; -import { Modal, ModalButtons } from '../common/Modal'; +import { + Modal, + ModalButtons, + ModalCloseButton, + ModalHeader, +} from '../common/Modal2'; import { Paragraph } from '../common/Paragraph'; import { Text } from '../common/Text'; import { View } from '../common/View'; -import { type CommonModalProps } from '../Modals'; type CreateEncryptionKeyModalProps = { - modalProps: CommonModalProps; options: { recreate?: boolean; }; }; export function CreateEncryptionKeyModal({ - modalProps, options = {}, }: CreateEncryptionKeyModalProps) { const [password, setPassword] = useState(''); @@ -57,146 +60,153 @@ export function CreateEncryptionKeyModal({ dispatch(sync()); setLoading(false); - modalProps.onClose(); } } return ( - <Modal - {...modalProps} - title={isRecreating ? 'Generate new key' : 'Enable encryption'} - onClose={modalProps.onClose} - > - <View - style={{ - maxWidth: 600, - overflowX: 'hidden', - overflowY: 'auto', - flex: 1, - }} - > - {!isRecreating ? ( - <> - <Paragraph style={{ marginTop: 5 }}> - To enable end-to-end encryption, you need to create a key. We will - generate a key based on a password and use it to encrypt from now - on. <strong>This requires a sync reset</strong> and all other - devices will have to revert to this version of your data.{' '} - <Link - variant="external" - to="https://actualbudget.org/docs/getting-started/sync/#end-to-end-encryption" - linkColor="purple" - > - Learn more - </Link> - </Paragraph> - <Paragraph> - <ul - className={`${css({ - marginTop: 0, - '& li': { marginBottom: 8 }, - })}`} - > - <li> - <strong>Important:</strong> if you forget this password{' '} - <em>and</em> you don’t have any local copies of your data, you - will lose access to all your data. The data cannot be - decrypted without the password. - </li> - <li> - This key only applies to this file. You will need to generate - a new key for each file you want to encrypt. - </li> - <li> - If you’ve already downloaded your data on other devices, you - will need to reset them. Actual will automatically take you - through this process. - </li> - <li> - It is recommended for the encryption password to be different - than the log-in password in order to better protect your data. - </li> - </ul> - </Paragraph> - </> - ) : ( - <> - <Paragraph style={{ marginTop: 5 }}> - This will generate a new key for encrypting your data.{' '} - <strong>This requires a sync reset</strong> and all other devices - will have to revert to this version of your data. Actual will take - you through that process on those devices.{' '} - <Link - variant="external" - to="https://actualbudget.org/docs/getting-started/sync/#end-to-end-encryption" - linkColor="purple" - > - Learn more - </Link> - </Paragraph> - <Paragraph> - Key generation is randomized. The same password will create - different keys, so this will change your key regardless of the - password being different. - </Paragraph> - </> - )} - </View> - <form - onSubmit={e => { - e.preventDefault(); - onCreateKey(); - }} - > - <View style={{ alignItems: 'center' }}> - <Text style={{ fontWeight: 600, marginBottom: 3 }}>Password</Text> - - {error && ( - <View - style={{ - color: theme.errorText, - textAlign: 'center', - fontSize: 13, - marginBottom: 3, - }} - > - {error} - </View> - )} - - <InitialFocus> - <Input - type={showPassword ? 'text' : 'password'} - style={{ - width: isNarrowWidth ? '100%' : '50%', - height: isNarrowWidth ? styles.mobileMinHeight : undefined, - }} - onChange={e => setPassword(e.target.value)} - /> - </InitialFocus> - <Text style={{ marginTop: 5 }}> - <label style={{ userSelect: 'none' }}> - <input - type="checkbox" - onClick={() => setShowPassword(!showPassword)} - />{' '} - Show password - </label> - </Text> - </View> - - <ModalButtons style={{ marginTop: 20 }}> - <ButtonWithLoading - variant="primary" + <Modal name="create-encryption-key"> + {({ state: { close } }) => ( + <> + <ModalHeader + title={isRecreating ? 'Generate new key' : 'Enable encryption'} + rightContent={<ModalCloseButton onClick={close} />} + /> + <View style={{ - height: isNarrowWidth ? styles.mobileMinHeight : undefined, + maxWidth: 600, + overflowX: 'hidden', + overflowY: 'auto', + flex: 1, }} - isLoading={loading} > - Enable - </ButtonWithLoading> - </ModalButtons> - </form> + {!isRecreating ? ( + <> + <Paragraph style={{ marginTop: 5 }}> + To enable end-to-end encryption, you need to create a key. We + will generate a key based on a password and use it to encrypt + from now on. <strong>This requires a sync reset</strong> and + all other devices will have to revert to this version of your + data.{' '} + <Link + variant="external" + to="https://actualbudget.org/docs/getting-started/sync/#end-to-end-encryption" + linkColor="purple" + > + Learn more + </Link> + </Paragraph> + <Paragraph> + <ul + className={`${css({ + marginTop: 0, + '& li': { marginBottom: 8 }, + })}`} + > + <li> + <strong>Important:</strong> if you forget this password{' '} + <em>and</em> you don’t have any local copies of your data, + you will lose access to all your data. The data cannot be + decrypted without the password. + </li> + <li> + This key only applies to this file. You will need to + generate a new key for each file you want to encrypt. + </li> + <li> + If you’ve already downloaded your data on other devices, + you will need to reset them. Actual will automatically + take you through this process. + </li> + <li> + It is recommended for the encryption password to be + different than the log-in password in order to better + protect your data. + </li> + </ul> + </Paragraph> + </> + ) : ( + <> + <Paragraph style={{ marginTop: 5 }}> + This will generate a new key for encrypting your data.{' '} + <strong>This requires a sync reset</strong> and all other + devices will have to revert to this version of your data. + Actual will take you through that process on those devices.{' '} + <Link + variant="external" + to="https://actualbudget.org/docs/getting-started/sync/#end-to-end-encryption" + linkColor="purple" + > + Learn more + </Link> + </Paragraph> + <Paragraph> + Key generation is randomized. The same password will create + different keys, so this will change your key regardless of the + password being different. + </Paragraph> + </> + )} + </View> + <Form + onSubmit={e => { + e.preventDefault(); + onCreateKey(); + close(); + }} + > + <View style={{ alignItems: 'center' }}> + <Text style={{ fontWeight: 600, marginBottom: 3 }}>Password</Text> + + {error && ( + <View + style={{ + color: theme.errorText, + textAlign: 'center', + fontSize: 13, + marginBottom: 3, + }} + > + {error} + </View> + )} + + <InitialFocus> + <Input + type={showPassword ? 'text' : 'password'} + style={{ + width: isNarrowWidth ? '100%' : '50%', + height: isNarrowWidth ? styles.mobileMinHeight : undefined, + }} + onChange={e => setPassword(e.target.value)} + /> + </InitialFocus> + <Text style={{ marginTop: 5 }}> + <label style={{ userSelect: 'none' }}> + <input + type="checkbox" + onClick={() => setShowPassword(!showPassword)} + />{' '} + Show password + </label> + </Text> + </View> + + <ModalButtons style={{ marginTop: 20 }}> + <ButtonWithLoading + type="submit" + style={{ + height: isNarrowWidth ? styles.mobileMinHeight : undefined, + }} + isLoading={loading} + variant="primary" + > + Enable + </ButtonWithLoading> + </ModalButtons> + </Form> + </> + )} </Modal> ); } diff --git a/packages/desktop-client/src/components/modals/CreateLocalAccountModal.tsx b/packages/desktop-client/src/components/modals/CreateLocalAccountModal.tsx index 0e3dcc07917a837c7544b8437237b9630a7b386a..2689ddd1267215245fb92f1faef220e3e24454aa 100644 --- a/packages/desktop-client/src/components/modals/CreateLocalAccountModal.tsx +++ b/packages/desktop-client/src/components/modals/CreateLocalAccountModal.tsx @@ -1,10 +1,11 @@ // @ts-strict-ignore import React, { type FormEvent, useState } from 'react'; import { Form } from 'react-aria-components'; +import { useDispatch } from 'react-redux'; +import { closeModal, createAccount } from 'loot-core/client/actions'; import { toRelaxedNumber } from 'loot-core/src/shared/util'; -import { type BoundActions } from '../../hooks/useActions'; import { useNavigate } from '../../hooks/useNavigate'; import { theme } from '../../style'; import { Button } from '../common/Button2'; @@ -13,22 +14,20 @@ import { InitialFocus } from '../common/InitialFocus'; import { InlineField } from '../common/InlineField'; import { Input } from '../common/Input'; import { Link } from '../common/Link'; -import { Modal, ModalButtons, ModalTitle } from '../common/Modal'; +import { + Modal, + ModalButtons, + ModalCloseButton, + ModalHeader, + ModalTitle, +} from '../common/Modal2'; import { Text } from '../common/Text'; import { View } from '../common/View'; import { Checkbox } from '../forms'; -import { type CommonModalProps } from '../Modals'; -type CreateLocalAccountProps = { - modalProps: CommonModalProps; - actions: BoundActions; -}; - -export function CreateLocalAccountModal({ - modalProps, - actions, -}: CreateLocalAccountProps) { +export function CreateLocalAccountModal() { const navigate = useNavigate(); + const dispatch = useDispatch(); const [name, setName] = useState(''); const [offbudget, setOffbudget] = useState(false); const [balance, setBalance] = useState('0'); @@ -48,132 +47,135 @@ export function CreateLocalAccountModal({ setBalanceError(balanceError); if (!nameError && !balanceError) { - actions.closeModal(); - const id = await actions.createAccount( - name, - toRelaxedNumber(balance), - offbudget, + dispatch(closeModal()); + const id = await dispatch( + createAccount(name, toRelaxedNumber(balance), offbudget), ); navigate('/accounts/' + id); } }; return ( - <Modal - title={<ModalTitle title="Create Local Account" shrinkOnOverflow />} - {...modalProps} - > - {() => ( - <View> - <Form onSubmit={onSubmit}> - <InlineField label="Name" width="100%"> - <InitialFocus> - <Input - name="name" - value={name} - onChange={event => setName(event.target.value)} - onBlur={event => { - const name = event.target.value.trim(); - setName(name); - if (name && nameError) { - setNameError(false); - } - }} - style={{ flex: 1 }} - /> - </InitialFocus> - </InlineField> - {nameError && ( - <FormError style={{ marginLeft: 75 }}>Name is required</FormError> - )} - - <View - style={{ - width: '100%', - flexDirection: 'row', - justifyContent: 'flex-end', - }} - > - <View style={{ flexDirection: 'column' }}> - <View - style={{ - flexDirection: 'row', - justifyContent: 'flex-end', - }} - > - <Checkbox - id="offbudget" - name="offbudget" - checked={offbudget} - onChange={() => setOffbudget(!offbudget)} + <Modal name="add-local-account"> + {({ state: { close } }) => ( + <> + <ModalHeader + title={<ModalTitle title="Create Local Account" shrinkOnOverflow />} + rightContent={<ModalCloseButton onClick={close} />} + /> + <View> + <Form onSubmit={onSubmit}> + <InlineField label="Name" width="100%"> + <InitialFocus> + <Input + name="name" + value={name} + onChange={event => setName(event.target.value)} + onBlur={event => { + const name = event.target.value.trim(); + setName(name); + if (name && nameError) { + setNameError(false); + } + }} + style={{ flex: 1 }} /> - <label - htmlFor="offbudget" + </InitialFocus> + </InlineField> + {nameError && ( + <FormError style={{ marginLeft: 75 }}> + Name is required + </FormError> + )} + + <View + style={{ + width: '100%', + flexDirection: 'row', + justifyContent: 'flex-end', + }} + > + <View style={{ flexDirection: 'column' }}> + <View style={{ - userSelect: 'none', - verticalAlign: 'center', + flexDirection: 'row', + justifyContent: 'flex-end', }} > - Off-budget - </label> - </View> - <div - style={{ - textAlign: 'right', - fontSize: '0.7em', - color: theme.pageTextLight, - marginTop: 3, - }} - > - <Text> - This cannot be changed later. <br /> {'\n'} - See{' '} - <Link - variant="external" - linkColor="muted" - to="https://actualbudget.org/docs/accounts/#off-budget-accounts" + <Checkbox + id="offbudget" + name="offbudget" + checked={offbudget} + onChange={() => setOffbudget(!offbudget)} + /> + <label + htmlFor="offbudget" + style={{ + userSelect: 'none', + verticalAlign: 'center', + }} > - Accounts Overview - </Link>{' '} - for more information. - </Text> - </div> + Off-budget + </label> + </View> + <div + style={{ + textAlign: 'right', + fontSize: '0.7em', + color: theme.pageTextLight, + marginTop: 3, + }} + > + <Text> + This cannot be changed later. <br /> {'\n'} + See{' '} + <Link + variant="external" + linkColor="muted" + to="https://actualbudget.org/docs/accounts/#off-budget-accounts" + > + Accounts Overview + </Link>{' '} + for more information. + </Text> + </div> + </View> </View> - </View> - <InlineField label="Balance" width="100%"> - <Input - name="balance" - inputMode="decimal" - value={balance} - onChange={event => setBalance(event.target.value)} - onBlur={event => { - const balance = event.target.value.trim(); - setBalance(balance); - if (validateBalance(balance) && balanceError) { - setBalanceError(false); - } - }} - style={{ flex: 1 }} - /> - </InlineField> - {balanceError && ( - <FormError style={{ marginLeft: 75 }}> - Balance must be a number - </FormError> - )} + <InlineField label="Balance" width="100%"> + <Input + name="balance" + inputMode="decimal" + value={balance} + onChange={event => setBalance(event.target.value)} + onBlur={event => { + const balance = event.target.value.trim(); + setBalance(balance); + if (validateBalance(balance) && balanceError) { + setBalanceError(false); + } + }} + style={{ flex: 1 }} + /> + </InlineField> + {balanceError && ( + <FormError style={{ marginLeft: 75 }}> + Balance must be a number + </FormError> + )} - <ModalButtons> - <Button onPress={() => modalProps.onBack()}>Back</Button> - <Button - type="submit" - variant="primary" - style={{ marginLeft: 10 }} - > - Create - </Button> - </ModalButtons> - </Form> - </View> + <ModalButtons> + <Button onPress={close}>Back</Button> + <Button + type="submit" + variant="primary" + style={{ marginLeft: 10 }} + > + Create + </Button> + </ModalButtons> + </Form> + </View> + </> )} </Modal> ); diff --git a/packages/desktop-client/src/components/modals/EditField.jsx b/packages/desktop-client/src/components/modals/EditField.jsx index 3ffc5168b2d7aaa85951adad6b5b940cf08fc64c..e34eb7e58ad657c507594b3d3e94ffcc63745f27 100644 --- a/packages/desktop-client/src/components/modals/EditField.jsx +++ b/packages/desktop-client/src/components/modals/EditField.jsx @@ -10,23 +10,18 @@ import { useResponsive } from '../../ResponsiveProvider'; import { theme } from '../../style'; import { Button } from '../common/Button2'; import { Input } from '../common/Input'; -import { Modal } from '../common/Modal'; +import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2'; import { View } from '../common/View'; import { SectionLabel } from '../forms'; import { DateSelect } from '../select/DateSelect'; -export function EditField({ modalProps, name, onSubmit, onClose }) { +export function EditField({ name, onSubmit, onClose }) { const dateFormat = useDateFormat() || 'MM/dd/yyyy'; - const onCloseInner = () => { - modalProps.onClose(); - onClose?.(); - }; function onSelectNote(value, mode) { if (value != null) { onSubmit(name, value, mode); } - onCloseInner(); } function onSelect(value) { @@ -38,7 +33,6 @@ export function EditField({ modalProps, name, onSubmit, onClose }) { onSubmit(name, value); } - onCloseInner(); } const itemStyle = { @@ -62,7 +56,7 @@ export function EditField({ modalProps, name, onSubmit, onClose }) { const today = currentDay(); label = 'Date'; minWidth = 350; - editor = ( + editor = ({ close }) => ( <DateSelect value={formatDate(parseISO(today), dateFormat)} dateFormat={dateFormat} @@ -71,6 +65,7 @@ export function EditField({ modalProps, name, onSubmit, onClose }) { onUpdate={() => {}} onSelect={date => { onSelect(dayFromDate(parseDate(date, 'yyyy-MM-dd', new Date()))); + close(); }} /> ); @@ -78,7 +73,7 @@ export function EditField({ modalProps, name, onSubmit, onClose }) { case 'notes': label = 'Notes'; - editor = ( + editor = ({ close }) => ( <> <View style={{ @@ -192,7 +187,10 @@ export function EditField({ modalProps, name, onSubmit, onClose }) { id="noteInput" autoFocus focused={true} - onEnter={e => onSelectNote(e.target.value, noteAmend)} + onEnter={e => { + onSelectNote(e.target.value, noteAmend); + close(); + }} style={inputStyle} /> </> @@ -201,10 +199,13 @@ export function EditField({ modalProps, name, onSubmit, onClose }) { case 'amount': label = 'Amount'; - editor = ( + editor = ({ close }) => ( <Input focused={true} - onEnter={e => onSelect(e.target.value)} + onEnter={e => { + onSelect(e.target.value); + close(); + }} style={inputStyle} /> ); @@ -215,34 +216,40 @@ export function EditField({ modalProps, name, onSubmit, onClose }) { return ( <Modal - title={label} + name="edit-field" noAnimation={!isNarrowWidth} - showHeader={isNarrowWidth} - focusAfterClose={false} - {...modalProps} - onClose={onCloseInner} - style={{ - flex: 0, - height: isNarrowWidth ? '85vh' : 275, - padding: '15px 10px', - ...(minWidth && { minWidth }), - backgroundColor: theme.menuAutoCompleteBackground, + onClose={onClose} + containerProps={{ + style: { + height: isNarrowWidth ? '85vh' : 275, + padding: '15px 10px', + ...(minWidth && { minWidth }), + backgroundColor: theme.menuAutoCompleteBackground, + }, }} > - {() => ( - <View> - {!isNarrowWidth && ( - <SectionLabel + {({ state: { close } }) => ( + <> + {isNarrowWidth && ( + <ModalHeader title={label} - style={{ - alignSelf: 'center', - color: theme.menuAutoCompleteText, - marginBottom: 10, - }} + rightContent={<ModalCloseButton onClick={close} />} /> )} - <View style={{ flex: 1 }}>{editor}</View> - </View> + <View> + {!isNarrowWidth && ( + <SectionLabel + title={label} + style={{ + alignSelf: 'center', + color: theme.menuAutoCompleteText, + marginBottom: 10, + }} + /> + )} + <View style={{ flex: 1 }}>{editor({ close })}</View> + </View> + </> )} </Modal> ); diff --git a/packages/desktop-client/src/components/modals/EditRule.jsx b/packages/desktop-client/src/components/modals/EditRule.jsx index b54ec5d654fe92815e7a865a0346576e4ff90dc9..2256bc2877563674cca9dab9343c7078ab1b532a 100644 --- a/packages/desktop-client/src/components/modals/EditRule.jsx +++ b/packages/desktop-client/src/components/modals/EditRule.jsx @@ -42,7 +42,7 @@ import { SvgInformationOutline } from '../../icons/v1'; import { styles, theme } from '../../style'; import { Button } from '../common/Button2'; import { Menu } from '../common/Menu'; -import { Modal } from '../common/Modal'; +import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2'; import { Select } from '../common/Select'; import { Stack } from '../common/Stack'; import { Text } from '../common/Text'; @@ -671,7 +671,7 @@ const conditionFields = [ ['amount-outflow', mapField('amount', { outflow: true })], ]); -export function EditRule({ modalProps, defaultRule, onSave: originalOnSave }) { +export function EditRule({ defaultRule, onSave: originalOnSave }) { const [conditions, setConditions] = useState( defaultRule.conditions.map(parse), ); @@ -888,7 +888,6 @@ export function EditRule({ modalProps, defaultRule, onSave: originalOnSave }) { } originalOnSave?.(rule); - modalProps.onClose(); } } @@ -902,256 +901,264 @@ export function EditRule({ modalProps, defaultRule, onSave: originalOnSave }) { const showSplitButton = actionSplits.length > 0; return ( - <Modal - title="Rule" - {...modalProps} - style={{ ...modalProps.style, flex: 'inherit' }} - > - {() => ( - <View - style={{ - maxWidth: '100%', - width: 900, - height: '80vh', - flexGrow: 0, - flexShrink: 0, - flexBasis: 'auto', - overflow: 'hidden', - color: theme.pageTextLight, - }} - > + <Modal name="edit-rule"> + {({ state: { close } }) => ( + <> + <ModalHeader + title="Rule" + rightContent={<ModalCloseButton onClick={close} />} + /> <View style={{ - flexDirection: 'row', - alignItems: 'center', - marginBottom: 15, - padding: '0 20px', + maxWidth: '100%', + width: 900, + height: '80vh', + flexGrow: 0, + flexShrink: 0, + flexBasis: 'auto', + overflow: 'hidden', + color: theme.pageTextLight, }} > - <Text style={{ marginRight: 15 }}>Stage of rule:</Text> - - <Stack direction="row" align="center" spacing={1}> - <StageButton - selected={stage === 'pre'} - onSelect={() => onChangeStage('pre')} - > - Pre - </StageButton> - <StageButton - selected={stage === null} - onSelect={() => onChangeStage(null)} - > - Default - </StageButton> - <StageButton - selected={stage === 'post'} - onSelect={() => onChangeStage('post')} - > - Post - </StageButton> - - <StageInfo /> - </Stack> - </View> + <View + style={{ + flexDirection: 'row', + alignItems: 'center', + marginBottom: 15, + padding: '0 20px', + }} + > + <Text style={{ marginRight: 15 }}>Stage of rule:</Text> - <View - innerRef={scrollableEl} - style={{ - borderBottom: '1px solid ' + theme.tableBorder, - padding: 20, - overflow: 'auto', - maxHeight: 'calc(100% - 300px)', - }} - > - <View style={{ flexShrink: 0 }}> - <View style={{ marginBottom: 30 }}> - <Text style={{ marginBottom: 15 }}> - If - <FieldSelect - data-testid="conditions-op" - style={{ display: 'inline-flex' }} - fields={[ - ['and', 'all'], - ['or', 'any'], - ]} - value={conditionsOp} - onChange={onChangeConditionsOp} - /> - of these conditions match: - </Text> + <Stack direction="row" align="center" spacing={1}> + <StageButton + selected={stage === 'pre'} + onSelect={() => onChangeStage('pre')} + > + Pre + </StageButton> + <StageButton + selected={stage === null} + onSelect={() => onChangeStage(null)} + > + Default + </StageButton> + <StageButton + selected={stage === 'post'} + onSelect={() => onChangeStage('post')} + > + Post + </StageButton> - <ConditionsList - conditionsOp={conditionsOp} - conditions={conditions} - editorStyle={editorStyle} - isSchedule={isSchedule} - onChangeConditions={conds => setConditions(conds)} - /> - </View> + <StageInfo /> + </Stack> + </View> - <Text style={{ marginBottom: 15 }}> - Then apply these actions: - </Text> - <View style={{ flex: 1 }}> - {actionSplits.length === 0 && ( - <Button - style={{ alignSelf: 'flex-start' }} - onPress={addInitialAction} - > - Add action - </Button> - )} - <Stack spacing={2} data-testid="action-split-list"> - {actionSplits.map(({ id, actions }, splitIndex) => ( - <View - key={id} - nativeStyle={ - actionSplits.length > 1 - ? { - borderColor: theme.tableBorder, - borderWidth: '1px', - borderRadius: '5px', - padding: '5px', - } - : {} - } + <View + innerRef={scrollableEl} + style={{ + borderBottom: '1px solid ' + theme.tableBorder, + padding: 20, + overflow: 'auto', + maxHeight: 'calc(100% - 300px)', + }} + > + <View style={{ flexShrink: 0 }}> + <View style={{ marginBottom: 30 }}> + <Text style={{ marginBottom: 15 }}> + If + <FieldSelect + data-testid="conditions-op" + style={{ display: 'inline-flex' }} + fields={[ + ['and', 'all'], + ['or', 'any'], + ]} + value={conditionsOp} + onChange={onChangeConditionsOp} + /> + of these conditions match: + </Text> + + <ConditionsList + conditionsOp={conditionsOp} + conditions={conditions} + editorStyle={editorStyle} + isSchedule={isSchedule} + onChangeConditions={conds => setConditions(conds)} + /> + </View> + + <Text style={{ marginBottom: 15 }}> + Then apply these actions: + </Text> + <View style={{ flex: 1 }}> + {actionSplits.length === 0 && ( + <Button + style={{ alignSelf: 'flex-start' }} + onPress={addInitialAction} > - {actionSplits.length > 1 && ( - <Stack - direction="row" - justify="space-between" - spacing={1} - > - <Text - style={{ - ...styles.smallText, - marginBottom: '10px', - }} + Add action + </Button> + )} + <Stack spacing={2} data-testid="action-split-list"> + {actionSplits.map(({ id, actions }, splitIndex) => ( + <View + key={id} + nativeStyle={ + actionSplits.length > 1 + ? { + borderColor: theme.tableBorder, + borderWidth: '1px', + borderRadius: '5px', + padding: '5px', + } + : {} + } + > + {actionSplits.length > 1 && ( + <Stack + direction="row" + justify="space-between" + spacing={1} > - {splitIndex === 0 - ? 'Apply to all' - : `Split ${splitIndex}`} - </Text> - {splitIndex && ( - <Button - variant="bare" - onPress={() => onRemoveSplit(splitIndex)} + <Text style={{ - width: 20, - height: 20, + ...styles.smallText, + marginBottom: '10px', }} - aria-label="Delete split" > - <SvgDelete + {splitIndex === 0 + ? 'Apply to all' + : `Split ${splitIndex}`} + </Text> + {splitIndex && ( + <Button + variant="bare" + onPress={() => onRemoveSplit(splitIndex)} style={{ - width: 8, - height: 8, - color: 'inherit', + width: 20, + height: 20, }} + aria-label="Delete split" + > + <SvgDelete + style={{ + width: 8, + height: 8, + color: 'inherit', + }} + /> + </Button> + )} + </Stack> + )} + <Stack spacing={2} data-testid="action-list"> + {actions.map((action, actionIndex) => ( + <View key={actionIndex}> + <ActionEditor + ops={['set', 'link-schedule']} + action={action} + editorStyle={editorStyle} + onChange={(name, value) => { + onChangeAction(action, name, value); + }} + onDelete={() => onRemoveAction(action)} + onAdd={() => + addActionToSplitAfterIndex( + splitIndex, + actionIndex, + ) + } /> - </Button> - )} + </View> + ))} </Stack> - )} - <Stack spacing={2} data-testid="action-list"> - {actions.map((action, actionIndex) => ( - <View key={actionIndex}> - <ActionEditor - ops={['set', 'link-schedule']} - action={action} - editorStyle={editorStyle} - onChange={(name, value) => { - onChangeAction(action, name, value); - }} - onDelete={() => onRemoveAction(action)} - onAdd={() => - addActionToSplitAfterIndex( - splitIndex, - actionIndex, - ) - } - /> - </View> - ))} - </Stack> - - {actions.length === 0 && ( - <Button - style={{ alignSelf: 'flex-start', marginTop: 5 }} - onPress={() => - addActionToSplitAfterIndex(splitIndex, -1) - } - > - Add action - </Button> - )} - </View> - ))} - </Stack> - {showSplitButton && ( + + {actions.length === 0 && ( + <Button + style={{ alignSelf: 'flex-start', marginTop: 5 }} + onPress={() => + addActionToSplitAfterIndex(splitIndex, -1) + } + > + Add action + </Button> + )} + </View> + ))} + </Stack> + {showSplitButton && ( + <Button + style={{ alignSelf: 'flex-start', marginTop: 15 }} + onPress={() => { + addActionToSplitAfterIndex(actionSplits.length, -1); + }} + data-testid="add-split-transactions" + > + {actionSplits.length > 1 + ? 'Add another split' + : 'Split into multiple transactions'} + </Button> + )} + </View> + </View> + </View> + + <SelectedProvider instance={selectedInst}> + <View style={{ padding: '20px', flex: 1 }}> + <View + style={{ + flexDirection: 'row', + alignItems: 'center', + marginBottom: 12, + }} + > + <Text style={{ color: theme.pageTextLight, marginBottom: 0 }}> + This rule applies to these transactions: + </Text> + + <View style={{ flex: 1 }} /> + <Button + isDisabled={selectedInst.items.size === 0} + onPress={onApply} + > + Apply actions ({selectedInst.items.size}) + </Button> + </View> + + <SimpleTransactionsTable + transactions={transactions} + fields={getTransactionFields( + conditions, + getActions(actionSplits), + )} + style={{ + border: '1px solid ' + theme.tableBorder, + borderRadius: '6px 6px 0 0', + }} + /> + + <Stack + direction="row" + justify="flex-end" + style={{ marginTop: 20 }} + > + <Button onClick={close}>Cancel</Button> <Button - style={{ alignSelf: 'flex-start', marginTop: 15 }} + variant="primary" onPress={() => { - addActionToSplitAfterIndex(actionSplits.length, -1); + onSave(); + close(); }} - data-testid="add-split-transactions" > - {actionSplits.length > 1 - ? 'Add another split' - : 'Split into multiple transactions'} + Save </Button> - )} + </Stack> </View> - </View> + </SelectedProvider> </View> - - <SelectedProvider instance={selectedInst}> - <View style={{ padding: '20px', flex: 1 }}> - <View - style={{ - flexDirection: 'row', - alignItems: 'center', - marginBottom: 12, - }} - > - <Text style={{ color: theme.pageTextLight, marginBottom: 0 }}> - This rule applies to these transactions: - </Text> - - <View style={{ flex: 1 }} /> - <Button - isDisabled={selectedInst.items.size === 0} - onPress={onApply} - > - Apply actions ({selectedInst.items.size}) - </Button> - </View> - - <SimpleTransactionsTable - transactions={transactions} - fields={getTransactionFields( - conditions, - getActions(actionSplits), - )} - style={{ - border: '1px solid ' + theme.tableBorder, - borderRadius: '6px 6px 0 0', - }} - /> - - <Stack - direction="row" - justify="flex-end" - style={{ marginTop: 20 }} - > - <Button onPress={() => modalProps.onClose()}>Cancel</Button> - <Button variant="primary" onPress={() => onSave()}> - Save - </Button> - </Stack> - </View> - </SelectedProvider> - </View> + </> )} </Modal> ); diff --git a/packages/desktop-client/src/components/modals/FixEncryptionKeyModal.tsx b/packages/desktop-client/src/components/modals/FixEncryptionKeyModal.tsx index 4efbe26ed4de31bcab4ca9c3849f19b37fac684f..c48a6e175fe92452f98137c81fdb087ae8ef49be 100644 --- a/packages/desktop-client/src/components/modals/FixEncryptionKeyModal.tsx +++ b/packages/desktop-client/src/components/modals/FixEncryptionKeyModal.tsx @@ -12,19 +12,21 @@ import { Button, ButtonWithLoading } from '../common/Button2'; import { InitialFocus } from '../common/InitialFocus'; import { Input } from '../common/Input'; import { Link } from '../common/Link'; -import { Modal, ModalButtons } from '../common/Modal'; +import { + Modal, + ModalButtons, + ModalCloseButton, + ModalHeader, +} from '../common/Modal2'; import { Paragraph } from '../common/Paragraph'; import { Text } from '../common/Text'; import { View } from '../common/View'; -import { type CommonModalProps } from '../Modals'; type FixEncryptionKeyModalProps = { - modalProps: CommonModalProps; options: FinanceModals['fix-encryption-key']; }; export function FixEncryptionKeyModal({ - modalProps, options = {}, }: FixEncryptionKeyModalProps) { const { hasExistingKey, cloudFileId, onSuccess } = options; @@ -50,122 +52,128 @@ export function FixEncryptionKeyModal({ return; } - modalProps.onClose(); onSuccess?.(); } } return ( - <Modal - {...modalProps} - title={ - hasExistingKey ? 'Unable to decrypt file' : 'This file is encrypted' - } - onClose={modalProps.onClose} - > - <View - style={{ - maxWidth: 500, - overflowX: 'hidden', - overflowY: 'auto', - flex: 1, - }} - > - {hasExistingKey ? ( - <Paragraph> - This file was encrypted with a different key than you are currently - using. This probably means you changed your password. Enter your - current password to update your key.{' '} - <Link - variant="external" - to="https://actualbudget.org/docs/getting-started/sync/#end-to-end-encryption" - > - Learn more - </Link> - </Paragraph> - ) : ( - <Paragraph> - We don’t have a key that encrypts or decrypts this file. Enter the - password for this file to create the key for encryption.{' '} - <Link - variant="external" - to="https://actualbudget.org/docs/getting-started/sync/#end-to-end-encryption" - > - Learn more - </Link> - </Paragraph> - )} - </View> - <Form - onSubmit={e => { - e.preventDefault(); - onUpdateKey(); - }} - > - <View - style={{ - marginTop: 15, - flexDirection: 'column', - alignItems: 'center', - }} - > - <Text style={{ fontWeight: 600, marginBottom: 5 }}>Password</Text>{' '} - {error && ( + <Modal name="fix-encryption-key"> + {({ state: { close } }) => ( + <> + <ModalHeader + title={ + hasExistingKey + ? 'Unable to decrypt file' + : 'This file is encrypted' + } + rightContent={<ModalCloseButton onClick={close} />} + /> + <View + style={{ + maxWidth: 500, + overflowX: 'hidden', + overflowY: 'auto', + flex: 1, + }} + > + {hasExistingKey ? ( + <Paragraph> + This file was encrypted with a different key than you are + currently using. This probably means you changed your password. + Enter your current password to update your key.{' '} + <Link + variant="external" + to="https://actualbudget.org/docs/getting-started/sync/#end-to-end-encryption" + > + Learn more + </Link> + </Paragraph> + ) : ( + <Paragraph> + We don’t have a key that encrypts or decrypts this file. Enter + the password for this file to create the key for encryption.{' '} + <Link + variant="external" + to="https://actualbudget.org/docs/getting-started/sync/#end-to-end-encryption" + > + Learn more + </Link> + </Paragraph> + )} + </View> + <Form + onSubmit={e => { + e.preventDefault(); + onUpdateKey(); + close(); + }} + > <View style={{ - color: theme.errorText, - textAlign: 'center', - fontSize: 13, - marginBottom: 3, + marginTop: 15, + flexDirection: 'column', + alignItems: 'center', }} > - {error} + <Text style={{ fontWeight: 600, marginBottom: 5 }}>Password</Text>{' '} + {error && ( + <View + style={{ + color: theme.errorText, + textAlign: 'center', + fontSize: 13, + marginBottom: 3, + }} + > + {error} + </View> + )} + <InitialFocus> + <Input + type={showPassword ? 'text' : 'password'} + style={{ + width: isNarrowWidth ? '100%' : '50%', + height: isNarrowWidth ? styles.mobileMinHeight : undefined, + }} + onChange={e => setPassword(e.target.value)} + /> + </InitialFocus> + <Text style={{ marginTop: 5 }}> + <label style={{ userSelect: 'none' }}> + <input + type="checkbox" + onClick={() => setShowPassword(!showPassword)} + />{' '} + Show password + </label> + </Text> </View> - )} - <InitialFocus> - <Input - type={showPassword ? 'text' : 'password'} - style={{ - width: isNarrowWidth ? '100%' : '50%', - height: isNarrowWidth ? styles.mobileMinHeight : undefined, - }} - onChange={e => setPassword(e.target.value)} - /> - </InitialFocus> - <Text style={{ marginTop: 5 }}> - <label style={{ userSelect: 'none' }}> - <input - type="checkbox" - onClick={() => setShowPassword(!showPassword)} - />{' '} - Show password - </label> - </Text> - </View> - <ModalButtons style={{ marginTop: 20 }}> - <Button - variant="normal" - style={{ - height: isNarrowWidth ? styles.mobileMinHeight : undefined, - marginRight: 10, - }} - onPress={() => modalProps.onBack()} - > - Back - </Button> - <ButtonWithLoading - type="submit" - variant="primary" - style={{ - height: isNarrowWidth ? styles.mobileMinHeight : undefined, - }} - isLoading={loading} - > - {hasExistingKey ? 'Update key' : 'Create key'} - </ButtonWithLoading> - </ModalButtons> - </Form> + <ModalButtons style={{ marginTop: 20 }}> + <Button + variant="normal" + style={{ + height: isNarrowWidth ? styles.mobileMinHeight : undefined, + marginRight: 10, + }} + onPress={close} + > + Back + </Button> + <ButtonWithLoading + type="submit" + variant="primary" + style={{ + height: isNarrowWidth ? styles.mobileMinHeight : undefined, + }} + isLoading={loading} + > + {hasExistingKey ? 'Update key' : 'Create key'} + </ButtonWithLoading> + </ModalButtons> + </Form> + </> + )} </Modal> ); } diff --git a/packages/desktop-client/src/components/modals/GoCardlessExternalMsg.tsx b/packages/desktop-client/src/components/modals/GoCardlessExternalMsg.tsx index 9cb6e106bc3053649539082af458f9ee50744bad..2dbbc049eefbb2841f4c3572ac0fb743ee372b1b 100644 --- a/packages/desktop-client/src/components/modals/GoCardlessExternalMsg.tsx +++ b/packages/desktop-client/src/components/modals/GoCardlessExternalMsg.tsx @@ -16,11 +16,10 @@ import { Error, Warning } from '../alerts'; import { Autocomplete } from '../autocomplete/Autocomplete'; import { Button } from '../common/Button2'; import { Link } from '../common/Link'; -import { Modal } from '../common/Modal'; +import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2'; import { Paragraph } from '../common/Paragraph'; import { View } from '../common/View'; import { FormField, FormLabel } from '../forms'; -import { type CommonModalProps } from '../Modals'; import { COUNTRY_OPTIONS } from './countries'; @@ -74,7 +73,6 @@ function renderError(error: 'unknown' | 'timeout') { } type GoCardlessExternalMsgProps = { - modalProps: CommonModalProps; onMoveExternal: (arg: { institutionId: string; }) => Promise<{ error?: 'unknown' | 'timeout'; data?: GoCardlessToken }>; @@ -83,10 +81,9 @@ type GoCardlessExternalMsgProps = { }; export function GoCardlessExternalMsg({ - modalProps, onMoveExternal, onSuccess, - onClose: originalOnClose, + onClose, }: GoCardlessExternalMsgProps) { const dispatch = useDispatch(); @@ -126,11 +123,6 @@ export function GoCardlessExternalMsg({ setSuccess(true); } - function onClose() { - originalOnClose?.(); - modalProps.onClose(); - } - async function onContinue() { setWaiting('accounts'); await onSuccess(data.current); @@ -232,69 +224,78 @@ export function GoCardlessExternalMsg({ return ( <Modal - title="Link Your Bank" - {...modalProps} + name="gocardless-external-msg" onClose={onClose} - style={{ flex: 0 }} + containerProps={{ style: { width: '30vw' } }} > - {() => ( - <View> - <Paragraph style={{ fontSize: 15 }}> - To link your bank account, you will be redirected to a new page - where GoCardless will ask to connect to your bank. GoCardless will - not be able to withdraw funds from your accounts. - </Paragraph> + {({ state: { close } }) => ( + <> + <ModalHeader + title="Link Your Bank" + rightContent={<ModalCloseButton onClick={close} />} + /> + <View> + <Paragraph style={{ fontSize: 15 }}> + To link your bank account, you will be redirected to a new page + where GoCardless will ask to connect to your bank. GoCardless will + not be able to withdraw funds from your accounts. + </Paragraph> - {error && renderError(error)} + {error && renderError(error)} - {waiting || isConfigurationLoading ? ( - <View style={{ alignItems: 'center', marginTop: 15 }}> - <AnimatedLoading - color={theme.pageTextDark} - style={{ width: 20, height: 20 }} - /> - <View style={{ marginTop: 10, color: theme.pageText }}> - {isConfigurationLoading - ? 'Checking GoCardless configuration..' - : waiting === 'browser' - ? 'Waiting on GoCardless...' - : waiting === 'accounts' - ? 'Loading accounts...' - : null} - </View> + {waiting || isConfigurationLoading ? ( + <View style={{ alignItems: 'center', marginTop: 15 }}> + <AnimatedLoading + color={theme.pageTextDark} + style={{ width: 20, height: 20 }} + /> + <View style={{ marginTop: 10, color: theme.pageText }}> + {isConfigurationLoading + ? 'Checking GoCardless configuration..' + : waiting === 'browser' + ? 'Waiting on GoCardless...' + : waiting === 'accounts' + ? 'Loading accounts...' + : null} + </View> - {waiting === 'browser' && ( - <Link variant="text" onClick={onJump} style={{ marginTop: 10 }}> - (Account linking not opening in a new tab? Click here) - </Link> - )} - </View> - ) : success ? ( - <Button - variant="primary" - style={{ - padding: '10px 0', - fontSize: 15, - fontWeight: 600, - marginTop: 10, - }} - onPress={onContinue} - > - Success! Click to continue → - </Button> - ) : isConfigured || isGoCardlessSetupComplete ? ( - renderLinkButton() - ) : ( - <> - <Paragraph style={{ color: theme.errorText }}> - GoCardless integration has not yet been configured. - </Paragraph> - <Button variant="primary" onPress={onGoCardlessInit}> - Configure GoCardless integration + {waiting === 'browser' && ( + <Link + variant="text" + onClick={onJump} + style={{ marginTop: 10 }} + > + (Account linking not opening in a new tab? Click here) + </Link> + )} + </View> + ) : success ? ( + <Button + variant="primary" + style={{ + padding: '10px 0', + fontSize: 15, + fontWeight: 600, + marginTop: 10, + }} + onPress={onContinue} + > + Success! Click to continue → </Button> - </> - )} - </View> + ) : isConfigured || isGoCardlessSetupComplete ? ( + renderLinkButton() + ) : ( + <> + <Paragraph style={{ color: theme.errorText }}> + GoCardless integration has not yet been configured. + </Paragraph> + <Button variant="primary" onPress={onGoCardlessInit}> + Configure GoCardless integration + </Button> + </> + )} + </View> + </> )} </Modal> ); diff --git a/packages/desktop-client/src/components/modals/GoCardlessInitialise.tsx b/packages/desktop-client/src/components/modals/GoCardlessInitialise.tsx index 021d488a345df89d29bbcf4168b5524de1927823..3ba1ea3d0f9753f8eb359234b30bcbf71ea273f3 100644 --- a/packages/desktop-client/src/components/modals/GoCardlessInitialise.tsx +++ b/packages/desktop-client/src/components/modals/GoCardlessInitialise.tsx @@ -7,19 +7,21 @@ import { Error } from '../alerts'; import { ButtonWithLoading } from '../common/Button2'; import { Input } from '../common/Input'; import { Link } from '../common/Link'; -import { Modal, ModalButtons } from '../common/Modal'; -import type { ModalProps } from '../common/Modal'; +import { + Modal, + ModalButtons, + ModalCloseButton, + ModalHeader, +} from '../common/Modal2'; import { Text } from '../common/Text'; import { View } from '../common/View'; import { FormField, FormLabel } from '../forms'; type GoCardlessInitialiseProps = { - modalProps?: Partial<ModalProps>; onSuccess: () => void; }; export const GoCardlessInitialise = ({ - modalProps, onSuccess, }: GoCardlessInitialiseProps) => { const [secretId, setSecretId] = useState(''); @@ -47,69 +49,79 @@ export const GoCardlessInitialise = ({ ]); onSuccess(); - modalProps.onClose(); setIsLoading(false); }; return ( - <Modal title="Set-up GoCardless" size={{ width: 300 }} {...modalProps}> - <View style={{ display: 'flex', gap: 10 }}> - <Text> - In order to enable bank-sync via GoCardless (only for EU banks) you - will need to create access credentials. This can be done by creating - an account with{' '} - <Link - variant="external" - to="https://actualbudget.org/docs/advanced/bank-sync/" - linkColor="purple" - > - GoCardless - </Link> - . - </Text> - - <FormField> - <FormLabel title="Secret ID:" htmlFor="secret-id-field" /> - <Input - id="secret-id-field" - type="password" - value={secretId} - onChangeValue={value => { - setSecretId(value); - setIsValid(true); - }} + <Modal name="gocardless-init" containerProps={{ style: { width: '30vw' } }}> + {({ state: { close } }) => ( + <> + <ModalHeader + title="Set-up GoCardless" + rightContent={<ModalCloseButton onClick={close} />} /> - </FormField> + <View style={{ display: 'flex', gap: 10 }}> + <Text> + In order to enable bank-sync via GoCardless (only for EU banks) + you will need to create access credentials. This can be done by + creating an account with{' '} + <Link + variant="external" + to="https://actualbudget.org/docs/advanced/bank-sync/" + linkColor="purple" + > + GoCardless + </Link> + . + </Text> - <FormField> - <FormLabel title="Secret Key:" htmlFor="secret-key-field" /> - <Input - id="secret-key-field" - type="password" - value={secretKey} - onChangeValue={value => { - setSecretKey(value); - setIsValid(true); - }} - /> - </FormField> + <FormField> + <FormLabel title="Secret ID:" htmlFor="secret-id-field" /> + <Input + id="secret-id-field" + type="password" + value={secretId} + onChangeValue={value => { + setSecretId(value); + setIsValid(true); + }} + /> + </FormField> + + <FormField> + <FormLabel title="Secret Key:" htmlFor="secret-key-field" /> + <Input + id="secret-key-field" + type="password" + value={secretKey} + onChangeValue={value => { + setSecretKey(value); + setIsValid(true); + }} + /> + </FormField> - {!isValid && ( - <Error> - It is required to provide both the secret id and secret key. - </Error> - )} - </View> + {!isValid && ( + <Error> + It is required to provide both the secret id and secret key. + </Error> + )} + </View> - <ModalButtons> - <ButtonWithLoading - variant="primary" - isLoading={isLoading} - onPress={onSubmit} - > - Save and continue - </ButtonWithLoading> - </ModalButtons> + <ModalButtons> + <ButtonWithLoading + variant="primary" + isLoading={isLoading} + onPress={() => { + onSubmit(); + close(); + }} + > + Save and continue + </ButtonWithLoading> + </ModalButtons> + </> + )} </Modal> ); }; diff --git a/packages/desktop-client/src/components/modals/HoldBufferModal.tsx b/packages/desktop-client/src/components/modals/HoldBufferModal.tsx index 292f92c8aaadcaa1eea71fdb8f6c2231db37b105..d2fe1cb4ac14715ef0aec5415285fce7126fce73 100644 --- a/packages/desktop-client/src/components/modals/HoldBufferModal.tsx +++ b/packages/desktop-client/src/components/modals/HoldBufferModal.tsx @@ -5,23 +5,18 @@ import { rolloverBudget } from 'loot-core/client/queries'; import { styles } from '../../style'; import { Button } from '../common/Button2'; import { InitialFocus } from '../common/InitialFocus'; -import { Modal } from '../common/Modal'; +import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2'; import { View } from '../common/View'; import { FieldLabel } from '../mobile/MobileForms'; -import { type CommonModalProps } from '../Modals'; import { useSheetValue } from '../spreadsheet/useSheetValue'; import { AmountInput } from '../util/AmountInput'; type HoldBufferModalProps = { - modalProps: CommonModalProps; month: string; onSubmit: (amount: number) => void; }; -export function HoldBufferModal({ - modalProps, - onSubmit, -}: HoldBufferModalProps) { +export function HoldBufferModal({ onSubmit }: HoldBufferModalProps) { const available = useSheetValue(rolloverBudget.toBudget); const [amount, setAmount] = useState<number>(0); @@ -29,54 +24,58 @@ export function HoldBufferModal({ if (newAmount) { onSubmit?.(newAmount); } - - modalProps.onClose(); }; return ( - <Modal - title="Hold Buffer" - showHeader - focusAfterClose={false} - {...modalProps} - > - <View> - <FieldLabel title="Hold this amount:" /> - <InitialFocus> - <AmountInput - value={available} - autoDecimals={true} + <Modal name="hold-buffer"> + {({ state: { close } }) => ( + <> + <ModalHeader + title="Hold Buffer" + rightContent={<ModalCloseButton onClick={close} />} + /> + <View> + <FieldLabel title="Hold this amount:" /> + <InitialFocus> + <AmountInput + value={available} + autoDecimals={true} + style={{ + marginLeft: styles.mobileEditingPadding, + marginRight: styles.mobileEditingPadding, + }} + inputStyle={{ + height: styles.mobileMinHeight, + }} + onUpdate={setAmount} + onEnter={() => { + _onSubmit(amount); + close(); + }} + /> + </InitialFocus> + </View> + <View style={{ - marginLeft: styles.mobileEditingPadding, - marginRight: styles.mobileEditingPadding, + justifyContent: 'center', + alignItems: 'center', + paddingTop: 10, }} - inputStyle={{ - height: styles.mobileMinHeight, - }} - onUpdate={setAmount} - onEnter={() => _onSubmit(amount)} - /> - </InitialFocus> - </View> - <View - style={{ - justifyContent: 'center', - alignItems: 'center', - paddingTop: 10, - }} - > - <Button - variant="primary" - style={{ - height: styles.mobileMinHeight, - marginLeft: styles.mobileEditingPadding, - marginRight: styles.mobileEditingPadding, - }} - onPress={() => _onSubmit(amount)} - > - Hold - </Button> - </View> + > + <Button + variant="primary" + style={{ + height: styles.mobileMinHeight, + marginLeft: styles.mobileEditingPadding, + marginRight: styles.mobileEditingPadding, + }} + onPress={() => _onSubmit(amount)} + > + Hold + </Button> + </View> + </> + )} </Modal> ); } diff --git a/packages/desktop-client/src/components/modals/ImportTransactions.jsx b/packages/desktop-client/src/components/modals/ImportTransactions.jsx index 1c41776d74197afe788691376d001bd4e61b2ff5..56242cd4f56472818a72790a4af01145d819b35e 100644 --- a/packages/desktop-client/src/components/modals/ImportTransactions.jsx +++ b/packages/desktop-client/src/components/modals/ImportTransactions.jsx @@ -14,9 +14,9 @@ import { useDateFormat } from '../../hooks/useDateFormat'; import { useLocalPrefs } from '../../hooks/useLocalPrefs'; import { SvgDownAndRightArrow } from '../../icons/v2'; import { theme, styles } from '../../style'; -import { Button, ButtonWithLoading } from '../common/Button'; +import { Button, ButtonWithLoading } from '../common/Button2'; import { Input } from '../common/Input'; -import { Modal } from '../common/Modal'; +import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2'; import { Select } from '../common/Select'; import { Stack } from '../common/Stack'; import { Text } from '../common/Text'; @@ -836,7 +836,7 @@ function FieldMappings({ ); } -export function ImportTransactions({ modalProps, options }) { +export function ImportTransactions({ options }) { const dateFormat = useDateFormat() || 'MM/dd/yyyy'; const prefs = useLocalPrefs(); const { @@ -1206,8 +1206,6 @@ export function ImportTransactions({ modalProps, options }) { if (onImported) { onImported(didChange); } - - modalProps.onClose(); } const runImportPreviewCallback = useCallback(async () => { @@ -1382,309 +1380,323 @@ export function ImportTransactions({ modalProps, options }) { return ( <Modal - title={ - 'Import transactions' + (filetype ? ` (${filetype.toUpperCase()})` : '') - } - {...modalProps} - loading={loadingState === 'parsing'} - style={{ width: 800 }} + name="import-transactions" + isLoading={loadingState === 'parsing'} + containerProps={{ style: { width: 800 } }} > - {error && !error.parsed && ( - <View style={{ alignItems: 'center', marginBottom: 15 }}> - <Text style={{ marginRight: 10, color: theme.errorText }}> - <strong>Error:</strong> {error.message} - </Text> - </View> - )} - {(!error || !error.parsed) && ( - <View - style={{ - flex: 'unset', - height: 300, - border: '1px solid ' + theme.tableBorder, - }} - > - <TableHeader headers={headers} /> - - <TableWithNavigator - items={transactions.filter( - trans => - !trans.isMatchedTransaction || - (trans.isMatchedTransaction && reconcile), - )} - fields={['payee', 'category', 'amount']} - style={{ backgroundColor: theme.tableHeaderBackground }} - getItemKey={index => index} - renderEmpty={() => { - return ( - <View - style={{ - textAlign: 'center', - marginTop: 25, - color: theme.tableHeaderText, - fontStyle: 'italic', - }} - > - No transactions found - </View> - ); - }} - renderItem={({ key, style, item }) => ( - <View key={key} style={style}> - <Transaction - transaction={item} - showParsed={filetype === 'csv' || filetype === 'qif'} - parseDateFormat={parseDateFormat} - dateFormat={dateFormat} - fieldMappings={fieldMappings} - splitMode={splitMode} - inOutMode={inOutMode} - outValue={outValue} - flipAmount={flipAmount} - multiplierAmount={multiplierAmount} - categories={categories.list} - onCheckTransaction={onCheckTransaction} - reconcile={reconcile} - /> - </View> - )} + {({ state: { close } }) => ( + <> + <ModalHeader + title={ + 'Import transactions' + + (filetype ? ` (${filetype.toUpperCase()})` : '') + } + rightContent={<ModalCloseButton onClick={close} />} /> - </View> - )} - {error && error.parsed && ( - <View - style={{ - color: theme.errorText, - alignItems: 'center', - marginTop: 10, - }} - > - <Text style={{ maxWidth: 450, marginBottom: 15 }}> - <strong>Error:</strong> {error.message} - </Text> - {error.parsed && ( - <Button onPress={() => onNewFile()}>Select new file...</Button> + {error && !error.parsed && ( + <View style={{ alignItems: 'center', marginBottom: 15 }}> + <Text style={{ marginRight: 10, color: theme.errorText }}> + <strong>Error:</strong> {error.message} + </Text> + </View> )} - </View> - )} - - {filetype === 'csv' && ( - <View style={{ marginTop: 10 }}> - <FieldMappings - transactions={transactions} - onChange={onUpdateFields} - mappings={fieldMappings} - splitMode={splitMode} - inOutMode={inOutMode} - hasHeaderRow={hasHeaderRow} - /> - </View> - )} - - {isOfxFile(filetype) && ( - <CheckboxOption - id="form_fallback_missing_payee" - checked={fallbackMissingPayeeToMemo} - onChange={() => { - setFallbackMissingPayeeToMemo(state => !state); - parse( - filename, - getParseOptions('ofx', { - fallbackMissingPayeeToMemo: !fallbackMissingPayeeToMemo, - }), - ); - }} - > - Use Memo as a fallback for empty Payees - </CheckboxOption> - )} - {(isOfxFile(filetype) || isCamtFile(filetype)) && ( - <CheckboxOption - id="form_dont_reconcile" - checked={reconcile} - onChange={() => { - setReconcile(!reconcile); - }} - > - Merge with existing transactions - </CheckboxOption> - )} - - {/*Import Options */} - {(filetype === 'qif' || filetype === 'csv') && ( - <View style={{ marginTop: 10 }}> - <Stack - direction="row" - align="flex-start" - spacing={1} - style={{ marginTop: 5 }} - > - {/*Date Format */} - <View> - {(filetype === 'qif' || filetype === 'csv') && ( - <DateFormatSelect - transactions={transactions} - fieldMappings={fieldMappings} - parseDateFormat={parseDateFormat} - onChange={value => { - setParseDateFormat(value); - runImportPreview(); - }} - /> + {(!error || !error.parsed) && ( + <View + style={{ + flex: 'unset', + height: 300, + border: '1px solid ' + theme.tableBorder, + }} + > + <TableHeader headers={headers} /> + + <TableWithNavigator + items={transactions.filter( + trans => + !trans.isMatchedTransaction || + (trans.isMatchedTransaction && reconcile), + )} + fields={['payee', 'category', 'amount']} + style={{ backgroundColor: theme.tableHeaderBackground }} + getItemKey={index => index} + renderEmpty={() => { + return ( + <View + style={{ + textAlign: 'center', + marginTop: 25, + color: theme.tableHeaderText, + fontStyle: 'italic', + }} + > + No transactions found + </View> + ); + }} + renderItem={({ key, style, item }) => ( + <View key={key} style={style}> + <Transaction + transaction={item} + showParsed={filetype === 'csv' || filetype === 'qif'} + parseDateFormat={parseDateFormat} + dateFormat={dateFormat} + fieldMappings={fieldMappings} + splitMode={splitMode} + inOutMode={inOutMode} + outValue={outValue} + flipAmount={flipAmount} + multiplierAmount={multiplierAmount} + categories={categories.list} + onCheckTransaction={onCheckTransaction} + reconcile={reconcile} + /> + </View> + )} + /> + </View> + )} + {error && error.parsed && ( + <View + style={{ + color: theme.errorText, + alignItems: 'center', + marginTop: 10, + }} + > + <Text style={{ maxWidth: 450, marginBottom: 15 }}> + <strong>Error:</strong> {error.message} + </Text> + {error.parsed && ( + <Button onPress={() => onNewFile()}>Select new file...</Button> )} </View> + )} - {/* CSV Options */} - {filetype === 'csv' && ( - <View style={{ marginLeft: 10, gap: 5 }}> - <SectionLabel title="CSV OPTIONS" /> - <label - style={{ - display: 'flex', - flexDirection: 'row', - gap: 5, - alignItems: 'baseline', - }} - > - Delimiter: - <Select - options={[ - [',', ','], - [';', ';'], - ['|', '|'], - ['\t', 'tab'], - ]} - value={delimiter} - onChange={value => { - setDelimiter(value); - parse( - filename, - getParseOptions('csv', { - delimiter: value, - hasHeaderRow, - }), - ); - }} - style={{ width: 50 }} - /> - </label> - <CheckboxOption - id="form_has_header" - checked={hasHeaderRow} - onChange={() => { - setHasHeaderRow(!hasHeaderRow); - parse( - filename, - getParseOptions('csv', { - delimiter, - hasHeaderRow: !hasHeaderRow, - }), - ); - }} - > - File has header row - </CheckboxOption> - <CheckboxOption - id="clear_on_import" - checked={clearOnImport} - onChange={() => { - setClearOnImport(!clearOnImport); - }} - > - Clear transactions on import - </CheckboxOption> - <CheckboxOption - id="form_dont_reconcile" - checked={reconcile} - onChange={() => { - setReconcile(!reconcile); - }} - > - Merge with existing transactions - </CheckboxOption> - </View> - )} + {filetype === 'csv' && ( + <View style={{ marginTop: 10 }}> + <FieldMappings + transactions={transactions} + onChange={onUpdateFields} + mappings={fieldMappings} + splitMode={splitMode} + inOutMode={inOutMode} + hasHeaderRow={hasHeaderRow} + /> + </View> + )} - <View style={{ flex: 1 }} /> - - <View style={{ marginRight: 10, gap: 5 }}> - <SectionLabel title="AMOUNT OPTIONS" /> - <CheckboxOption - id="form_flip" - checked={flipAmount} - disabled={splitMode || inOutMode} - onChange={() => { - setFlipAmount(!flipAmount); - runImportPreview(); - }} + {isOfxFile(filetype) && ( + <CheckboxOption + id="form_fallback_missing_payee" + checked={fallbackMissingPayeeToMemo} + onChange={() => { + setFallbackMissingPayeeToMemo(state => !state); + parse( + filename, + getParseOptions('ofx', { + fallbackMissingPayeeToMemo: !fallbackMissingPayeeToMemo, + }), + ); + }} + > + Use Memo as a fallback for empty Payees + </CheckboxOption> + )} + {(isOfxFile(filetype) || isCamtFile(filetype)) && ( + <CheckboxOption + id="form_dont_reconcile" + checked={reconcile} + onChange={() => { + setReconcile(!reconcile); + }} + > + Merge with existing transactions + </CheckboxOption> + )} + + {/*Import Options */} + {(filetype === 'qif' || filetype === 'csv') && ( + <View style={{ marginTop: 10 }}> + <Stack + direction="row" + align="flex-start" + spacing={1} + style={{ marginTop: 5 }} > - Flip amount - </CheckboxOption> - {filetype === 'csv' && ( - <> + {/*Date Format */} + <View> + {(filetype === 'qif' || filetype === 'csv') && ( + <DateFormatSelect + transactions={transactions} + fieldMappings={fieldMappings} + parseDateFormat={parseDateFormat} + onChange={value => { + setParseDateFormat(value); + runImportPreview(); + }} + /> + )} + </View> + + {/* CSV Options */} + {filetype === 'csv' && ( + <View style={{ marginLeft: 10, gap: 5 }}> + <SectionLabel title="CSV OPTIONS" /> + <label + style={{ + display: 'flex', + flexDirection: 'row', + gap: 5, + alignItems: 'baseline', + }} + > + Delimiter: + <Select + options={[ + [',', ','], + [';', ';'], + ['|', '|'], + ['\t', 'tab'], + ]} + value={delimiter} + onChange={value => { + setDelimiter(value); + parse( + filename, + getParseOptions('csv', { + delimiter: value, + hasHeaderRow, + }), + ); + }} + style={{ width: 50 }} + /> + </label> + <CheckboxOption + id="form_has_header" + checked={hasHeaderRow} + onChange={() => { + setHasHeaderRow(!hasHeaderRow); + parse( + filename, + getParseOptions('csv', { + delimiter, + hasHeaderRow: !hasHeaderRow, + }), + ); + }} + > + File has header row + </CheckboxOption> + <CheckboxOption + id="clear_on_import" + checked={clearOnImport} + onChange={() => { + setClearOnImport(!clearOnImport); + }} + > + Clear transactions on import + </CheckboxOption> + <CheckboxOption + id="form_dont_reconcile" + checked={reconcile} + onChange={() => { + setReconcile(!reconcile); + }} + > + Merge with existing transactions + </CheckboxOption> + </View> + )} + + <View style={{ flex: 1 }} /> + + <View style={{ marginRight: 10, gap: 5 }}> + <SectionLabel title="AMOUNT OPTIONS" /> <CheckboxOption - id="form_split" - checked={splitMode} - disabled={inOutMode || flipAmount} + id="form_flip" + checked={flipAmount} + disabled={splitMode || inOutMode} onChange={() => { - onSplitMode(); + setFlipAmount(!flipAmount); runImportPreview(); }} > - Split amount into separate inflow/outflow columns + Flip amount </CheckboxOption> - <InOutOption - inOutMode={inOutMode} - outValue={outValue} - disabled={splitMode || flipAmount} + {filetype === 'csv' && ( + <> + <CheckboxOption + id="form_split" + checked={splitMode} + disabled={inOutMode || flipAmount} + onChange={() => { + onSplitMode(); + runImportPreview(); + }} + > + Split amount into separate inflow/outflow columns + </CheckboxOption> + <InOutOption + inOutMode={inOutMode} + outValue={outValue} + disabled={splitMode || flipAmount} + onToggle={() => { + setInOutMode(!inOutMode); + runImportPreview(); + }} + onChangeText={setOutValue} + /> + </> + )} + <MultiplierOption + multiplierEnabled={multiplierEnabled} + multiplierAmount={multiplierAmount} onToggle={() => { - setInOutMode(!inOutMode); + setMultiplierEnabled(!multiplierEnabled); + setMultiplierAmount(''); runImportPreview(); }} - onChangeText={setOutValue} + onChangeAmount={onMultiplierChange} /> - </> - )} - <MultiplierOption - multiplierEnabled={multiplierEnabled} - multiplierAmount={multiplierAmount} - onToggle={() => { - setMultiplierEnabled(!multiplierEnabled); - setMultiplierAmount(''); - runImportPreview(); + </View> + </Stack> + </View> + )} + + <View style={{ flexDirection: 'row', marginTop: 5 }}> + {/*Submit Button */} + <View + style={{ + alignSelf: 'flex-end', + flexDirection: 'row', + alignItems: 'center', + gap: '1em', + }} + > + <ButtonWithLoading + variant="primary" + isDisabled={ + transactions?.filter(trans => !trans.isMatchedTransaction) + .length === 0 + } + isLoading={loadingState === 'importing'} + onPress={() => { + onImport(); + close(); }} - onChangeAmount={onMultiplierChange} - /> + > + Import{' '} + { + transactions?.filter(trans => !trans.isMatchedTransaction) + .length + }{' '} + transactions + </ButtonWithLoading> </View> - </Stack> - </View> + </View> + </> )} - - <View style={{ flexDirection: 'row', marginTop: 5 }}> - {/*Submit Button */} - <View - style={{ - alignSelf: 'flex-end', - flexDirection: 'row', - alignItems: 'center', - gap: '1em', - }} - > - <ButtonWithLoading - variant="primary" - isDisabled={ - transactions?.filter(trans => !trans.isMatchedTransaction) - .length === 0 - } - isLoading={loadingState === 'importing'} - onPress={onImport} - > - Import{' '} - {transactions?.filter(trans => !trans.isMatchedTransaction).length}{' '} - transactions - </ButtonWithLoading> - </View> - </View> </Modal> ); } diff --git a/packages/desktop-client/src/components/modals/KeyboardShortcutModal.tsx b/packages/desktop-client/src/components/modals/KeyboardShortcutModal.tsx index 5a4a3a0b1203f9a81942bad5624d01f603b826a5..0860c39ad05c15e476a7a6f74952fa9c75662635 100644 --- a/packages/desktop-client/src/components/modals/KeyboardShortcutModal.tsx +++ b/packages/desktop-client/src/components/modals/KeyboardShortcutModal.tsx @@ -3,14 +3,10 @@ import { useLocation } from 'react-router-dom'; import * as Platform from 'loot-core/src/client/platform'; import { type CSSProperties } from '../../style'; -import { Modal, type ModalProps } from '../common/Modal'; +import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2'; import { Text } from '../common/Text'; import { View } from '../common/View'; -type KeyboardShortcutsModalProps = { - modalProps?: Partial<ModalProps>; -}; - type KeyIconProps = { shortcut: string; style?: CSSProperties; @@ -152,156 +148,191 @@ function Shortcut({ ); } -export function KeyboardShortcutModal({ - modalProps, -}: KeyboardShortcutsModalProps) { +export function KeyboardShortcutModal() { const location = useLocation(); const onBudget = location.pathname.startsWith('/budget'); const onAccounts = location.pathname.startsWith('/accounts'); const ctrl = Platform.OS === 'mac' ? '⌘' : 'Ctrl'; return ( - <Modal title="Keyboard Shortcuts" {...modalProps}> - <View - style={{ - flexDirection: 'row', - fontSize: 13, - }} - > - <View> - <Shortcut shortcut="?" description="Show this help dialog" /> - <Shortcut - shortcut="O" - description="Close the current budget and open another" - meta={ctrl} - /> - <Shortcut - shortcut="P" - description="Toggle the privacy filter" - meta={ctrl} - shift={true} + <Modal name="keyboard-shortcuts"> + {({ state: { close } }) => ( + <> + <ModalHeader + title="Keyboard Shortcuts" + rightContent={<ModalCloseButton onClick={close} />} /> - {onBudget && ( - <Shortcut - shortcut="0" - description="View current month" - style={{ - fontVariantNumeric: 'slashed-zero', - }} - /> - )} - {onAccounts && ( - <> - <Shortcut shortcut="Enter" description="Move down when editing" /> + <View + style={{ + flexDirection: 'row', + fontSize: 13, + }} + > + <View> + <Shortcut shortcut="?" description="Show this help dialog" /> <Shortcut - shortcut="Enter" - description="Move up when editing" - shift={true} - /> - <Shortcut - shortcut="I" - description="Import transactions" + shortcut="O" + description="Close the current budget and open another" meta={ctrl} /> - <Shortcut shortcut="B" description="Bank sync" meta={ctrl} /> - <GroupHeading group="Select a transaction, then" /> - <Shortcut - shortcut="J" - description="Move to the next transaction down" - /> - <Shortcut - shortcut="K" - description="Move to the next transaction up" - /> - <Shortcut - shortcut="↑" - description="Move to the next transaction down and scroll" - /> - <Shortcut - shortcut="↓" - description="Move to the next transaction up and scroll" - /> <Shortcut - shortcut="Space" - description="Toggle selection of current transaction" - /> - <Shortcut - shortcut="Space" - description="Toggle all transactions between current and most recently selected transaction" + shortcut="P" + description="Toggle the privacy filter" + meta={ctrl} shift={true} /> - </> - )} - </View> - <View - style={{ - marginRight: 15, - }} - > - <Shortcut - shortcut="Z" - description="Undo the last change" - meta={ctrl} - /> - <Shortcut - shortcut="Z" - description="Redo the last undone change" - shift={true} - meta={ctrl} - /> - {onBudget && ( - <> - <Shortcut shortcut="â†" description="View previous month" /> - <Shortcut shortcut="→" description="View next month" /> - </> - )} - {onAccounts && ( - <> + {onBudget && ( + <Shortcut + shortcut="0" + description="View current month" + style={{ + fontVariantNumeric: 'slashed-zero', + }} + /> + )} + {onAccounts && ( + <> + <Shortcut + shortcut="Enter" + description="Move down when editing" + /> + <Shortcut + shortcut="Enter" + description="Move up when editing" + shift={true} + /> + <Shortcut + shortcut="I" + description="Import transactions" + meta={ctrl} + /> + <Shortcut shortcut="B" description="Bank sync" meta={ctrl} /> + <GroupHeading group="Select a transaction, then" /> + <Shortcut + shortcut="J" + description="Move to the next transaction down" + /> + <Shortcut + shortcut="K" + description="Move to the next transaction up" + /> + <Shortcut + shortcut="↑" + description="Move to the next transaction down and scroll" + /> + <Shortcut + shortcut="↓" + description="Move to the next transaction up and scroll" + /> + <Shortcut + shortcut="Space" + description="Toggle selection of current transaction" + /> + <Shortcut + shortcut="Space" + description="Toggle all transactions between current and most recently selected transaction" + shift={true} + /> + </> + )} + </View> + <View + style={{ + marginRight: 15, + }} + > <Shortcut - shortcut="A" - description="Select all transactions" + shortcut="Z" + description="Undo the last change" meta={ctrl} /> - <Shortcut shortcut="Tab" description="Move right when editing" /> <Shortcut - shortcut="Tab" - description="Move left when editing" + shortcut="Z" + description="Redo the last undone change" shift={true} + meta={ctrl} /> - <Shortcut shortcut="T" description="Add a new transaction" /> - <Shortcut shortcut="F" description="Filter transactions" /> - <GroupHeading group="With transaction(s) selected" /> - <Shortcut - shortcut="F" - description="Filter to the selected transactions" - /> - <Shortcut - shortcut="D" - description="Delete selected transactions" - /> - <Shortcut - shortcut="A" - description="Set account for selected transactions" - /> - <Shortcut - shortcut="P" - description="Set payee for selected transactions" - /> - <Shortcut - shortcut="N" - description="Set notes for selected transactions" - /> - <Shortcut - shortcut="C" - description="Set category for selected transactions" - /> - <Shortcut - shortcut="L" - description="Toggle cleared for current transaction" - /> - </> - )} - </View> - </View> + {onAccounts && ( + <> + <Shortcut + shortcut="Enter" + description="Move up when editing" + shift={true} + /> + <Shortcut + shortcut="Tab" + description="Move left when editing" + shift={true} + /> + {onBudget && ( + <> + <Shortcut + shortcut="â†" + description="View previous month" + /> + <Shortcut shortcut="→" description="View next month" /> + </> + )} + {onAccounts && ( + <> + <Shortcut + shortcut="A" + description="Select all transactions" + meta={ctrl} + /> + <Shortcut + shortcut="Tab" + description="Move right when editing" + /> + <Shortcut + shortcut="Tab" + description="Move left when editing" + shift={true} + /> + <Shortcut + shortcut="T" + description="Add a new transaction" + /> + <Shortcut + shortcut="F" + description="Filter transactions" + /> + <GroupHeading group="With transaction(s) selected" /> + <Shortcut + shortcut="F" + description="Filter to the selected transactions" + /> + <Shortcut + shortcut="D" + description="Delete selected transactions" + /> + <Shortcut + shortcut="A" + description="Set account for selected transactions" + /> + <Shortcut + shortcut="P" + description="Set payee for selected transactions" + /> + <Shortcut + shortcut="N" + description="Set notes for selected transactions" + /> + <Shortcut + shortcut="C" + description="Set category for selected transactions" + /> + <Shortcut + shortcut="L" + description="Toggle cleared for current transaction" + /> + </> + )} + </> + )} + </View> + </View> + </> + )} </Modal> ); } diff --git a/packages/desktop-client/src/components/modals/LoadBackup.jsx b/packages/desktop-client/src/components/modals/LoadBackup.jsx index 9ed813034a38d80f768d546f040c4a336702a7dc..21d750d07645866123b0d308e27a1475fc86d0bb 100644 --- a/packages/desktop-client/src/components/modals/LoadBackup.jsx +++ b/packages/desktop-client/src/components/modals/LoadBackup.jsx @@ -1,12 +1,14 @@ import React, { Component, useState, useEffect } from 'react'; +import { useDispatch } from 'react-redux'; +import { loadBackup, makeBackup } from 'loot-core/client/actions'; import { send, listen, unlisten } from 'loot-core/src/platform/client/fetch'; import { useLocalPref } from '../../hooks/useLocalPref'; import { theme } from '../../style'; import { Block } from '../common/Block'; import { Button } from '../common/Button2'; -import { Modal } from '../common/Modal'; +import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2'; import { Text } from '../common/Text'; import { View } from '../common/View'; import { Row, Cell } from '../table'; @@ -48,13 +50,8 @@ class BackupTable extends Component { } } -export function LoadBackup({ - budgetId, - watchUpdates, - backupDisabled, - actions, - modalProps, -}) { +export function LoadBackup({ budgetId, watchUpdates, backupDisabled }) { + const dispatch = useDispatch(); const [backups, setBackups] = useState([]); const [prefsBudgetId] = useLocalPref('id'); const budgetIdToLoad = budgetId || prefsBudgetId; @@ -74,66 +71,73 @@ export function LoadBackup({ const previousBackups = backups.filter(backup => !backup.isLatest); return ( - <Modal title="Load Backup" {...modalProps} style={{ flex: 0 }}> - {() => ( - <View style={{ marginBottom: 30 }}> - <View - style={{ - margin: 20, - marginTop: 0, - marginBottom: 15, - lineHeight: 1.5, - }} - > - {latestBackup ? ( - <Block> - <Block style={{ marginBottom: 10 }}> - <Text style={{ fontWeight: 600 }}> - You are currently working from a backup. - </Text>{' '} - You can load a different backup or revert to the original - version below. + <Modal name="load-backup" containerProps={{ style: { maxWidth: '30vw' } }}> + {({ state: { close } }) => ( + <> + <ModalHeader + title="Load Backup" + rightContent={<ModalCloseButton onClick={close} />} + /> + <View style={{ marginBottom: 30 }}> + <View + style={{ + margin: 20, + marginTop: 0, + marginBottom: 15, + lineHeight: 1.5, + }} + > + {latestBackup ? ( + <Block> + <Block style={{ marginBottom: 10 }}> + <Text style={{ fontWeight: 600 }}> + You are currently working from a backup. + </Text>{' '} + You can load a different backup or revert to the original + version below. + </Block> + <Button + variant="primary" + onPress={() => + dispatch(loadBackup(budgetIdToLoad, latestBackup.id)) + } + > + Revert to original version + </Button> </Block> - <Button - variant="primary" - onPress={() => - actions.loadBackup(budgetIdToLoad, latestBackup.id) - } - > - Revert to original version - </Button> + ) : ( + <View style={{ alignItems: 'flex-start' }}> + <Block style={{ marginBottom: 10 }}> + Select a backup to load. After loading a backup, you will + have a chance to revert to the current version in this + screen.{' '} + <Text style={{ fontWeight: 600 }}> + If you use a backup, you will have to setup all your + devices to sync from the new budget. + </Text> + </Block> + <Button + variant="primary" + isDisabled={backupDisabled} + onPress={() => dispatch(makeBackup())} + > + Backup now + </Button> + </View> + )} + </View> + {previousBackups.length === 0 ? ( + <Block style={{ color: theme.tableTextLight, marginLeft: 20 }}> + No backups available </Block> ) : ( - <View style={{ alignItems: 'flex-start' }}> - <Block style={{ marginBottom: 10 }}> - Select a backup to load. After loading a backup, you will have - a chance to revert to the current version in this screen.{' '} - <Text style={{ fontWeight: 600 }}> - If you use a backup, you will have to setup all your devices - to sync from the new budget. - </Text> - </Block> - <Button - variant="primary" - isDisabled={backupDisabled} - onPress={() => actions.makeBackup()} - > - Backup now - </Button> - </View> + <BackupTable + backups={previousBackups} + onSelect={id => dispatch(loadBackup(budgetIdToLoad, id))} + /> )} </View> - {previousBackups.length === 0 ? ( - <Block style={{ color: theme.tableTextLight, marginLeft: 20 }}> - No backups available - </Block> - ) : ( - <BackupTable - backups={previousBackups} - onSelect={id => actions.loadBackup(budgetIdToLoad, id)} - /> - )} - </View> + </> )} </Modal> ); diff --git a/packages/desktop-client/src/components/modals/ManageRulesModal.tsx b/packages/desktop-client/src/components/modals/ManageRulesModal.tsx index 9d1a690222ec4d9b402e836985f669056dbdf158..898c9992a2aeffa85f2dc9cda97460f6f21e30ce 100644 --- a/packages/desktop-client/src/components/modals/ManageRulesModal.tsx +++ b/packages/desktop-client/src/components/modals/ManageRulesModal.tsx @@ -4,19 +4,14 @@ import { useLocation } from 'react-router-dom'; import { isNonProductionEnvironment } from 'loot-core/src/shared/environment'; -import { Modal } from '../common/Modal'; +import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2'; import { ManageRules } from '../ManageRules'; -import { type CommonModalProps } from '../Modals'; type ManageRulesModalProps = { - modalProps: CommonModalProps; payeeId?: string; }; -export function ManageRulesModal({ - modalProps, - payeeId, -}: ManageRulesModalProps) { +export function ManageRulesModal({ payeeId }: ManageRulesModalProps) { const [loading, setLoading] = useState(true); const location = useLocation(); if (isNonProductionEnvironment()) { @@ -28,8 +23,16 @@ export function ManageRulesModal({ } return ( - <Modal title="Rules" loading={loading} {...modalProps}> - {() => <ManageRules isModal payeeId={payeeId} setLoading={setLoading} />} + <Modal name="manage-rules" isLoading={loading}> + {({ state: { close } }) => ( + <> + <ModalHeader + title="Rules" + rightContent={<ModalCloseButton onClick={close} />} + /> + <ManageRules isModal payeeId={payeeId} setLoading={setLoading} /> + </> + )} </Modal> ); } diff --git a/packages/desktop-client/src/components/modals/MergeUnusedPayees.jsx b/packages/desktop-client/src/components/modals/MergeUnusedPayees.jsx index fc1535a8953da96df50cb523e193a8daad88da1b..2a82568695ad9667f3bb3ddf6d9f1c58b8566a48 100644 --- a/packages/desktop-client/src/components/modals/MergeUnusedPayees.jsx +++ b/packages/desktop-client/src/components/modals/MergeUnusedPayees.jsx @@ -8,14 +8,14 @@ import { usePayees } from '../../hooks/usePayees'; import { theme } from '../../style'; import { Information } from '../alerts'; import { Button } from '../common/Button2'; -import { Modal, ModalButtons } from '../common/Modal'; +import { Modal, ModalButtons } from '../common/Modal2'; import { Paragraph } from '../common/Paragraph'; import { Text } from '../common/Text'; import { View } from '../common/View'; const highlightStyle = { color: theme.pageTextPositive }; -export function MergeUnusedPayees({ modalProps, payeeIds, targetPayeeId }) { +export function MergeUnusedPayees({ payeeIds, targetPayeeId }) { const allPayees = usePayees(); const modalStack = useSelector(state => state.modals.modalStack); const isEditingRule = !!modalStack.find(m => m.name === 'edit-rule'); @@ -63,8 +63,6 @@ export function MergeUnusedPayees({ modalProps, payeeIds, targetPayeeId }) { ruleId = id; } - modalProps.onClose(); - return ruleId; } @@ -78,13 +76,8 @@ export function MergeUnusedPayees({ modalProps, payeeIds, targetPayeeId }) { } return ( - <Modal - title="Merge payee?" - showHeader={false} - {...modalProps} - style={modalProps.style} - > - {() => ( + <Modal name="merge-unused-payees"> + {({ state: { close } }) => ( <View style={{ padding: 20, maxWidth: 500 }}> <View> <Paragraph style={{ marginBottom: 10, fontWeight: 500 }}> @@ -159,22 +152,25 @@ export function MergeUnusedPayees({ modalProps, payeeIds, targetPayeeId }) { <Button variant="primary" style={{ marginRight: 10 }} - onPress={onMerge} + onPress={() => { + onMerge(); + close(); + }} > Merge </Button> {!isEditingRule && ( <Button style={{ marginRight: 10 }} - onPress={onMergeAndCreateRule} + onPress={() => { + onMergeAndCreateRule(); + close(); + }} > Merge and edit rule </Button> )} - <Button - style={{ marginRight: 10 }} - onPress={() => modalProps.onBack()} - > + <Button style={{ marginRight: 10 }} onPress={close}> Do nothing </Button> </ModalButtons> diff --git a/packages/desktop-client/src/components/modals/NotesModal.tsx b/packages/desktop-client/src/components/modals/NotesModal.tsx index 04bd358237f450fcb9e004a1a5e737df42192d38..1ef3e00ad0fd819343130d1278ef369dc573210d 100644 --- a/packages/desktop-client/src/components/modals/NotesModal.tsx +++ b/packages/desktop-client/src/components/modals/NotesModal.tsx @@ -4,87 +4,86 @@ import React, { useEffect, useState } from 'react'; import { useNotes } from '../../hooks/useNotes'; import { SvgCheck } from '../../icons/v2'; import { Button } from '../common/Button2'; -import { Modal } from '../common/Modal'; +import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2'; import { View } from '../common/View'; -import { type CommonModalProps } from '../Modals'; import { Notes } from '../Notes'; type NotesModalProps = { - modalProps: CommonModalProps; id: string; name: string; onSave: (id: string, notes: string) => void; }; -export function NotesModal({ modalProps, id, name, onSave }: NotesModalProps) { +export function NotesModal({ id, name, onSave }: NotesModalProps) { const originalNotes = useNotes(id); const [notes, setNotes] = useState(originalNotes); useEffect(() => setNotes(originalNotes), [originalNotes]); - function _onClose() { - modalProps?.onClose(); - } - function _onSave() { if (notes !== originalNotes) { onSave?.(id, notes); } - - _onClose(); } return ( <Modal - title={`Notes: ${name}`} - showHeader - focusAfterClose={false} - {...modalProps} - onClose={_onClose} - style={{ - height: '50vh', + name="notes" + containerProps={{ + style: { height: '50vh' }, }} > - <View - style={{ - flex: 1, - flexDirection: 'column', - }} - > - <Notes - notes={notes} - editable={true} - focused={true} - getStyle={() => ({ - borderRadius: 6, - flex: 1, - minWidth: 0, - })} - onChange={setNotes} - /> - <View - style={{ - flexDirection: 'column', - alignItems: 'center', - justifyItems: 'center', - width: '100%', - paddingTop: 10, - }} - > - <Button - variant="primary" + {({ state: { close } }) => ( + <> + <ModalHeader + title={`Notes: ${name}`} + rightContent={<ModalCloseButton onClick={close} />} + /> + <View style={{ - fontSize: 17, - fontWeight: 400, - width: '100%', + flex: 1, + flexDirection: 'column', }} - onPress={_onSave} > - <SvgCheck width={17} height={17} style={{ paddingRight: 5 }} /> - Save notes - </Button> - </View> - </View> + <Notes + notes={notes} + editable={true} + focused={true} + getStyle={() => ({ + borderRadius: 6, + flex: 1, + minWidth: 0, + })} + onChange={setNotes} + /> + <View + style={{ + flexDirection: 'column', + alignItems: 'center', + justifyItems: 'center', + width: '100%', + paddingTop: 10, + }} + > + <Button + variant="primary" + style={{ + fontSize: 17, + fontWeight: 400, + width: '100%', + }} + onPress={() => { + _onSave(); + close(); + }} + > + <SvgCheck width={17} height={17} style={{ paddingRight: 5 }} /> + Save notes + </Button> + </View> + </View> + </> + )} </Modal> ); } diff --git a/packages/desktop-client/src/components/modals/PayeeAutocompleteModal.tsx b/packages/desktop-client/src/components/modals/PayeeAutocompleteModal.tsx index 952361cacc735f1186bde7537e817853d1f6b3c3..9ab0dc1d166358b4b3e8ee5fc7faf6fb2991725f 100644 --- a/packages/desktop-client/src/components/modals/PayeeAutocompleteModal.tsx +++ b/packages/desktop-client/src/components/modals/PayeeAutocompleteModal.tsx @@ -6,17 +6,19 @@ import { usePayees } from '../../hooks/usePayees'; import { useResponsive } from '../../ResponsiveProvider'; import { theme } from '../../style'; import { PayeeAutocomplete } from '../autocomplete/PayeeAutocomplete'; -import { ModalCloseButton, Modal, ModalTitle } from '../common/Modal'; -import { type CommonModalProps } from '../Modals'; +import { + ModalCloseButton, + Modal, + ModalTitle, + ModalHeader, +} from '../common/Modal2'; type PayeeAutocompleteModalProps = { - modalProps: CommonModalProps; autocompleteProps: ComponentPropsWithoutRef<typeof PayeeAutocomplete>; onClose: () => void; }; export function PayeeAutocompleteModal({ - modalProps, autocompleteProps, onClose, }: PayeeAutocompleteModalProps) { @@ -24,11 +26,6 @@ export function PayeeAutocompleteModal({ const accounts = useAccounts() || []; const navigate = useNavigate(); - const _onClose = () => { - modalProps.onClose(); - onClose?.(); - }; - const { isNarrowWidth } = useResponsive(); const defaultAutocompleteProps = { containerProps: { style: { height: isNarrowWidth ? '90vh' : 275 } }, @@ -38,41 +35,49 @@ export function PayeeAutocompleteModal({ return ( <Modal - title={ - <ModalTitle - title="Payee" - getStyle={() => ({ color: theme.menuAutoCompleteText })} - /> - } + name="payee-autocomplete" noAnimation={!isNarrowWidth} - showHeader={isNarrowWidth} - focusAfterClose={false} - {...modalProps} - onClose={_onClose} - style={{ - height: isNarrowWidth ? '85vh' : 275, - backgroundColor: theme.menuAutoCompleteBackground, + onClose={onClose} + containerProps={{ + style: { + height: isNarrowWidth ? '85vh' : 275, + backgroundColor: theme.menuAutoCompleteBackground, + }, }} - CloseButton={props => ( - <ModalCloseButton - {...props} - style={{ color: theme.menuAutoCompleteText }} - /> - )} > - <PayeeAutocomplete - payees={payees} - accounts={accounts} - focused={true} - embedded={true} - closeOnBlur={false} - onClose={_onClose} - onManagePayees={onManagePayees} - showManagePayees={!isNarrowWidth} - showMakeTransfer={!isNarrowWidth} - {...defaultAutocompleteProps} - {...autocompleteProps} - /> + {({ state: { close } }) => ( + <> + {isNarrowWidth && ( + <ModalHeader + title={ + <ModalTitle + title="Payee" + getStyle={() => ({ color: theme.menuAutoCompleteText })} + /> + } + rightContent={ + <ModalCloseButton + onClick={close} + style={{ color: theme.menuAutoCompleteText }} + /> + } + /> + )} + <PayeeAutocomplete + payees={payees} + accounts={accounts} + focused={true} + embedded={true} + closeOnBlur={false} + onClose={close} + onManagePayees={onManagePayees} + showManagePayees={!isNarrowWidth} + showMakeTransfer={!isNarrowWidth} + {...defaultAutocompleteProps} + {...autocompleteProps} + /> + </> + )} </Modal> ); } diff --git a/packages/desktop-client/src/components/modals/ReportBalanceMenuModal.tsx b/packages/desktop-client/src/components/modals/ReportBalanceMenuModal.tsx index 9b624653479f8e39c77e948255be0fdb62165d34..01808eea13d8a351a6dc8bde588c70c4784188c4 100644 --- a/packages/desktop-client/src/components/modals/ReportBalanceMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/ReportBalanceMenuModal.tsx @@ -9,19 +9,18 @@ import { DefaultCarryoverIndicator, } from '../budget/BalanceWithCarryover'; import { BalanceMenu } from '../budget/report/BalanceMenu'; -import { Modal, ModalTitle } from '../common/Modal'; +import { + Modal, + ModalCloseButton, + ModalHeader, + ModalTitle, +} from '../common/Modal2'; import { Text } from '../common/Text'; import { View } from '../common/View'; -import { type CommonModalProps } from '../Modals'; -type ReportBalanceMenuModalProps = ComponentPropsWithoutRef< - typeof BalanceMenu -> & { - modalProps: CommonModalProps; -}; +type ReportBalanceMenuModalProps = ComponentPropsWithoutRef<typeof BalanceMenu>; export function ReportBalanceMenuModal({ - modalProps, categoryId, onCarryover, }: ReportBalanceMenuModalProps) { @@ -39,55 +38,58 @@ export function ReportBalanceMenuModal({ } return ( - <Modal - title={<ModalTitle title={category.name} shrinkOnOverflow />} - showHeader - focusAfterClose={false} - {...modalProps} - > - <View - style={{ - justifyContent: 'center', - alignItems: 'center', - marginBottom: 20, - }} - > - <Text - style={{ - fontSize: 17, - fontWeight: 400, - }} - > - Balance - </Text> - <BalanceWithCarryover - disabled - style={{ - textAlign: 'center', - ...styles.veryLargeText, - }} - carryover={reportBudget.catCarryover(categoryId)} - balance={reportBudget.catBalance(categoryId)} - goal={reportBudget.catGoal(categoryId)} - budgeted={reportBudget.catBudgeted(categoryId)} - carryoverIndicator={({ style }) => - DefaultCarryoverIndicator({ - style: { - width: 15, - height: 15, - display: 'inline-flex', - position: 'relative', - ...style, - }, - }) - } - /> - </View> - <BalanceMenu - categoryId={categoryId} - getItemStyle={() => defaultMenuItemStyle} - onCarryover={onCarryover} - /> + <Modal name="report-balance-menu"> + {({ state: { close } }) => ( + <> + <ModalHeader + title={<ModalTitle title={category.name} shrinkOnOverflow />} + rightContent={<ModalCloseButton onClick={close} />} + /> + <View + style={{ + justifyContent: 'center', + alignItems: 'center', + marginBottom: 20, + }} + > + <Text + style={{ + fontSize: 17, + fontWeight: 400, + }} + > + Balance + </Text> + <BalanceWithCarryover + disabled + style={{ + textAlign: 'center', + ...styles.veryLargeText, + }} + carryover={reportBudget.catCarryover(categoryId)} + balance={reportBudget.catBalance(categoryId)} + goal={reportBudget.catGoal(categoryId)} + budgeted={reportBudget.catBudgeted(categoryId)} + carryoverIndicator={({ style }) => + DefaultCarryoverIndicator({ + style: { + width: 15, + height: 15, + display: 'inline-flex', + position: 'relative', + ...style, + }, + }) + } + /> + </View> + <BalanceMenu + categoryId={categoryId} + getItemStyle={() => defaultMenuItemStyle} + onCarryover={onCarryover} + /> + </> + )} </Modal> ); } diff --git a/packages/desktop-client/src/components/modals/ReportBudgetMenuModal.tsx b/packages/desktop-client/src/components/modals/ReportBudgetMenuModal.tsx index 13ac4bd665764394cc79fee38c67b410d5937c4a..b28976d6379e69cb0c295e6458cdaf0e3c8517f7 100644 --- a/packages/desktop-client/src/components/modals/ReportBudgetMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/ReportBudgetMenuModal.tsx @@ -10,23 +10,25 @@ import { amountToInteger, integerToAmount } from 'loot-core/shared/util'; import { useCategory } from '../../hooks/useCategory'; import { type CSSProperties, theme, styles } from '../../style'; import { BudgetMenu } from '../budget/report/BudgetMenu'; -import { Modal, ModalTitle } from '../common/Modal'; +import { + Modal, + ModalCloseButton, + ModalHeader, + ModalTitle, +} from '../common/Modal2'; import { Text } from '../common/Text'; import { View } from '../common/View'; import { FocusableAmountInput } from '../mobile/transactions/FocusableAmountInput'; -import { type CommonModalProps } from '../Modals'; import { useSheetValue } from '../spreadsheet/useSheetValue'; type ReportBudgetMenuModalProps = ComponentPropsWithoutRef< typeof BudgetMenu > & { - modalProps: CommonModalProps; categoryId: string; onUpdateBudget: (amount: number) => void; }; export function ReportBudgetMenuModal({ - modalProps, categoryId, onUpdateBudget, onCopyLastMonthAverage, @@ -57,51 +59,54 @@ export function ReportBudgetMenuModal({ } return ( - <Modal - title={<ModalTitle title={category.name} shrinkOnOverflow />} - showHeader - focusAfterClose={false} - {...modalProps} - > - <View - style={{ - justifyContent: 'center', - alignItems: 'center', - marginBottom: 20, - }} - > - <Text - style={{ - fontSize: 17, - fontWeight: 400, - }} - > - Budget - </Text> - <FocusableAmountInput - value={integerToAmount(budgeted || 0)} - focused={amountFocused} - onFocus={() => setAmountFocused(true)} - onBlur={() => setAmountFocused(false)} - onEnter={() => modalProps.onClose()} - zeroSign="+" - focusedStyle={{ - width: 'auto', - padding: '5px', - paddingLeft: '20px', - paddingRight: '20px', - minWidth: '100%', - }} - textStyle={{ ...styles.veryLargeText, textAlign: 'center' }} - onUpdateAmount={_onUpdateBudget} - /> - </View> - <BudgetMenu - getItemStyle={() => defaultMenuItemStyle} - onCopyLastMonthAverage={onCopyLastMonthAverage} - onSetMonthsAverage={onSetMonthsAverage} - onApplyBudgetTemplate={onApplyBudgetTemplate} - /> + <Modal name="report-budget-menu"> + {({ state: { close } }) => ( + <> + <ModalHeader + title={<ModalTitle title={category.name} shrinkOnOverflow />} + rightContent={<ModalCloseButton onClick={close} />} + /> + <View + style={{ + justifyContent: 'center', + alignItems: 'center', + marginBottom: 20, + }} + > + <Text + style={{ + fontSize: 17, + fontWeight: 400, + }} + > + Budget + </Text> + <FocusableAmountInput + value={integerToAmount(budgeted || 0)} + focused={amountFocused} + onFocus={() => setAmountFocused(true)} + onBlur={() => setAmountFocused(false)} + onEnter={close} + zeroSign="+" + focusedStyle={{ + width: 'auto', + padding: '5px', + paddingLeft: '20px', + paddingRight: '20px', + minWidth: '100%', + }} + textStyle={{ ...styles.veryLargeText, textAlign: 'center' }} + onUpdateAmount={_onUpdateBudget} + /> + </View> + <BudgetMenu + getItemStyle={() => defaultMenuItemStyle} + onCopyLastMonthAverage={onCopyLastMonthAverage} + onSetMonthsAverage={onSetMonthsAverage} + onApplyBudgetTemplate={onApplyBudgetTemplate} + /> + </> + )} </Modal> ); } diff --git a/packages/desktop-client/src/components/modals/ReportBudgetMonthMenuModal.tsx b/packages/desktop-client/src/components/modals/ReportBudgetMonthMenuModal.tsx index 72eee1de25175895623a6a09a4a4bfd5b1529673..4c996dfc0afd00704318e06a9b6ed9420e57454a 100644 --- a/packages/desktop-client/src/components/modals/ReportBudgetMonthMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/ReportBudgetMonthMenuModal.tsx @@ -9,30 +9,23 @@ import { SvgNotesPaper } from '../../icons/v2'; import { type CSSProperties, styles, theme } from '../../style'; import { BudgetMonthMenu } from '../budget/report/budgetsummary/BudgetMonthMenu'; import { Button } from '../common/Button2'; -import { Modal, ModalTitle } from '../common/Modal'; +import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2'; import { View } from '../common/View'; -import { type CommonModalProps } from '../Modals'; import { Notes } from '../Notes'; type ReportBudgetMonthMenuModalProps = { - modalProps: CommonModalProps; month: string; onBudgetAction: (month: string, action: string, arg?: unknown) => void; onEditNotes: (month: string) => void; }; export function ReportBudgetMonthMenuModal({ - modalProps, month, onBudgetAction, onEditNotes, }: ReportBudgetMonthMenuModalProps) { const originalNotes = useNotes(`budget-${month}`); - const onClose = () => { - modalProps.onClose(); - }; - const _onEditNotes = () => { onEditNotes?.(month); }; @@ -60,120 +53,127 @@ export function ReportBudgetMonthMenuModal({ return ( <Modal - title={<ModalTitle title={monthUtils.format(month, 'MMMM ‘yy')} />} - showHeader - focusAfterClose={false} - {...modalProps} - onClose={onClose} - style={{ - height: '50vh', + name="report-budget-month-menu" + containerProps={{ + style: { height: '50vh' }, }} > - <View - style={{ - flex: 1, - flexDirection: 'column', - }} - > - <View - style={{ - display: showMore ? 'none' : undefined, - overflowY: 'auto', - flex: 1, - }} - > - <Notes - notes={originalNotes?.length > 0 ? originalNotes : 'No notes'} - editable={false} - focused={false} - getStyle={() => ({ - borderRadius: 6, - ...((!originalNotes || originalNotes.length === 0) && { - justifySelf: 'center', - alignSelf: 'center', - color: theme.pageTextSubdued, - }), - })} + {({ state: { close } }) => ( + <> + <ModalHeader + title={monthUtils.format(month, 'MMMM ‘yy')} + rightContent={<ModalCloseButton onClick={close} />} /> - </View> - <View style={{ paddingTop: 10, gap: 5 }}> <View style={{ - display: showMore ? 'none' : undefined, - flexDirection: 'row', - flexWrap: 'wrap', - justifyContent: 'space-between', - alignContent: 'space-between', + flex: 1, + flexDirection: 'column', }} > - <Button style={buttonStyle} onPress={_onEditNotes}> - <SvgNotesPaper - width={20} - height={20} - style={{ paddingRight: 5 }} - /> - Edit notes - </Button> - </View> - <View> - <Button - variant="bare" - style={({ isPressed, isHovered }) => ({ - ...buttonStyle, - ...(isPressed || isHovered - ? { backgroundColor: 'transparent', color: buttonStyle.color } - : {}), - })} - onPress={onShowMore} + <View + style={{ + display: showMore ? 'none' : undefined, + overflowY: 'auto', + flex: 1, + }} > - {!showMore ? ( - <SvgCheveronUp - width={30} - height={30} - style={{ paddingRight: 5 }} - /> - ) : ( - <SvgCheveronDown - width={30} - height={30} - style={{ paddingRight: 5 }} - /> - )} - Actions - </Button> + <Notes + notes={originalNotes?.length > 0 ? originalNotes : 'No notes'} + editable={false} + focused={false} + getStyle={() => ({ + borderRadius: 6, + ...((!originalNotes || originalNotes.length === 0) && { + justifySelf: 'center', + alignSelf: 'center', + color: theme.pageTextSubdued, + }), + })} + /> + </View> + <View style={{ paddingTop: 10, gap: 5 }}> + <View + style={{ + display: showMore ? 'none' : undefined, + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'space-between', + alignContent: 'space-between', + }} + > + <Button style={buttonStyle} onPress={_onEditNotes}> + <SvgNotesPaper + width={20} + height={20} + style={{ paddingRight: 5 }} + /> + Edit notes + </Button> + </View> + <View> + <Button + variant="bare" + style={({ isPressed, isHovered }) => ({ + ...buttonStyle, + ...(isPressed || isHovered + ? { + backgroundColor: 'transparent', + color: buttonStyle.color, + } + : {}), + })} + onPress={onShowMore} + > + {!showMore ? ( + <SvgCheveronUp + width={30} + height={30} + style={{ paddingRight: 5 }} + /> + ) : ( + <SvgCheveronDown + width={30} + height={30} + style={{ paddingRight: 5 }} + /> + )} + Actions + </Button> + </View> + </View> + {showMore && ( + <BudgetMonthMenu + style={{ overflowY: 'auto', paddingTop: 10 }} + getItemStyle={() => defaultMenuItemStyle} + onCopyLastMonthBudget={() => { + onBudgetAction(month, 'copy-last'); + close(); + }} + onSetBudgetsToZero={() => { + onBudgetAction(month, 'set-zero'); + close(); + }} + onSetMonthsAverage={numberOfMonths => { + onBudgetAction(month, `set-${numberOfMonths}-avg`); + close(); + }} + onCheckTemplates={() => { + onBudgetAction(month, 'check-templates'); + close(); + }} + onApplyBudgetTemplates={() => { + onBudgetAction(month, 'apply-goal-template'); + close(); + }} + onOverwriteWithBudgetTemplates={() => { + onBudgetAction(month, 'overwrite-goal-template'); + close(); + }} + /> + )} </View> - </View> - {showMore && ( - <BudgetMonthMenu - style={{ overflowY: 'auto', paddingTop: 10 }} - getItemStyle={() => defaultMenuItemStyle} - onCopyLastMonthBudget={() => { - onBudgetAction(month, 'copy-last'); - onClose(); - }} - onSetBudgetsToZero={() => { - onBudgetAction(month, 'set-zero'); - onClose(); - }} - onSetMonthsAverage={numberOfMonths => { - onBudgetAction(month, `set-${numberOfMonths}-avg`); - onClose(); - }} - onCheckTemplates={() => { - onBudgetAction(month, 'check-templates'); - onClose(); - }} - onApplyBudgetTemplates={() => { - onBudgetAction(month, 'apply-goal-template'); - onClose(); - }} - onOverwriteWithBudgetTemplates={() => { - onBudgetAction(month, 'overwrite-goal-template'); - onClose(); - }} - /> - )} - </View> + </> + )} </Modal> ); } diff --git a/packages/desktop-client/src/components/modals/ReportBudgetSummaryModal.tsx b/packages/desktop-client/src/components/modals/ReportBudgetSummaryModal.tsx index 144da8fe02cdf44e4ff52a703b95c5209b86627f..7515dec21e20ff04571b56bec38787c99547a64a 100644 --- a/packages/desktop-client/src/components/modals/ReportBudgetSummaryModal.tsx +++ b/packages/desktop-client/src/components/modals/ReportBudgetSummaryModal.tsx @@ -7,40 +7,45 @@ import { styles } from '../../style'; import { ExpenseTotal } from '../budget/report/budgetsummary/ExpenseTotal'; import { IncomeTotal } from '../budget/report/budgetsummary/IncomeTotal'; import { Saved } from '../budget/report/budgetsummary/Saved'; -import { Modal } from '../common/Modal'; +import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2'; import { Stack } from '../common/Stack'; -import { type CommonModalProps } from '../Modals'; import { NamespaceContext } from '../spreadsheet/NamespaceContext'; type ReportBudgetSummaryModalProps = { - modalProps: CommonModalProps; month: string; }; export function ReportBudgetSummaryModal({ month, - modalProps, }: ReportBudgetSummaryModalProps) { const currentMonth = monthUtils.currentMonth(); return ( - <Modal title="Budget Summary" {...modalProps}> - <NamespaceContext.Provider value={sheetForMonth(month)}> - <Stack - spacing={2} - style={{ - alignSelf: 'center', - backgroundColor: 'transparent', - borderRadius: 4, - }} - > - <IncomeTotal style={{ ...styles.mediumText }} /> - <ExpenseTotal style={{ ...styles.mediumText }} /> - </Stack> - <Saved - projected={month >= currentMonth} - style={{ ...styles.mediumText, marginTop: 20 }} - /> - </NamespaceContext.Provider> + <Modal name="report-budget-summary"> + {({ state: { close } }) => ( + <> + <ModalHeader + title="Budget Summary" + rightContent={<ModalCloseButton onClick={close} />} + /> + <NamespaceContext.Provider value={sheetForMonth(month)}> + <Stack + spacing={2} + style={{ + alignSelf: 'center', + backgroundColor: 'transparent', + borderRadius: 4, + }} + > + <IncomeTotal style={{ ...styles.mediumText }} /> + <ExpenseTotal style={{ ...styles.mediumText }} /> + </Stack> + <Saved + projected={month >= currentMonth} + style={{ ...styles.mediumText, marginTop: 20 }} + /> + </NamespaceContext.Provider> + </> + )} </Modal> ); } diff --git a/packages/desktop-client/src/components/modals/RolloverBalanceMenuModal.tsx b/packages/desktop-client/src/components/modals/RolloverBalanceMenuModal.tsx index 29a4ea856a14154e239ec43748d7d1fda7a68ccb..c35073f262d92b14ac3aa7d2a173ac3fac88fa16 100644 --- a/packages/desktop-client/src/components/modals/RolloverBalanceMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/RolloverBalanceMenuModal.tsx @@ -9,19 +9,20 @@ import { DefaultCarryoverIndicator, } from '../budget/BalanceWithCarryover'; import { BalanceMenu } from '../budget/rollover/BalanceMenu'; -import { Modal, ModalTitle } from '../common/Modal'; +import { + Modal, + ModalCloseButton, + ModalHeader, + ModalTitle, +} from '../common/Modal2'; import { Text } from '../common/Text'; import { View } from '../common/View'; -import { type CommonModalProps } from '../Modals'; type RolloverBalanceMenuModalProps = ComponentPropsWithoutRef< typeof BalanceMenu -> & { - modalProps: CommonModalProps; -}; +>; export function RolloverBalanceMenuModal({ - modalProps, categoryId, onCarryover, onTransfer, @@ -41,57 +42,60 @@ export function RolloverBalanceMenuModal({ } return ( - <Modal - title={<ModalTitle title={category.name} shrinkOnOverflow />} - showHeader - focusAfterClose={false} - {...modalProps} - > - <View - style={{ - justifyContent: 'center', - alignItems: 'center', - marginBottom: 20, - }} - > - <Text - style={{ - fontSize: 17, - fontWeight: 400, - }} - > - Balance - </Text> - <BalanceWithCarryover - disabled - style={{ - textAlign: 'center', - ...styles.veryLargeText, - }} - carryover={rolloverBudget.catCarryover(categoryId)} - balance={rolloverBudget.catBalance(categoryId)} - goal={rolloverBudget.catGoal(categoryId)} - budgeted={rolloverBudget.catBudgeted(categoryId)} - carryoverIndicator={({ style }) => - DefaultCarryoverIndicator({ - style: { - width: 15, - height: 15, - display: 'inline-flex', - position: 'relative', - ...style, - }, - }) - } - /> - </View> - <BalanceMenu - categoryId={categoryId} - getItemStyle={() => defaultMenuItemStyle} - onCarryover={onCarryover} - onTransfer={onTransfer} - onCover={onCover} - /> + <Modal name="rollover-balance-menu"> + {({ state: { close } }) => ( + <> + <ModalHeader + title={<ModalTitle title={category.name} shrinkOnOverflow />} + rightContent={<ModalCloseButton onClick={close} />} + /> + <View + style={{ + justifyContent: 'center', + alignItems: 'center', + marginBottom: 20, + }} + > + <Text + style={{ + fontSize: 17, + fontWeight: 400, + }} + > + Balance + </Text> + <BalanceWithCarryover + disabled + style={{ + textAlign: 'center', + ...styles.veryLargeText, + }} + carryover={rolloverBudget.catCarryover(categoryId)} + balance={rolloverBudget.catBalance(categoryId)} + goal={rolloverBudget.catGoal(categoryId)} + budgeted={rolloverBudget.catBudgeted(categoryId)} + carryoverIndicator={({ style }) => + DefaultCarryoverIndicator({ + style: { + width: 15, + height: 15, + display: 'inline-flex', + position: 'relative', + ...style, + }, + }) + } + /> + </View> + <BalanceMenu + categoryId={categoryId} + getItemStyle={() => defaultMenuItemStyle} + onCarryover={onCarryover} + onTransfer={onTransfer} + onCover={onCover} + /> + </> + )} </Modal> ); } diff --git a/packages/desktop-client/src/components/modals/RolloverBudgetMenuModal.tsx b/packages/desktop-client/src/components/modals/RolloverBudgetMenuModal.tsx index d54587acc991d6859d5f7e4846e9cea8ff19d379..5bd9c24407e9536d2b1bb8d759c319fc5069c908 100644 --- a/packages/desktop-client/src/components/modals/RolloverBudgetMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/RolloverBudgetMenuModal.tsx @@ -10,23 +10,25 @@ import { amountToInteger, integerToAmount } from 'loot-core/shared/util'; import { useCategory } from '../../hooks/useCategory'; import { type CSSProperties, theme, styles } from '../../style'; import { BudgetMenu } from '../budget/rollover/BudgetMenu'; -import { Modal, ModalTitle } from '../common/Modal'; +import { + Modal, + ModalCloseButton, + ModalHeader, + ModalTitle, +} from '../common/Modal2'; import { Text } from '../common/Text'; import { View } from '../common/View'; import { FocusableAmountInput } from '../mobile/transactions/FocusableAmountInput'; -import { type CommonModalProps } from '../Modals'; import { useSheetValue } from '../spreadsheet/useSheetValue'; type RolloverBudgetMenuModalProps = ComponentPropsWithoutRef< typeof BudgetMenu > & { - modalProps: CommonModalProps; categoryId: string; onUpdateBudget: (amount: number) => void; }; export function RolloverBudgetMenuModal({ - modalProps, categoryId, onUpdateBudget, onCopyLastMonthAverage, @@ -57,51 +59,54 @@ export function RolloverBudgetMenuModal({ } return ( - <Modal - title={<ModalTitle title={category.name} shrinkOnOverflow />} - showHeader - focusAfterClose={false} - {...modalProps} - > - <View - style={{ - justifyContent: 'center', - alignItems: 'center', - marginBottom: 20, - }} - > - <Text - style={{ - fontSize: 17, - fontWeight: 400, - }} - > - Budget - </Text> - <FocusableAmountInput - value={integerToAmount(budgeted || 0)} - focused={amountFocused} - onFocus={() => setAmountFocused(true)} - onBlur={() => setAmountFocused(false)} - onEnter={() => modalProps.onClose()} - zeroSign="+" - focusedStyle={{ - width: 'auto', - padding: '5px', - paddingLeft: '20px', - paddingRight: '20px', - minWidth: '100%', - }} - textStyle={{ ...styles.veryLargeText, textAlign: 'center' }} - onUpdateAmount={_onUpdateBudget} - /> - </View> - <BudgetMenu - getItemStyle={() => defaultMenuItemStyle} - onCopyLastMonthAverage={onCopyLastMonthAverage} - onSetMonthsAverage={onSetMonthsAverage} - onApplyBudgetTemplate={onApplyBudgetTemplate} - /> + <Modal name="rollover-budget-menu"> + {({ state: { close } }) => ( + <> + <ModalHeader + title={<ModalTitle title={category.name} shrinkOnOverflow />} + rightContent={<ModalCloseButton onClick={close} />} + /> + <View + style={{ + justifyContent: 'center', + alignItems: 'center', + marginBottom: 20, + }} + > + <Text + style={{ + fontSize: 17, + fontWeight: 400, + }} + > + Budget + </Text> + <FocusableAmountInput + value={integerToAmount(budgeted || 0)} + focused={amountFocused} + onFocus={() => setAmountFocused(true)} + onBlur={() => setAmountFocused(false)} + onEnter={close} + zeroSign="+" + focusedStyle={{ + width: 'auto', + padding: '5px', + paddingLeft: '20px', + paddingRight: '20px', + minWidth: '100%', + }} + textStyle={{ ...styles.veryLargeText, textAlign: 'center' }} + onUpdateAmount={_onUpdateBudget} + /> + </View> + <BudgetMenu + getItemStyle={() => defaultMenuItemStyle} + onCopyLastMonthAverage={onCopyLastMonthAverage} + onSetMonthsAverage={onSetMonthsAverage} + onApplyBudgetTemplate={onApplyBudgetTemplate} + /> + </> + )} </Modal> ); } diff --git a/packages/desktop-client/src/components/modals/RolloverBudgetMonthMenuModal.tsx b/packages/desktop-client/src/components/modals/RolloverBudgetMonthMenuModal.tsx index 70702422596d4b44fecb3fa54b27f5c515b64ec1..a34353e5dcb63f44cb6e22e0cb7ee67c0e5c457d 100644 --- a/packages/desktop-client/src/components/modals/RolloverBudgetMonthMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/RolloverBudgetMonthMenuModal.tsx @@ -9,30 +9,23 @@ import { SvgNotesPaper } from '../../icons/v2'; import { type CSSProperties, styles, theme } from '../../style'; import { BudgetMonthMenu } from '../budget/rollover/budgetsummary/BudgetMonthMenu'; import { Button } from '../common/Button2'; -import { Modal, ModalTitle } from '../common/Modal'; +import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2'; import { View } from '../common/View'; -import { type CommonModalProps } from '../Modals'; import { Notes } from '../Notes'; type RolloverBudgetMonthMenuModalProps = { - modalProps: CommonModalProps; month: string; onBudgetAction: (month: string, action: string, arg?: unknown) => void; onEditNotes: (month: string) => void; }; export function RolloverBudgetMonthMenuModal({ - modalProps, month, onBudgetAction, onEditNotes, }: RolloverBudgetMonthMenuModalProps) { const originalNotes = useNotes(`budget-${month}`); - const onClose = () => { - modalProps.onClose(); - }; - const _onEditNotes = () => { onEditNotes?.(month); }; @@ -60,124 +53,131 @@ export function RolloverBudgetMonthMenuModal({ return ( <Modal - title={<ModalTitle title={monthUtils.format(month, 'MMMM ‘yy')} />} - showHeader - focusAfterClose={false} - {...modalProps} - onClose={onClose} - style={{ - height: '50vh', + name="rollover-budget-month-menu" + containerProps={{ + style: { height: '50vh' }, }} > - <View - style={{ - flex: 1, - flexDirection: 'column', - }} - > - <View - style={{ - display: showMore ? 'none' : undefined, - overflowY: 'auto', - flex: 1, - }} - > - <Notes - notes={originalNotes?.length > 0 ? originalNotes : 'No notes'} - editable={false} - focused={false} - getStyle={() => ({ - borderRadius: 6, - ...((!originalNotes || originalNotes.length === 0) && { - justifySelf: 'center', - alignSelf: 'center', - color: theme.pageTextSubdued, - }), - })} + {({ state: { close } }) => ( + <> + <ModalHeader + title={monthUtils.format(month, 'MMMM ‘yy')} + rightContent={<ModalCloseButton onClick={close} />} /> - </View> - <View style={{ paddingTop: 10, gap: 5 }}> <View style={{ - display: showMore ? 'none' : undefined, - flexDirection: 'row', - flexWrap: 'wrap', - justifyContent: 'space-between', - alignContent: 'space-between', + flex: 1, + flexDirection: 'column', }} > - <Button style={buttonStyle} onPress={_onEditNotes}> - <SvgNotesPaper - width={20} - height={20} - style={{ paddingRight: 5 }} - /> - Edit notes - </Button> - </View> - <View> - <Button - variant="bare" - style={({ isPressed, isHovered }) => ({ - ...buttonStyle, - ...(isPressed || isHovered - ? { backgroundColor: 'transparent', color: buttonStyle.color } - : {}), - })} - onPress={onShowMore} + <View + style={{ + display: showMore ? 'none' : undefined, + overflowY: 'auto', + flex: 1, + }} > - {!showMore ? ( - <SvgCheveronUp - width={30} - height={30} - style={{ paddingRight: 5 }} - /> - ) : ( - <SvgCheveronDown - width={30} - height={30} - style={{ paddingRight: 5 }} - /> - )} - Actions - </Button> + <Notes + notes={originalNotes?.length > 0 ? originalNotes : 'No notes'} + editable={false} + focused={false} + getStyle={() => ({ + borderRadius: 6, + ...((!originalNotes || originalNotes.length === 0) && { + justifySelf: 'center', + alignSelf: 'center', + color: theme.pageTextSubdued, + }), + })} + /> + </View> + <View style={{ paddingTop: 10, gap: 5 }}> + <View + style={{ + display: showMore ? 'none' : undefined, + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'space-between', + alignContent: 'space-between', + }} + > + <Button style={buttonStyle} onPress={_onEditNotes}> + <SvgNotesPaper + width={20} + height={20} + style={{ paddingRight: 5 }} + /> + Edit notes + </Button> + </View> + <View> + <Button + variant="bare" + style={({ isPressed, isHovered }) => ({ + ...buttonStyle, + ...(isPressed || isHovered + ? { + backgroundColor: 'transparent', + color: buttonStyle.color, + } + : {}), + })} + onPress={onShowMore} + > + {!showMore ? ( + <SvgCheveronUp + width={30} + height={30} + style={{ paddingRight: 5 }} + /> + ) : ( + <SvgCheveronDown + width={30} + height={30} + style={{ paddingRight: 5 }} + /> + )} + Actions + </Button> + </View> + </View> + {showMore && ( + <BudgetMonthMenu + style={{ overflowY: 'auto', paddingTop: 10 }} + getItemStyle={() => defaultMenuItemStyle} + onCopyLastMonthBudget={() => { + onBudgetAction(month, 'copy-last'); + close(); + }} + onSetBudgetsToZero={() => { + onBudgetAction(month, 'set-zero'); + close(); + }} + onSetMonthsAverage={numberOfMonths => { + onBudgetAction(month, `set-${numberOfMonths}-avg`); + close(); + }} + onCheckTemplates={() => { + onBudgetAction(month, 'check-templates'); + close(); + }} + onApplyBudgetTemplates={() => { + onBudgetAction(month, 'apply-goal-template'); + close(); + }} + onOverwriteWithBudgetTemplates={() => { + onBudgetAction(month, 'overwrite-goal-template'); + close(); + }} + onEndOfMonthCleanup={() => { + onBudgetAction(month, 'cleanup-goal-template'); + close(); + }} + /> + )} </View> - </View> - {showMore && ( - <BudgetMonthMenu - style={{ overflowY: 'auto', paddingTop: 10 }} - getItemStyle={() => defaultMenuItemStyle} - onCopyLastMonthBudget={() => { - onBudgetAction(month, 'copy-last'); - onClose(); - }} - onSetBudgetsToZero={() => { - onBudgetAction(month, 'set-zero'); - onClose(); - }} - onSetMonthsAverage={numberOfMonths => { - onBudgetAction(month, `set-${numberOfMonths}-avg`); - onClose(); - }} - onCheckTemplates={() => { - onBudgetAction(month, 'check-templates'); - onClose(); - }} - onApplyBudgetTemplates={() => { - onBudgetAction(month, 'apply-goal-template'); - onClose(); - }} - onOverwriteWithBudgetTemplates={() => { - onBudgetAction(month, 'overwrite-goal-template'); - onClose(); - }} - onEndOfMonthCleanup={() => { - onBudgetAction(month, 'cleanup-goal-template'); - onClose(); - }} - /> - )} - </View> + </> + )} </Modal> ); } diff --git a/packages/desktop-client/src/components/modals/RolloverBudgetSummaryModal.tsx b/packages/desktop-client/src/components/modals/RolloverBudgetSummaryModal.tsx index a2bba5ee9fc1098ee62bc34307dcd18393a1e7c4..cbde09f1b4022daaada16d23ad8ec3042658664d 100644 --- a/packages/desktop-client/src/components/modals/RolloverBudgetSummaryModal.tsx +++ b/packages/desktop-client/src/components/modals/RolloverBudgetSummaryModal.tsx @@ -8,13 +8,11 @@ import { format, sheetForMonth, prevMonth } from 'loot-core/src/shared/months'; import { styles } from '../../style'; import { ToBudgetAmount } from '../budget/rollover/budgetsummary/ToBudgetAmount'; import { TotalsList } from '../budget/rollover/budgetsummary/TotalsList'; -import { Modal } from '../common/Modal'; -import { type CommonModalProps } from '../Modals'; +import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2'; import { NamespaceContext } from '../spreadsheet/NamespaceContext'; import { useSheetValue } from '../spreadsheet/useSheetValue'; type RolloverBudgetSummaryModalProps = { - modalProps: CommonModalProps; onBudgetAction: (month: string, action: string, arg?: unknown) => void; month: string; }; @@ -22,7 +20,6 @@ type RolloverBudgetSummaryModalProps = { export function RolloverBudgetSummaryModal({ month, onBudgetAction, - modalProps, }: RolloverBudgetSummaryModalProps) { const dispatch = useDispatch(); const prevMonthName = format(prevMonth(month), 'MMM'); @@ -79,42 +76,52 @@ export function RolloverBudgetSummaryModal({ const onResetHoldBuffer = () => { onBudgetAction(month, 'reset-hold'); - modalProps.onClose(); }; - const onClick = () => { + const onClick = ({ close }: { close: () => void }) => { dispatch( pushModal('rollover-summary-to-budget-menu', { month, onTransfer: openTransferAvailableModal, onCover: openCoverOverbudgetedModal, - onResetHoldBuffer, + onResetHoldBuffer: () => { + onResetHoldBuffer(); + close(); + }, onHoldBuffer, }), ); }; return ( - <Modal title="Budget Summary" {...modalProps}> - <NamespaceContext.Provider value={sheetForMonth(month)}> - <TotalsList - prevMonthName={prevMonthName} - style={{ - ...styles.mediumText, - }} - /> - <ToBudgetAmount - prevMonthName={prevMonthName} - style={{ - ...styles.mediumText, - marginTop: 15, - }} - amountStyle={{ - ...styles.underlinedText, - }} - onClick={onClick} - /> - </NamespaceContext.Provider> + <Modal name="rollover-budget-summary"> + {({ state: { close } }) => ( + <> + <ModalHeader + title="Budget Summary" + rightContent={<ModalCloseButton onClick={close} />} + /> + <NamespaceContext.Provider value={sheetForMonth(month)}> + <TotalsList + prevMonthName={prevMonthName} + style={{ + ...styles.mediumText, + }} + /> + <ToBudgetAmount + prevMonthName={prevMonthName} + style={{ + ...styles.mediumText, + marginTop: 15, + }} + amountStyle={{ + ...styles.underlinedText, + }} + onClick={() => onClick({ close })} + /> + </NamespaceContext.Provider> + </> + )} </Modal> ); } diff --git a/packages/desktop-client/src/components/modals/RolloverToBudgetMenuModal.tsx b/packages/desktop-client/src/components/modals/RolloverToBudgetMenuModal.tsx index c94e247be0e3423e28dbadc3be499911c00568fe..d21d5dbc29bf19ca40f75bda8f25080459c9d448 100644 --- a/packages/desktop-client/src/components/modals/RolloverToBudgetMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/RolloverToBudgetMenuModal.tsx @@ -2,17 +2,13 @@ import React, { type ComponentPropsWithoutRef } from 'react'; import { type CSSProperties, theme, styles } from '../../style'; import { ToBudgetMenu } from '../budget/rollover/budgetsummary/ToBudgetMenu'; -import { Modal } from '../common/Modal'; -import { type CommonModalProps } from '../Modals'; +import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2'; type RolloverToBudgetMenuModalProps = ComponentPropsWithoutRef< typeof ToBudgetMenu -> & { - modalProps: CommonModalProps; -}; +>; export function RolloverToBudgetMenuModal({ - modalProps, onTransfer, onCover, onHoldBuffer, @@ -26,14 +22,22 @@ export function RolloverToBudgetMenuModal({ }; return ( - <Modal showHeader focusAfterClose={false} {...modalProps}> - <ToBudgetMenu - getItemStyle={() => defaultMenuItemStyle} - onTransfer={onTransfer} - onCover={onCover} - onHoldBuffer={onHoldBuffer} - onResetHoldBuffer={onResetHoldBuffer} - /> + <Modal name="rollover-summary-to-budget-menu"> + {({ state: { close } }) => ( + <> + <ModalHeader + showLogo + rightContent={<ModalCloseButton onClick={close} />} + /> + <ToBudgetMenu + getItemStyle={() => defaultMenuItemStyle} + onTransfer={onTransfer} + onCover={onCover} + onHoldBuffer={onHoldBuffer} + onResetHoldBuffer={onResetHoldBuffer} + /> + </> + )} </Modal> ); } diff --git a/packages/desktop-client/src/components/modals/ScheduledTransactionMenuModal.tsx b/packages/desktop-client/src/components/modals/ScheduledTransactionMenuModal.tsx index 46c31083f3798a415a197ecef7477e1af1c18877..0436554a7739fd8684cf9ae0e4be3827d8e7e2d8 100644 --- a/packages/desktop-client/src/components/modals/ScheduledTransactionMenuModal.tsx +++ b/packages/desktop-client/src/components/modals/ScheduledTransactionMenuModal.tsx @@ -6,17 +6,18 @@ import { type Query } from 'loot-core/shared/query'; import { type CSSProperties, theme, styles } from '../../style'; import { Menu } from '../common/Menu'; -import { Modal, ModalTitle } from '../common/Modal'; +import { + Modal, + ModalCloseButton, + ModalHeader, + ModalTitle, +} from '../common/Modal2'; import { Text } from '../common/Text'; import { View } from '../common/View'; -import { type CommonModalProps } from '../Modals'; -type ScheduledTransactionMenuModalProps = ScheduledTransactionMenuProps & { - modalProps: CommonModalProps; -}; +type ScheduledTransactionMenuModalProps = ScheduledTransactionMenuProps; export function ScheduledTransactionMenuModal({ - modalProps, transactionId, onSkip, onPost, @@ -41,30 +42,35 @@ export function ScheduledTransactionMenuModal({ } return ( - <Modal - title={<ModalTitle title={schedule.name || ''} shrinkOnOverflow />} - showHeader - focusAfterClose={false} - {...modalProps} - > - <View - style={{ - justifyContent: 'center', - alignItems: 'center', - marginBottom: 20, - }} - > - <Text style={{ fontSize: 17, fontWeight: 400 }}>Scheduled date</Text> - <Text style={{ fontSize: 17, fontWeight: 700 }}> - {format(schedule.next_date, 'MMMM dd, yyyy')} - </Text> - </View> - <ScheduledTransactionMenu - transactionId={transactionId} - onPost={onPost} - onSkip={onSkip} - getItemStyle={() => defaultMenuItemStyle} - /> + <Modal name="scheduled-transaction-menu"> + {({ state: { close } }) => ( + <> + <ModalHeader + title={<ModalTitle title={schedule.name || ''} shrinkOnOverflow />} + rightContent={<ModalCloseButton onClick={close} />} + /> + <View + style={{ + justifyContent: 'center', + alignItems: 'center', + marginBottom: 20, + }} + > + <Text style={{ fontSize: 17, fontWeight: 400 }}> + Scheduled date + </Text> + <Text style={{ fontSize: 17, fontWeight: 700 }}> + {format(schedule.next_date, 'MMMM dd, yyyy')} + </Text> + </View> + <ScheduledTransactionMenu + transactionId={transactionId} + onPost={onPost} + onSkip={onSkip} + getItemStyle={() => defaultMenuItemStyle} + /> + </> + )} </Modal> ); } diff --git a/packages/desktop-client/src/components/modals/SelectLinkedAccounts.jsx b/packages/desktop-client/src/components/modals/SelectLinkedAccounts.jsx index 4dcc8696cb82347f4e3c2fcc51fab315cf18bf85..f246eaaa3cd171480c2af7e635feec393311ff35 100644 --- a/packages/desktop-client/src/components/modals/SelectLinkedAccounts.jsx +++ b/packages/desktop-client/src/components/modals/SelectLinkedAccounts.jsx @@ -1,10 +1,17 @@ import React, { useState } from 'react'; +import { useDispatch } from 'react-redux'; + +import { + linkAccount, + linkAccountSimpleFin, + unlinkAccount, +} from 'loot-core/client/actions'; import { useAccounts } from '../../hooks/useAccounts'; import { theme } from '../../style'; import { Autocomplete } from '../autocomplete/Autocomplete'; import { Button } from '../common/Button2'; -import { Modal } from '../common/Modal'; +import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2'; import { Text } from '../common/Text'; import { View } from '../common/View'; import { PrivacyFilter } from '../PrivacyFilter'; @@ -17,13 +24,12 @@ const addOffBudgetAccountOption = { }; export function SelectLinkedAccounts({ - modalProps, requisitionId, externalAccounts, - actions, syncSource, }) { externalAccounts.sort((a, b) => a.name.localeCompare(b.name)); + const dispatch = useDispatch(); const localAccounts = useAccounts().filter(a => a.closed === 0); const [chosenAccounts, setChosenAccounts] = useState(() => { return Object.fromEntries( @@ -41,7 +47,7 @@ export function SelectLinkedAccounts({ localAccounts .filter(acc => acc.account_id) .filter(acc => !chosenLocalAccountIds.includes(acc.id)) - .forEach(acc => actions.unlinkAccount(acc.id)); + .forEach(acc => dispatch(unlinkAccount(acc.id))); // Link new accounts Object.entries(chosenAccounts).forEach( @@ -59,29 +65,31 @@ export function SelectLinkedAccounts({ // Finally link the matched account if (syncSource === 'simpleFin') { - actions.linkAccountSimpleFin( - externalAccount, - chosenLocalAccountId !== addOnBudgetAccountOption.id && - chosenLocalAccountId !== addOffBudgetAccountOption.id - ? chosenLocalAccountId - : undefined, - offBudget, + dispatch( + linkAccountSimpleFin( + externalAccount, + chosenLocalAccountId !== addOnBudgetAccountOption.id && + chosenLocalAccountId !== addOffBudgetAccountOption.id + ? chosenLocalAccountId + : undefined, + offBudget, + ), ); } else { - actions.linkAccount( - requisitionId, - externalAccount, - chosenLocalAccountId !== addOnBudgetAccountOption.id && - chosenLocalAccountId !== addOffBudgetAccountOption.id - ? chosenLocalAccountId - : undefined, - offBudget, + dispatch( + linkAccount( + requisitionId, + externalAccount, + chosenLocalAccountId !== addOnBudgetAccountOption.id && + chosenLocalAccountId !== addOffBudgetAccountOption.id + ? chosenLocalAccountId + : undefined, + offBudget, + ), ); } }, ); - - actions.closeModal(); } const unlinkedAccounts = localAccounts.filter( @@ -103,9 +111,16 @@ export function SelectLinkedAccounts({ } return ( - <Modal title="Link Accounts" {...modalProps} style={{ width: 800 }}> - {() => ( + <Modal + name="select-linked-accounts" + containerProps={{ style: { width: 800 } }} + > + {({ state: { close } }) => ( <> + <ModalHeader + title="Link Accounts" + rightContent={<ModalCloseButton onClick={close} />} + /> <Text style={{ marginBottom: 10 }}> We found the following accounts. Select which ones you want to add: </Text> @@ -161,7 +176,10 @@ export function SelectLinkedAccounts({ > <Button variant="primary" - onPress={onNext} + onPress={() => { + onNext(); + close(); + }} isDisabled={!Object.keys(chosenAccounts).length} > Link accounts diff --git a/packages/desktop-client/src/components/modals/SimpleFinInitialise.tsx b/packages/desktop-client/src/components/modals/SimpleFinInitialise.tsx index 37d0a2d788307344fb397ddc1045c71053aeb8a1..1392d38cc49c9a338e57d753808e6efdb24824c5 100644 --- a/packages/desktop-client/src/components/modals/SimpleFinInitialise.tsx +++ b/packages/desktop-client/src/components/modals/SimpleFinInitialise.tsx @@ -7,19 +7,21 @@ import { Error } from '../alerts'; import { ButtonWithLoading } from '../common/Button2'; import { Input } from '../common/Input'; import { Link } from '../common/Link'; -import { Modal, ModalButtons } from '../common/Modal'; -import type { ModalProps } from '../common/Modal'; +import { + Modal, + ModalButtons, + ModalCloseButton, + ModalHeader, +} from '../common/Modal2'; import { Text } from '../common/Text'; import { View } from '../common/View'; import { FormField, FormLabel } from '../forms'; type SimpleFinInitialiseProps = { - modalProps?: Partial<ModalProps>; onSuccess: () => void; }; export const SimpleFinInitialise = ({ - modalProps, onSuccess, }: SimpleFinInitialiseProps) => { const [token, setToken] = useState(''); @@ -40,52 +42,62 @@ export const SimpleFinInitialise = ({ }); onSuccess(); - modalProps.onClose(); setIsLoading(false); }; return ( - <Modal title="Set-up SimpleFIN" size={{ width: 300 }} {...modalProps}> - <View style={{ display: 'flex', gap: 10 }}> - <Text> - In order to enable bank-sync via SimpleFIN (only for North American - banks) you will need to create a token. This can be done by creating - an account with{' '} - <Link - variant="external" - to="https://beta-bridge.simplefin.org/" - linkColor="purple" - > - SimpleFIN - </Link> - . - </Text> - - <FormField> - <FormLabel title="Token:" htmlFor="token-field" /> - <Input - id="token-field" - type="password" - value={token} - onChangeValue={value => { - setToken(value); - setIsValid(true); - }} + <Modal name="simplefin-init" containerProps={{ style: { width: 300 } }}> + {({ state: { close } }) => ( + <> + <ModalHeader + title="Set-up SimpleFIN" + rightContent={<ModalCloseButton onClick={close} />} /> - </FormField> + <View style={{ display: 'flex', gap: 10 }}> + <Text> + In order to enable bank-sync via SimpleFIN (only for North + American banks) you will need to create a token. This can be done + by creating an account with{' '} + <Link + variant="external" + to="https://beta-bridge.simplefin.org/" + linkColor="purple" + > + SimpleFIN + </Link> + . + </Text> + + <FormField> + <FormLabel title="Token:" htmlFor="token-field" /> + <Input + id="token-field" + type="password" + value={token} + onChangeValue={value => { + setToken(value); + setIsValid(true); + }} + /> + </FormField> - {!isValid && <Error>It is required to provide a token.</Error>} - </View> + {!isValid && <Error>It is required to provide a token.</Error>} + </View> - <ModalButtons> - <ButtonWithLoading - variant="primary" - isLoading={isLoading} - onPress={onSubmit} - > - Save and continue - </ButtonWithLoading> - </ModalButtons> + <ModalButtons> + <ButtonWithLoading + variant="primary" + isLoading={isLoading} + onPress={() => { + onSubmit(); + close(); + }} + > + Save and continue + </ButtonWithLoading> + </ModalButtons> + </> + )} </Modal> ); }; diff --git a/packages/desktop-client/src/components/modals/SingleInputModal.tsx b/packages/desktop-client/src/components/modals/SingleInputModal.tsx index 8ba91fcb7e294ec30e06b9a9933ceba6c26004e6..162a53f187dc2a0106bfea676651e48aaf644024 100644 --- a/packages/desktop-client/src/components/modals/SingleInputModal.tsx +++ b/packages/desktop-client/src/components/modals/SingleInputModal.tsx @@ -1,19 +1,23 @@ // @ts-strict-ignore -import React, { type ComponentProps, useState, type FormEvent } from 'react'; +import React, { + useState, + type ComponentType, + type ComponentPropsWithoutRef, + type FormEvent, +} from 'react'; import { Form } from 'react-aria-components'; import { styles } from '../../style'; import { Button } from '../common/Button2'; import { FormError } from '../common/FormError'; import { InitialFocus } from '../common/InitialFocus'; -import { Modal } from '../common/Modal'; +import { Modal, ModalCloseButton, type ModalHeader } from '../common/Modal2'; import { View } from '../common/View'; import { InputField } from '../mobile/MobileForms'; -import { type CommonModalProps } from '../Modals'; type SingleInputModalProps = { - modalProps: Partial<CommonModalProps>; - title: ComponentProps<typeof Modal>['title']; + name: string; + Header: ComponentType<ComponentPropsWithoutRef<typeof ModalHeader>>; buttonText: string; onSubmit: (value: string) => void; onValidate?: (value: string) => string[]; @@ -21,8 +25,8 @@ type SingleInputModalProps = { }; export function SingleInputModal({ - modalProps, - title, + name, + Header, buttonText, onSubmit, onValidate, @@ -41,52 +45,61 @@ export function SingleInputModal({ } onSubmit?.(value); - modalProps.onClose(); }; return ( - <Modal title={title} {...modalProps}> - <Form onSubmit={_onSubmit}> - <View> - <InitialFocus> - <InputField - placeholder={inputPlaceholder} - defaultValue={value} - onChangeValue={setValue} - /> - </InitialFocus> - {errorMessage && ( - <FormError + <Modal name={name}> + {({ state: { close } }) => ( + <> + <Header rightContent={<ModalCloseButton onClick={close} />} /> + <Form + onSubmit={e => { + _onSubmit(e); + close(); + }} + > + <View> + <InitialFocus> + <InputField + placeholder={inputPlaceholder} + defaultValue={value} + onChangeValue={setValue} + /> + </InitialFocus> + {errorMessage && ( + <FormError + style={{ + paddingTop: 5, + marginLeft: styles.mobileEditingPadding, + marginRight: styles.mobileEditingPadding, + }} + > + * {errorMessage} + </FormError> + )} + </View> + <View style={{ - paddingTop: 5, - marginLeft: styles.mobileEditingPadding, - marginRight: styles.mobileEditingPadding, + justifyContent: 'center', + alignItems: 'center', + paddingTop: 10, }} > - * {errorMessage} - </FormError> - )} - </View> - <View - style={{ - justifyContent: 'center', - alignItems: 'center', - paddingTop: 10, - }} - > - <Button - type="submit" - variant="primary" - style={{ - height: styles.mobileMinHeight, - marginLeft: styles.mobileEditingPadding, - marginRight: styles.mobileEditingPadding, - }} - > - {buttonText} - </Button> - </View> - </Form> + <Button + type="submit" + variant="primary" + style={{ + height: styles.mobileMinHeight, + marginLeft: styles.mobileEditingPadding, + marginRight: styles.mobileEditingPadding, + }} + > + {buttonText} + </Button> + </View> + </Form> + </> + )} </Modal> ); } diff --git a/packages/desktop-client/src/components/modals/TransferModal.tsx b/packages/desktop-client/src/components/modals/TransferModal.tsx index 0361c0685fc33f2b744b8f7d8ece3d8c75eda595..31b58ebd587a87c2a36f47277ff2e7abcba76e32 100644 --- a/packages/desktop-client/src/components/modals/TransferModal.tsx +++ b/packages/desktop-client/src/components/modals/TransferModal.tsx @@ -8,14 +8,12 @@ import { styles } from '../../style'; import { addToBeBudgetedGroup } from '../budget/util'; import { Button } from '../common/Button2'; import { InitialFocus } from '../common/InitialFocus'; -import { Modal } from '../common/Modal'; +import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2'; import { View } from '../common/View'; import { FieldLabel, TapField } from '../mobile/MobileForms'; -import { type CommonModalProps } from '../Modals'; import { AmountInput } from '../util/AmountInput'; type TransferModalProps = { - modalProps: CommonModalProps; title: string; month: string; amount: number; @@ -24,7 +22,6 @@ type TransferModalProps = { }; export function TransferModal({ - modalProps, title, month, amount: initialAmount, @@ -62,65 +59,74 @@ export function TransferModal({ if (newAmount && categoryId) { onSubmit?.(newAmount, categoryId); } - - modalProps.onClose(); }; const toCategory = categories.find(c => c.id === toCategoryId); return ( - <Modal title={title} showHeader focusAfterClose={false} {...modalProps}> - <View> - <View> - <FieldLabel title="Transfer this amount:" /> - <InitialFocus> - <AmountInput - value={initialAmount} - autoDecimals={true} - style={{ - marginLeft: styles.mobileEditingPadding, - marginRight: styles.mobileEditingPadding, - }} - inputStyle={{ - height: styles.mobileMinHeight, - }} - onUpdate={setAmount} - onEnter={() => { - if (!toCategoryId) { - openCategoryModal(); - } - }} - /> - </InitialFocus> - </View> + <Modal name="transfer"> + {({ state: { close } }) => ( + <> + <ModalHeader + title={title} + rightContent={<ModalCloseButton onClick={close} />} + /> + <View> + <View> + <FieldLabel title="Transfer this amount:" /> + <InitialFocus> + <AmountInput + value={initialAmount} + autoDecimals={true} + style={{ + marginLeft: styles.mobileEditingPadding, + marginRight: styles.mobileEditingPadding, + }} + inputStyle={{ + height: styles.mobileMinHeight, + }} + onUpdate={setAmount} + onEnter={() => { + if (!toCategoryId) { + openCategoryModal(); + } + }} + /> + </InitialFocus> + </View> - <FieldLabel title="To:" /> - <TapField - tabIndex={0} - value={toCategory?.name} - onClick={openCategoryModal} - /> + <FieldLabel title="To:" /> + <TapField + tabIndex={0} + value={toCategory?.name} + onClick={openCategoryModal} + /> - <View - style={{ - justifyContent: 'center', - alignItems: 'center', - paddingTop: 10, - }} - > - <Button - variant="primary" - style={{ - height: styles.mobileMinHeight, - marginLeft: styles.mobileEditingPadding, - marginRight: styles.mobileEditingPadding, - }} - onPress={() => _onSubmit(amount, toCategoryId)} - > - Transfer - </Button> - </View> - </View> + <View + style={{ + justifyContent: 'center', + alignItems: 'center', + paddingTop: 10, + }} + > + <Button + variant="primary" + style={{ + height: styles.mobileMinHeight, + marginLeft: styles.mobileEditingPadding, + marginRight: styles.mobileEditingPadding, + }} + onPress={() => { + _onSubmit(amount, toCategoryId); + close(); + }} + > + Transfer + </Button> + </View> + </View> + </> + )} </Modal> ); } diff --git a/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx b/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx index e3238f741fd4c2daab5f49d283711b518fff6c21..745d2d6d5c085a9112f771998739badb25aae8bd 100644 --- a/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx +++ b/packages/desktop-client/src/components/schedules/DiscoverSchedules.tsx @@ -7,7 +7,6 @@ import { q } from 'loot-core/src/shared/query'; import { getRecurringDescription } from 'loot-core/src/shared/schedules'; import type { DiscoverScheduleEntity } from 'loot-core/src/types/models'; -import type { BoundActions } from '../../hooks/useActions'; import { useDateFormat } from '../../hooks/useDateFormat'; import { useSelected, @@ -18,11 +17,10 @@ import { import { useSendPlatformRequest } from '../../hooks/useSendPlatformRequest'; import { theme } from '../../style'; import { ButtonWithLoading } from '../common/Button2'; -import { Modal } from '../common/Modal'; +import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2'; import { Paragraph } from '../common/Paragraph'; import { Stack } from '../common/Stack'; import { View } from '../common/View'; -import { type CommonModalProps } from '../Modals'; import { Table, TableHeader, Row, Field, SelectCell } from '../table'; import { DisplayId } from '../util/DisplayId'; @@ -124,13 +122,7 @@ function DiscoverSchedulesTable({ ); } -export function DiscoverSchedules({ - modalProps, - actions, -}: { - modalProps: CommonModalProps; - actions: BoundActions; -}) { +export function DiscoverSchedules() { const { data, isLoading } = useSendPlatformRequest('schedule/discover'); const schedules = data || []; @@ -173,47 +165,57 @@ export function DiscoverSchedules({ } setCreating(false); - actions.popModal(); } return ( <Modal - title="Found schedules" - size={{ width: 850, height: 650 }} - {...modalProps} + name="schedules-discover" + containerProps={{ style: { width: 850, height: 650 } }} > - <Paragraph> - We found some possible schedules in your current transactions. Select - the ones you want to create. - </Paragraph> - <Paragraph> - If you expected a schedule here and don’t see it, it might be because - the payees of the transactions don’t match. Make sure you rename payees - on all transactions for a schedule to be the same payee. - </Paragraph> - - <SelectedProvider instance={selectedInst}> - <DiscoverSchedulesTable loading={isLoading} schedules={schedules} /> - </SelectedProvider> - - <Stack - direction="row" - align="center" - justify="flex-end" - style={{ - paddingTop: 20, - paddingBottom: 0, - }} - > - <ButtonWithLoading - variant="primary" - isLoading={creating} - isDisabled={selectedInst.items.size === 0} - onPress={onCreate} - > - Create schedules - </ButtonWithLoading> - </Stack> + {({ state: { close } }) => ( + <> + <ModalHeader + title="Found Schedules" + rightContent={<ModalCloseButton onClick={close} />} + /> + <Paragraph> + We found some possible schedules in your current transactions. + Select the ones you want to create. + </Paragraph> + <Paragraph> + If you expected a schedule here and don’t see it, it might be + because the payees of the transactions don’t match. Make sure you + rename payees on all transactions for a schedule to be the same + payee. + </Paragraph> + + <SelectedProvider instance={selectedInst}> + <DiscoverSchedulesTable loading={isLoading} schedules={schedules} /> + </SelectedProvider> + + <Stack + direction="row" + align="center" + justify="flex-end" + style={{ + paddingTop: 20, + paddingBottom: 0, + }} + > + <ButtonWithLoading + variant="primary" + isLoading={creating} + isDisabled={selectedInst.items.size === 0} + onPress={() => { + onCreate(); + close(); + }} + > + Create schedules + </ButtonWithLoading> + </Stack> + </> + )} </Modal> ); } diff --git a/packages/desktop-client/src/components/schedules/PostsOfflineNotification.jsx b/packages/desktop-client/src/components/schedules/PostsOfflineNotification.jsx index 429c5119d2143ed4f4f44445182538c3e76c1932..b19c0dc9e503ed648ce4b1fb19f06b28f4d1e364 100644 --- a/packages/desktop-client/src/components/schedules/PostsOfflineNotification.jsx +++ b/packages/desktop-client/src/components/schedules/PostsOfflineNotification.jsx @@ -1,78 +1,96 @@ import React from 'react'; +import { useDispatch } from 'react-redux'; import { useLocation } from 'react-router-dom'; +import { popModal } from 'loot-core/client/actions'; import { send } from 'loot-core/src/platform/client/fetch'; import { theme } from '../../style'; import { Button } from '../common/Button2'; -import { Modal } from '../common/Modal'; +import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2'; import { Paragraph } from '../common/Paragraph'; import { Stack } from '../common/Stack'; import { Text } from '../common/Text'; import { DisplayId } from '../util/DisplayId'; -export function PostsOfflineNotification({ modalProps, actions }) { +export function PostsOfflineNotification() { const location = useLocation(); + const dispatch = useDispatch(); const payees = (location.state && location.state.payees) || []; const plural = payees.length > 1; async function onPost() { await send('schedule/force-run-service'); - actions.popModal(); + dispatch(popModal()); } return ( - <Modal title="Post transactions?" size="small" {...modalProps}> - <Paragraph> - {payees.length > 0 ? ( - <Text> - The {plural ? 'payees ' : 'payee '} - {payees.map((id, idx) => ( - <Text key={id}> - <Text style={{ color: theme.pageTextPositive }}> - <DisplayId id={id} type="payees" /> - </Text> - {idx === payees.length - 1 - ? ' ' - : idx === payees.length - 2 - ? ', and ' - : ', '} + <Modal name="schedule-posts-offline-notification"> + {({ state: { close } }) => ( + <> + <ModalHeader + title="Post transactions?" + rightContent={<ModalCloseButton onClick={close} />} + /> + <Paragraph> + {payees.length > 0 ? ( + <Text> + The {plural ? 'payees ' : 'payee '} + {payees.map((id, idx) => ( + <Text key={id}> + <Text style={{ color: theme.pageTextPositive }}> + <DisplayId id={id} type="payees" /> + </Text> + {idx === payees.length - 1 + ? ' ' + : idx === payees.length - 2 + ? ', and ' + : ', '} + </Text> + ))} </Text> - ))} - </Text> - ) : ( - <Text>There {plural ? 'are payees ' : 'is a payee '} that </Text> - )} + ) : ( + <Text>There {plural ? 'are payees ' : 'is a payee '} that </Text> + )} - <Text> - {plural ? 'have ' : 'has '} schedules that are due today. Usually we - automatically post transactions for these, but you are offline or - syncing failed. In order to avoid duplicate transactions, we let you - choose whether or not to create transactions for these schedules. - </Text> - </Paragraph> - <Paragraph> - Be aware that other devices may have already created these transactions. - If you have multiple devices, make sure you only do this on one device - or you will have duplicate transactions. - </Paragraph> - <Paragraph> - You can always manually post a transaction later for a due schedule by - selecting the schedule and clicking “Post transaction†in the action - menu. - </Paragraph> - <Stack - direction="row" - justify="flex-end" - style={{ marginTop: 20 }} - spacing={2} - > - <Button onPress={actions.popModal}>Decide later</Button> - <Button variant="primary" onPress={onPost}> - Post transactions - </Button> - </Stack> + <Text> + {plural ? 'have ' : 'has '} schedules that are due today. Usually + we automatically post transactions for these, but you are offline + or syncing failed. In order to avoid duplicate transactions, we + let you choose whether or not to create transactions for these + schedules. + </Text> + </Paragraph> + <Paragraph> + Be aware that other devices may have already created these + transactions. If you have multiple devices, make sure you only do + this on one device or you will have duplicate transactions. + </Paragraph> + <Paragraph> + You can always manually post a transaction later for a due schedule + by selecting the schedule and clicking “Post transaction†in the + action menu. + </Paragraph> + <Stack + direction="row" + justify="flex-end" + style={{ marginTop: 20 }} + spacing={2} + > + <Button onPress={close}>Decide later</Button> + <Button + variant="primary" + onPress={() => { + onPost(); + close(); + }} + > + Post transactions + </Button> + </Stack> + </> + )} </Modal> ); } diff --git a/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx b/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx index 12d4b9ca4c295f7c44d874fe6597637e0f8e0bfd..5c89c4fc27519d0c6f16501248b949cdfc1711c3 100644 --- a/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx +++ b/packages/desktop-client/src/components/schedules/ScheduleDetails.jsx @@ -16,7 +16,7 @@ import { theme } from '../../style'; import { AccountAutocomplete } from '../autocomplete/AccountAutocomplete'; import { PayeeAutocomplete } from '../autocomplete/PayeeAutocomplete'; import { Button } from '../common/Button2'; -import { Modal } from '../common/Modal'; +import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2'; import { Stack } from '../common/Stack'; import { Text } from '../common/Text'; import { View } from '../common/View'; @@ -69,7 +69,7 @@ function updateScheduleConditions(schedule, fields) { }; } -export function ScheduleDetails({ modalProps, actions, id, transaction }) { +export function ScheduleDetails({ id, transaction }) { const adding = id == null; const fromTrans = transaction != null; const payees = getPayeesById(usePayees()); @@ -400,7 +400,6 @@ export function ScheduleDetails({ modalProps, actions, id, transaction }) { if (adding) { await onLinkTransactions([...selectedInst.items], res.data); } - actions.popModal(); } } @@ -448,342 +447,356 @@ export function ScheduleDetails({ modalProps, actions, id, transaction }) { // This is derived from the date const repeats = state.fields.date ? !!state.fields.date.frequency : false; return ( - <Modal - title={payee ? `Schedule: ${payee.name}` : 'Schedule'} - size="medium" - {...modalProps} - > - <Stack direction="row" style={{ marginTop: 10 }}> - <FormField style={{ flex: 1 }}> - <FormLabel title="Schedule Name" htmlFor="name-field" /> - <GenericInput - field="string" - type="string" - value={state.fields.name} - multi={false} - onChange={e => { - dispatch({ type: 'set-field', field: 'name', value: e }); - }} - /> - </FormField> - </Stack> - <Stack direction="row" style={{ marginTop: 20 }}> - <FormField style={{ flex: 1 }}> - <FormLabel title="Payee" id="payee-label" htmlFor="payee-field" /> - <PayeeAutocomplete - value={state.fields.payee} - labelProps={{ id: 'payee-label' }} - inputProps={{ id: 'payee-field', placeholder: '(none)' }} - onSelect={id => - dispatch({ type: 'set-field', field: 'payee', value: id }) - } - /> - </FormField> - - <FormField style={{ flex: 1 }}> - <FormLabel - title="Account" - id="account-label" - htmlFor="account-field" + <Modal name="schedule-edit"> + {({ state: { close } }) => ( + <> + <ModalHeader + title={payee ? `Schedule: ${payee.name}` : 'Schedule'} + rightContent={<ModalCloseButton onClick={close} />} /> - <AccountAutocomplete - includeClosedAccounts={false} - value={state.fields.account} - labelProps={{ id: 'account-label' }} - inputProps={{ id: 'account-field', placeholder: '(none)' }} - onSelect={id => - dispatch({ type: 'set-field', field: 'account', value: id }) - } - /> - </FormField> - - <FormField style={{ flex: 1 }}> - <Stack direction="row" align="center" style={{ marginBottom: 3 }}> - <FormLabel - title="Amount" - htmlFor="amount-field" - style={{ margin: 0, flex: 1 }} - /> - <OpSelect - ops={['isapprox', 'is', 'isbetween']} - value={state.fields.amountOp} - formatOp={op => { - switch (op) { - case 'is': - return 'is exactly'; - case 'isapprox': - return 'is approximately'; - case 'isbetween': - return 'is between'; - default: - throw new Error('Invalid op for select: ' + op); - } - }} - style={{ - padding: '0 10px', - color: theme.pageTextLight, - fontSize: 12, - }} - onChange={(_, op) => - dispatch({ type: 'set-field', field: 'amountOp', value: op }) - } - /> + <Stack direction="row" style={{ marginTop: 10 }}> + <FormField style={{ flex: 1 }}> + <FormLabel title="Schedule Name" htmlFor="name-field" /> + <GenericInput + field="string" + type="string" + value={state.fields.name} + multi={false} + onChange={e => { + dispatch({ type: 'set-field', field: 'name', value: e }); + }} + /> + </FormField> </Stack> - {state.fields.amountOp === 'isbetween' ? ( - <BetweenAmountInput - defaultValue={state.fields.amount} - onChange={value => - dispatch({ - type: 'set-field', - field: 'amount', - value, - }) - } - /> - ) : ( - <AmountInput - id="amount-field" - value={state.fields.amount} - onUpdate={value => - dispatch({ - type: 'set-field', - field: 'amount', - value, - }) - } - /> - )} - </FormField> - </Stack> - - <View style={{ marginTop: 20 }}> - <FormLabel title="Date" /> - </View> - - <Stack direction="row" align="flex-start" justify="space-between"> - <View style={{ width: '13.44rem' }}> - {repeats ? ( - <RecurringSchedulePicker - value={state.fields.date} - onChange={value => - dispatch({ type: 'set-field', field: 'date', value }) - } - /> - ) : ( - <DateSelect - value={state.fields.date} - onSelect={date => - dispatch({ type: 'set-field', field: 'date', value: date }) - } - dateFormat={dateFormat} - /> - )} - - {state.upcomingDates && ( - <View style={{ fontSize: 13, marginTop: 20 }}> - <Text style={{ color: theme.pageTextLight, fontWeight: 600 }}> - Upcoming dates - </Text> - <Stack - direction="column" - spacing={1} - style={{ marginTop: 10, color: theme.pageTextLight }} - > - {state.upcomingDates.map(date => ( - <View key={date}> - {monthUtils.format(date, `${dateFormat} EEEE`)} - </View> - ))} - </Stack> - </View> - )} - </View> - - <View - style={{ - marginTop: 5, - flexDirection: 'row', - alignItems: 'center', - userSelect: 'none', - }} - > - <Checkbox - id="form_repeats" - checked={repeats} - onChange={e => { - dispatch({ type: 'set-repeats', repeats: e.target.checked }); - }} - /> - <label htmlFor="form_repeats" style={{ userSelect: 'none' }}> - Repeats - </label> - </View> - - <Stack align="flex-end"> - <View - style={{ - marginTop: 5, - flexDirection: 'row', - alignItems: 'center', - userSelect: 'none', - justifyContent: 'flex-end', - }} - > - <Checkbox - id="form_posts_transaction" - checked={state.fields.posts_transaction} - onChange={e => { - dispatch({ - type: 'set-field', - field: 'posts_transaction', - value: e.target.checked, - }); - }} - /> - <label - htmlFor="form_posts_transaction" - style={{ userSelect: 'none' }} - > - Automatically add transaction - </label> - </View> + <Stack direction="row" style={{ marginTop: 20 }}> + <FormField style={{ flex: 1 }}> + <FormLabel title="Payee" id="payee-label" htmlFor="payee-field" /> + <PayeeAutocomplete + value={state.fields.payee} + labelProps={{ id: 'payee-label' }} + inputProps={{ id: 'payee-field', placeholder: '(none)' }} + onSelect={id => + dispatch({ type: 'set-field', field: 'payee', value: id }) + } + /> + </FormField> - <Text - style={{ - width: 350, - textAlign: 'right', - color: theme.pageTextLight, - marginTop: 10, - fontSize: 13, - lineHeight: '1.4em', - }} - > - If checked, the schedule will automatically create transactions for - you in the specified account - </Text> - - {!adding && state.schedule.rule && ( - <Stack direction="row" align="center" style={{ marginTop: 20 }}> - {state.isCustom && ( - <Text + <FormField style={{ flex: 1 }}> + <FormLabel + title="Account" + id="account-label" + htmlFor="account-field" + /> + <AccountAutocomplete + includeClosedAccounts={false} + value={state.fields.account} + labelProps={{ id: 'account-label' }} + inputProps={{ id: 'account-field', placeholder: '(none)' }} + onSelect={id => + dispatch({ type: 'set-field', field: 'account', value: id }) + } + /> + </FormField> + + <FormField style={{ flex: 1 }}> + <Stack direction="row" align="center" style={{ marginBottom: 3 }}> + <FormLabel + title="Amount" + htmlFor="amount-field" + style={{ margin: 0, flex: 1 }} + /> + <OpSelect + ops={['isapprox', 'is', 'isbetween']} + value={state.fields.amountOp} + formatOp={op => { + switch (op) { + case 'is': + return 'is exactly'; + case 'isapprox': + return 'is approximately'; + case 'isbetween': + return 'is between'; + default: + throw new Error('Invalid op for select: ' + op); + } + }} style={{ + padding: '0 10px', color: theme.pageTextLight, - fontSize: 13, - textAlign: 'right', - width: 350, + fontSize: 12, }} - > - This schedule has custom conditions and actions - </Text> + onChange={(_, op) => + dispatch({ + type: 'set-field', + field: 'amountOp', + value: op, + }) + } + /> + </Stack> + {state.fields.amountOp === 'isbetween' ? ( + <BetweenAmountInput + defaultValue={state.fields.amount} + onChange={value => + dispatch({ + type: 'set-field', + field: 'amount', + value, + }) + } + /> + ) : ( + <AmountInput + id="amount-field" + value={state.fields.amount} + onUpdate={value => + dispatch({ + type: 'set-field', + field: 'amount', + value, + }) + } + /> + )} + </FormField> + </Stack> + + <View style={{ marginTop: 20 }}> + <FormLabel title="Date" /> + </View> + + <Stack direction="row" align="flex-start" justify="space-between"> + <View style={{ width: '13.44rem' }}> + {repeats ? ( + <RecurringSchedulePicker + value={state.fields.date} + onChange={value => + dispatch({ type: 'set-field', field: 'date', value }) + } + /> + ) : ( + <DateSelect + value={state.fields.date} + onSelect={date => + dispatch({ type: 'set-field', field: 'date', value: date }) + } + dateFormat={dateFormat} + /> + )} + + {state.upcomingDates && ( + <View style={{ fontSize: 13, marginTop: 20 }}> + <Text style={{ color: theme.pageTextLight, fontWeight: 600 }}> + Upcoming dates + </Text> + <Stack + direction="column" + spacing={1} + style={{ marginTop: 10, color: theme.pageTextLight }} + > + {state.upcomingDates.map(date => ( + <View key={date}> + {monthUtils.format(date, `${dateFormat} EEEE`)} + </View> + ))} + </Stack> + </View> )} - <Button onPress={() => onEditRule()} isDisabled={adding}> - Edit as rule - </Button> - </Stack> - )} - </Stack> - </Stack> - - <View style={{ marginTop: 30, flex: 1 }}> - <SelectedProvider instance={selectedInst}> - {adding ? ( - <View style={{ flexDirection: 'row', padding: '5px 0' }}> - <Text style={{ color: theme.pageTextLight }}> - These transactions match this schedule: - </Text> - <View style={{ flex: 1 }} /> - <Text style={{ color: theme.pageTextLight }}> - Select transactions to link on save - </Text> </View> - ) : ( - <View style={{ flexDirection: 'row', alignItems: 'center' }}> - <Button - variant="bare" + + <View + style={{ + marginTop: 5, + flexDirection: 'row', + alignItems: 'center', + userSelect: 'none', + }} + > + <Checkbox + id="form_repeats" + checked={repeats} + onChange={e => { + dispatch({ type: 'set-repeats', repeats: e.target.checked }); + }} + /> + <label htmlFor="form_repeats" style={{ userSelect: 'none' }}> + Repeats + </label> + </View> + + <Stack align="flex-end"> + <View style={{ - color: - state.transactionsMode === 'linked' - ? theme.pageTextLink - : theme.pageTextSubdued, - marginRight: 10, - fontSize: 14, + marginTop: 5, + flexDirection: 'row', + alignItems: 'center', + userSelect: 'none', + justifyContent: 'flex-end', }} - onPress={() => onSwitchTransactions('linked')} > - Linked transactions - </Button>{' '} - <Button - variant="bare" + <Checkbox + id="form_posts_transaction" + checked={state.fields.posts_transaction} + onChange={e => { + dispatch({ + type: 'set-field', + field: 'posts_transaction', + value: e.target.checked, + }); + }} + /> + <label + htmlFor="form_posts_transaction" + style={{ userSelect: 'none' }} + > + Automatically add transaction + </label> + </View> + + <Text style={{ - color: - state.transactionsMode === 'matched' - ? theme.pageTextLink - : theme.pageTextSubdued, - fontSize: 14, + width: 350, + textAlign: 'right', + color: theme.pageTextLight, + marginTop: 10, + fontSize: 13, + lineHeight: '1.4em', }} - onPress={() => onSwitchTransactions('matched')} > - Find matching transactions - </Button> - <View style={{ flex: 1 }} /> - <SelectedItemsButton - name="transactions" - items={ - state.transactionsMode === 'linked' - ? [{ name: 'unlink', text: 'Unlink from schedule' }] - : [{ name: 'link', text: 'Link to schedule' }] + If checked, the schedule will automatically create transactions + for you in the specified account + </Text> + + {!adding && state.schedule.rule && ( + <Stack direction="row" align="center" style={{ marginTop: 20 }}> + {state.isCustom && ( + <Text + style={{ + color: theme.pageTextLight, + fontSize: 13, + textAlign: 'right', + width: 350, + }} + > + This schedule has custom conditions and actions + </Text> + )} + <Button onPress={() => onEditRule()} isDisabled={adding}> + Edit as rule + </Button> + </Stack> + )} + </Stack> + </Stack> + + <View style={{ marginTop: 30, flex: 1 }}> + <SelectedProvider instance={selectedInst}> + {adding ? ( + <View style={{ flexDirection: 'row', padding: '5px 0' }}> + <Text style={{ color: theme.pageTextLight }}> + These transactions match this schedule: + </Text> + <View style={{ flex: 1 }} /> + <Text style={{ color: theme.pageTextLight }}> + Select transactions to link on save + </Text> + </View> + ) : ( + <View style={{ flexDirection: 'row', alignItems: 'center' }}> + <Button + variant="bare" + style={{ + color: + state.transactionsMode === 'linked' + ? theme.pageTextLink + : theme.pageTextSubdued, + marginRight: 10, + fontSize: 14, + }} + onPress={() => onSwitchTransactions('linked')} + > + Linked transactions + </Button>{' '} + <Button + variant="bare" + style={{ + color: + state.transactionsMode === 'matched' + ? theme.pageTextLink + : theme.pageTextSubdued, + fontSize: 14, + }} + onPress={() => onSwitchTransactions('matched')} + > + Find matching transactions + </Button> + <View style={{ flex: 1 }} /> + <SelectedItemsButton + name="transactions" + items={ + state.transactionsMode === 'linked' + ? [{ name: 'unlink', text: 'Unlink from schedule' }] + : [{ name: 'link', text: 'Link to schedule' }] + } + onSelect={(name, ids) => { + switch (name) { + case 'link': + onLinkTransactions(ids); + break; + case 'unlink': + onUnlinkTransactions(ids); + break; + default: + } + }} + /> + </View> + )} + + <SimpleTransactionsTable + renderEmpty={ + <NoTransactionsMessage + error={state.error} + transactionsMode={state.transactionsMode} + /> } - onSelect={(name, ids) => { - switch (name) { - case 'link': - onLinkTransactions(ids); - break; - case 'unlink': - onUnlinkTransactions(ids); - break; - default: - } + transactions={state.transactions} + fields={['date', 'payee', 'amount']} + style={{ + border: '1px solid ' + theme.tableBorder, + borderRadius: 4, + overflow: 'hidden', + marginTop: 5, + maxHeight: 200, }} /> - </View> - )} + </SelectedProvider> + </View> - <SimpleTransactionsTable - renderEmpty={ - <NoTransactionsMessage - error={state.error} - transactionsMode={state.transactionsMode} - /> - } - transactions={state.transactions} - fields={['date', 'payee', 'amount']} - style={{ - border: '1px solid ' + theme.tableBorder, - borderRadius: 4, - overflow: 'hidden', - marginTop: 5, - maxHeight: 200, - }} - /> - </SelectedProvider> - </View> - - <Stack - direction="row" - justify="flex-end" - align="center" - style={{ marginTop: 20 }} - > - {state.error && ( - <Text style={{ color: theme.errorText }}>{state.error}</Text> - )} - <Button style={{ marginRight: 10 }} onPress={actions.popModal}> - Cancel - </Button> - <Button variant="primary" onPress={onSave}> - {adding ? 'Add' : 'Save'} - </Button> - </Stack> + <Stack + direction="row" + justify="flex-end" + align="center" + style={{ marginTop: 20 }} + > + {state.error && ( + <Text style={{ color: theme.errorText }}>{state.error}</Text> + )} + <Button style={{ marginRight: 10 }} onPress={close}> + Cancel + </Button> + <Button + variant="primary" + onPress={() => { + onSave(); + close(); + }} + > + {adding ? 'Add' : 'Save'} + </Button> + </Stack> + </> + )} </Modal> ); } diff --git a/packages/desktop-client/src/components/schedules/ScheduleLink.tsx b/packages/desktop-client/src/components/schedules/ScheduleLink.tsx index 7f989a784793b8d21339b9dfe1f1d2dad8b55349..666a040c4fe76a1d76852edf4656ab48d5bd3370 100644 --- a/packages/desktop-client/src/components/schedules/ScheduleLink.tsx +++ b/packages/desktop-client/src/components/schedules/ScheduleLink.tsx @@ -8,25 +8,19 @@ import { send } from 'loot-core/src/platform/client/fetch'; import { type Query } from 'loot-core/src/shared/query'; import { type TransactionEntity } from 'loot-core/src/types/models'; -import { type BoundActions } from '../../hooks/useActions'; import { SvgAdd } from '../../icons/v0'; import { Button } from '../common/Button2'; -import { Modal } from '../common/Modal'; +import { Modal, ModalCloseButton, ModalHeader } from '../common/Modal2'; import { Search } from '../common/Search'; import { Text } from '../common/Text'; import { View } from '../common/View'; -import { type CommonModalProps } from '../Modals'; import { ROW_HEIGHT, SchedulesTable } from './SchedulesTable'; export function ScheduleLink({ - modalProps, - actions, transactionIds: ids, getTransaction, }: { - actions: BoundActions; - modalProps?: CommonModalProps; transactionIds: string[]; getTransaction: (transactionId: string) => TransactionEntity; }) { @@ -50,11 +44,9 @@ export function ScheduleLink({ updated: ids.map(id => ({ id, schedule: scheduleId })), }); } - actions.popModal(); } async function onCreate() { - actions.popModal(); dispatch( pushModal('schedule-edit', { id: null, @@ -64,62 +56,83 @@ export function ScheduleLink({ } return ( - <Modal title="Link Schedule" size={{ width: 800 }} {...modalProps}> - <View - style={{ - flexDirection: 'row', - gap: 4, - marginBottom: 20, - alignItems: 'center', - }} - > - <Text> - Choose the schedule{' '} - {ids?.length > 1 - ? `these ${ids.length} transactions belong` - : `this transaction belongs`}{' '} - to: - </Text> - <Search - inputRef={searchInput} - isInModal - width={300} - placeholder="Filter schedules…" - value={filter} - onChange={setFilter} - /> - {ids.length === 1 && ( - <Button - variant="primary" - style={{ marginLeft: 15, padding: '4px 10px' }} - onPress={onCreate} + <Modal + name="schedule-link" + containerProps={{ + style: { + width: 800, + }, + }} + > + {({ state: { close } }) => ( + <> + <ModalHeader + title="Link Schedule" + rightContent={<ModalCloseButton onClick={close} />} + /> + <View + style={{ + flexDirection: 'row', + gap: 4, + marginBottom: 20, + alignItems: 'center', + }} > - <SvgAdd style={{ width: '20', padding: '3' }} /> - Create New - </Button> - )} - </View> + <Text> + Choose the schedule{' '} + {ids?.length > 1 + ? `these ${ids.length} transactions belong` + : `this transaction belongs`}{' '} + to: + </Text> + <Search + inputRef={searchInput} + isInModal + width={300} + placeholder="Filter schedules…" + value={filter} + onChange={setFilter} + /> + {ids.length === 1 && ( + <Button + variant="primary" + style={{ marginLeft: 15, padding: '4px 10px' }} + onPress={() => { + close(); + onCreate(); + }} + > + <SvgAdd style={{ width: '20', padding: '3' }} /> + Create New + </Button> + )} + </View> - <View - style={{ - flex: `1 1 ${ - (ROW_HEIGHT - 1) * (Math.max(schedules.length, 1) + 1) - }px`, - marginTop: 15, - maxHeight: '50vh', - }} - > - <SchedulesTable - allowCompleted={false} - filter={filter} - minimal={true} - onAction={() => {}} - onSelect={onSelect} - schedules={schedules} - statuses={statuses} - style={null} - /> - </View> + <View + style={{ + flex: `1 1 ${ + (ROW_HEIGHT - 1) * (Math.max(schedules.length, 1) + 1) + }px`, + marginTop: 15, + maxHeight: '50vh', + }} + > + <SchedulesTable + allowCompleted={false} + filter={filter} + minimal={true} + onAction={() => {}} + onSelect={id => { + onSelect(id); + close(); + }} + schedules={schedules} + statuses={statuses} + style={null} + /> + </View> + </> + )} </Modal> ); } diff --git a/packages/desktop-client/src/hooks/useModalState.ts b/packages/desktop-client/src/hooks/useModalState.ts new file mode 100644 index 0000000000000000000000000000000000000000..732a69a9623b5fca0e3a16e1cc4e2d348dbd2701 --- /dev/null +++ b/packages/desktop-client/src/hooks/useModalState.ts @@ -0,0 +1,44 @@ +import { useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; + +import { popModal } from 'loot-core/client/actions'; +import { type State } from 'loot-core/client/state-types'; +import { type Modal } from 'loot-core/client/state-types/modals'; + +type ModalState = { + onClose: () => void; + modalStack: Modal[]; + activeModal?: string; + isActive: (name: string) => boolean; + isHidden: boolean; +}; + +export function useModalState(): ModalState { + const modalStack = useSelector((state: State) => state.modals.modalStack); + const isHidden = useSelector((state: State) => state.modals.isHidden); + const dispatch = useDispatch(); + + const popModalCallback = useCallback(() => { + dispatch(popModal()); + }, [dispatch]); + + const lastModal = modalStack[modalStack.length - 1]; + const isActive = useCallback( + (name: string) => { + if (name === lastModal?.name) { + return true; + } + + return false; + }, + [lastModal?.name], + ); + + return { + onClose: popModalCallback, + modalStack, + activeModal: lastModal?.name, + isActive, + isHidden, + }; +} diff --git a/packages/loot-core/src/client/actions/modals.ts b/packages/loot-core/src/client/actions/modals.ts index 879092f454e8bba369126d55aa2eb07bcfd02a7d..3d0a23ccee1175811ad8181ec18ab9bf43bac992 100644 --- a/packages/loot-core/src/client/actions/modals.ts +++ b/packages/loot-core/src/client/actions/modals.ts @@ -8,6 +8,7 @@ import type { ModalWithOptions, ModalType, FinanceModals, + Modal, } from '../state-types/modals'; export function pushModal<M extends keyof ModalWithOptions>( @@ -36,8 +37,7 @@ export function replaceModal<M extends ModalType>( name: M, options?: FinanceModals[M], ): ReplaceModalAction { - // @ts-expect-error TS is unable to determine that `name` and `options` match - const modal: M = { name, options }; + const modal: Modal = { name, options }; return { type: constants.REPLACE_MODAL, modal }; } diff --git a/packages/loot-core/src/client/state-types/modals.d.ts b/packages/loot-core/src/client/state-types/modals.d.ts index 3c6b4ab858811fa29ab97023267d1b7e0ec25368..cf801cb2e523455a801587565fb00cc717703f87 100644 --- a/packages/loot-core/src/client/state-types/modals.d.ts +++ b/packages/loot-core/src/client/state-types/modals.d.ts @@ -142,6 +142,7 @@ type FinanceModals = { onSave: (category: CategoryEntity) => void; onEditNotes: (id: string) => void; onDelete: (categoryId: string) => void; + onToggleVisibility: (categoryId: string) => void; onBudgetAction: (month: string, action: string, args?: unknown) => void; onClose?: () => void; }; @@ -167,6 +168,7 @@ type FinanceModals = { onAddCategory: (groupId: string, isIncome: boolean) => void; onEditNotes: (id: string) => void; onDelete: (groupId: string) => void; + onToggleVisibility: (groupId: string) => void; onClose?: () => void; }; notes: { @@ -292,3 +294,9 @@ export type ModalsState = { modalStack: Modal[]; isHidden: boolean; }; + +type Modal = { + name: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + options?: any; +}; diff --git a/upcoming-release-notes/2946.md b/upcoming-release-notes/2946.md new file mode 100644 index 0000000000000000000000000000000000000000..f430704fb4dbef74583ee101ac3901f60e8f235d --- /dev/null +++ b/upcoming-release-notes/2946.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [joel-jeremy] +--- + +Port finance modals to react-aria-components Modal.