Skip to content
Snippets Groups Projects
  • Jed Fox's avatar
    beef97d7
    Move the welcome modal to an interstitial, add import button (#762) · beef97d7
    Jed Fox authored
    I noticed that the first run flow is suboptimal for people who want to
    import an existing file from Actual/YNAB. I’ve moved the welcome modal
    into the management app and set it up to appear when there are no
    budgets available (which also has the benefit of allowing people to see
    the modal again!)
    
    I think there’s some weirdness around getting the modal to reappear when
    deleting a budget file which I want to work out before merging this.
    
    This PR also reorganizes the management app a bit to reduce usage of
    modals (currently, hitting escape while the budget list is open leaves
    you with a blank page).
    
    <img width="539" alt="Screenshot_2023-03-18 08 53 54"
    src="https://user-images.githubusercontent.com/25517624/226107462-b2b88791-1015-4397-b290-c64e7fcc0f41.png">
    
    - [x] Ensure modal consistently appears when needed (no longer a modal!)
    - [x] Fix e2e tests
    Move the welcome modal to an interstitial, add import button (#762)
    Jed Fox authored
    I noticed that the first run flow is suboptimal for people who want to
    import an existing file from Actual/YNAB. I’ve moved the welcome modal
    into the management app and set it up to appear when there are no
    budgets available (which also has the benefit of allowing people to see
    the modal again!)
    
    I think there’s some weirdness around getting the modal to reappear when
    deleting a budget file which I want to work out before merging this.
    
    This PR also reorganizes the management app a bit to reduce usage of
    modals (currently, hitting escape while the budget list is open leaves
    you with a blank page).
    
    <img width="539" alt="Screenshot_2023-03-18 08 53 54"
    src="https://user-images.githubusercontent.com/25517624/226107462-b2b88791-1015-4397-b290-c64e7fcc0f41.png">
    
    - [x] Ensure modal consistently appears when needed (no longer a modal!)
    - [x] Fix e2e tests
Modals.js 8.93 KiB
import React from 'react';
import { connect } from 'react-redux';
import { Route, Switch } from 'react-router-dom';

import Component from '@reactions/component';
import { createLocation } from 'history';
import { bindActionCreators } from 'redux';

import * as actions from 'loot-core/src/client/actions';
import { send, listen, unlisten } from 'loot-core/src/platform/client/fetch';
import BudgetSummary from 'loot-design/src/components/modals/BudgetSummary';
import CloseAccount from 'loot-design/src/components/modals/CloseAccount';
import ConfigureLinkedAccounts from 'loot-design/src/components/modals/ConfigureLinkedAccounts';
import CreateLocalAccount from 'loot-design/src/components/modals/CreateLocalAccount';
import EditField from 'loot-design/src/components/modals/EditField';
import ImportTransactions from 'loot-design/src/components/modals/ImportTransactions';
import LoadBackup from 'loot-design/src/components/modals/LoadBackup';
import NordigenExternalMsg from 'loot-design/src/components/modals/NordigenExternalMsg';
import PlaidExternalMsg from 'loot-design/src/components/modals/PlaidExternalMsg';
import SelectLinkedAccounts from 'loot-design/src/components/modals/SelectLinkedAccounts';

import useSyncServerStatus from '../hooks/useSyncServerStatus';

import ConfirmCategoryDelete from './modals/ConfirmCategoryDelete';
import CreateAccount from './modals/CreateAccount';
import CreateEncryptionKey from './modals/CreateEncryptionKey';
import EditRule from './modals/EditRule';
import FixEncryptionKey from './modals/FixEncryptionKey';
import ManageRulesModal from './modals/ManageRulesModal';
import MergeUnusedPayees from './modals/MergeUnusedPayees';

function Modals({
  history,
  modalStack,
  isHidden,
  accounts,
  categoryGroups,
  categories,
  payees,
  budgetId,
  actions,
}) {
  const syncServerStatus = useSyncServerStatus();

  return modalStack.map(({ name, options = {} }, idx) => {
    const modalProps = {
      onClose: actions.popModal,
      onBack: actions.popModal,
      showBack: idx > 0,
      isCurrent: idx === modalStack.length - 1,
      isHidden,
      stackIndex: idx,
    };

    let location = createLocation('/' + name);
    return (
      <Switch key={name} location={location}>
        <Route path="/import-transactions">
          <ImportTransactions modalProps={modalProps} options={options} />
        </Route>

        <Route path="/add-account">
          <CreateAccount
            modalProps={modalProps}
            actions={actions}
            syncServerStatus={syncServerStatus}
          />
        </Route>

        <Route path="/add-local-account">
          <CreateLocalAccount
            modalProps={modalProps}
            actions={actions}
            history={history}
          />
        </Route>

        <Route path="/close-account">
          <CloseAccount
            modalProps={modalProps}
            account={options.account}
            balance={options.balance}
            canDelete={options.canDelete}
            accounts={accounts.filter(acct => acct.closed === 0)}
            categoryGroups={categoryGroups}
            actions={actions}
          />
        </Route>

        <Route path="/select-linked-accounts">
          <SelectLinkedAccounts
            modalProps={modalProps}
            accounts={options.accounts}
            requisitionId={options.requisitionId}
            actualAccounts={accounts.filter(acct => acct.closed === 0)}
            upgradingAccountId={options.upgradingAccountId}
            actions={actions}
          />
        </Route>

        <Route path="/configure-linked-accounts">
          <ConfigureLinkedAccounts
            modalProps={modalProps}
            institution={options.institution}
            publicToken={options.publicToken}
            accounts={options.accounts}
            upgradingId={options.upgradingId}
            actions={actions}
          />
        </Route>

        <Route
          path="/confirm-category-delete"
          render={() => {
            const { category, group, onDelete } = options;
            return (
              <ConfirmCategoryDelete
                modalProps={modalProps}
                actions={actions}
                category={categories.find(c => c.id === category)}
                group={categoryGroups.find(g => g.id === group)}
                categoryGroups={categoryGroups}
                onDelete={onDelete}
              />
            );
          }}
        />

        <Route
          path="/load-backup"
          render={() => {
            return (
              <Component
                initialState={{ backups: [] }}
                didMount={async ({ setState }) => {
                  setState({
                    backups: await send('backups-get', { id: budgetId }),
                  });

                  listen('backups-updated', backups => {
                    setState({ backups });
                  });
                }}
                willUnmount={() => {
                  unlisten('backups-updated');
                }}
              >
                {({ state }) => (
                  <LoadBackup
                    budgetId={budgetId}
                    modalProps={modalProps}
                    actions={actions}
                    backups={state.backups}
                  />
                )}
              </Component>
            );
          }}
        />

        <Route
          path="/manage-rules"
          render={() => {
            return (
              <ManageRulesModal
                history={history}
                modalProps={modalProps}
                payeeId={options.payeeId}
              />
            );
          }}
        />

        <Route
          path="/edit-rule"
          render={() => {
            return (
              <EditRule
                history={history}
                modalProps={modalProps}
                defaultRule={options.rule}
                onSave={options.onSave}
              />
            );
          }}
        />

        <Route
          path="/merge-unused-payees"
          render={() => {
            return (
              <MergeUnusedPayees
                history={history}
                modalProps={modalProps}
                payeeIds={options.payeeIds}
                targetPayeeId={options.targetPayeeId}
              />
            );
          }}
        />

        <Route
          path="/plaid-external-msg"
          render={() => {
            return (
              <PlaidExternalMsg
                modalProps={modalProps}
                actions={actions}
                onMoveExternal={options.onMoveExternal}
                onClose={() => {
                  options.onClose && options.onClose();
                  send('poll-web-token-stop');
                }}
                onSuccess={options.onSuccess}
              />
            );
          }}
        />
        <Route
          path="/nordigen-external-msg"
          render={() => {
            return (
              <NordigenExternalMsg
                modalProps={modalProps}
                actions={actions}
                onMoveExternal={options.onMoveExternal}
                onClose={() => {
                  options.onClose && options.onClose();
                  send('nordigen-poll-web-token-stop');
                }}
                onSuccess={options.onSuccess}
              />
            );
          }}
        />

        <Route
          path="/create-encryption-key"
          render={() => {
            return (
              <CreateEncryptionKey
                key={name}
                modalProps={modalProps}
                actions={actions}
                options={options}
              />
            );
          }}
        />

        <Route
          path="/fix-encryption-key"
          render={() => {
            return (
              <FixEncryptionKey
                key={name}
                modalProps={modalProps}
                actions={actions}
                options={options}
              />
            );
          }}
        />

        <Route
          path="/edit-field"
          render={() => {
            return (
              <EditField
                key={name}
                modalProps={modalProps}
                actions={actions}
                name={options.name}
                onSubmit={options.onSubmit}
              />
            );
          }}
        />

        <Route path="/budget-summary">
          <BudgetSummary
            key={name}
            modalProps={modalProps}
            month={options.month}
            actions={actions}
          />
        </Route>
      </Switch>
    );
  });
}

export default connect(
  state => ({
    modalStack: state.modals.modalStack,
    isHidden: state.modals.isHidden,
    accounts: state.queries.accounts,
    categoryGroups: state.queries.categories.grouped,
    categories: state.queries.categories.list,
    payees: state.queries.payees,
    budgetId: state.prefs.local && state.prefs.local.id,
  }),
  dispatch => ({ actions: bindActionCreators(actions, dispatch) }),
)(Modals);