feat: fix items list datatable.

This commit is contained in:
a.bouhuolia
2021-02-08 13:17:11 +02:00
parent adac2386bb
commit 304f0c9ae5
43 changed files with 777 additions and 835 deletions

View File

@@ -0,0 +1,52 @@
import React, { memo } from 'react';
import { Popover, Position, Classes } from '@blueprintjs/core';
import { saveInvoke } from 'utils';
const POPPER_MODIFIERS = {
preventOverflow: { boundariesElement: 'viewport' },
};
function ContextMenu(props) {
const { bindMenu, isOpen, children, onClosed, popoverProps } = props;
const handleClosed = () => {
requestAnimationFrame(() => saveInvoke(onClosed));
};
const handleInteraction = (nextOpenState) => {
if (!nextOpenState) {
// Delay the actual hiding till the event queue clears
// to avoid flicker of opening twice
requestAnimationFrame(() => saveInvoke(onClosed));
}
};
return (
<div className={Classes.CONTEXT_MENU_POPOVER_TARGET} {...bindMenu}>
<Popover
onClosed={handleClosed}
modifiers={POPPER_MODIFIERS}
content={children}
enforceFocus={true}
isOpen={isOpen}
minimal={true}
position={Position.RIGHT_TOP}
target={<div />}
usePortal={false}
onInteraction={handleInteraction}
{...popoverProps}
/>
</div>
);
}
export default memo(ContextMenu, (prevProps, nextProps) => {
if (
prevProps.isOpen === nextProps.isOpen &&
prevProps.bindMenu.style === nextProps.bindMenu.style
) {
return true;
} else {
return false;
}
});

View File

@@ -1,131 +0,0 @@
/*
* 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();
}
};
}

View File

@@ -10,22 +10,20 @@ import {
Position,
} from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import { useHistory } from 'react-router-dom';
import { Icon } from 'components';
/**
* Dashboard action views list.
*/
export default function DashboardActionViewsList({
resourceName,
views,
onChange,
}) {
const history = useHistory();
const handleClickViewItem = (view) => {
history.push(
view ? `/${resourceName}/${view.id}/custom_view` : '/accounts',
);
onChange && onChange(view);
};
const viewsMenuItems = views.map((view) => {
return (
<MenuItem onClick={() => handleClickViewItem(view)} text={view.name} />

View File

@@ -19,13 +19,14 @@ import { Icon, Hint, If } from 'components';
import withSearch from 'containers/GeneralSearch/withSearch';
import withDashboardActions from 'containers/Dashboard/withDashboardActions';
import withDashboard from 'containers/Dashboard/withDashboard';
import withSettings from 'containers/Settings/withSettings';
import QuickNewDropdown from 'containers/QuickNewDropdown/QuickNewDropdown';
import { compose } from 'utils';
function DashboardTopbar({
// #withDashboard
pageTitle,
pageSubtitle,
editViewId,
// #withDashboardActions
@@ -35,6 +36,9 @@ function DashboardTopbar({
// #withDashboard
sidebarExpended,
// #withSettings
organizationName,
// #withGlobalSearch
openGlobalSearch,
}) {
@@ -100,11 +104,7 @@ function DashboardTopbar({
</div>
</If>
<If condition={pageSubtitle}>
<h3>{pageSubtitle}</h3>
</If>
<If condition={pageSubtitle && editViewId}>
<If condition={editViewId}>
<Button
className={Classes.MINIMAL + ' button--view-edit'}
icon={<Icon icon="pen" iconSize={13} />}
@@ -117,6 +117,10 @@ function DashboardTopbar({
<DashboardBreadcrumbs />
</div>
{/* <div class="dashboard__organization-name">
{ organizationName }
</div> */}
<DashboardBackLink />
</div>
@@ -158,11 +162,13 @@ function DashboardTopbar({
export default compose(
withSearch,
withDashboard(({ pageTitle, pageSubtitle, editViewId, sidebarExpended }) => ({
withDashboard(({ pageTitle, editViewId, sidebarExpended }) => ({
pageTitle,
pageSubtitle,
editViewId,
sidebarExpended,
})),
withSettings(({ organizationSettings }) => ({
organizationName: organizationSettings.name,
})),
withDashboardActions,
)(DashboardTopbar);

View File

@@ -1,15 +1,19 @@
import React, { useState, useRef, useMemo } from 'react';
import React, { useRef, useState, useEffect } from 'react';
import { FormattedMessage as T } from 'react-intl';
import PropTypes from 'prop-types';
import { Button, Tabs, Tab, Tooltip, Position } from '@blueprintjs/core';
import { debounce } from 'lodash';
import { useHistory } from 'react-router';
import { debounce } from 'lodash';
import { If, Icon } from 'components';
import { saveInvoke } from 'utils';
import { saveInvoke } from 'utils';
/**
* Dashboard views tabs.
*
*/
export default function DashboardViewsTabs({
initialViewId = 0,
viewId,
currentViewId,
tabs,
defaultTabText = <T id={'all'} />,
allTab = true,
@@ -17,41 +21,38 @@ export default function DashboardViewsTabs({
resourceName,
onNewViewTabClick,
onChange,
onTabClick,
OnThrottledChange,
throttleTime = 250,
}) {
const history = useHistory();
const [currentView, setCurrentView] = useState(initialViewId || 0);
useEffect(() => {
if (typeof currentViewId !== 'undefined' && currentViewId !== currentView) {
setCurrentView(currentViewId || 0);
}
}, [currentView, setCurrentView, currentViewId]);
const throttledOnChange = useRef(
debounce((viewId) => saveInvoke(OnThrottledChange, viewId), throttleTime),
);
// Trigger `onChange` and `onThrottledChange` events.
const triggerOnChange = (viewId) => {
saveInvoke(onChange, viewId);
throttledOnChange.current(viewId);
};
// Handles click a new view.
const handleClickNewView = () => {
history.push(`/custom_views/${resourceName}/new`);
onNewViewTabClick && onNewViewTabClick();
};
const handleTabClick = (viewId) => {
saveInvoke(onTabClick, viewId);
};
const mappedTabs = useMemo(
() => tabs.map((tab) => ({ ...tab, onTabClick: handleTabClick })),
[tabs, handleTabClick],
);
const handleViewLinkClick = () => {
saveInvoke(onNewViewTabClick);
};
const debounceChangeHistory = useRef(
debounce((toUrl) => {
history.push(toUrl);
}, 250),
);
// Handle tabs change.
const handleTabsChange = (viewId) => {
const toPath = viewId ? `${viewId}/custom_view` : '';
debounceChangeHistory.current(`/${resourceName}/${toPath}`);
setCurrentView(viewId);
saveInvoke(onChange, viewId);
triggerOnChange(viewId)
};
return (
@@ -62,13 +63,11 @@ export default function DashboardViewsTabs({
className="tabs--dashboard-views"
onChange={handleTabsChange}
>
{allTab && (
<Tab id={0} title={defaultTabText} onClick={handleViewLinkClick} />
)}
{mappedTabs.map((tab) => (
<Tab id={tab.id} title={tab.name} onClick={handleTabClick} />
))}
{allTab && <Tab id={0} title={defaultTabText} />}
{tabs.map((tab) => (
<Tab id={tab.id} title={tab.name} />
))}
<If condition={newViewTab}>
<Tooltip
content={<T id={'create_a_new_view'} />}
@@ -93,5 +92,6 @@ DashboardViewsTabs.propTypes = {
onNewViewTabClick: PropTypes.func,
onChange: PropTypes.func,
onTabClick: PropTypes.func,
OnThrottledChange: PropTypes.func,
throttleTime: PropTypes.number,
};

View File

@@ -75,6 +75,8 @@ export default function DataTable(props) {
TableWrapperRenderer,
TableTBodyRenderer,
TablePaginationRenderer,
...restProps
} = props;
const selectionColumnObj = {
@@ -117,6 +119,8 @@ export default function DataTable(props) {
autoResetSortBy,
autoResetFilters,
autoResetRowState,
...restProps
},
useSortBy,
useExpanded,

View File

@@ -3,6 +3,7 @@ import classNames from 'classnames';
import { ScrollSyncPane } from 'react-scroll-sync';
import { If } from 'components';
import TableContext from './TableContext';
import MaterialProgressBar from 'components/MaterialProgressBar';
function TableHeaderCell({ column, index }) {
const {
@@ -77,7 +78,7 @@ function TableHeaderGroup({ headerGroup }) {
export default function TableHeader() {
const {
table: { headerGroups, page },
props: { TableHeaderSkeletonRenderer, headerLoading },
props: { TableHeaderSkeletonRenderer, headerLoading, progressBarLoading },
} = useContext(TableContext);
if (headerLoading && TableHeaderSkeletonRenderer) {
@@ -89,6 +90,9 @@ export default function TableHeader() {
{headerGroups.map((headerGroup) => (
<TableHeaderGroup headerGroup={headerGroup} />
))}
<If condition={progressBarLoading}>
<MaterialProgressBar />
</If>
</div>
</ScrollSyncPane>
);

View File

@@ -1,11 +1,12 @@
import React, { useCallback, useContext } from 'react';
import { If, Pagination } from 'components';
import TableContext from './TableContext';
import { saveInvoke } from 'utils';
/**
* Table pagination.
*/
export default function TablePagination({}) {
export default function TablePagination() {
const {
table: {
gotoPage,
@@ -13,28 +14,39 @@ export default function TablePagination({}) {
pageCount,
state: { pageIndex, pageSize },
},
props: { pagination, loading },
props: { pagination, loading, onPaginationChange },
} = useContext(TableContext);
const triggerOnPaginationChange = useCallback((payload) => {
saveInvoke(onPaginationChange, payload)
}, [onPaginationChange]);
// Handles the page changing.
const handlePageChange = useCallback(
(currentPage) => {
gotoPage(currentPage - 1);
({ page, pageSize }) => {
const pageIndex = page - 1;
gotoPage(pageIndex);
triggerOnPaginationChange({ page, pageSize });
},
[gotoPage],
[gotoPage, triggerOnPaginationChange],
);
// Handles the page size changing.
const handlePageSizeChange = useCallback(
(pageSize, currentPage) => {
({ pageSize, page }) => {
gotoPage(0);
setPageSize(pageSize);
triggerOnPaginationChange({ page, pageSize });
},
[gotoPage, setPageSize],
[gotoPage, setPageSize, triggerOnPaginationChange],
);
return (
<If condition={pagination && !loading}>
<Pagination
initialPage={pageIndex + 1}
currentPage={pageIndex + 1}
total={pageSize * pageCount}
size={pageSize}
onPageChange={handlePageChange}

View File

@@ -1,32 +1,36 @@
import React, { useContext } from 'react';
import classNames from 'classnames';
import { ContextMenu } from '@blueprintjs/core';
import useContextMenu from 'react-use-context-menu';
import TableContext from './TableContext';
import { saveInvoke } from 'utils';
import { ContextMenu } from 'components';
/**
* Table row.
*/
export default function TableRow({ row, className, style }) {
const {
props: { TableCellRenderer, rowContextMenu, rowClassNames },
props: {
TableCellRenderer,
rowContextMenu,
rowClassNames,
ContextMenu: ContextMenuContent,
},
table,
} = useContext(TableContext);
// Handle rendering row context menu.
const handleRowContextMenu = (row) => (e) => {
if (typeof rowContextMenu === 'function') {
e.preventDefault();
const tr = e.currentTarget.closest('.tr');
tr.classList.add('is-context-menu-active');
const [
bindMenu,
bindMenuItem,
useContextTrigger,
{ coords, setVisible, isVisible },
] = useContextMenu();
const DropdownEl = rowContextMenu({ row });
ContextMenu.show(DropdownEl, { left: e.clientX, top: e.clientY }, () => {
tr.classList.remove('is-context-menu-active');
});
}
};
const [bindTrigger] = useContextTrigger({
collect: () => 'Title',
});
return (
<div
@@ -40,12 +44,21 @@ export default function TableRow({ row, className, style }) {
className,
),
style,
onContextMenu: handleRowContextMenu(row)
})}
{...bindTrigger}
>
{row.cells.map((cell, index) => (
<TableCellRenderer cell={cell} row={row} index={index + 1} />
))}
<ContextMenu
bindMenu={bindMenu}
isOpen={isVisible}
coords={coords}
onClosed={() => setVisible(false)}
>
<ContextMenuContent {...table} row={row} />
</ContextMenu>
</div>
);
}

View File

@@ -0,0 +1,13 @@
import React from 'react';
import 'style/components/MaterialProgressBar.scss';
export default function MaterialProgressBar() {
return (
<div class="progress-container">
<div class="progress-materializecss">
<div class="indeterminate"></div>
</div>
</div>
);
}

View File

@@ -1,6 +1,6 @@
import React, { useReducer, useEffect } from 'react';
import classNames from 'classnames';
import { Button, ButtonGroup, Intent, HTMLSelect, } from '@blueprintjs/core';
import { Button, ButtonGroup, Intent, HTMLSelect } from '@blueprintjs/core';
import { FormattedMessage as T } from 'react-intl';
import PropTypes from 'prop-types';
import { range } from 'lodash';
@@ -8,6 +8,12 @@ import { Icon } from 'components';
import 'style/components/DataTable/Pagination.scss';
const TYPE = {
PAGE_CHANGE: 'PAGE_CHANGE',
PAGE_SIZE_CHANGE: 'PAGE_SIZE_CHANGE',
INITIALIZE: 'INITIALIZE',
};
const getState = ({ currentPage, size, total }) => {
const totalPages = Math.ceil(total / size);
const visibleItems = 5;
@@ -16,7 +22,7 @@ const getState = ({ currentPage, size, total }) => {
// create an array of pages to ng-repeat in the pager control
let startPage, endPage;
if (totalPages <= visibleItems) {
// less than {visibleItems} total pages so show
// less than {visibleItems} total pages so show
startPage = 1;
endPage = totalPages;
} else {
@@ -50,11 +56,6 @@ const getState = ({ currentPage, size, total }) => {
};
};
const TYPE = {
PAGE_CHANGE: 'PAGE_CHANGE',
PAGE_SIZE_CHANGE: 'PAGE_SIZE_CHANGE',
INITIALIZE: 'INITIALIZE',
};
const reducer = (state, action) => {
switch (action.type) {
case TYPE.PAGE_CHANGE:
@@ -63,15 +64,15 @@ const reducer = (state, action) => {
size: state.size,
total: state.total,
});
case TYPE.PAGE_SIZE_CHANGE:
case TYPE.PAGE_SIZE_CHANGE:
return getState({
currentPage: state.currentPage,
size: action.size,
total: state.total,
});
case TYPE.INITIALIZE:
case TYPE.INITIALIZE:
return getState({
currentPage: state.currentPage,
currentPage: action.page,
size: action.size,
total: action.total,
});
@@ -80,82 +81,92 @@ const reducer = (state, action) => {
}
};
const Pagination = ({
initialPage,
function Pagination({
currentPage,
total,
size,
pageSizesOptions = [5, 12, 20, 30, 50, 75, 100, 150],
onPageChange,
onPageSizeChange,
}) => {
}) {
const [state, dispatch] = useReducer(
reducer,
{ currentPage: initialPage, total, size },
{ currentPage, total, size },
getState,
);
useEffect(() => {
dispatch({
type: 'INITIALIZE',
type: TYPE.INITIALIZE,
total,
size,
page: currentPage,
});
}, [total, size]);
}, [total, size, currentPage]);
return (
<div class="pagination">
<div class="pagination__buttons-group">
<ButtonGroup>
<Button
disabled={state.currentPage === 1}
onClick={() => {
dispatch({ type: 'PAGE_CHANGE', page: state.currentPage - 1 });
onPageChange(state.currentPage - 1);
}}
minimal={true}
className={'pagination__item pagination__item--previous'}
icon={<Icon icon={'arrow-back-24'} iconSize={12} />}
>
<T id='previous' />
</Button>
{state.pages.map((page) => (
<ButtonGroup>
<Button
key={page}
intent={state.currentPage === page ? Intent.PRIMARY : Intent.NONE}
disabled={state.currentPage === page}
disabled={state.currentPage <= 1}
onClick={() => {
dispatch({ type: 'PAGE_CHANGE', page });
onPageChange(page);
dispatch({ type: 'PAGE_CHANGE', page: state.currentPage - 1 });
const page = state.currentPage - 1;
const { size: pageSize } = state;
onPageChange({ page, pageSize });
}}
minimal={true}
className={classNames(
'pagination__item',
'pagination__item--page',
{
'is-active': state.currentPage === page,
}
)}
className={'pagination__item pagination__item--previous'}
icon={<Icon icon={'arrow-back-24'} iconSize={12} />}
>
{ page }
<T id="previous" />
</Button>
))}
<Button
disabled={state.currentPage === state.totalPages}
onClick={() => {
dispatch({
type: 'PAGE_CHANGE',
page: state.currentPage + 1
});
onPageChange(state.currentPage + 1);
}}
minimal={true}
className={'pagination__item pagination__item--next'}
icon={<Icon icon={'arrow-forward-24'} iconSize={12} />}
>
<T id='next' />
</Button>
</ButtonGroup>
{state.pages.map((page) => (
<Button
key={page}
intent={state.currentPage === page ? Intent.PRIMARY : Intent.NONE}
disabled={state.currentPage === page}
onClick={() => {
dispatch({ type: 'PAGE_CHANGE', page });
const { size: pageSize } = state;
onPageChange({ page, pageSize });
}}
minimal={true}
className={classNames(
'pagination__item',
'pagination__item--page',
{
'is-active': state.currentPage === page,
},
)}
>
{page}
</Button>
))}
<Button
disabled={state.currentPage === state.totalPages}
onClick={() => {
dispatch({
type: 'PAGE_CHANGE',
page: state.currentPage + 1,
});
const page = state.currentPage + 1;
const { size: pageSize } = state;
onPageChange({ page, pageSize });
}}
minimal={true}
className={'pagination__item pagination__item--next'}
icon={<Icon icon={'arrow-forward-24'} iconSize={12} />}
>
<T id="next" />
</Button>
</ButtonGroup>
</div>
<div class="pagination__controls">
@@ -167,11 +178,11 @@ const Pagination = ({
value={state.currentPage}
onChange={(event) => {
const page = parseInt(event.currentTarget.value, 10);
const { size: pageSize } = state;
dispatch({ type: 'PAGE_CHANGE', page });
onPageChange(page);
onPageChange({ page, pageSize });
}}
minimal={true}
/>
</div>
@@ -186,26 +197,28 @@ const Pagination = ({
dispatch({ type: 'PAGE_SIZE_CHANGE', size: pageSize });
dispatch({ type: 'PAGE_CHANGE', page: 1 });
onPageSizeChange(pageSize, 1);
onPageSizeChange({ pageSize, page: 1 });
}}
minimal={true}
/>
</div>
</div>
<div class="pagination__info">
<T id={'showing_current_page_to_total'} values={{
currentPage: state.currentPage,
totalPages: state.totalPages,
total: total,
}} />
<T
id={'showing_current_page_to_total'}
values={{
currentPage: state.currentPage,
totalPages: state.totalPages,
total: total,
}}
/>
</div>
</div>
);
};
}
Pagination.propTypes = {
initialPage: PropTypes.number.isRequired,
currentPage: PropTypes.number.isRequired,
size: PropTypes.number.isRequired,
total: PropTypes.number.isRequired,
onPageChange: PropTypes.func,
@@ -213,7 +226,7 @@ Pagination.propTypes = {
};
Pagination.defaultProps = {
initialPage: 1,
currentPage: 1,
size: 25,
};

View File

@@ -45,7 +45,7 @@ import PageFormBigNumber from './PageFormBigNumber';
import AccountsMultiSelect from './AccountsMultiSelect';
import CustomersMultiSelect from './CustomersMultiSelect';
import Skeleton from './Skeleton'
import ContextMenu from './ContextMenu'
import TableFastCell from './Datatable/TableFastCell';
const Hint = FieldHint;
@@ -99,4 +99,5 @@ export {
CustomersMultiSelect,
TableFastCell,
Skeleton,
ContextMenu
};