diff --git a/packages/desktop-client/src/components/GlobalKeys.js b/packages/desktop-client/src/components/GlobalKeys.js index dc37943ec10751b5dfb85ae2d3f249e79d771392..61b413b1efdbaf459222801286c0f3aee387c3ec 100644 --- a/packages/desktop-client/src/components/GlobalKeys.js +++ b/packages/desktop-client/src/components/GlobalKeys.js @@ -1,17 +1,17 @@ -import React from 'react'; -import { withRouter } from 'react-router-dom'; +import { useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; import * as Platform from 'loot-core/src/client/platform'; -class GlobalKeys extends React.Component { - componentDidMount() { +export default function GlobalKeys() { + let history = useHistory(); + useEffect(() => { const handleKeys = e => { if (Platform.isBrowser) { return; } if (e.metaKey) { - const { history } = this.props; switch (e.code) { case 'Digit1': history.push('/budget'); @@ -34,18 +34,8 @@ class GlobalKeys extends React.Component { document.addEventListener('keydown', handleKeys); - this.cleanupListeners = () => { - document.removeEventListener('keydown', handleKeys); - }; - } - - componentWillUnmount() { - this.cleanupListeners(); - } + return () => document.removeEventListener('keydown', handleKeys); + }, []); - render() { - return null; - } + return null; } - -export default withRouter(GlobalKeys); diff --git a/packages/desktop-client/src/components/Tutorial.js b/packages/desktop-client/src/components/Tutorial.js deleted file mode 100644 index 7c7c46fade04533d6e98dc40ee734c28372d0156..0000000000000000000000000000000000000000 --- a/packages/desktop-client/src/components/Tutorial.js +++ /dev/null @@ -1,274 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import { connect } from 'react-redux'; - -import PropTypes from 'prop-types'; -import { bindActionCreators } from 'redux'; - -import * as actions from 'loot-core/src/client/actions'; - -import BudgetCategories from './tutorial/BudgetCategories'; -import BudgetInitial from './tutorial/BudgetInitial'; -import BudgetNewIncome from './tutorial/BudgetNewIncome'; -import BudgetNextMonth from './tutorial/BudgetNextMonth'; -import BudgetSummary from './tutorial/BudgetSummary'; -import CategoryBalance from './tutorial/CategoryBalance'; -import Final from './tutorial/Final'; -import Intro from './tutorial/Intro'; -import Overspending from './tutorial/Overspending'; -import TransactionAdd from './tutorial/TransactionAdd'; -import TransactionEnter from './tutorial/TransactionEnter'; - -function generatePath(innerRect, outerRect) { - const i = innerRect; - const o = outerRect; - // prettier-ignore - return ` - M0,0 ${o.width},0 ${o.width},${o.height} L0,${o.height} L0,0 Z - M${i.left},${i.top} L${i.left+i.width},${i.top} L${i.left+i.width},${i.top+i.height} L${i.left},${i.top+i.height} L${i.left},${i.top} Z - `; -} - -function expandRect({ top, left, width, height }, padding) { - if (typeof padding === 'number') { - return { - top: top - padding, - left: left - padding, - width: width + padding * 2, - height: height + padding * 2, - }; - } else if (padding) { - return { - top: top - (padding.top || 0), - left: left - (padding.left || 0), - width: width + (padding.right || 0) + (padding.left || 0), - height: height + (padding.bottom || 0) + (padding.top || 0), - }; - } - - return { top, left, width, height }; -} - -function withinWindow(rect) { - return { - top: rect.top, - left: rect.left, - width: Math.min(rect.left + rect.width, window.innerWidth) - rect.left, - height: Math.min(rect.top + rect.height, window.innerHeight) - rect.top, - }; -} - -class MeasureNodes extends React.Component { - state = { measurements: null }; - - componentDidMount() { - window.addEventListener('resize', () => { - setTimeout(() => this.updateMeasurements(true), 0); - }); - this.updateMeasurements(); - } - - componentDidUpdate(prevProps) { - if (prevProps.nodes !== this.props.nodes) { - this.updateMeasurements(); - } - } - - updateMeasurements() { - this.setState({ - measurements: this.props.nodes.map(node => node.getBoundingClientRect()), - }); - } - - render() { - const { children } = this.props; - const { measurements } = this.state; - return measurements ? children(...measurements) : null; - } -} - -class Tutorial extends React.Component { - state = { highlightRect: null, windowRect: null }; - - static contextTypes = { - getTutorialNode: PropTypes.func, - endTutorial: PropTypes.func, - }; - - onClose = didQuitEarly => { - // The difference between these is `endTutorial` permanently - // disable the tutorial. If the user walked all the way through - // it, never show it to them again. Otherwise they will see if - // again if they create a new budget. - if (didQuitEarly) { - this.props.closeTutorial(); - } else { - this.props.endTutorial(); - } - }; - - getContent(stage, targetRect, navigationProps) { - switch (stage) { - case 'budget-summary': - return ( - <BudgetSummary - fromYNAB={this.props.fromYNAB} - targetRect={targetRect} - navigationProps={navigationProps} - /> - ); - case 'budget-categories': - return ( - <BudgetCategories - targetRect={targetRect} - navigationProps={navigationProps} - /> - ); - case 'transaction-add': - return ( - <TransactionAdd - targetRect={targetRect} - navigationProps={navigationProps} - /> - ); - case 'budget-new-income': - return ( - <BudgetNewIncome - targetRect={targetRect} - navigationProps={navigationProps} - /> - ); - case 'budget-next-month': - return <div>hi</div>; - default: - throw new Error( - `Encountered an unexpected error rendering the tutorial content for ${stage}`, - ); - } - } - - render() { - const { stage, fromYNAB, nextTutorialStage, closeTutorial } = this.props; - if (stage === null) { - return null; - } - - const navigationProps = { - nextTutorialStage: this.props.nextTutorialStage, - previousTutorialStage: this.props.previousTutorialStage, - closeTutorial: () => this.onClose(true), - endTutorial: () => this.onClose(false), - }; - - switch (stage) { - case 'intro': - return ( - <Intro - nextTutorialStage={nextTutorialStage} - closeTutorial={closeTutorial} - fromYNAB={fromYNAB} - /> - ); - case 'budget-initial': - return ( - <BudgetInitial - nextTutorialStage={nextTutorialStage} - closeTutorial={closeTutorial} - navigationProps={navigationProps} - /> - ); - case 'budget-next-month': - return ( - <BudgetNextMonth - nextTutorialStage={nextTutorialStage} - closeTutorial={closeTutorial} - navigationProps={navigationProps} - /> - ); - case 'budget-next-month2': - return ( - <BudgetNextMonth - nextTutorialStage={nextTutorialStage} - closeTutorial={closeTutorial} - navigationProps={navigationProps} - stepTwo={true} - /> - ); - case 'transaction-enter': - return ( - <TransactionEnter - fromYNAB={fromYNAB} - navigationProps={navigationProps} - /> - ); - case 'budget-category-balance': - return <CategoryBalance navigationProps={navigationProps} />; - case 'budget-overspending': - return <Overspending navigationProps={navigationProps} />; - case 'budget-overspending2': - return ( - <Overspending navigationProps={navigationProps} stepTwo={true} /> - ); - case 'final': - return ( - <Final - nextTutorialStage={nextTutorialStage} - closeTutorial={closeTutorial} - navigationProps={navigationProps} - /> - ); - default: - // Default case defined below (outside the switch statement) - } - - const { node: targetNode, expand } = this.context.getTutorialNode(stage); - - return ( - <MeasureNodes nodes={[targetNode.parentNode, document.body]}> - {(targetRect, windowRect) => { - targetRect = withinWindow( - expandRect(expandRect(targetRect, 5), expand), - ); - - return ( - <div> - {ReactDOM.createPortal( - <svg - width={windowRect.width} - height={windowRect.height} - viewBox={'0 0 ' + windowRect.width + ' ' + windowRect.height} - version="1.1" - xmlns="http://www.w3.org/2000/svg" - style={{ - position: 'absolute', - top: 0, - left: 0, - zIndex: 1000, - pointerEvents: 'none', - }} - > - <path - fill="rgba(0, 0, 0, .2)" - fill-rule="evenodd" - d={generatePath(targetRect, windowRect)} - style={{ pointerEvents: 'fill' }} - /> - </svg>, - document.body, - )} - {this.getContent(stage, targetRect, navigationProps)} - </div> - ); - }} - </MeasureNodes> - ); - } -} - -export default connect( - state => ({ - stage: state.tutorial.stage, - fromYNAB: state.tutorial.fromYNAB, - }), - dispatch => bindActionCreators(actions, dispatch), -)(Tutorial); diff --git a/packages/desktop-client/src/components/TutorialPoints.js b/packages/desktop-client/src/components/TutorialPoints.js deleted file mode 100644 index 083fa16508d8ee4e6f248bada4c9fa7aa3fb1e56..0000000000000000000000000000000000000000 --- a/packages/desktop-client/src/components/TutorialPoints.js +++ /dev/null @@ -1,41 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; - -import PropTypes from 'prop-types'; - -class Tutorial extends React.Component { - static childContextTypes = { - setTutorialNode: PropTypes.func, - getTutorialNode: PropTypes.func, - endTutorial: PropTypes.func, - }; - - constructor() { - super(); - this.nodes = {}; - } - - getChildContext() { - return { - setTutorialNode: this.setTutorialNode, - getTutorialNode: this.getTutorialNode, - }; - } - - setTutorialNode = (name, node, expand) => { - this.nodes[name] = { node, expand }; - }; - - getTutorialNode = (name, node) => { - return this.nodes[name]; - }; - - render() { - const { children } = this.props; - return React.Children.only(children); - } -} - -export default connect(state => ({ deactivated: state.tutorial.deactivated }))( - Tutorial, -); diff --git a/packages/desktop-client/src/components/accounts/MobileAccountDetails.js b/packages/desktop-client/src/components/accounts/MobileAccountDetails.js index 229d6298630c99397390a646292f9a11effb4496..be0e132b029dbed9d90c68339a245df744c15ab4 100644 --- a/packages/desktop-client/src/components/accounts/MobileAccountDetails.js +++ b/packages/desktop-client/src/components/accounts/MobileAccountDetails.js @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React, { useState, useMemo } from 'react'; import { Link } from 'react-router-dom'; import { @@ -16,62 +16,52 @@ import SearchAlternate from 'loot-design/src/svg/v2/SearchAlternate'; import { TransactionList } from './MobileTransaction'; -class TransactionSearchInput extends React.Component { - state = { text: '' }; +function TransactionSearchInput({ accountName, onSearch }) { + const [text, setText] = useState(''); - performSearch = () => { - this.props.onSearch(this.state.text); - }; - - onChange = text => { - this.setState({ text }, this.performSearch); - }; - - render() { - const { accountName } = this.props; - const { text } = this.state; - - return ( - <View + return ( + <View + style={{ + flexDirection: 'row', + alignItems: 'center', + backgroundColor: colors.n11, + margin: '11px auto 4px', + borderRadius: 4, + padding: 10, + width: '100%', + }} + > + <InputWithContent + leftContent={ + <SearchAlternate + style={{ + width: 13, + height: 13, + flexShrink: 0, + color: text ? colors.p7 : 'inherit', + margin: 5, + marginRight: 0, + }} + /> + } + value={text} + onUpdate={text => { + setText(text); + onSearch(text); + }} + placeholder={`Search ${accountName}`} style={{ - flexDirection: 'row', - alignItems: 'center', backgroundColor: colors.n11, - margin: '11px auto 4px', - borderRadius: 4, - padding: 10, - width: '100%', + border: `1px solid ${colors.n9}`, + fontSize: 15, + flex: 1, + height: 32, + marginLeft: 4, + padding: 8, }} - > - <InputWithContent - leftContent={ - <SearchAlternate - style={{ - width: 13, - height: 13, - flexShrink: 0, - color: text ? colors.p7 : 'inherit', - margin: 5, - marginRight: 0, - }} - /> - } - value={text} - onUpdate={this.onChange} - placeholder={`Search ${accountName}`} - style={{ - backgroundColor: colors.n11, - border: `1px solid ${colors.n9}`, - fontSize: 15, - flex: 1, - height: 32, - marginLeft: 4, - padding: 8, - }} - /> - </View> - ); - } + /> + </View> + ); } const LEFT_RIGHT_FLEX_WIDTH = 70; @@ -149,8 +139,8 @@ export default function AccountDetails({ > {account.name} </View> - {/* - TODO: connect to an add transaction modal + {/* + TODO: connect to an add transaction modal Only left here but hidden for flex centering of the account name. */} <Link to="transaction/new" style={{ visibility: 'hidden' }}> diff --git a/packages/desktop-client/src/components/manager/ManagementApp.js b/packages/desktop-client/src/components/manager/ManagementApp.js index 087a9a6fc5e1d49f0f1a8a562c52fcf130961b20..a5ff781e55f334f96f06d327077fb2263524634f 100644 --- a/packages/desktop-client/src/components/manager/ManagementApp.js +++ b/packages/desktop-client/src/components/manager/ManagementApp.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { connect } from 'react-redux'; import { Switch, Redirect, Router, Route } from 'react-router-dom'; @@ -49,170 +49,163 @@ function Version() { ); } -class ManagementApp extends React.Component { - constructor(props) { - super(props); - this.mounted = true; - this.history = createBrowserHistory(); - window.__history = this.history; - } - - async componentDidMount() { +function ManagementApp({ + files, + userData, + loadingText, + managerHasInitialized, + setAppState, + getUserData, + loadAllFiles, +}) { + const history = createBrowserHistory(); + window.__history = history; + + // runs on mount only + useEffect(() => { // An action may have been triggered from outside, and we don't // want to override its loading message so we only show the // initial loader if there isn't already a message - let alreadyLoading = this.props.loadingText != null; + let alreadyLoading = loadingText != null; // Remember: this component is remounted every time the user // closes a budget. That's why we keep `managerHasInitialized` in // redux so that it persists across renders. This will show the // loading spinner on first run, but never again since we'll have // a cached list of files and can show them - if (!this.props.managerHasInitialized) { + if (!managerHasInitialized) { if (!alreadyLoading) { - this.props.setAppState({ loadingText: '' }); + setAppState({ loadingText: '' }); } - } else { - // If it's not the first time rendering, immediately show the - // modal since we should have cached data - this.showModal(); } - let userData = await this.props.getUserData(); - if (userData) { - await this.props.loadAllFiles(); + async function fetchData() { + let userData = await getUserData(); + if (userData) { + await loadAllFiles(); + } + + // TODO: There is a race condition here. The user could perform an + // action that starts loading in between where `alreadyLoading` + // was captured and this would clear it. We really only want to + // ever clear the initial loading screen, so we need a "loading + // id" of some kind. + setAppState({ + managerHasInitialized: true, + ...(!alreadyLoading ? { loadingText: null } : null), + }); } - // TODO: There is a race condition here. The user could perform an - // action that starts loading in between where `alreadyLoading` - // was captured and this would clear it. We really only want to - // ever clear the initial loading screen, so we need a "loading - // id" of some kind. - this.props.setAppState({ - managerHasInitialized: true, - ...(!alreadyLoading ? { loadingText: null } : null), - }); - } + fetchData(); + }, []); - componentWillUnmount() { - this.mounted = false; + if (!managerHasInitialized) { + return null; } - render() { - let { files, userData, managerHasInitialized } = this.props; - - if (!managerHasInitialized) { - return null; - } - - return ( - <Router history={this.history}> - <View style={{ height: '100%' }}> - <View + return ( + <Router history={history}> + <View style={{ height: '100%' }}> + <View + style={{ + position: 'absolute', + top: 0, + left: 0, + right: 0, + height: 40, + WebkitAppRegion: 'drag', + }} + /> + <View + style={{ + position: 'absolute', + bottom: 40, + right: 15, + }} + > + <Notifications style={{ - position: 'absolute', - top: 0, - left: 0, - right: 0, - height: 40, - WebkitAppRegion: 'drag', + position: 'relative', + left: 'initial', + right: 'initial', }} /> + </View> + + {managerHasInitialized && ( <View style={{ + alignItems: 'center', + bottom: 0, + justifyContent: 'center', + left: 0, + padding: 20, position: 'absolute', - bottom: 40, - right: 15, + right: 0, + top: 0, }} > - <Notifications - style={{ - position: 'relative', - left: 'initial', - right: 'initial', - }} - /> - </View> + {userData && files ? ( + <> + <Switch> + <Route exact path="/config-server" component={ConfigServer} /> + <Route + exact + path="/change-password" + component={ChangePassword} + /> + {files && files.length > 0 ? ( + <Route exact path="/" component={BudgetList} /> + ) : ( + <Route exact path="/" component={WelcomeScreen} /> + )} + {/* Redirect all other pages to this route */} + <Route path="/" render={() => <Redirect to="/" />} /> + </Switch> - {managerHasInitialized && ( - <View - style={{ - alignItems: 'center', - bottom: 0, - justifyContent: 'center', - left: 0, - padding: 20, - position: 'absolute', - right: 0, - top: 0, - }} - > - {userData && files ? ( - <> + <View + style={{ + position: 'absolute', + top: 0, + right: 0, + padding: '6px 10px', + zIndex: 4000, + }} + > <Switch> + <Route exact path="/config-server" component={null} /> <Route exact - path="/config-server" - component={ConfigServer} - /> - <Route - exact - path="/change-password" - component={ChangePassword} + path="/" + render={() => ( + <LoggedInUser style={{ padding: '4px 7px' }} /> + )} /> - {files && files.length > 0 ? ( - <Route exact path="/" component={BudgetList} /> - ) : ( - <Route exact path="/" component={WelcomeScreen} /> - )} - {/* Redirect all other pages to this route */} - <Route path="/" render={() => <Redirect to="/" />} /> </Switch> - - <View - style={{ - position: 'absolute', - top: 0, - right: 0, - padding: '6px 10px', - zIndex: 4000, - }} - > - <Switch> - <Route exact path="/config-server" component={null} /> - <Route - exact - path="/" - render={() => ( - <LoggedInUser style={{ padding: '4px 7px' }} /> - )} - /> - </Switch> - </View> - </> - ) : ( - <Switch> - <Route exact path="/login" component={Login} /> - <Route exact path="/error" component={Error} /> - <Route exact path="/config-server" component={ConfigServer} /> - <Route exact path="/bootstrap" component={Bootstrap} /> - {/* Redirect all other pages to this route */} - <Route path="/" render={() => <Redirect to="/bootstrap" />} /> - </Switch> - )} - </View> - )} - - <Switch> - <Route exact path="/config-server" component={null} /> - <Route path="/" component={ServerURL} /> - </Switch> - <Version /> - </View> - <Modals history={this.history} /> - </Router> - ); - } + </View> + </> + ) : ( + <Switch> + <Route exact path="/login" component={Login} /> + <Route exact path="/error" component={Error} /> + <Route exact path="/config-server" component={ConfigServer} /> + <Route exact path="/bootstrap" component={Bootstrap} /> + {/* Redirect all other pages to this route */} + <Route path="/" render={() => <Redirect to="/bootstrap" />} /> + </Switch> + )} + </View> + )} + + <Switch> + <Route exact path="/config-server" component={null} /> + <Route path="/" component={ServerURL} /> + </Switch> + <Version /> + </View> + <Modals history={history} /> + </Router> + ); } export default connect(state => { diff --git a/packages/desktop-client/src/components/modals/ConfirmCategoryDelete.js b/packages/desktop-client/src/components/modals/ConfirmCategoryDelete.js index f03a5acd012b608f7f0e8b4e32a3d8adbd3271df..b187f54f3da8bb128f0321048c56da4d88eadd60 100644 --- a/packages/desktop-client/src/components/modals/ConfirmCategoryDelete.js +++ b/packages/desktop-client/src/components/modals/ConfirmCategoryDelete.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { NativeCategorySelect } from 'loot-design/src/components/CategorySelect'; import { @@ -10,29 +10,26 @@ import { } from 'loot-design/src/components/common'; import { colors } from 'loot-design/src/style'; -class ConfirmCategoryDelete extends React.Component { - state = { transferCategory: null, error: null }; +export default function ConfirmCategoryDelete({ + modalProps, + category, + group, + categoryGroups, + onDelete, +}) { + const [transferCategory, setTransferCategory] = useState(null); + const [error, setError] = useState(null); - componentDidMount() { + const inputRef = useRef(null); + + useEffect(() => { // Hack: 200ms is the timing of the modal animation setTimeout(() => { - this.input.focus(); + inputRef.current.focus(); }, 200); - } - - onDelete = () => { - let { transferCategory } = this.state; - let { onDelete } = this.props; - - if (!transferCategory) { - this.setState({ error: 'required-transfer' }); - } else { - onDelete(transferCategory); - this.props.modalProps.onClose(); - } - }; + }, []); - renderError = error => { + const renderError = error => { let msg; switch (error) { @@ -46,83 +43,84 @@ class ConfirmCategoryDelete extends React.Component { return <Text style={{ marginTop: 15, color: colors.r4 }}>{msg}</Text>; }; - render() { - const { modalProps, category, group, categoryGroups } = this.props; - const { transferCategory, error } = this.state; - 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> - )} + const isIncome = !!(category || group).is_income; - {error && this.renderError(error)} + 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> + )} - <View - style={{ - marginTop: 20, - flexDirection: 'row', - justifyContent: 'flex-start', - alignItems: 'center', - }} - > - <Text>Transfer to:</Text> + {error && renderError(error)} - <View style={{ flex: 1, marginLeft: 10, marginRight: 30 }}> - <NativeCategorySelect - ref={el => (this.input = el)} - 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, - ), - })) - } - name="category" - value={transferCategory} - onChange={e => - this.setState({ transferCategory: e.target.value }) - } - /> - </View> + <View + style={{ + marginTop: 20, + flexDirection: 'row', + justifyContent: 'flex-start', + alignItems: 'center', + }} + > + <Text>Transfer to:</Text> - <Button primary onClick={() => this.onDelete()}> - Delete - </Button> + <View style={{ flex: 1, marginLeft: 10, marginRight: 30 }}> + <NativeCategorySelect + ref={inputRef} + 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, + ), + })) + } + name="category" + value={transferCategory} + onChange={e => setTransferCategory(e.target.value)} + /> </View> + + <Button + primary + onClick={() => { + if (!transferCategory) { + setError('required-transfer'); + } else { + onDelete(transferCategory); + modalProps.onClose(); + } + }} + > + Delete + </Button> </View> - )} - </Modal> - ); - } + </View> + )} + </Modal> + ); } - -export default ConfirmCategoryDelete; diff --git a/packages/desktop-client/src/components/reports/Container.js b/packages/desktop-client/src/components/reports/Container.js index 5542081d8829cdb7e7a1b7ac52daad1984917d63..31906cf5b8b32ec16f091765546d12c9abc12abc 100644 --- a/packages/desktop-client/src/components/reports/Container.js +++ b/packages/desktop-client/src/components/reports/Container.js @@ -1,27 +1,21 @@ -import React from 'react'; +import React, { useRef } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; import { View } from 'loot-design/src/components/common'; -class Container extends React.Component { - render() { - const { style, children } = this.props; +export default function Container({ style, children }) { + const portalHost = useRef(null); - return ( - <View - style={[{ height: 300, position: 'relative', flexShrink: 0 }, style]} - > - <div ref={el => (this.portalHost = el)} /> - <AutoSizer> - {({ width, height }) => ( - <div style={{ width, height }}> - {children(width, height, this.portalHost)} - </div> - )} - </AutoSizer> - </View> - ); - } + return ( + <View style={[{ height: 300, position: 'relative', flexShrink: 0 }, style]}> + <div ref={portalHost} /> + <AutoSizer> + {({ width, height }) => ( + <div style={{ width, height }}> + {children(width, height, portalHost.current)} + </div> + )} + </AutoSizer> + </View> + ); } - -export default Container; diff --git a/packages/desktop-client/src/components/reports/index.js b/packages/desktop-client/src/components/reports/index.js index 9c6054282d3dba60ccdd989a0f320460196f3b72..ba204ac8d6413801b9ac0972fb8ffd267898d94a 100644 --- a/packages/desktop-client/src/components/reports/index.js +++ b/packages/desktop-client/src/components/reports/index.js @@ -7,16 +7,12 @@ import CashFlow from './CashFlow'; import NetWorth from './NetWorth'; import Overview from './Overview'; -class Reports extends React.Component { - render() { - return ( - <View style={{ flex: 1 }} data-testid="reports-page"> - <Route path="/reports" exact component={Overview} /> - <Route path="/reports/net-worth" exact component={NetWorth} /> - <Route path="/reports/cash-flow" exact component={CashFlow} /> - </View> - ); - } +export default function Reports() { + return ( + <View style={{ flex: 1 }} data-testid="reports-page"> + <Route path="/reports" exact component={Overview} /> + <Route path="/reports/net-worth" exact component={NetWorth} /> + <Route path="/reports/cash-flow" exact component={CashFlow} /> + </View> + ); } - -export default Reports; diff --git a/packages/loot-design/src/components/TutorialPoint.js b/packages/loot-design/src/components/TutorialPoint.js deleted file mode 100644 index c2884c1662fd3b2e57a9221ebb52c2ba66213f92..0000000000000000000000000000000000000000 --- a/packages/loot-design/src/components/TutorialPoint.js +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react'; - -import PropTypes from 'prop-types'; - -class TutorialPoint extends React.Component { - static contextTypes = { - setTutorialNode: PropTypes.func, - }; - - ref = el => { - if (this.context.setTutorialNode) { - this.context.setTutorialNode(this.props.name, el, this.props.expand); - } - }; - - render() { - return <div ref={this.ref} />; - } -} - -export default TutorialPoint; diff --git a/packages/loot-design/src/components/common.js b/packages/loot-design/src/components/common.js index 1ada04125e6497f2f635ddead5e94dd4f8455fe1..a405235a5527f5a1139e3178b021306279d9b8c1 100644 --- a/packages/loot-design/src/components/common.js +++ b/packages/loot-design/src/components/common.js @@ -1105,62 +1105,45 @@ export function InitialFocus({ children }) { return React.cloneElement(children, { inputRef: node }); } -export class HoverTarget extends React.Component { - state = { hovered: false }; +export function HoverTarget({ + style, + contentStyle, + children, + renderContent, + disabled, +}) { + let [hovered, setHovered] = useState(false); - onMouseEnter = () => { - if (!this.props.disabled) { - this.setState({ hovered: true }); + const onMouseEnter = useCallback(() => { + if (!disabled) { + setHovered(true); } - }; + }, [disabled]); - onMouseLeave = () => { - if (!this.props.disabled) { - this.setState({ hovered: false }); + const onMouseLeave = useCallback(() => { + if (!disabled) { + setHovered(false); } - }; + }, [disabled]); - componentDidUpdate(prevProps) { - let { disabled } = this.props; - if (disabled && this.state.hovered) { - this.setState({ hovered: false }); + useEffect(() => { + if (disabled && hovered) { + setHovered(false); } - } + }, [disabled, hovered]); - render() { - let { style, contentStyle, children, renderContent } = this.props; - return ( - <View style={style}> - <View - onMouseEnter={this.onMouseEnter} - onMouseLeave={this.onMouseLeave} - style={contentStyle} - > - {children} - </View> - {this.state.hovered && renderContent()} - </View> - ); - } -} - -export class TooltipTarget extends React.Component { - state = { clicked: false }; - - render() { - return ( - <View style={[{ position: 'relative' }, this.props.style]}> - <View - style={{ flex: 1 }} - onClick={() => this.setState({ clicked: true })} - > - {this.props.children} - </View> - {this.state.clicked && - this.props.renderContent(() => this.setState({ clicked: false }))} + return ( + <View style={style}> + <View + onMouseEnter={onMouseEnter} + onMouseLeave={onMouseLeave} + style={contentStyle} + > + {children} </View> - ); - } + {hovered && renderContent()} + </View> + ); } export function Label({ title, style }) { diff --git a/upcoming-release-notes/783.md b/upcoming-release-notes/783.md new file mode 100644 index 0000000000000000000000000000000000000000..68d086d9ff14b324254cd47c3416862dbb534ae7 --- /dev/null +++ b/upcoming-release-notes/783.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [j-f1] +--- + +Remove a few unused class components, convert a few components to functions