mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-18 22:00:31 +00:00
refactoring: migrating to react-query to manage service-side state.
This commit is contained in:
@@ -2,9 +2,7 @@ import React from 'react';
|
||||
import { RawIntlProvider } from 'react-intl';
|
||||
import { Router, Switch, Route } from 'react-router';
|
||||
import { createBrowserHistory } from 'history';
|
||||
import { ReactQueryConfigProvider } from 'react-query';
|
||||
import { ReactQueryDevtools } from 'react-query-devtools';
|
||||
|
||||
import { QueryClientProvider, QueryClient } from 'react-query';
|
||||
import 'style/App.scss';
|
||||
|
||||
import PrivateRoute from 'components/Guards/PrivateRoute';
|
||||
@@ -17,14 +15,18 @@ function App({ locale }) {
|
||||
const history = createBrowserHistory();
|
||||
|
||||
const queryConfig = {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
const queryClient = new QueryClient(queryConfig);
|
||||
|
||||
return (
|
||||
<RawIntlProvider value={intl}>
|
||||
<div className="App">
|
||||
<ReactQueryConfigProvider config={queryConfig}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<div className="App">
|
||||
<Router history={history}>
|
||||
<Switch>
|
||||
<Route path={'/auth'}>
|
||||
@@ -38,9 +40,8 @@ function App({ locale }) {
|
||||
</Router>
|
||||
|
||||
<GlobalErrors />
|
||||
<ReactQueryDevtools />
|
||||
</ReactQueryConfigProvider>
|
||||
</div>
|
||||
</div>
|
||||
</QueryClientProvider>
|
||||
</RawIntlProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import classNames from 'classnames';
|
||||
import { CLASSES } from 'common/classes';
|
||||
|
||||
export default function CategoriesSelectList({
|
||||
categoriesList,
|
||||
categories,
|
||||
selecetedCategoryId,
|
||||
defaultSelectText = <T id={'select_category'} />,
|
||||
onCategorySelected,
|
||||
@@ -41,7 +41,7 @@ export default function CategoriesSelectList({
|
||||
|
||||
return (
|
||||
<ListSelect
|
||||
items={categoriesList}
|
||||
items={categories}
|
||||
selectedItemProp={'id'}
|
||||
selectedItem={selecetedCategoryId}
|
||||
textProp={'name'}
|
||||
|
||||
131
client/src/components/ContextMenu.tsx
Normal file
131
client/src/components/ContextMenu.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright 2016 Palantir Technologies, Inc. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// import classNames from 'classnames';
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
// import { polyfill } from "react-lifecycles-compat";
|
||||
|
||||
import {
|
||||
Popover,
|
||||
Classes,
|
||||
Position,
|
||||
} from '@blueprintjs/core';
|
||||
|
||||
|
||||
// import { IOverlayLifecycleProps } from "../overlay/overlay";
|
||||
// import { Popover } from "../popover/popover";
|
||||
// import { PopperModifiers } from "../popover/popoverSharedProps";
|
||||
|
||||
export interface IOffset {
|
||||
left: number;
|
||||
top: number;
|
||||
}
|
||||
|
||||
interface IContextMenuState {
|
||||
isOpen: boolean;
|
||||
isDarkTheme: boolean;
|
||||
menu?: JSX.Element;
|
||||
offset?: IOffset;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
const POPPER_MODIFIERS = {
|
||||
preventOverflow: { boundariesElement: "viewport" },
|
||||
};
|
||||
const TRANSITION_DURATION = 100;
|
||||
|
||||
// type IContextMenuProps = IOverlayLifecycleProps;
|
||||
|
||||
/* istanbul ignore next */
|
||||
|
||||
export default class ContextMenu extends React.PureComponent {
|
||||
public state: IContextMenuState = {
|
||||
isDarkTheme: false,
|
||||
isOpen: false,
|
||||
};
|
||||
|
||||
public render() {
|
||||
// prevent right-clicking in a context menu
|
||||
const content = <div onContextMenu={this.cancelContextMenu}>{this.state.menu}</div>;
|
||||
const popoverClassName = {};
|
||||
|
||||
// HACKHACK: workaround until we have access to Popper#scheduleUpdate().
|
||||
// https://github.com/palantir/blueprint/issues/692
|
||||
// Generate key based on offset so a new Popover instance is created
|
||||
// when offset changes, to force recomputing position.
|
||||
const key = this.state.offset === undefined ? "" : `${this.state.offset.left}x${this.state.offset.top}`;
|
||||
|
||||
// wrap the popover in a positioned div to make sure it is properly
|
||||
// offset on the screen.
|
||||
return (
|
||||
<div className={Classes.CONTEXT_MENU_POPOVER_TARGET} style={this.state.offset}>
|
||||
<Popover
|
||||
{...this.props}
|
||||
backdropProps={{ onContextMenu: this.handleBackdropContextMenu }}
|
||||
content={content}
|
||||
enforceFocus={false}
|
||||
key={key}
|
||||
hasBackdrop={true}
|
||||
isOpen={this.state.isOpen}
|
||||
minimal={true}
|
||||
// modifiers={POPPER_MODIFIERS}
|
||||
onInteraction={this.handlePopoverInteraction}
|
||||
position={Position.RIGHT_TOP}
|
||||
// popoverClassName={popoverClassName}
|
||||
target={<div />}
|
||||
transitionDuration={TRANSITION_DURATION}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
public show(menu: JSX.Element, offset: IOffset, onClose?: () => void, isDarkTheme = false) {
|
||||
this.setState({ isOpen: true, menu, offset, onClose, isDarkTheme });
|
||||
}
|
||||
|
||||
public hide() {
|
||||
this.state.onClose?.();
|
||||
this.setState({ isOpen: false, onClose: undefined });
|
||||
}
|
||||
|
||||
private cancelContextMenu = (e: React.SyntheticEvent<HTMLDivElement>) => e.preventDefault();
|
||||
|
||||
private handleBackdropContextMenu = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
// React function to remove from the event pool, useful when using a event within a callback
|
||||
e.persist();
|
||||
e.preventDefault();
|
||||
// wait for backdrop to disappear so we can find the "real" element at event coordinates.
|
||||
// timeout duration is equivalent to transition duration so we know it's animated out.
|
||||
setTimeout(() => {
|
||||
// retrigger context menu event at the element beneath the backdrop.
|
||||
// if it has a `contextmenu` event handler then it'll be invoked.
|
||||
// if it doesn't, no native menu will show (at least on OSX) :(
|
||||
const newTarget = document.elementFromPoint(e.clientX, e.clientY);
|
||||
const { view, ...newEventInit } = e;
|
||||
newTarget?.dispatchEvent(new MouseEvent("contextmenu", newEventInit));
|
||||
}, TRANSITION_DURATION);
|
||||
};
|
||||
|
||||
private handlePopoverInteraction = (nextOpenState: boolean) => {
|
||||
if (!nextOpenState) {
|
||||
// delay the actual hiding till the event queue clears
|
||||
// to avoid flicker of opening twice
|
||||
this.hide();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -5,9 +5,11 @@ import { Button, Tabs, Tab, Tooltip, Position } from '@blueprintjs/core';
|
||||
import { debounce } from 'lodash';
|
||||
import { useHistory } from 'react-router';
|
||||
import { If, Icon } from 'components';
|
||||
import { saveInvoke } from 'utils';
|
||||
|
||||
export default function DashboardViewsTabs({
|
||||
initialViewId = 0,
|
||||
viewId,
|
||||
tabs,
|
||||
defaultTabText = <T id={'all'} />,
|
||||
allTab = true,
|
||||
@@ -26,16 +28,16 @@ export default function DashboardViewsTabs({
|
||||
};
|
||||
|
||||
const handleTabClick = (viewId) => {
|
||||
onTabClick && onTabClick(viewId);
|
||||
saveInvoke(onTabClick, viewId);
|
||||
};
|
||||
|
||||
const mappedTabs = useMemo(
|
||||
() => tabs.map((tab) => ({ ...tab, onTabClick: handleTabClick })),
|
||||
[tabs],
|
||||
[tabs, handleTabClick],
|
||||
);
|
||||
|
||||
const handleViewLinkClick = () => {
|
||||
onNewViewTabClick && onNewViewTabClick();
|
||||
saveInvoke(onNewViewTabClick);
|
||||
};
|
||||
|
||||
const debounceChangeHistory = useRef(
|
||||
@@ -49,7 +51,7 @@ export default function DashboardViewsTabs({
|
||||
debounceChangeHistory.current(`/${resourceName}/${toPath}`);
|
||||
|
||||
setCurrentView(viewId);
|
||||
onChange && onChange(viewId);
|
||||
saveInvoke(onChange, viewId);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -26,15 +26,14 @@ function DashboardPrivatePages({
|
||||
// #withSubscriptionsActions
|
||||
requestFetchSubscriptions,
|
||||
}) {
|
||||
// Fetch all user's organizatins.
|
||||
// Fetches all user's organizatins.
|
||||
const fetchOrganizations = useQuery(
|
||||
['organizations'], () => requestAllOrganizations(),
|
||||
);
|
||||
|
||||
// Fetchs organization subscriptions.
|
||||
// Fetches organization subscriptions.
|
||||
const fetchSuscriptions = useQuery(
|
||||
['susbcriptions'], () => requestFetchSubscriptions(),
|
||||
{ enabled: fetchOrganizations.data },
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@@ -83,6 +83,7 @@ export default function DataTable(props) {
|
||||
minWidth: selectionColumnWidth,
|
||||
width: selectionColumnWidth,
|
||||
maxWidth: selectionColumnWidth,
|
||||
skeletonWidthMin: 100,
|
||||
// The header can use the table's getToggleAllRowsSelectedProps method
|
||||
// to render a checkbox
|
||||
Header: TableIndeterminateCheckboxHeader,
|
||||
@@ -198,4 +199,7 @@ DataTable.defaultProps = {
|
||||
TableTBodyRenderer: TableTBody,
|
||||
TablePaginationRenderer: TablePagination,
|
||||
TableNoResultsRowRenderer: TableNoResultsRow,
|
||||
|
||||
noResults: 'There is no results in the table.',
|
||||
payload: {},
|
||||
};
|
||||
@@ -27,11 +27,13 @@ function TableHeaderCell({ column, index }) {
|
||||
</span>
|
||||
</If>
|
||||
|
||||
<div {...column.getSortByToggleProps({
|
||||
className: classNames('cell-inner', {
|
||||
'text-overview': column.textOverview,
|
||||
})
|
||||
})}>
|
||||
<div
|
||||
{...column.getSortByToggleProps({
|
||||
className: classNames('cell-inner', {
|
||||
'text-overview': column.textOverview,
|
||||
}),
|
||||
})}
|
||||
>
|
||||
{column.render('Header')}
|
||||
|
||||
<If condition={column.isSorted}>
|
||||
@@ -74,9 +76,13 @@ function TableHeaderGroup({ headerGroup }) {
|
||||
*/
|
||||
export default function TableHeader() {
|
||||
const {
|
||||
table: { headerGroups },
|
||||
table: { headerGroups, page },
|
||||
props: { TableHeaderSkeletonRenderer, headerLoading },
|
||||
} = useContext(TableContext);
|
||||
|
||||
if (headerLoading && TableHeaderSkeletonRenderer) {
|
||||
return <TableHeaderSkeletonRenderer />;
|
||||
}
|
||||
return (
|
||||
<ScrollSyncPane>
|
||||
<div className="thead">
|
||||
|
||||
42
client/src/components/Datatable/TableHeaderSkeleton.js
Normal file
42
client/src/components/Datatable/TableHeaderSkeleton.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import React, { useContext } from 'react';
|
||||
import TableContext from './TableContext';
|
||||
import { Skeleton } from 'components';
|
||||
|
||||
function TableHeaderCell({ column }) {
|
||||
const { skeletonWidthMax = 100, skeletonWidthMin = 40 } = column;
|
||||
|
||||
return (
|
||||
<div
|
||||
{...column.getHeaderProps({
|
||||
className: 'th',
|
||||
})}
|
||||
>
|
||||
<Skeleton minWidth={skeletonWidthMin} maxWidth={skeletonWidthMax} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Table skeleton rows.
|
||||
*/
|
||||
export default function TableSkeletonHeader({}) {
|
||||
const {
|
||||
table: { headerGroups },
|
||||
} = useContext(TableContext);
|
||||
|
||||
return (
|
||||
<div class="thead">
|
||||
{headerGroups.map((headerGroup) => (
|
||||
<div
|
||||
{...headerGroup.getHeaderGroupProps({
|
||||
className: 'tr',
|
||||
})}
|
||||
>
|
||||
{headerGroup.headers.map((column) => (
|
||||
<TableHeaderCell column={column} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -9,7 +9,7 @@ export default function TableRows() {
|
||||
table: { prepareRow, page },
|
||||
props: { TableRowRenderer, TableCellRenderer },
|
||||
} = useContext(TableContext);
|
||||
|
||||
|
||||
return page.map((row) => {
|
||||
prepareRow(row);
|
||||
return <TableRowRenderer row={row} TableCellRenderer={TableCellRenderer} />;
|
||||
|
||||
44
client/src/components/Datatable/TableSkeletonRows.js
Normal file
44
client/src/components/Datatable/TableSkeletonRows.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import React, { useContext } from 'react';
|
||||
import TableContext from './TableContext';
|
||||
import { Skeleton } from 'components';
|
||||
|
||||
/**
|
||||
* Table header cell.
|
||||
*/
|
||||
function TableHeaderCell({ column }) {
|
||||
const { skeletonWidthMax = 100, skeletonWidthMin = 40 } = column;
|
||||
|
||||
return (
|
||||
<div
|
||||
{...column.getHeaderProps({
|
||||
className: 'td',
|
||||
})}
|
||||
>
|
||||
<Skeleton minWidth={skeletonWidthMin} maxWidth={skeletonWidthMax} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Table skeleton rows.
|
||||
*/
|
||||
export default function TableSkeletonRows({}) {
|
||||
const {
|
||||
table: { headerGroups },
|
||||
} = useContext(TableContext);
|
||||
const skeletonRows = 10;
|
||||
|
||||
return Array.from({ length: skeletonRows }).map(() => {
|
||||
return headerGroups.map((headerGroup) => (
|
||||
<div
|
||||
{...headerGroup.getHeaderGroupProps({
|
||||
className: 'tr',
|
||||
})}
|
||||
>
|
||||
{headerGroup.headers.map((column) => (
|
||||
<TableHeaderCell column={column} />
|
||||
))}
|
||||
</div>
|
||||
));
|
||||
});
|
||||
}
|
||||
@@ -11,9 +11,11 @@ import EstimateNumberDialog from 'containers/Dialogs/EstimateNumberDialog';
|
||||
import ReceiptNumberDialog from 'containers/Dialogs/ReceiptNumberDialog';
|
||||
import InvoiceNumberDialog from 'containers/Dialogs/InvoiceNumberDialog';
|
||||
import InventoryAdjustmentDialog from 'containers/Dialogs/InventoryAdjustmentFormDialog';
|
||||
|
||||
import PaymentViaVoucherDialog from 'containers/Dialogs/PaymentViaVoucherDialog';
|
||||
|
||||
/**
|
||||
* Dialogs container.
|
||||
*/
|
||||
export default function DialogsContainer() {
|
||||
return (
|
||||
<div>
|
||||
@@ -27,7 +29,7 @@ export default function DialogsContainer() {
|
||||
<InviteUserDialog dialogName={'invite-user'} />
|
||||
<ExchangeRateFormDialog dialogName={'exchangeRate-form'} />
|
||||
<ItemCategoryDialog dialogName={'item-category-form'} />
|
||||
<InventoryAdjustmentDialog dialogName={'inventory-adjustment-form'} />
|
||||
<InventoryAdjustmentDialog dialogName={'inventory-adjustment'} />
|
||||
<PaymentViaVoucherDialog dialogName={'payment-via-voucher'} />
|
||||
</div>
|
||||
);
|
||||
|
||||
19
client/src/components/Skeleton.js
Normal file
19
client/src/components/Skeleton.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import 'style/components/Skeleton.scss';
|
||||
|
||||
import { randomNumber } from 'utils';
|
||||
|
||||
/**
|
||||
* Skeleton component.
|
||||
*/
|
||||
export default function Skeleton({
|
||||
Tag = 'span',
|
||||
minWidth = 40,
|
||||
maxWidth = 100,
|
||||
}) {
|
||||
const randomWidth = useMemo(() => randomNumber(minWidth, maxWidth), [
|
||||
minWidth,
|
||||
maxWidth,
|
||||
]);
|
||||
return <Tag className={'skeleton'} style={{ width: `${randomWidth}%` }} />;
|
||||
}
|
||||
@@ -44,7 +44,7 @@ import InputPrependText from './Forms/InputPrependText';
|
||||
import PageFormBigNumber from './PageFormBigNumber';
|
||||
import AccountsMultiSelect from './AccountsMultiSelect';
|
||||
import CustomersMultiSelect from './CustomersMultiSelect';
|
||||
|
||||
import Skeleton from './Skeleton'
|
||||
|
||||
import TableFastCell from './Datatable/TableFastCell';
|
||||
|
||||
@@ -97,6 +97,6 @@ export {
|
||||
AccountsMultiSelect,
|
||||
DataTableEditable,
|
||||
CustomersMultiSelect,
|
||||
|
||||
TableFastCell,
|
||||
Skeleton,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user