diff --git a/packages/desktop-client/src/components/FinancesApp.js b/packages/desktop-client/src/components/FinancesApp.js index eccbab12aedd8528fb9b926cfc2fa88aea845fae..bdc6f44822249a2565f7d06c66d9853e7a95a8bd 100644 --- a/packages/desktop-client/src/components/FinancesApp.js +++ b/packages/desktop-client/src/components/FinancesApp.js @@ -24,9 +24,11 @@ import BankSyncStatus from './BankSyncStatus'; import Budget from './budget'; import FloatableSidebar, { SidebarProvider } from './FloatableSidebar'; import GlobalKeys from './GlobalKeys'; +import { ManageRulesPage } from './ManageRulesPage'; import Modals from './Modals'; import Notifications from './Notifications'; import { PageTypeProvider } from './Page'; +import { ManagePayeesPage } from './payees/ManagePayeesPage'; import Reports from './reports'; import Schedules from './schedules'; import DiscoverSchedules from './schedules/DiscoverSchedules'; @@ -97,6 +99,8 @@ function Routes({ location }) { component={PostsOfflineNotification} /> + <Route path="/rules" exact component={ManageRulesPage} /> + <Route path="/payees" exact component={ManagePayeesPage} /> <Route path="/tools/fix-splits" exact component={FixSplitsTool} /> <Route diff --git a/packages/desktop-client/src/components/modals/ManageRules.js b/packages/desktop-client/src/components/ManageRules.js similarity index 84% rename from packages/desktop-client/src/components/modals/ManageRules.js rename to packages/desktop-client/src/components/ManageRules.js index 93eb2a0f383aea01310c9d04e04d5d149e920103..37c8bc0f790c18c0ca8d1343f9fdfdd66772d8a8 100644 --- a/packages/desktop-client/src/components/modals/ManageRules.js +++ b/packages/desktop-client/src/components/ManageRules.js @@ -43,6 +43,8 @@ import useSelected, { import { colors } from 'loot-design/src/style'; import ArrowRight from 'loot-design/src/svg/RightArrow2'; +import { Page } from './Page'; + let SchedulesQuery = liveQueryContext(q('schedules').select('*')); export function Value({ @@ -448,7 +450,7 @@ function RulesHeader() { let dispatchSelected = useSelectedDispatch(); return ( - <TableHeader> + <TableHeader version="v2" style={{}}> <SelectCell exposed={true} focused={false} @@ -500,14 +502,17 @@ function RulesList({ ); } -export default function ManageRules({ history, modalProps, payeeId }) { +export default function ManageRules({ + isModal, + payeeId, + setLoading = () => {} +}) { let [allRules, setAllRules] = useState(null); let [rules, setRules] = useState(null); let dispatch = useDispatch(); let navigator = useTableNavigator(rules, ['select', 'edit']); let selectedInst = useSelected('manage-rules', allRules, []); let [hoveredRule, setHoveredRule] = useState(null); - let [loading, setLoading] = useState(true); let tableRef = useRef(null); async function loadRules() { @@ -515,7 +520,9 @@ export default function ManageRules({ history, modalProps, payeeId }) { let loadedRules = null; if (payeeId) { - loadedRules = await send('payees-get-rules', { id: payeeId }); + loadedRules = await send('payees-get-rules', { + id: payeeId + }); } else { loadedRules = await send('rules-get'); } @@ -594,7 +601,14 @@ export default function ManageRules({ history, modalProps, payeeId }) { rule: { stage: null, conditions: [{ op: 'is', field: 'payee', value: null, type: 'id' }], - actions: [{ op: 'set', field: 'category', value: null, type: 'id' }] + actions: [ + { + op: 'set', + field: 'category', + value: null, + type: 'id' + } + ] }, onSave: async newRule => { let newRules = await loadRules(); @@ -620,89 +634,78 @@ export default function ManageRules({ history, modalProps, payeeId }) { return null; } - return ( - <Modal - title="Rules" - padding={0} - loading={loading} - {...modalProps} - style={[modalProps.style, { flex: 1, maxWidth: '90%' }]} + let actions = ( + <View + style={{ + flexDirection: 'row', + alignItems: 'center', + padding: isModal ? '13px 15px' : '0 0 15px', + borderTop: '1px solid ' + colors.border + }} > - {() => ( - <SchedulesQuery.Provider> - <SelectedProvider instance={selectedInst}> - <View style={{ height: '70vh' }}> - <View style={{ flex: 1 }}> - <RulesHeader /> - <SimpleTable - ref={tableRef} - data={rules} - navigator={navigator} - loadMore={loadMore} - // Hide the last border of the item in the table - style={{ marginBottom: -1 }} - > - <RulesList - rules={rules} - selectedItems={selectedInst.items} - navigator={navigator} - hoveredRule={hoveredRule} - onHover={onHover} - onEditRule={onEditRule} - /> - </SimpleTable> - </View> - - <View - style={{ - flexDirection: 'row', - alignItems: 'center', - padding: '13px 15px', - borderTop: '1px solid ' + colors.border - }} - > - <View - style={{ - color: colors.n4, - flexDirection: 'row', - alignItems: 'center', - width: '50%' - }} - > - <Text> - Rules are always run in the order that you see them.{' '} - <ExternalLink - asAnchor={true} - href="https://actualbudget.github.io/docs/Budgeting/rules/" - style={{ color: colors.n4 }} - > - Learn more - </ExternalLink> - </Text> - </View> - - <View style={{ flex: 1 }} /> - - <Stack - direction="row" - align="center" - justify="flex-end" - spacing={2} - > - {selectedInst.items.size > 0 && ( - <Button onClick={onDeleteSelected}> - Delete {selectedInst.items.size} rules - </Button> - )} - <Button primary onClick={onCreateRule}> - Create new rule - </Button> - </Stack> - </View> - </View> - </SelectedProvider> - </SchedulesQuery.Provider> - )} - </Modal> + <View + style={{ + color: colors.n4, + flexDirection: 'row', + alignItems: 'center', + width: '50%' + }} + > + <Text> + Rules are always run in the order that you see them.{' '} + <ExternalLink + asAnchor={true} + href="https://actualbudget.github.io/docs/Budgeting/rules/" + style={{ color: colors.n4 }} + > + Learn more + </ExternalLink> + </Text> + </View> + + <View style={{ flex: 1 }} /> + + <Stack direction="row" align="center" justify="flex-end" spacing={2}> + {selectedInst.items.size > 0 && ( + <Button onClick={onDeleteSelected}> + Delete {selectedInst.items.size} rules + </Button> + )} + <Button primary onClick={onCreateRule}> + Create new rule + </Button> + </Stack> + </View> + ); + + return ( + <SchedulesQuery.Provider> + <SelectedProvider instance={selectedInst}> + <View style={{ overflow: 'hidden' }}> + {!isModal && actions} + <View style={{ flex: 1 }}> + <RulesHeader /> + <SimpleTable + ref={tableRef} + data={rules} + navigator={navigator} + loadMore={loadMore} + // Hide the last border of the item in the table + style={{ marginBottom: -1 }} + > + <RulesList + rules={rules} + selectedItems={selectedInst.items} + navigator={navigator} + hoveredRule={hoveredRule} + onHover={onHover} + onEditRule={onEditRule} + /> + </SimpleTable> + </View> + {isModal && actions} + </View> + </SelectedProvider> + </SchedulesQuery.Provider> ); } diff --git a/packages/desktop-client/src/components/ManageRulesPage.js b/packages/desktop-client/src/components/ManageRulesPage.js new file mode 100644 index 0000000000000000000000000000000000000000..e1ebfbfc409ad8778cf02eb70f68f1287f816aca --- /dev/null +++ b/packages/desktop-client/src/components/ManageRulesPage.js @@ -0,0 +1,12 @@ +import React from 'react'; + +import ManageRules from './ManageRules'; +import { Page } from './Page'; + +export function ManageRulesPage() { + return ( + <Page title="Rules"> + <ManageRules /> + </Page> + ); +} diff --git a/packages/desktop-client/src/components/Modals.js b/packages/desktop-client/src/components/Modals.js index 2dc967f42c53b4b0d71f551a84b17e93588122c2..008778629eec9d0eac07d76126ff6971c1c1fdd5 100644 --- a/packages/desktop-client/src/components/Modals.js +++ b/packages/desktop-client/src/components/Modals.js @@ -22,10 +22,9 @@ import CreateAccount from './modals/CreateAccount'; import CreateEncryptionKey from './modals/CreateEncryptionKey'; import EditRule from './modals/EditRule'; import FixEncryptionKey from './modals/FixEncryptionKey'; -import ManageRules from './modals/ManageRules'; +import ManageRulesModal from './modals/ManageRulesModal'; import MergeUnusedPayees from './modals/MergeUnusedPayees'; import WelcomeScreen from './modals/WelcomeScreen'; -import ManagePayeesWithData from './payees/ManagePayeesWithData'; function Modals({ history, @@ -150,26 +149,11 @@ function Modals({ }} /> - <Route - path="/manage-payees" - render={() => { - return ( - <ManagePayeesWithData - history={history} - modalProps={modalProps} - initialSelectedIds={ - options.selectedPayee ? [options.selectedPayee] : undefined - } - /> - ); - }} - /> - <Route path="/manage-rules" render={() => { return ( - <ManageRules + <ManageRulesModal history={history} modalProps={modalProps} payeeId={options.payeeId} diff --git a/packages/desktop-client/src/components/accounts/Account.js b/packages/desktop-client/src/components/accounts/Account.js index 1a240eb165ab7837b8c5eca7daf15e289114ff9e..11e0f511b65b68b97d05389a9ee3ae2507c54f73 100644 --- a/packages/desktop-client/src/components/accounts/Account.js +++ b/packages/desktop-client/src/components/accounts/Account.js @@ -1400,10 +1400,6 @@ class AccountInternal extends React.PureComponent { return this.props.matchedTransactions.includes(id); }; - onManagePayees = id => { - this.props.pushModal('manage-payees', { selectedPayee: id }); - }; - onCreatePayee = name => { let trimmed = name.trim(); if (trimmed !== '') { @@ -1790,7 +1786,6 @@ class AccountInternal extends React.PureComponent { onCloseAddTransaction={() => this.setState({ isAdding: false }) } - onManagePayees={this.onManagePayees} onCreatePayee={this.onCreatePayee} /> </View> diff --git a/packages/desktop-client/src/components/accounts/Filters.js b/packages/desktop-client/src/components/accounts/Filters.js index d366dd6cfdaadad0f77aac522ae9f99192942466..0899e512322a1520d0f6b757e55d2f03c1dfdbe8 100644 --- a/packages/desktop-client/src/components/accounts/Filters.js +++ b/packages/desktop-client/src/components/accounts/Filters.js @@ -34,7 +34,7 @@ import { colors } from 'loot-design/src/style'; import DeleteIcon from 'loot-design/src/svg/Delete'; import SettingsSliderAlternate from 'loot-design/src/svg/v2/SettingsSliderAlternate'; -import { Value } from '../modals/ManageRules'; +import { Value } from '../ManageRules'; import GenericInput from '../util/GenericInput'; let filterFields = [ diff --git a/packages/desktop-client/src/components/accounts/TransactionList.js b/packages/desktop-client/src/components/accounts/TransactionList.js index 8e2f16ba00612cd1f64d215873363e9f8bb34cba..eb030b3179fc9ab0297cc61442f0a7bfb9a65437 100644 --- a/packages/desktop-client/src/components/accounts/TransactionList.js +++ b/packages/desktop-client/src/components/accounts/TransactionList.js @@ -1,5 +1,6 @@ import React, { useRef, useEffect, useCallback, useLayoutEffect } from 'react'; import { useDispatch } from 'react-redux'; +import { useHistory } from 'react-router'; import { send } from 'loot-core/src/platform/client/fetch'; import { @@ -78,13 +79,13 @@ export default function TransactionList({ onRefetch, onRefetchUpToRow, onCloseAddTransaction, - onManagePayees, onCreatePayee }) { let dispatch = useDispatch(); let table = useRef(); let transactionsLatest = useRef(); let scrollTo = useRef(); + let history = useHistory(); // useEffect(() => { // if (scrollTo.current) { @@ -160,6 +161,13 @@ export default function TransactionList({ return newTransaction; }, []); + let onManagePayees = useCallback( + id => { + history.push('/payees', { selectedPayee: id }); + }, + [history] + ); + return ( <TransactionTable ref={tableRef} diff --git a/packages/desktop-client/src/components/modals/ManageRulesModal.js b/packages/desktop-client/src/components/modals/ManageRulesModal.js new file mode 100644 index 0000000000000000000000000000000000000000..9362a1ee5d59a3706cdb1346d0858e698b9c9692 --- /dev/null +++ b/packages/desktop-client/src/components/modals/ManageRulesModal.js @@ -0,0 +1,31 @@ +import React, { useState } from 'react'; + +import { Modal } from 'loot-design/src/components/common'; +import { + isDevelopmentEnvironment, + isPreviewEnvironment +} from 'loot-design/src/util/environment'; + +import ManageRules from '../ManageRules'; + +export default function ManageRulesModal({ modalProps, payeeId }) { + let [loading, setLoading] = useState(true); + if (isDevelopmentEnvironment() || isPreviewEnvironment()) { + if (location.pathname !== '/payees') { + throw new Error( + `Possibly invalid use of ManageRulesModal, add the current url '${location.pathname}' to the allowlist if you're confident the modal can never appear on top of the '/rules' page.` + ); + } + } + return ( + <Modal + title="Rules" + padding={0} + loading={loading} + {...modalProps} + style={[modalProps.style, { flex: 1, maxWidth: '90%', maxHeight: '90%' }]} + > + {() => <ManageRules isModal payeeId={payeeId} setLoading={setLoading} />} + </Modal> + ); +} diff --git a/packages/desktop-client/src/components/payees/ManagePayeesPage.js b/packages/desktop-client/src/components/payees/ManagePayeesPage.js new file mode 100644 index 0000000000000000000000000000000000000000..9c28b860105c039d58140f341368aec1dfde2fee --- /dev/null +++ b/packages/desktop-client/src/components/payees/ManagePayeesPage.js @@ -0,0 +1,20 @@ +import React from 'react'; +import { useLocation } from 'react-router'; + +import { Page } from '../Page'; +import ManagePayeesWithData from './ManagePayeesWithData'; + +export function ManagePayeesPage() { + let location = useLocation(); + return ( + <Page title="Payees"> + <ManagePayeesWithData + initialSelectedIds={ + location.state && location.state.selectedPayee + ? [location.state.selectedPayee] + : null + } + /> + </Page> + ); +} diff --git a/packages/desktop-client/src/components/payees/ManagePayeesWithData.js b/packages/desktop-client/src/components/payees/ManagePayeesWithData.js index 2b7c56619d7541b94c4bc8677a91966532e437de..ade382cb11557fe5bc51d2f01b7b8a05d2d25f02 100644 --- a/packages/desktop-client/src/components/payees/ManagePayeesWithData.js +++ b/packages/desktop-client/src/components/payees/ManagePayeesWithData.js @@ -8,7 +8,6 @@ import { applyChanges } from 'loot-core/src/shared/util'; import { ManagePayees } from 'loot-design/src/components/payees'; function ManagePayeesWithData({ - history, modalProps, initialSelectedIds, lastUndoState, @@ -53,10 +52,7 @@ function ManagePayeesWithData({ } }); - undo.setUndoState('openModal', 'manage-payees'); - return () => { - undo.setUndoState('openModal', null); unlisten(); }; }, []); diff --git a/packages/desktop-client/src/components/schedules/DiscoverSchedules.js b/packages/desktop-client/src/components/schedules/DiscoverSchedules.js index f9fff59daf1fb46eb33bd1e36ecd8695fc802a53..cd161bbda7c36d7884e339caa027959b0f3f15e8 100644 --- a/packages/desktop-client/src/components/schedules/DiscoverSchedules.js +++ b/packages/desktop-client/src/components/schedules/DiscoverSchedules.js @@ -26,13 +26,14 @@ import useSelected, { } from 'loot-design/src/components/useSelected'; import { colors } from 'loot-design/src/style'; -import { Page } from '../Page'; +import { Page, usePageType } from '../Page'; import DisplayId from '../util/DisplayId'; import { ScheduleAmountCell } from './SchedulesTable'; let ROW_HEIGHT = 43; function DiscoverSchedulesTable({ schedules, loading }) { + let pageType = usePageType(); let selectedItems = useSelectedItems(); let dispatchSelected = useSelectedDispatch(); @@ -100,9 +101,12 @@ function DiscoverSchedulesTable({ schedules, loading }) { </TableHeader> <Table rowHeight={ROW_HEIGHT} - backgroundColor="transparent" version="v2" - style={{ flex: 1, backgroundColor: 'transparent' }} + backgroundColor={pageType.type === 'modal' ? 'transparent' : undefined} + style={{ + flex: 1, + backgroundColor: pageType.type === 'modal' ? 'transparent' : undefined + }} items={schedules} loading={loading} isSelected={id => selectedItems.has(id)} @@ -114,7 +118,7 @@ function DiscoverSchedulesTable({ schedules, loading }) { } export default function DiscoverSchedules() { - let location = useLocation(); + let pageType = usePageType(); let history = useHistory(); let [schedules, setSchedules] = useState(); let [creating, setCreating] = useState(false); @@ -174,11 +178,7 @@ export default function DiscoverSchedules() { on all transactions for a schedule to be the same payee. </P> <P> - You can always do this later - {Platform.isBrowser - ? ' from the "Find schedules" item in the sidebar menu' - : ' from the "Tools > Find schedules" menu item'} - . + You can always do this later from “More Tools†→ “Find Schedules.†</P> <SelectedProvider instance={selectedInst}> @@ -192,9 +192,11 @@ export default function DiscoverSchedules() { direction="row" align="center" justify="flex-end" - style={{ paddingTop: 20 }} + style={{ + paddingTop: 20, + paddingBottom: pageType.type === 'modal' ? 0 : 20 + }} > - <Button onClick={() => history.goBack()}>Do nothing</Button> <ButtonWithLoading primary loading={creating} diff --git a/packages/desktop-electron/index.js b/packages/desktop-electron/index.js index 3ef5c6d4a73fde7bd1f20abc0f70be6c836803d9..631810808845011f9b6d127c22b440c24934d86b 100644 --- a/packages/desktop-electron/index.js +++ b/packages/desktop-electron/index.js @@ -217,11 +217,7 @@ function updateMenu(isBudgetOpen) { const fileItems = file.submenu.items; fileItems .filter( - item => - item.label === 'Start Tutorial' || - item.label === 'Manage Payees...' || - item.label === 'Manage Rules...' || - item.label === 'Load Backup...' + item => item.label === 'Start Tutorial' || item.label === 'Load Backup...' ) .map(item => (item.enabled = isBudgetOpen)); diff --git a/packages/desktop-electron/menu.js b/packages/desktop-electron/menu.js index bd797f2cebec4f58571e15ff695dc9d73770ab14..05fe9caa2e1d437ddcc345975401aeaea7551784 100644 --- a/packages/desktop-electron/menu.js +++ b/packages/desktop-electron/menu.js @@ -19,34 +19,6 @@ function getMenu(isDev, createWindow) { // } // } // }, - { - label: 'Manage Payees...', - enabled: false, - click(item, focusedWindow) { - if ( - focusedWindow && - focusedWindow.webContents.getTitle() === 'Actual' - ) { - focusedWindow.webContents.executeJavaScript( - '__actionsForMenu.pushModal("manage-payees")' - ); - } - } - }, - { - label: 'Manage Rules...', - enabled: false, - click(item, focusedWindow) { - if ( - focusedWindow && - focusedWindow.webContents.getTitle() === 'Actual' - ) { - focusedWindow.webContents.executeJavaScript( - '__actionsForMenu.pushModal("manage-rules")' - ); - } - } - }, { label: 'Load Backup...', enabled: false, diff --git a/packages/loot-design/src/components/common.js b/packages/loot-design/src/components/common.js index 4ce357e24d053b75a5412783bad2af5f9903c29f..7403dcf98d1f3e5998b947a02fd48da9502c6a0b 100644 --- a/packages/loot-design/src/components/common.js +++ b/packages/loot-design/src/components/common.js @@ -823,7 +823,8 @@ export function Modal({ <View style={{ padding: 20, - position: 'relative' + position: 'relative', + flexShrink: 0 }} > {showTitle && ( diff --git a/packages/loot-design/src/components/payees.js b/packages/loot-design/src/components/payees.js index 009f8f91f1b367eb68cc990a07214d91b67a5429..35d5a75eaf0fbd20fa5d230300828373551d44bf 100644 --- a/packages/loot-design/src/components/payees.js +++ b/packages/loot-design/src/components/payees.js @@ -30,6 +30,7 @@ import { } from './common'; import { Table, + TableHeader, Row, Cell, InputCell, @@ -230,7 +231,7 @@ function PayeeTableHeader() { return ( <View> - <Row + <TableHeader borderColor={borderColor} style={{ backgroundColor: 'white', @@ -239,6 +240,7 @@ function PayeeTableHeader() { userSelect: 'none' }} collapsed={true} + version="v2" > <SelectCell exposed={true} @@ -247,7 +249,7 @@ function PayeeTableHeader() { onSelect={() => dispatchSelected({ type: 'select-all' })} /> <Cell value="Name" width="flex" /> - </Row> + </TableHeader> </View> ); } @@ -470,108 +472,90 @@ export const ManagePayees = React.forwardRef( let payeesById = getPayeesById(payees); return ( - <Modal - title="Payees" - padding={0} - {...modalProps} - style={[modalProps.style, { flex: 'inherit', maxWidth: '90%' }]} - > + <View style={{ height: '100%' }}> <View style={{ - maxWidth: '100%', - width: 900, - height: 550 + flexDirection: 'row', + alignItems: 'center', + padding: '0 10px 5px' }} > + <Component initialState={{ menuOpen: false }}> + {({ state, setState }) => ( + <View> + <Button + bare + style={{ marginRight: 10 }} + disabled={buttonsDisabled} + onClick={() => setState({ menuOpen: true })} + > + {buttonsDisabled + ? 'No payees selected' + : selected.items.size + + ' ' + + plural(selected.items.size, 'payee', 'payees')} + <ExpandArrow width={8} height={8} style={{ marginLeft: 5 }} /> + </Button> + {state.menuOpen && ( + <PayeeMenu + payeesById={payeesById} + selectedPayees={selected.items} + onClose={() => setState({ menuOpen: false })} + onDelete={onDelete} + onMerge={onMerge} + /> + )} + </View> + )} + </Component> + <View style={{ flex: 1 }} /> + <Input + placeholder="Filter payees..." + value={filter} + onChange={e => { + applyFilter(e.target.value); + tableNavigator.onEdit(null); + }} + style={{ + width: 350, + borderColor: 'transparent', + backgroundColor: colors.n11, + ':focus': { + backgroundColor: 'white', + '::placeholder': { color: colors.n8 } + } + }} + /> + </View> + + <SelectedProvider instance={selected} fetchAllIds={getSelectableIds}> <View style={{ - flexDirection: 'row', - alignItems: 'center', - padding: '0 10px' + flex: 1, + border: '1px solid ' + colors.border, + borderRadius: 4, + overflow: 'hidden' }} > - <Component initialState={{ menuOpen: false }}> - {({ state, setState }) => ( - <View> - <Button - bare - style={{ marginRight: 10 }} - disabled={buttonsDisabled} - onClick={() => setState({ menuOpen: true })} - > - {buttonsDisabled - ? 'No payees selected' - : selected.items.size + - ' ' + - plural(selected.items.size, 'payee', 'payees')} - <ExpandArrow - width={8} - height={8} - style={{ marginLeft: 5 }} - /> - </Button> - {state.menuOpen && ( - <PayeeMenu - payeesById={payeesById} - selectedPayees={selected.items} - onClose={() => setState({ menuOpen: false })} - onDelete={onDelete} - onMerge={onMerge} - /> - )} - </View> - )} - </Component> - <View style={{ flex: 1 }} /> - <Input - placeholder="Filter payees..." - value={filter} - onChange={e => { - applyFilter(e.target.value); - tableNavigator.onEdit(null); - }} - style={{ - width: 350, - borderColor: 'transparent', - backgroundColor: colors.n11, - ':focus': { - backgroundColor: 'white', - '::placeholder': { color: colors.n8 } - } - }} - /> + <PayeeTableHeader /> + {filteredPayees.length === 0 ? ( + <EmptyMessage text="No payees" style={{ marginTop: 15 }} /> + ) : ( + <PayeeTable + ref={table} + payees={filteredPayees} + ruleCounts={ruleCounts} + categoryGroups={categoryGroups} + highlightedRows={highlightedRows} + navigator={tableNavigator} + onUpdate={onUpdate} + onViewRules={onViewRules} + onCreateRule={onCreateRule} + /> + )} </View> - - <SelectedProvider instance={selected} fetchAllIds={getSelectableIds}> - <View - style={{ - flex: 1, - border: '1px solid ' + colors.border, - borderRadius: 4, - overflow: 'hidden', - margin: 5 - }} - > - <PayeeTableHeader /> - {filteredPayees.length === 0 ? ( - <EmptyMessage text="No payees" style={{ marginTop: 15 }} /> - ) : ( - <PayeeTable - ref={table} - payees={filteredPayees} - ruleCounts={ruleCounts} - categoryGroups={categoryGroups} - highlightedRows={highlightedRows} - navigator={tableNavigator} - onUpdate={onUpdate} - onViewRules={onViewRules} - onCreateRule={onCreateRule} - /> - )} - </View> - </SelectedProvider> - </View> - </Modal> + </SelectedProvider> + </View> ); } ); diff --git a/packages/loot-design/src/components/sidebar.js b/packages/loot-design/src/components/sidebar.js index 669c69fd05a681566bbee46c773167a324939b3e..a5f0218c0db8fc8add44dda44d966f175c5852aa 100644 --- a/packages/loot-design/src/components/sidebar.js +++ b/packages/loot-design/src/components/sidebar.js @@ -1,5 +1,7 @@ -import React, { useState, useMemo } from 'react'; +import React, { useState, useMemo, useCallback, useEffect } from 'react'; +import { RectButton } from 'react-native-gesture-handler'; import { useDispatch } from 'react-redux'; +import { useLocation, useHistory } from 'react-router'; import { withRouter } from 'react-router-dom'; import { css } from 'glamor'; @@ -11,10 +13,12 @@ import PiggyBank from 'loot-design/src/svg/v1/PiggyBank'; import { styles, colors } from '../style'; import Add from '../svg/v1/Add'; +import ChevronRight from '../svg/v1/CheveronRight'; import Cog from '../svg/v1/Cog'; import DotsHorizontalTriple from '../svg/v1/DotsHorizontalTriple'; import Reports from '../svg/v1/Reports'; import Wallet from '../svg/v1/Wallet'; +import Wrench from '../svg/v1/Wrench'; import ArrowButtonLeft1 from '../svg/v2/ArrowButtonLeft1'; import CalendarIcon from '../svg/v2/Calendar'; import { @@ -32,64 +36,73 @@ import CellValue from './spreadsheet/CellValue'; export const SIDEBAR_WIDTH = 240; -export function Item({ +function Item({ children, icon, title, style, + indent = 0, to, exact, - onButtonPress + onClick, + button, + forceHover = false, + forceActive = false }) { - const showButton = title === 'Accounts'; + const hoverStyle = { + backgroundColor: colors.n2 + }; + const activeStyle = { + borderLeft: '4px solid ' + colors.p8, + paddingLeft: 19 + indent - 4, + color: colors.p8 + }; + const linkStyle = [ + { + ...styles.mediumText, + paddingTop: 9, + paddingBottom: 9, + paddingLeft: 19 + indent, + paddingRight: 10, + textDecoration: 'none', + color: colors.n9, + ...(forceHover ? hoverStyle : {}), + ...(forceActive ? activeStyle : {}) + }, + { ':hover': hoverStyle } + ]; + + const content = ( + <View + style={{ + flexDirection: 'row', + alignItems: 'center', + height: 20 + }} + > + {icon} + <Block style={{ marginLeft: 8 }}>{title}</Block> + <View style={{ flex: 1 }} /> + {button} + </View> + ); return ( - <View style={style}> - <AnchorLink - style={[ - { - ...styles.mediumText, - paddingTop: 9, - paddingBottom: 9, - paddingLeft: 19, - paddingRight: 10, - textDecoration: 'none', - color: colors.n9 - }, - { ':hover': { backgroundColor: colors.n2 } } - ]} - to={to} - exact={exact} - activeStyle={{ - borderLeft: '4px solid ' + colors.p8, - paddingLeft: 19 - 4, - color: colors.p8 - }} - > - <View - style={{ - flexDirection: 'row', - alignItems: 'center', - height: 20 - }} + <View style={[{ flexShrink: 0 }, style]}> + {onClick ? ( + <RectButton onClick={onClick}> + <View style={linkStyle}>{content}</View> + </RectButton> + ) : ( + <AnchorLink + style={linkStyle} + to={to} + exact={exact} + activeStyle={activeStyle} > - {icon} - <Block style={{ marginLeft: 8 }}>{title}</Block> - <View style={{ flex: 1 }} /> - {showButton && ( - <Button - bare - onClick={e => { - e.stopPropagation(); - e.preventDefault(); - onButtonPress(); - }} - > - <Add width={12} height={12} style={{ color: colors.n6 }} /> - </Button> - )} - </View> - </AnchorLink> + {content} + </AnchorLink> + )} {children ? <View style={{ marginTop: 5 }}>{children}</View> : null} </View> @@ -212,13 +225,10 @@ function Account({ ); } -export function Accounts({ +function Accounts({ accounts, failedAccounts, updatedAccounts, - to, - icon, - history, getAccountPath, budgetedAccountPath, offBudgetAccountPath, @@ -269,98 +279,89 @@ export function Accounts({ }; return ( - <Item - title="Accounts" - to={to} - icon={icon} - exact={true} - style={{ marginBottom: 5, flex: 1 }} - onButtonPress={onAddAccount} - > - <View style={{ overflow: 'auto', marginTop: -5 }}> - {budgetedAccounts.length > 0 && ( - <Account - name="For budget" - to={budgetedAccountPath} - query={getOnBudgetBalance()} - style={{ marginTop: 15, color: colors.n6 }} - /> - )} + <View> + {budgetedAccounts.length > 0 && ( + <Account + name="For budget" + to={budgetedAccountPath} + query={getOnBudgetBalance()} + style={{ marginTop: 15, color: colors.n6 }} + /> + )} - {budgetedAccounts.map((account, i) => ( - <Account - key={account.id} - name={account.name} - account={account} - connected={!!account.bankId} - failed={failedAccounts && failedAccounts.has(account.id)} - updated={updatedAccounts && updatedAccounts.includes(account.id)} - to={getAccountPath(account)} - query={getBalanceQuery(account)} - onDragChange={onDragChange} - onDrop={onReorder} - outerStyle={makeDropPadding(i, budgetedAccounts.length)} - /> - ))} + {budgetedAccounts.map((account, i) => ( + <Account + key={account.id} + name={account.name} + account={account} + connected={!!account.bankId} + failed={failedAccounts && failedAccounts.has(account.id)} + updated={updatedAccounts && updatedAccounts.includes(account.id)} + to={getAccountPath(account)} + query={getBalanceQuery(account)} + onDragChange={onDragChange} + onDrop={onReorder} + outerStyle={makeDropPadding(i, budgetedAccounts.length)} + /> + ))} + + {offbudgetAccounts.length > 0 && ( + <Account + name="Off budget" + to={offBudgetAccountPath} + query={getOffBudgetBalance()} + style={{ color: colors.n6 }} + /> + )} - {offbudgetAccounts.length > 0 && ( - <Account - name="Off budget" - to={offBudgetAccountPath} - query={getOffBudgetBalance()} - style={{ color: colors.n6 }} - /> - )} + {offbudgetAccounts.map((account, i) => ( + <Account + key={account.id} + name={account.name} + account={account} + connected={!!account.bankId} + failed={failedAccounts && failedAccounts.has(account.id)} + updated={updatedAccounts && updatedAccounts.includes(account.id)} + to={getAccountPath(account)} + query={getBalanceQuery(account)} + onDragChange={onDragChange} + onDrop={onReorder} + outerStyle={makeDropPadding(i, offbudgetAccounts.length)} + /> + ))} + + {closedAccounts.length > 0 && ( + <View + style={[ + accountNameStyle, + { + marginTop: 15, + color: colors.n6, + flexDirection: 'row', + userSelect: 'none', + alignItems: 'center', + flexShrink: 0 + } + ]} + onClick={onToggleClosedAccounts} + > + {'Closed Accounts' + (showClosedAccounts ? '' : '...')} + </View> + )} - {offbudgetAccounts.map((account, i) => ( + {showClosedAccounts && + closedAccounts.map((account, i) => ( <Account key={account.id} name={account.name} account={account} - connected={!!account.bankId} - failed={failedAccounts && failedAccounts.has(account.id)} - updated={updatedAccounts && updatedAccounts.includes(account.id)} to={getAccountPath(account)} query={getBalanceQuery(account)} onDragChange={onDragChange} onDrop={onReorder} - outerStyle={makeDropPadding(i, offbudgetAccounts.length)} /> ))} - - {closedAccounts.length > 0 && ( - <View - style={[ - accountNameStyle, - { - marginTop: 15, - color: colors.n6, - flexDirection: 'row', - userSelect: 'none', - alignItems: 'center', - flexShrink: 0 - } - ]} - onClick={onToggleClosedAccounts} - > - {'Closed Accounts' + (showClosedAccounts ? '' : '...')} - </View> - )} - - {showClosedAccounts && - closedAccounts.map((account, i) => ( - <Account - key={account.id} - name={account.name} - account={account} - to={getAccountPath(account)} - query={getBalanceQuery(account)} - onDragChange={onDragChange} - onDrop={onReorder} - /> - ))} - </View> - </Item> + </View> ); } @@ -382,18 +383,6 @@ const MenuButton = withRouter(function MenuButton({ history }) { setMenuOpen(false); switch (type) { - case 'open-payees': - dispatch(pushModal('manage-payees')); - break; - case 'open-rules': - dispatch(pushModal('manage-rules')); - break; - case 'find-schedules': - history.push('/schedule/discover', { locationPtr: history.location }); - break; - case 'repair-splits': - history.push('/tools/fix-splits', { locationPtr: history.location }); - break; case 'settings': history.push('/settings'); break; @@ -408,11 +397,6 @@ const MenuButton = withRouter(function MenuButton({ history }) { } let items = [ - { name: 'open-payees', text: 'Manage Payees' }, - { name: 'open-rules', text: 'Manage Rules' }, - { name: 'find-schedules', text: 'Find schedules' }, - { name: 'repair-splits', text: 'Repair split transactions' }, - Menu.line, { name: 'settings', text: 'Settings' }, { name: 'help', text: 'Help' }, { name: 'close', text: 'Close File' } @@ -446,6 +430,78 @@ const MenuButton = withRouter(function MenuButton({ history }) { ); }); +function Tools() { + let [isOpen, setOpen] = useState(false); + let location = useLocation(); + let history = useHistory(); + let onToggle = useCallback(() => setOpen(open => !open), []); + + let items = [ + { name: 'payees', text: 'Payees' }, + { name: 'rules', text: 'Rules' }, + { name: 'find-schedules', text: 'Find schedules' }, + { name: 'repair-splits', text: 'Repair split transactions' } + ]; + + let onMenuSelect = useCallback( + type => { + switch (type) { + case 'payees': + history.push('/payees'); + break; + case 'rules': + history.push('/rules'); + break; + case 'find-schedules': + history.push('/schedule/discover'); + break; + case 'repair-splits': + history.push('/tools/fix-splits', { locationPtr: history.location }); + break; + default: + } + setOpen(false); + }, + [history] + ); + + return ( + <View style={{ flexShrink: 0 }}> + <Item + title="More Tools" + icon={<Wrench width={15} height={15} style={{ color: 'inherit' }} />} + exact={true} + onClick={onToggle} + style={{ pointerEvents: isOpen ? 'none' : 'auto' }} + forceHover={isOpen} + forceActive={[ + '/payees', + '/rules', + '/tools', + '/schedule/discover' + ].some(route => location.pathname.startsWith(route))} + button={ + <ChevronRight + width={12} + height={12} + style={{ color: colors.n6, marginRight: 6 }} + /> + } + /> + {isOpen && ( + <Tooltip + position="right" + offset={-8} + style={{ padding: 0 }} + onClose={onToggle} + > + <Menu onMenuSelect={onMenuSelect} items={items} /> + </Tooltip> + )} + </View> + ); +} + export function Sidebar({ style, budgetName, @@ -470,7 +526,6 @@ export function Sidebar({ { width: SIDEBAR_WIDTH, color: colors.n9, - overflow: 'auto', backgroundColor: colors.n1, '& .float': { opacity: 0, @@ -540,40 +595,66 @@ export function Sidebar({ {!hasWindowButtons && <ToggleButton onFloat={onFloat} />} {Platform.isBrowser && <MenuButton />} </View> - <Item - title="Budget" - icon={<Wallet width={15} height={15} style={{ color: 'inherit' }} />} - to="/budget" - /> - <Item - title="Reports" - icon={<Reports width={15} height={15} style={{ color: 'inherit' }} />} - to="/reports" - /> - <Item - title="Schedules" - icon={ - <CalendarIcon width={15} height={15} style={{ color: 'inherit' }} /> - } - to="/schedules" - /> - <Accounts - to="/accounts" - icon={<PiggyBank width={15} height={15} style={{ color: 'inherit' }} />} - accounts={accounts} - failedAccounts={failedAccounts} - updatedAccounts={updatedAccounts} - getAccountPath={account => `/accounts/${account.id}`} - budgetedAccountPath="/accounts/budgeted" - offBudgetAccountPath="/accounts/offbudget" - getBalanceQuery={getBalanceQuery} - getOnBudgetBalance={getOnBudgetBalance} - getOffBudgetBalance={getOffBudgetBalance} - showClosedAccounts={showClosedAccounts} - onAddAccount={onAddAccount} - onToggleClosedAccounts={onToggleClosedAccounts} - onReorder={onReorder} - /> + + <View style={{ overflow: 'auto' }}> + <Item + title="Budget" + icon={<Wallet width={15} height={15} style={{ color: 'inherit' }} />} + to="/budget" + /> + <Item + title="Reports" + icon={<Reports width={15} height={15} style={{ color: 'inherit' }} />} + to="/reports" + /> + + <Item + title="Schedules" + icon={ + <CalendarIcon width={15} height={15} style={{ color: 'inherit' }} /> + } + to="/schedules" + /> + + <Tools /> + + <Item + title="Accounts" + to="/accounts" + icon={ + <PiggyBank width={15} height={15} style={{ color: 'inherit' }} /> + } + exact={true} + button={ + <Button + bare + onClick={e => { + e.stopPropagation(); + e.preventDefault(); + onAddAccount(); + }} + > + <Add width={12} height={12} style={{ color: colors.n6 }} /> + </Button> + } + /> + + <Accounts + accounts={accounts} + failedAccounts={failedAccounts} + updatedAccounts={updatedAccounts} + getAccountPath={account => `/accounts/${account.id}`} + budgetedAccountPath="/accounts/budgeted" + offBudgetAccountPath="/accounts/offbudget" + getBalanceQuery={getBalanceQuery} + getOnBudgetBalance={getOnBudgetBalance} + getOffBudgetBalance={getOffBudgetBalance} + showClosedAccounts={showClosedAccounts} + onAddAccount={onAddAccount} + onToggleClosedAccounts={onToggleClosedAccounts} + onReorder={onReorder} + /> + </View> </View> ); } diff --git a/packages/loot-design/src/components/table.js b/packages/loot-design/src/components/table.js index 37cf78bb243b696b0685432f2186350f50162aff..8f2dbecc40dc1e1e41800ab824359e29f2d3b0fd 100644 --- a/packages/loot-design/src/components/table.js +++ b/packages/loot-design/src/components/table.js @@ -720,7 +720,11 @@ export function TableHeader({ headers, children, version, ...rowProps }) { return ( <View style={ - version === 'v2' && { borderRadius: '6px 6px 0 0', overflow: 'hidden' } + version === 'v2' && { + borderRadius: '6px 6px 0 0', + overflow: 'hidden', + flexShrink: 0 + } } > <Row @@ -984,7 +988,14 @@ export const Table = React.forwardRef( if (loading) { return ( <View - style={[{ flex: 1, justifyContent: 'center', alignItems: 'center' }]} + style={[ + { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor + } + ]} > <AnimatedLoading width={25} color={colors.n1} /> </View> diff --git a/packages/loot-design/src/components/tooltips.js b/packages/loot-design/src/components/tooltips.js index 1543446722a32bbdd034bc84e7b67a40369a2957..b82f12c73dc81eb50a313e7cabc17bd64da094b0 100644 --- a/packages/loot-design/src/components/tooltips.js +++ b/packages/loot-design/src/components/tooltips.js @@ -242,6 +242,8 @@ export class Tooltip extends React.Component { return 'top-center'; case 'top-center': return 'bottom-center'; + case 'right': + return 'right'; default: } } @@ -300,6 +302,9 @@ export class Tooltip extends React.Component { style.top = anchorRect.top + anchorRect.height + offset + 'px'; style.left = anchorRect.left + 'px'; style.width = anchorRect.width + 'px'; + } else if (position === 'right') { + style.top = anchorRect.top + 'px'; + style.left = anchorRect.left + anchorRect.width + offset + 'px'; } else { throw new Error('Invalid position for Tooltip: ' + position); }