Untitled
unknown
javascript
4 years ago
29 kB
6
Indexable
import React, { useMemo, useState, useRef, useEffect, forwardRef } from 'react' import { useTable, useRowSelect } from 'react-table' import { Link } from 'react-router-dom' import _uniq from 'lodash/uniq' import { FormattedMessage } from 'react-intl' import OutsideClicker from '_common/outside-click' import { ARCHIVE_SELECTED, DELETE_SELECTED } from '_data/action-types' import { LOCALSTORAGE_KEY, DATA_TABLE_IDS, CONTACT_TABLE_COLUMN_IDS, ESTIMATES_TABLE_COLUMN_IDS, ITEMS_TABLE_COLUMN_IDS, } from '_data/constants' import IconGearFill from 'bootstrap-icons/icons/gear-fill.svg' import IconArrowUpDown from 'bootstrap-icons/icons/chevron-expand.svg' import IconArrowUp from 'bootstrap-icons/icons/chevron-up.svg' import IconArrowDown from 'bootstrap-icons/icons/chevron-down.svg' import IconList from 'bootstrap-icons/icons/list.svg' function getSortLink(location, columnID, sortOrder) { const searchParams = new URLSearchParams(location.search) // Delete existing sort queries if any if (searchParams.has('sortBy')) { searchParams.delete('sortBy') } if (searchParams.has('sortOrder')) { searchParams.delete('sortOrder') } // Set the sort query searchParams.set('sortBy', columnID) searchParams.set('sortOrder', sortOrder) return location.pathname + '?' + searchParams.toString() } const RowCheckbox = forwardRef(({ indeterminate, ...rest }, ref) => { const defaultRef = useRef() const resolvedRef = ref || defaultRef useEffect(() => { resolvedRef.current.indeterminate = indeterminate }, [resolvedRef, indeterminate]) return ( <label className="d-flex justify-content-center mb-0"> <input type="checkbox" ref={resolvedRef} {...rest} /> </label> ) }) const ColumnTogglerMenus = ({ tableID, toggleColumnVisibility, hiddenColumns }) => { if (tableID === DATA_TABLE_IDS.contacts) { return ( <> <small className="font-weight-light font-weight-bolder text-black-50 pl-2 pb-2 d-block">Display Columns</small> <div className="custom-control custom-checkbox"> <input id="contacts-list.table-header.name" name={CONTACT_TABLE_COLUMN_IDS.name} type="checkbox" className="custom-control-input" onChange={toggleColumnVisibility} checked={!hiddenColumns.includes(CONTACT_TABLE_COLUMN_IDS.name)} /> <label className="custom-control-label-sm text-body font-weight-light" htmlFor="contacts-list.table-header.name"> <FormattedMessage id="contacts-list.table-header.name" defaultMessage="Name" /> </label> </div> <div className="custom-control custom-checkbox"> <input id="contacts-list.table-header.type" name={CONTACT_TABLE_COLUMN_IDS.types} type="checkbox" className="custom-control-input" onChange={toggleColumnVisibility} checked={!hiddenColumns.includes(CONTACT_TABLE_COLUMN_IDS.types)} /> <label className="custom-control-label-sm text-body font-weight-light" htmlFor="contacts-list.table-header.type"> <FormattedMessage id="contacts-list.table-header.type" defaultMessage="Types" /> </label> </div> <div className="custom-control custom-checkbox"> <input id="contacts-list.table-header.they-owe-you" name={CONTACT_TABLE_COLUMN_IDS.theyOweYou} type="checkbox" className="custom-control-input" onChange={toggleColumnVisibility} checked={!hiddenColumns.includes(CONTACT_TABLE_COLUMN_IDS.theyOweYou)} /> <label className="custom-control-label-sm text-body font-weight-light" htmlFor="contacts-list.table-header.they-owe-you"> <FormattedMessage id="contacts-list.table-header.they-owe-you" defaultMessage="They owe you" /> </label> </div> <div className="custom-control custom-checkbox"> <input id="contacts-list.table-header.you-owe-them" name={CONTACT_TABLE_COLUMN_IDS.youOweThem} type="checkbox" className="custom-control-input" onChange={toggleColumnVisibility} checked={!hiddenColumns.includes(CONTACT_TABLE_COLUMN_IDS.youOweThem)} /> <label className="custom-control-label-sm text-body font-weight-light" htmlFor="contacts-list.table-header.you-owe-them"> <FormattedMessage id="contacts-list.table-header.you-owe-them" defaultMessage="You owe them" /> </label> </div> </> ) } else if (tableID === DATA_TABLE_IDS.estimates) { return ( <> <small className="font-weight-light font-weight-bolder text-black-50 pl-2 pb-2 d-block">Display Columns</small> <div className="custom-control custom-checkbox"> <input id="estimates-list.table-header.estimates-name" name={ESTIMATES_TABLE_COLUMN_IDS.estimateRef} type="checkbox" className="custom-control-input" onChange={toggleColumnVisibility} checked={!hiddenColumns.includes(ESTIMATES_TABLE_COLUMN_IDS.estimateRef)} /> <label className="custom-control-label-sm text-body font-weight-light" htmlFor="estimates-list.table-header.estimates-name"> <FormattedMessage id="estimate-list.table-header.ref" defaultMessage="Reference" /> </label> </div> <div className="custom-control custom-checkbox"> <input id="estimates-list.table-header.status" name={ESTIMATES_TABLE_COLUMN_IDS.status} type="checkbox" className="custom-control-input" onChange={toggleColumnVisibility} checked={!hiddenColumns.includes(ESTIMATES_TABLE_COLUMN_IDS.status)} /> <label className="custom-control-label-sm text-body font-weight-light" htmlFor="estimates-list.table-header.status"> <FormattedMessage id="estimates-list.table-header.status" defaultMessage="Status" /> </label> </div> <div className="custom-control custom-checkbox"> <input id="estimates-list.table-header.client" name={ESTIMATES_TABLE_COLUMN_IDS.client} type="checkbox" className="custom-control-input" onChange={toggleColumnVisibility} checked={!hiddenColumns.includes(ESTIMATES_TABLE_COLUMN_IDS.client)} /> <label className="custom-control-label-sm text-body font-weight-light" htmlFor="estimates-list.table-header.client"> <FormattedMessage id="estimates-list.table-header.client" defaultMessage="Client" /> </label> </div> <div className="custom-control custom-checkbox"> <input id="estimates-list.table-header.net_amount" name={ESTIMATES_TABLE_COLUMN_IDS.amount} type="checkbox" className="custom-control-input" onChange={toggleColumnVisibility} checked={!hiddenColumns.includes(ESTIMATES_TABLE_COLUMN_IDS.amount)} /> <label className="custom-control-label-sm text-body font-weight-light" htmlFor="estimates-list.table-header.net_amount"> <FormattedMessage id="estimates-list.table-header.net_amount" defaultMessage="Amount" /> </label> </div> <div className="custom-control custom-checkbox"> <input id="estimates-list.table-header.invoice" name={ESTIMATES_TABLE_COLUMN_IDS.invoice} type="checkbox" className="custom-control-input" onChange={toggleColumnVisibility} checked={!hiddenColumns.includes(ESTIMATES_TABLE_COLUMN_IDS.invoice)} /> <label className="custom-control-label-sm text-body font-weight-light" htmlFor="estimates-list.table-header.invoice"> <FormattedMessage id="estimates-list.table-header.invoice" defaultMessage="Invoice" /> </label> </div> </> ) } else if (tableID === DATA_TABLE_IDS.items) { return ( <> <small className="font-weight-light font-weight-bolder text-black-50 pl-2 pb-2 d-block">Display Columns</small> <div className="custom-control custom-checkbox"> <input id="items-list.table-header.name" name={ITEMS_TABLE_COLUMN_IDS.name} type="checkbox" className="custom-control-input" onChange={toggleColumnVisibility} checked={!hiddenColumns.includes(ITEMS_TABLE_COLUMN_IDS.name)} /> <label className="custom-control-label-sm text-body font-weight-light" htmlFor="items-list.table-header.name"> <FormattedMessage id="items-list.table-header.name" defaultMessage="Name" /> </label> </div> <div className="custom-control custom-checkbox"> <input id="items-list.table-header.type" name={ITEMS_TABLE_COLUMN_IDS.type} type="checkbox" className="custom-control-input" onChange={toggleColumnVisibility} checked={!hiddenColumns.includes(ITEMS_TABLE_COLUMN_IDS.type)} /> <label className="custom-control-label-sm text-body font-weight-light" htmlFor="items-list.table-header.type"> <FormattedMessage id="items-list.table-header.type" defaultMessage="Type" /> </label> </div> <div className="custom-control custom-checkbox"> <input id="items-list.table-header.category" name={ITEMS_TABLE_COLUMN_IDS.category} type="checkbox" className="custom-control-input" onChange={toggleColumnVisibility} checked={!hiddenColumns.includes(ITEMS_TABLE_COLUMN_IDS.category)} /> <label className="custom-control-label-sm text-body font-weight-light" htmlFor="items-list.table-header.category"> <FormattedMessage id="items-list.table-header.category" defaultMessage="Category" /> </label> </div> <div className="custom-control custom-checkbox"> <input id="items-list.table-header.supplier" name={ITEMS_TABLE_COLUMN_IDS.supplier} type="checkbox" className="custom-control-input" onChange={toggleColumnVisibility} checked={!hiddenColumns.includes(ITEMS_TABLE_COLUMN_IDS.supplier)} /> <label className="custom-control-label-sm text-body font-weight-light" htmlFor="items-list.table-header.supplier"> <FormattedMessage id="items-list.table-header.supplier" defaultMessage="Supplier" /> </label> </div> <div className="custom-control custom-checkbox"> <input id="items-list.table-header.buying-price" name={ITEMS_TABLE_COLUMN_IDS.we_buy_it} type="checkbox" className="custom-control-input" onChange={toggleColumnVisibility} checked={!hiddenColumns.includes(ITEMS_TABLE_COLUMN_IDS.we_buy_it)} /> <label className="custom-control-label-sm text-body font-weight-light" htmlFor="items-list.table-header.buying-price"> <FormattedMessage id="items-list.table-header.buying-price" defaultMessage="We buy it at" /> </label> </div> <div className="custom-control custom-checkbox"> <input id="items-list.table-header.selling-price" name={ITEMS_TABLE_COLUMN_IDS.we_sell_it} type="checkbox" className="custom-control-input" onChange={toggleColumnVisibility} checked={!hiddenColumns.includes(ITEMS_TABLE_COLUMN_IDS.we_sell_it)} /> <label className="custom-control-label-sm text-body font-weight-light" htmlFor="items-list.table-header.selling-price"> <FormattedMessage id="items-list.table-header.selling-price" defaultMessage="We sell it at" /> </label> </div> </> ) } } const ColumnToggler = ({ tableID, setHiddenColumns, defaultHiddenColumns }) => { const [isDropdownOpen, toggleDropdown] = useState(false) const [hiddenColumns, setHiddenColumnsList] = useState(defaultHiddenColumns) const toggleColumnVisibility = (event) => { const { target: { name }, } = event let filteredColumns = '' if (hiddenColumns.includes(name)) { filteredColumns = hiddenColumns.filter((column) => column !== name) setHiddenColumnsList(filteredColumns) } else { filteredColumns = [...hiddenColumns, name] } // This updates the react states for checked unchecked of checkbox setHiddenColumnsList(filteredColumns) // Set preference in LC localStorage.setItem(`${LOCALSTORAGE_KEY.userPrefTableHiddenCols}-${tableID}`, JSON.stringify(filteredColumns)) // This func update hidden list of react table setHiddenColumns(filteredColumns) } return ( <OutsideClicker onClick={() => toggleDropdown(false)}> <div className="dropdown cursor-pointer d-flex align-items-center justify-content-center" aria-haspopup="true" aria-expanded={`${isDropdownOpen}`} onClick={() => toggleDropdown(!isDropdownOpen)}> <IconGearFill /> <div className={`dropdown-menu ${isDropdownOpen ? 'show' : ''} px-2 py-3 shadow-lg right-0 left-inherit`}> <ColumnTogglerMenus tableID={tableID} toggleColumnVisibility={toggleColumnVisibility} hiddenColumns={hiddenColumns} /> </div> </div> </OutsideClicker> ) } const RowOptionsMenus = ({ tableID, rowProperties, handleRowOptionsMenu }) => { if (tableID === DATA_TABLE_IDS.contacts) { const contactApiID = rowProperties.apiID return ( <> <Link className="dropdown-item" to={`/contacts/${contactApiID}`}> <FormattedMessage id="row-options.contact-view" defaultMessage="View contact" /> </Link> <Link className="dropdown-item" to={`/contacts/${contactApiID}/edit`}> <FormattedMessage id="row-options.contact-update" defaultMessage="Update contact" /> </Link> <Link className="dropdown-item" to={`/invoices/new/?contact=${contactApiID}`}> <FormattedMessage id="row-options.contact-invoice" defaultMessage="Create invoice" /> </Link> <Link className="dropdown-item" to={`/estimates/new/?contact=${contactApiID}`}> <FormattedMessage id="row-options.contact-estimate" defaultMessage="Create estimate" /> </Link> <button className="dropdown-item" onClick={() => handleRowOptionsMenu(ARCHIVE_SELECTED, contactApiID)}> <FormattedMessage id="row-options.contact-archive" defaultMessage="Archive contact" /> </button> <button className="dropdown-item" onClick={() => handleRowOptionsMenu(DELETE_SELECTED, contactApiID)}> <FormattedMessage id="row-options.contact-delete" defaultMessage="Delete contact" /> </button> </> ) } else if (tableID === DATA_TABLE_IDS.estimates) { const estimateID = rowProperties.apiID return ( <> <Link className="dropdown-item" to={`/estimates/${estimateID}`}> <FormattedMessage id="row-options.estimate-view" defaultMessage="View estimate" /> </Link> <Link className="dropdown-item" to={`/estimates/${estimateID}/edit`}> <FormattedMessage id="row-options.estimate-update" defaultMessage="Update estimate" /> </Link> <Link className="dropdown-item" to={`/invoices/new/?estimate=${estimateID}`}> <FormattedMessage id="row-options.estimate-convert-invoice" defaultMessage="Convert to invoice" /> </Link> <Link className="dropdown-item" to={`/email/?estimate=${estimateID}`}> <FormattedMessage id="row-options.estimate-send-email" defaultMessage="Send as email" /> </Link> <button className="dropdown-item" onClick={() => handleRowOptionsMenu(ARCHIVE_SELECTED, estimateID)}> <FormattedMessage id="row-options.estimate-archive" defaultMessage="Archive estimate" /> </button> <button className="dropdown-item" onClick={() => handleRowOptionsMenu(DELETE_SELECTED, estimateID)}> <FormattedMessage id="row-options.estimate-delete" defaultMessage="Delete estimate" /> </button> </> ) } else if (tableID === DATA_TABLE_IDS.items) { const itemID = rowProperties.apiID return ( <> <Link className="dropdown-item" to={`/inventory/products-and-services/${itemID}`}> <FormattedMessage id="row-options.item-view-edit" defaultMessage="View/edit item" /> </Link> <button className="dropdown-item" onClick={() => handleRowOptionsMenu(DELETE_SELECTED, itemID)}> <FormattedMessage id="row-options.item-delete" defaultMessage="Delete item" /> </button> </> ) } else if (tableID === DATA_TABLE_IDS.categories) { const categoryApiID = rowProperties.apiID return ( <> <Link className="dropdown-item" to={`/inventory/categories/${categoryApiID}`}> <FormattedMessage id="row-options.item-category-view-edit" defaultMessage="View/Edit category" /> </Link> <button className="dropdown-item" onClick={() => handleRowOptionsMenu(DELETE_SELECTED, categoryApiID)}> <FormattedMessage id="row-options.item-category-delete" defaultMessage="Delete category" /> </button> </> ) } else if (tableID === DATA_TABLE_IDS.units) { const unitApiID = rowProperties.apiID return ( <> <Link className="dropdown-item" to={`/inventory/units/${unitApiID}/edit`}> <FormattedMessage id="row-options.inventory-unit-view" defaultMessage="View/edit unit" /> </Link> <button className="dropdown-item" onClick={() => handleRowOptionsMenu(DELETE_SELECTED, unitApiID)}> <FormattedMessage id="row-options.inventory-unit-delete" defaultMessage="Delete unit" /> </button> </> ) } } const RowOptionsDropdown = ({ tableID, cell, handleRowOptionsMenu }) => { const [isDropdownOpen, toggleDropdown] = useState(false) const rowProperties = cell.row.original const areRowOptionsHidden = rowProperties?.areRowOptionsHidden ?? false if (areRowOptionsHidden) { return null } return ( <OutsideClicker onClick={() => toggleDropdown(false)}> <div className="dropdown cursor-pointer d-flex align-items-center justify-content-center" aria-haspopup="true" aria-expanded={`${isDropdownOpen}`} onClick={() => toggleDropdown(!isDropdownOpen)}> <IconList /> <div className={`dropdown-menu ${isDropdownOpen ? 'show' : ''} px-2 py-3 shadow-lg right-0 left-inherit`}> <RowOptionsMenus tableID={tableID} handleRowOptionsMenu={handleRowOptionsMenu} rowProperties={rowProperties} /> </div> </div> </OutsideClicker> ) } const BulkActionMenus = ({ tableID, rowsIDs, handleBulkActionSelect }) => { if (tableID === DATA_TABLE_IDS.contacts) { return ( <> <button className="dropdown-item" onClick={() => handleBulkActionSelect(ARCHIVE_SELECTED, rowsIDs)}> <FormattedMessage id="bulk-action.contact-archive" defaultMessage="Archive selected contact(s)" /> </button> <button className="dropdown-item" onClick={() => handleBulkActionSelect(DELETE_SELECTED, rowsIDs)}> <FormattedMessage id="bulk-action.contact-delete" defaultMessage="Delete selected contact(s)" /> </button> </> ) } else if (tableID === DATA_TABLE_IDS.items) { return ( <> <button className="dropdown-item" onClick={() => handleBulkActionSelect(DELETE_SELECTED, rowsIDs)}> <FormattedMessage id="bulk-action.item-delete" defaultMessage="Delete selected item(s)" /> </button> </> ) } else if (tableID === DATA_TABLE_IDS.categories) { return ( <button className="dropdown-item" onClick={() => handleBulkActionSelect(DELETE_SELECTED, rowsIDs)}> <FormattedMessage id="bulk-action.item-category-delete" defaultMessage="Delete selected category(ies)" /> </button> ) } else if (tableID === DATA_TABLE_IDS.units) { return ( <button className="dropdown-item" onClick={() => handleBulkActionSelect(DELETE_SELECTED, rowsIDs)}> <FormattedMessage id="bulk-action.inventory-unit-delete" defaultMessage="Delete selected unit(s)" /> </button> ) } } const BulkActionsDropdown = ({ selectedRowIds, handleBulkActionSelect, tableID }) => { const [isDropdownOpen, toggleDropdown] = useState(false) // The format of selected row is {id1:true,id2:false ...} const rowsIDs = Object.keys(selectedRowIds).filter((selectedRowID) => selectedRowIds[selectedRowID] === true) let shouldDisableBulkAction = true if (rowsIDs.length !== 0) { shouldDisableBulkAction = false } return ( <OutsideClicker onClick={() => toggleDropdown(false)}> <div className="btn-group"> <button type="button" className="btn btn-light border-info" disabled={shouldDisableBulkAction} onClick={() => toggleDropdown(!isDropdownOpen)}> <FormattedMessage id="more-actions-button.title" defaultMessage="Bulk action" /> </button> <button id="dropdown-toggle" type="button" className="btn btn-light border-info dropdown-toggle dropdown-toggle-split" disabled={shouldDisableBulkAction} onClick={() => toggleDropdown(!isDropdownOpen)}> <span className="sr-only sr-only-focusable" aria-haspopup="true" aria-expanded="false"> Toggle Bulk action menu </span> </button> <div className={`dropdown-menu ${isDropdownOpen ? 'show' : ''}`} aria-labelledby="Bulk action list"> <BulkActionMenus tableID={tableID} rowsIDs={rowsIDs} handleBulkActionSelect={handleBulkActionSelect} /> </div> </div> </OutsideClicker> ) } export default ({ tableID, tableColumns, tableData, location = {}, handleBulkActionSelect, handleRowOptionsMenu, defaultHiddenColumns = [], enableColumnHide = false, enableRowOptions = false, enableRowSelection = false, enableColumnSort = false, }) => { let rowSelectionColumn = null let rowOptionsColumn = null const userPreferedHiddenColums = JSON.parse(localStorage.getItem(`${LOCALSTORAGE_KEY.userPrefTableHiddenCols}-${tableID}`)) ?? [] const hiddenColumns = _uniq([...userPreferedHiddenColums, ...defaultHiddenColumns]) if (enableRowSelection) { rowSelectionColumn = { id: 'table.selection', Header: ({ getToggleAllRowsSelectedProps }) => <RowCheckbox {...getToggleAllRowsSelectedProps()} />, Cell: ({ row }) => <RowCheckbox {...row.getToggleRowSelectedProps()} />, headerClassName: 'w-2rem', rowClassName: 'w-2rem', } } if (enableColumnHide || enableRowOptions) { rowOptionsColumn = { id: 'table.properties', Header: ({ setHiddenColumns }) => enableColumnHide && ( <ColumnToggler tableID={tableID} setHiddenColumns={setHiddenColumns} defaultHiddenColumns={hiddenColumns} /> ), Cell: ({ cell }) => enableRowOptions && ( <RowOptionsDropdown tableID={tableID} handleRowOptionsMenu={handleRowOptionsMenu} cell={cell} /> ), headerClassName: 'w-2rem', rowClassName: 'w-2rem', } } const columns = useMemo(() => tableColumns, [...tableColumns]) const data = useMemo(() => tableData) const searchInURL = location?.search ?? '' const searchParams = new URLSearchParams(searchInURL) let sortBy = '' let sortOrder = '' if (searchParams.has('sortBy')) { sortBy = searchParams.get('sortBy') } if (searchParams.has('sortOrder')) { sortOrder = searchParams.get('sortOrder') } const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow, state: { selectedRowIds }, } = useTable( { columns, data, initialState: { hiddenColumns, }, autoResetPage: false, autoResetSelectedRows: false, }, useRowSelect, (hooks) => { hooks.visibleColumns.push((columns) => [rowSelectionColumn, ...columns, rowOptionsColumn].filter((column) => column !== null), ) }, ) return ( <> <table {...getTableProps()} className="mb-4"> <thead> {headerGroups.map((headerGroup) => ( <tr {...headerGroup.getHeaderGroupProps()}> {headerGroup.headers.map((column) => { if (column.id !== 'table.properties' && column.id !== 'table.selection') { const isRowSortedIncreasing = column.isSortable && column.id === sortBy && sortOrder === 'inc' && enableColumnSort const isRowSortedDecreasingly = column.isSortable && column.id === sortBy && sortOrder === 'dec' && enableColumnSort const isRowNotSorted = column.isSortable && column.id !== sortBy && enableColumnSort return ( <th {...column.getHeaderProps()} key={column.id} className={column.headerClassName}> <small className="text-uppercase font-weight-bold">{column.render('Header')} </small> {isRowSortedIncreasing && ( <Link to={getSortLink(location, column.id, 'dec')}> <IconArrowUp className="text-primary" /> </Link> )} {isRowSortedDecreasingly && ( <Link to={getSortLink(location, column.id, 'inc')}> <IconArrowDown className="text-primary" /> </Link> )} {isRowNotSorted && ( <Link to={getSortLink(location, column.id, 'inc')}> <IconArrowUpDown /> </Link> )} </th> ) } // For anyother type of column render a normal column header return ( <th {...column.getHeaderProps()} key={column.id} className={column.headerClassName}> {column.render('Header')} </th> ) })} </tr> ))} </thead> <tbody {...getTableBodyProps()}> {rows.map((row) => { prepareRow(row) return ( <tr {...row.getRowProps()}> {row.cells.map((cell) => { // Render bs pills if value is array if (Array.isArray(cell.value)) { return ( <td {...cell.getCellProps()} className={cell.column.rowClassName}> {cell.value.map((cellOfArray, index) => { return ( <span key={`_pill-${index}`} className="badge badge-pill mr-2 text-uppercase" style={{ backgroundColor: cellOfArray.color, color: cellOfArray.textColor }}> {cellOfArray.value} </span> ) })} </td> ) } return ( <td {...cell.getCellProps()} className={cell.column.rowClassName}> {cell.render('Cell')} </td> ) })} </tr> ) })} </tbody> </table> {tableID.length !== 0 && enableRowSelection && ( <BulkActionsDropdown selectedRowIds={selectedRowIds} handleBulkActionSelect={handleBulkActionSelect} tableID={tableID} /> )} </> ) }
Editor is loading...