Commit 65d3f2ec authored by Benjamin Bouvier's avatar Benjamin Bouvier
Browse files

[client] Introduce an URL factory (fixes #834);

parent 46dd91fe
......@@ -4,6 +4,7 @@ import moment from 'moment';
import { createSelector } from 'reselect';
import PropTypes from 'prop-types';
import URL from '../../urls';
import { get, actions } from '../../store';
import { translate as $t, localeComparator } from '../../helpers';
......@@ -265,7 +266,8 @@ const categoriesNamesSelector = createSelector(
const Export = connect(
(state, ownProps) => {
let currentAccountId = ownProps.match.params.currentAccountId;
let currentAccountId = URL.budgets.accountId(ownProps.match);
let operations = get.operationsByAccountId(state, currentAccountId);
let periods = [];
let currentDate = new Date();
......
......@@ -2,6 +2,7 @@ import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import URL from '../../urls';
import { get, actions } from '../../store';
import { translate as $t } from '../../helpers';
......@@ -48,19 +49,18 @@ class ChartsComponent extends React.Component {
};
render() {
const { currentAccountId } = this.props.match.params;
const pathPrefix = '/charts';
const currentAccountId = this.props.account.id;
let tabs = new Map();
tabs.set(`${pathPrefix}/all/${currentAccountId}`, {
tabs.set(URL.charts.url('all', currentAccountId), {
name: $t('client.charts.by_category'),
component: this.makeAllChart
});
tabs.set(`${pathPrefix}/balance/${currentAccountId}`, {
tabs.set(URL.charts.url('balance', currentAccountId), {
name: $t('client.charts.balance'),
component: this.makeBalanceChart
});
tabs.set(`${pathPrefix}/earnings/${currentAccountId}`, {
tabs.set(URL.charts.url('earnings', currentAccountId), {
name: $t('client.charts.differences_all'),
component: this.makePosNegChart
});
......@@ -75,7 +75,7 @@ class ChartsComponent extends React.Component {
<TabsContainer
tabs={tabs}
defaultTab={`${pathPrefix}/${defaultDisplay}/${currentAccountId}`}
defaultTab={URL.charts.url(defaultDisplay, currentAccountId)}
selectedTab={this.props.location.pathname}
history={this.props.history}
location={this.props.location}
......@@ -107,7 +107,8 @@ ChartsComponent.propTypes = {
};
const Export = connect((state, ownProps) => {
let accountId = ownProps.match.params.currentAccountId;
let accountId = URL.charts.accountId(ownProps.match);
let account = get.accountById(state, accountId);
let operations = get.operationsByAccountId(state, accountId);
let defaultDisplay = get.setting(state, 'default-chart-display-type');
......
......@@ -2,13 +2,14 @@ import React from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { actions, get, rx } from '../../store';
import {
debug as dbg,
translate as $t,
UNKNOWN_OPERATION_TYPE,
NONE_CATEGORY_ID
} from '../../helpers';
import { actions, get, rx } from '../../store';
import URL from '../../urls';
import Pair from './item';
import { MODAL_SLUG } from './default-params-modal.js';
......@@ -143,9 +144,9 @@ function computePrevNextThreshold(current) {
export default connect(
(state, props) => {
let { currentAccountId } = props.match.params;
let formatCurrency = get.accountById(state, currentAccountId).formatCurrency;
let currentAccountId = URL.duplicates.accountId(props.match);
let formatCurrency = get.accountById(state, currentAccountId).formatCurrency;
let duplicateThreshold = parseFloat(get.setting(state, 'duplicate-threshold'));
// Show the "more"/"fewer" button if there's a value after/before in the thresholds suite.
......
import React from 'react';
import { Switch, Redirect, Route, Link } from 'react-router-dom';
import { translate as $t } from '../../helpers';
import URL from '../../urls';
import NewBankForm from '../settings/bank-accesses/form';
import ImportModule from '../settings/backup/import';
import LocaleSelector from '../menu/locale-selector';
import WeboobInstallReadme from './weboob-readme';
const PATH_PREFIX = '/initialize';
const BASE_PATH = URL.initialize.url();
const NEW_BANK_PATH = URL.initialize.url('new-bank');
const IMPORT_PATH = URL.initialize.url('import');
const DEMO_PATH = URL.initialize.url('demo-mode');
export default class AccountWizard extends React.Component {
// TODO: Demo mode should be implemented
......@@ -20,7 +26,7 @@ export default class AccountWizard extends React.Component {
<p>{$t('client.accountwizard.description')}</p>
<nav className="init-panes">
<Link to={`${PATH_PREFIX}/new-bank`}>
<Link to={NEW_BANK_PATH}>
<h3>
<i className="fa fa-plus small-only" />
{$t('client.accountwizard.menu.add_first_access_title')}
......@@ -36,7 +42,7 @@ export default class AccountWizard extends React.Component {
</p>
</Link>
<Link to={`${PATH_PREFIX}/import`}>
<Link to={IMPORT_PATH}>
<h3>
<i className="fa fa-download small-only" />
{$t('client.accountwizard.menu.import_title')}
......@@ -52,7 +58,7 @@ export default class AccountWizard extends React.Component {
</p>
</Link>
<Link to={`${PATH_PREFIX}/demo-mode`} className="disabled">
<Link to={DEMO_PATH} className="disabled">
<h3>
<i className="fa fa-laptop small-only" />
{$t('client.accountwizard.menu.demo_title')}
......@@ -77,7 +83,7 @@ export default class AccountWizard extends React.Component {
<h1>{$t('client.accountwizard.letsgo')}</h1>
</header>
<NewBankForm isOnboarding={true} />
<Link className="btn danger" to={`${PATH_PREFIX}/`}>
<Link className="btn danger" to={BASE_PATH}>
{$t('client.general.cancel')}
</Link>
</div>
......@@ -91,7 +97,7 @@ export default class AccountWizard extends React.Component {
<p>{$t('client.accountwizard.import')}</p>
<div className="accountwizard-import">
<Link className="btn danger" to={`${PATH_PREFIX}/`} tabIndex="0">
<Link className="btn danger" to={BASE_PATH} tabIndex="0">
{$t('client.general.cancel')}
</Link>
<ImportModule />
......@@ -107,7 +113,7 @@ export default class AccountWizard extends React.Component {
<p>{$t('client.accountwizard.demomode_description')}</p>
<Link className="btn danger" to={`${PATH_PREFIX}/`}>
<Link className="btn danger" to={BASE_PATH}>
{$t('client.general.cancel')}
</Link>
</div>
......@@ -121,15 +127,12 @@ export default class AccountWizard extends React.Component {
<div className="wizard-content">
<div>
<Switch>
<Route
path={`${PATH_PREFIX}/new-bank`}
render={this.renderNewBankForm}
/>
<Route path={`${PATH_PREFIX}/import`} render={this.renderImport} />
<Route path={`${PATH_PREFIX}/demo-mode`} render={this.renderDemoMode} />
<Route path={`${PATH_PREFIX}`} render={this.renderMenu} />
<Route path={'/weboob-readme'} render={this.renderWeboobReadme} />
<Redirect to={PATH_PREFIX} />
<Route path={NEW_BANK_PATH} render={this.renderNewBankForm} />
<Route path={IMPORT_PATH} render={this.renderImport} />
<Route path={DEMO_PATH} render={this.renderDemoMode} />
<Route path={BASE_PATH} render={this.renderMenu} />
<Route path={URL.weboobReadme.url()} render={this.renderWeboobReadme} />
<Redirect to={BASE_PATH} />
</Switch>
</div>
</div>
......
......@@ -14,6 +14,7 @@ const AccountListItem = connect((state, props) => {
})(props => {
let { account, accountId } = props;
let { balance, formatCurrency } = account;
let newPathname = props.location.pathname.replace(props.currentAccountId, accountId);
return (
......
......@@ -3,6 +3,7 @@ import { connect } from 'react-redux';
import { NavLink } from 'react-router-dom';
import PropTypes from 'prop-types';
import URL from '../../urls';
import { translate as $t } from '../../helpers';
import { get } from '../../store';
import { findRedundantPairs } from '../duplicates';
......@@ -43,7 +44,7 @@ const DuplicatesEntry = connect((state, props) => {
// Do not display the badge if there are no duplicates.
const badge = numDuplicates ? <span className="badge">{numDuplicates}</span> : null;
return (
<Entry path={`/duplicates/${currentAccountId}`} icon="clone" className="duplicates">
<Entry path={URL.duplicates.url(currentAccountId)} icon="clone" className="duplicates">
<span>{$t('client.menu.duplicates')}</span>
{badge}
</Entry>
......@@ -51,46 +52,36 @@ const DuplicatesEntry = connect((state, props) => {
});
const Menu = props => {
let { currentAccountId } = props.match.params;
const determineSubsection = (section, defaultSubsection) => {
if (
props.match.params.section === section &&
typeof props.match.params.subsection !== 'undefined'
) {
return props.match.params.subsection;
}
return defaultSubsection;
};
let currentAccountId = URL.sections.accountId(props.match);
// Update the subsection in the links of the menu
const chartsSubsection = determineSubsection('charts', props.defaultChart);
const settingsSubsection = determineSubsection('settings', 'accounts');
const chartsSubsection = URL.sections.sub(props.match, 'charts', props.defaultChart);
const settingsSubsection = URL.sections.sub(props.match, 'settings', 'accounts');
return (
<nav className={props.isHidden ? 'menu-hidden' : ''}>
<BankList currentAccountId={currentAccountId} location={props.location} />
<ul className="sidebar-section-list">
<Entry path={`/reports/${currentAccountId}`} icon="briefcase">
<Entry path={URL.reports.url(currentAccountId)} icon="briefcase">
<span>{$t('client.menu.reports')}</span>
</Entry>
<Entry path={`/budget/${currentAccountId}`} icon="heartbeat">
<Entry path={URL.budgets.url(currentAccountId)} icon="heartbeat">
<span>{$t('client.menu.budget')}</span>
</Entry>
<Entry path={`/charts/${chartsSubsection}/${currentAccountId}`} icon="line-chart">
<Entry path={URL.charts.url(chartsSubsection, currentAccountId)} icon="line-chart">
<span>{$t('client.menu.charts')}</span>
</Entry>
{/* Pass down the location so that the active class is set
when changing location. */}
<DuplicatesEntry currentAccountId={currentAccountId} location={props.location} />
<Entry path={`/categories/${currentAccountId}`} icon="list-ul">
<Entry path={URL.categories.url(currentAccountId)} icon="list-ul">
{$t('client.menu.categories')}
</Entry>
<Entry path={`/settings/${settingsSubsection}/${currentAccountId}`} icon="cogs">
<Entry path={URL.settings.url(settingsSubsection, currentAccountId)} icon="cogs">
{$t('client.menu.settings')}
</Entry>
<Entry path={`/about/${currentAccountId}`} icon="question">
<Entry path={URL.about.url(currentAccountId)} icon="question">
{$t('client.menu.about')}
</Entry>
</ul>
......
......@@ -5,6 +5,7 @@ import { connect } from 'react-redux';
import { translate as $t, formatDate } from '../../helpers';
import { get, actions } from '../../store';
import URL from '../../urls';
import InfiniteList from '../ui/infinite-list';
......@@ -265,7 +266,8 @@ function computeTotal(state, filterFunction, operationIds) {
}
const Export = connect((state, ownProps) => {
let accountId = ownProps.match.params.currentAccountId;
let accountId = URL.reports.accountId(ownProps.match);
let account = get.accountById(state, accountId);
let operationIds = get.operationIdsByAccountId(state, accountId);
let hasSearchFields = get.hasSearchFields(state);
......
import React from 'react';
import PropTypes from 'prop-types';
import URL from '../../urls';
import { translate as $t } from '../../helpers';
import BankAccountsList from './bank-accesses';
......@@ -13,32 +14,30 @@ import LogsSection from './logs';
import TabsContainer from '../ui/tabs.js';
const SettingsComponents = props => {
const pathPrefix = '/settings';
let { currentAccountId } = props.match.params;
let currentAccountId = URL.settings.accountId(props.match);
let tabs = new Map();
tabs.set(`${pathPrefix}/accounts/${currentAccountId}`, {
tabs.set(URL.settings.url('accounts', currentAccountId), {
name: $t('client.settings.tab_accounts'),
component: BankAccountsList
});
tabs.set(`${pathPrefix}/emails/${currentAccountId}`, {
tabs.set(URL.settings.url('emails', currentAccountId), {
name: $t('client.settings.tab_alerts'),
component: EmailsParameters
});
tabs.set(`${pathPrefix}/backup/${currentAccountId}`, {
tabs.set(URL.settings.url('backup', currentAccountId), {
name: $t('client.settings.tab_backup'),
component: BackupParameters
});
tabs.set(`${pathPrefix}/weboob/${currentAccountId}`, {
tabs.set(URL.settings.url('weboob', currentAccountId), {
name: $t('client.settings.tab_weboob'),
component: WeboobParameters
});
tabs.set(`${pathPrefix}/themes/${currentAccountId}`, {
tabs.set(URL.settings.url('themes', currentAccountId), {
name: $t('client.settings.tab_themes'),
component: ThemesParameters
});
tabs.set(`${pathPrefix}/logs/${currentAccountId}`, {
tabs.set(URL.settings.url('logs', currentAccountId), {
name: $t('client.settings.tab_logs'),
component: LogsSection
});
......@@ -46,7 +45,7 @@ const SettingsComponents = props => {
return (
<TabsContainer
tabs={tabs}
defaultTab={`${pathPrefix}/accounts/${currentAccountId}`}
defaultTab={URL.settings.url('accounts', currentAccountId)}
selectedTab={props.location.pathname}
history={props.history}
location={props.location}
......
......@@ -8,6 +8,7 @@ import throttle from 'lodash.throttle';
// Global variables
import { get, init, rx, actions } from './store';
import { translate as $t, debug, computeIsSmallScreen } from './helpers';
import URL from './urls';
// Lazy loader
import LazyLoader from './components/lazyLoader';
......@@ -20,20 +21,17 @@ import OperationList from './components/operations';
import Budget from './components/budget';
import DuplicatesList from './components/duplicates';
import Settings from './components/settings';
import LocaleSelector from './components/menu/locale-selector';
import AccountWizard from './components/init/account-wizard';
import LocaleSelector from './components/menu/locale-selector';
import Menu from './components/menu';
import AccountWizard from './components/init/account-wizard';
import Loading from './components/ui/loading';
import ThemeLoaderTag from './components/ui/theme-link';
import ErrorReporter from './components/ui/error-reporter';
import Modal from './components/ui/modal';
// The list of the available sections.
const sections = ['about', 'budget', 'categories', 'charts', 'duplicates', 'reports', 'settings'];
const RESIZE_THROTTLING = 100;
// Lazy-loaded components
......@@ -52,7 +50,6 @@ const Charts = props => (
</LazyLoader>
);
// Now this really begins.
class BaseApp extends React.Component {
constructor(props) {
super(props);
......@@ -96,7 +93,7 @@ class BaseApp extends React.Component {
initializeKresus = props => {
if (!this.props.isWeboobInstalled) {
return <Redirect to="/weboob-readme" push={false} />;
return <Redirect to={URL.weboobReadme.url()} push={false} />;
}
if (!this.props.hasAccess) {
return <AccountWizard {...props} />;
......@@ -109,20 +106,20 @@ class BaseApp extends React.Component {
// url prefix. It will further redirect to '/#' but params.section will not match
// the default section (report) on the first render. This check avoids a warning
// error in the client logs".
return props.match && sections.includes(props.match.params.section) ? (
<span className="section-title">
&nbsp;/&nbsp;
{$t(`client.menu.${props.match.params.section}`)}
</span>
) : null;
let titleKey = URL.sections.title(props.match);
if (titleKey === null) {
return null;
}
let title = $t(`client.menu.${titleKey}`);
return <span className="section-title">&nbsp;/&nbsp;{title}</span>;
};
renderMain = () => {
if (!this.props.isWeboobInstalled) {
return <Redirect to="/weboob-readme" push={false} />;
return <Redirect to={URL.weboobReadme.url()} push={false} />;
}
if (!this.props.hasAccess) {
return <Redirect to="/initialize" push={false} />;
return <Redirect to={URL.initialize.url()} push={false} />;
}
let handleContentClick = this.props.isSmallScreen ? this.hideMenu : null;
......@@ -130,7 +127,7 @@ class BaseApp extends React.Component {
let { currentAccountId, initialAccountId, location, currentAccountExists } = this.props;
// This is to handle the case where the accountId in the URL exists, but does not
// match any account (for exemple the accountId was modified by the user).
// match any account (for example the accountId was modified by the user).
if (typeof currentAccountId !== 'undefined' && !currentAccountExists) {
return (
<Redirect
......@@ -152,30 +149,23 @@ class BaseApp extends React.Component {
<Link to="/">{$t('client.KRESUS')}</Link>
</h1>
<Route path="/:section" render={this.makeSectionTitle} />
<Route path={URL.sections.pattern} render={this.makeSectionTitle} />
<LocaleSelector />
</header>
<main>
<Route path="/:section/:subsection?/:currentAccountId" render={this.makeMenu} />
<Route path={URL.sections.genericPattern} render={this.makeMenu} />
<div id="content" onClick={handleContentClick}>
<Switch>
<Route path={'/reports/:currentAccountId'} component={OperationList} />
<Route path={'/budget/:currentAccountId'} component={Budget} />
<Route
path="/charts/:chartsPanel?/:currentAccountId"
component={Charts}
/>
<Route path="/categories/:currentAccountId" component={CategoryList} />
<Route
path="/duplicates/:currentAccountId"
component={DuplicatesList}
/>
<Route path="/settings/:tab?/:currentAccountId" component={Settings} />
<Route path="/about/:currentAccountId" component={About} />
<Redirect to={`/reports/${initialAccountId}`} push={false} />
<Route path={URL.reports.pattern} component={OperationList} />
<Route path={URL.budgets.pattern} component={Budget} />
<Route path={URL.charts.pattern} component={Charts} />
<Route path={URL.categories.pattern} component={CategoryList} />
<Route path={URL.duplicates.pattern} component={DuplicatesList} />
<Route path={URL.settings.pattern} component={Settings} />
<Route path={URL.about.pattern} component={About} />
<Redirect to={URL.reports.url(initialAccountId)} push={false} />
</Switch>
</div>
</main>
......@@ -194,8 +184,8 @@ class BaseApp extends React.Component {
return (
<Switch>
<Route path="/weboob-readme" render={this.makeWeboobOrRedirect} />
<Route path="/initialize/:subsection?" render={this.initializeKresus} />
<Route path={URL.weboobReadme.pattern} render={this.makeWeboobOrRedirect} />
<Route path={URL.initialize.pattern} render={this.initializeKresus} />
<Route render={this.renderMain} />
</Switch>
);
......@@ -276,10 +266,7 @@ export default function runKresus() {
<BrowserRouter basename={`${urlPrefix}/#`}>
<Provider store={rx}>
<Switch>
<Route
path="/:section/:subsection?/:currentAccountId"
render={makeKresus}
/>
<Route path={URL.sections.genericPattern} render={makeKresus} />
<Route component={Kresus} />
</Switch>
</Provider>
......
// The list of the available sections.
const SECTIONS = ['about', 'budget', 'categories', 'charts', 'duplicates', 'reports', 'settings'];
const URLs = {
duplicates: {
pattern: '/duplicates/:currentAccountId',
url(accountId) {
return `/duplicates/${accountId}`;
},
accountId(match) {
return match.params.currentAccountId;
}
},
reports: {
pattern: '/reports/:currentAccountId',
url(accountId) {
return `/reports/${accountId}`;
},
accountId(match) {
return match.params.currentAccountId;
}
},
budgets: {
pattern: '/budget/:currentAccountId',
url(accountId) {
return `/budget/${accountId}`;
},
accountId(match) {
return match.params.currentAccountId;
}
},
charts: {
pattern: '/charts/:chartsPanel?/:currentAccountId',
url(subsection, accountId) {
return `/charts/${subsection}/${accountId}`;
},
subsection(match) {
return match.params.currentAccountId;
},
accountId(match) {
return match.params.currentAccountId;
}
},
categories: {
pattern: '/categories/:currentAccountId',
url(accountId) {
return `/categories/${accountId}`;
}
},
settings: {
pattern: '/settings/:tab?/:currentAccountId',
url(subsection, accountId) {
return `/settings/${subsection}/${accountId}`;
},
accountId(match) {
return match.params.currentAccountId;
}
},
about: {
pattern: '/about/:currentAccountId',
url(accountId) {
return `/about/${accountId}`;
}
},
weboobReadme: {
pattern: '/weboob-readme',
url() {
return '/weboob-readme';
}
},
initialize: {
pattern: '/initialize/:subsection?',
url(subsection = null) {
if (subsection === null) {
return '/initialize/';
}
return `/initialize/${subsection}`;
}