TryGhost/Ghost

View on GitHub
apps/admin-x-design-system/src/global/table/DynamicTable.tsx

Summary

Maintainability
A
55 mins
Test Coverage
import React from 'react';
import {Heading} from '../..';
import clsx from 'clsx';
import {tableRowHoverBgClasses} from '../TableRow';

export type DynamicTableColumn = {
    title: string;
    minWidth?: string;
    maxWidth?: string;
    noWrap?: boolean;
    align?: 'left' | 'center' | 'right';
    valign?: 'top' | 'middle' | 'bottom';
    hidden?: boolean;
    disableRowClick?: boolean;
}

export type DynamicTableRow = {
    cells: React.ReactNode[];
    onClick?: () => void;
}

export interface DynamicTableProps {
    columns: DynamicTableColumn[];
    rows: DynamicTableRow[];
    horizontalScrolling?: boolean;
    absolute?: boolean;
    stickyHeader?: boolean;
    hideHeader?: boolean;
    headerBorder?: boolean;

    /**
     * Set this parameter if the table is the main content in a viewcontainer or on a page
     */
    singlePageTable?: boolean;
    pageHasSidebar?: boolean;

    border?: boolean;
    footerBorder?:boolean;
    footer?: React.ReactNode;
    stickyFooter?: boolean;
    containerClassName?: string;
    tableContainerClassName?: string;
    tableClassName?: string;
    thClassName?: string;
    tdClassName?: string;
    cellClassName?: string;
    trClassName?: string;
    footerClassName?: string;
}

const DynamicTable: React.FC<DynamicTableProps> = ({
    columns,
    rows,
    horizontalScrolling = false,
    absolute = false,
    stickyHeader = false,
    hideHeader = false,
    headerBorder = true,
    border = true,
    footer,
    footerBorder = true,
    stickyFooter = false,
    singlePageTable = false,
    pageHasSidebar = true,
    containerClassName,
    tableContainerClassName,
    tableClassName,
    thClassName,
    tdClassName,
    cellClassName,
    trClassName,
    footerClassName
}) => {
    let headerColID = 0;
    let rowID = 0;

    containerClassName = clsx(
        'flex max-h-full w-full flex-col',
        (stickyHeader || stickyFooter || absolute) ? 'absolute inset-0' : 'relative',
        containerClassName
    );

    tableContainerClassName = clsx(
        'flex-auto overflow-x-auto',
        !horizontalScrolling && 'w-full max-w-full',
        (singlePageTable && (stickyHeader || stickyFooter || absolute)) && `px-[4vw] tablet:px-12 ${pageHasSidebar ? 'min-[1640px]:px-[calc((100%-1320px)/2+48px)]' : 'xl:px-[calc((100%-1320px)/2+48px)]'}`,
        tableContainerClassName
    );

    tableClassName = clsx(
        'h-full max-h-full min-w-full flex-auto table-fixed',
        tableClassName
    );

    thClassName = clsx(
        'last-child:pr-5 bg-white py-3 text-left dark:bg-black [&:not(:first-child)]:pl-5',
        thClassName
    );

    tdClassName = clsx(
        'w-full border-b group-hover:border-grey-200 dark:group-hover:border-grey-900',
        border ? 'border-grey-200 dark:border-grey-900' : 'border-transparent',
        tdClassName
    );

    cellClassName = clsx(
        'flex h-full py-4',
        cellClassName
    );

    trClassName = clsx(
        'group',
        tableRowHoverBgClasses,
        trClassName
    );

    footerClassName = clsx(
        'bg-white dark:bg-black',
        (singlePageTable && stickyFooter) && `mx-[4vw] tablet:mx-12 ${pageHasSidebar ? 'min-[1640px]:mx-[calc((100%-1320px)/2+48px)]' : 'xl:mx-[calc((100%-1320px)/2+48px)]'}`,
        footer && 'py-4',
        stickyFooter && 'sticky inset-x-0 bottom-0',
        footerBorder && 'border-t border-grey-200 dark:border-grey-900',
        footerClassName
    );

    const footerContents = <footer className={footerClassName}>{footer}</footer>;

    return (
    // Outer container for testing. Should not be part of the table component
    // <div className='h-[40vh]'>

        <div className={containerClassName}>
            <div className={tableContainerClassName}>
                <table className={tableClassName}>
                    {!hideHeader &&
                    <thead className={stickyHeader ? 'sticky top-0' : ''}>
                        <tr>
                            {columns.map((column) => {
                                headerColID = headerColID + 1;
                                const thMaxWidth: string = column.maxWidth || 'auto';
                                const thMinWidth: string = column.minWidth || 'auto';
                                const thStyles = {
                                    maxWidth: thMaxWidth,
                                    minWidth: thMinWidth,
                                    width: thMaxWidth
                                };
                                return (
                                    <th key={'head-' + headerColID} className={thClassName} style={thStyles}>
                                        <Heading className='truncate' level={6}>{column.title}</Heading>
                                    </th>);
                            })}
                        </tr>
                        {headerBorder && (
                            <tr>
                                <th className='h-px bg-grey-200 p-0 dark:bg-grey-900' colSpan={columns.length}></th>
                            </tr>
                        )}
                    </thead>
                    }
                    <tbody>
                        {rows.map((row) => {
                            let colID = 0;
                            rowID = rowID + 1;
                            return <tr key={'row-' + rowID} className={trClassName}>
                                {row.cells.map((cell) => {
                                    const currentColumn: DynamicTableColumn = columns[colID] || {title: ''};

                                    let customTdClasses = tdClassName;
                                    customTdClasses = clsx(
                                        customTdClasses,
                                        // currentColumn.noWrap ? 'truncate' : '',
                                        currentColumn.align === 'center' && 'text-center',
                                        currentColumn.align === 'right' && 'text-right'
                                    );

                                    if (rowID === rows.length && footerBorder) {
                                        customTdClasses = clsx(
                                            customTdClasses,
                                            'border-none'
                                        );
                                    }

                                    const tdMaxWidth: string = (currentColumn !== undefined && currentColumn.maxWidth) || 'auto';
                                    const tdMinWidth: string = (currentColumn !== undefined && currentColumn.minWidth) || 'auto';
                                    const tdStyles = {
                                        maxWidth: tdMaxWidth,
                                        minWidth: tdMinWidth,
                                        width: tdMaxWidth
                                    };
                                    let customCellClasses = cellClassName;
                                    customCellClasses = clsx(
                                        customCellClasses,
                                        colID !== 0 && 'pl-5',
                                        (colID === columns.length - 1) && 'pr-5',
                                        currentColumn.noWrap ? 'truncate' : '',
                                        currentColumn.valign === 'middle' || !currentColumn.valign && 'items-center',
                                        currentColumn.valign === 'top' && 'items-start',
                                        currentColumn.valign === 'bottom' && 'items-end'
                                    );
                                    if (row.onClick && !currentColumn.disableRowClick) {
                                        customCellClasses = clsx(
                                            customCellClasses,
                                            'cursor-pointer'
                                        );
                                    }
                                    if (currentColumn.hidden) {
                                        customCellClasses = clsx(
                                            customCellClasses,
                                            'opacity-0 group-hover:opacity-100'
                                        );
                                    }
                                    const data = (
                                        <td key={colID} className={customTdClasses} style={tdStyles}>
                                            <div className={customCellClasses} onClick={(row.onClick && !currentColumn.disableRowClick) ? row.onClick : (() => {})}>{cell}</div>
                                        </td>
                                    );
                                    colID = colID + 1;
                                    return data;
                                })}
                            </tr>;
                        })}
                    </tbody>
                </table>
                {!stickyFooter && footerContents}
            </div>
            {stickyFooter && footerContents}
        </div>

    // </div>
    );
};

export default DynamicTable;