diff --git a/src/custom/CatalogDesignTable/CatalogDesignTable.tsx b/src/custom/CatalogDesignTable/CatalogDesignTable.tsx new file mode 100644 index 00000000..8808b42d --- /dev/null +++ b/src/custom/CatalogDesignTable/CatalogDesignTable.tsx @@ -0,0 +1,150 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import _ from 'lodash'; +import { useEffect, useRef, useState } from 'react'; +import { PublishIcon } from '../../icons'; +import { CHARCOAL, useTheme } from '../../theme'; +import { Pattern } from '../CustomCatalog/CustomCard'; +import { useWindowDimensions } from '../Helpers/Dimension'; +import PromptComponent from '../Prompt'; +import { PromptRef } from '../Prompt/promt-component'; +import ResponsiveDataTable from '../ResponsiveDataTable'; +import UnpublishTooltipIcon from './UnpublishTooltipIcon'; + +interface CatalogDesignsTableProps { + patterns: Pattern[]; + filter: any; + columns: Array; + totalCount: number; + sortOrder: string; + setSortOrder: (order: string) => void; + pageSize: number; + setPageSize: (size: number) => void; + page: number; + setPage: (page: number) => void; + columnVisibility: Record; + colViews: Record | undefined; + handleBulkDeleteModal: (patterns: Pattern[], modalRef: React.RefObject) => void; + handleBulkpatternsDataUnpublishModal: ( + selected: any, + patterns: Pattern[], + modalRef: React.RefObject + ) => void; +} + +export const CatalogDesignsTable: React.FC = ({ + patterns, + filter, + columns = [], + totalCount = 0, + sortOrder = '', + setSortOrder, + pageSize = 10, + setPageSize, + page = 0, + setPage, + columnVisibility = {}, + colViews = {}, + handleBulkDeleteModal, + handleBulkpatternsDataUnpublishModal +}) => { + const [tableCols, updateCols] = useState>([]); + const { width } = useWindowDimensions(); + const smallScreen = width <= 360; + const theme = useTheme(); + const modalRef = useRef(null); + + useEffect(() => { + if (Array.isArray(columns) && columns.length > 0) { + updateCols(columns); + } + }, [columns]); + + const options: any = { + selectableRows: _.isNil(filter) ? 'none' : 'multiple', + serverSide: true, + filterType: 'multiselect', + responsive: smallScreen ? 'vertical' : 'standard', + count: totalCount, + rowsPerPage: pageSize, + page, + elevation: 0, + onTableChange: (action: string, tableState: any) => { + const sortInfo = tableState.announceText ? tableState.announceText.split(' : ') : []; + let order = ''; + if (tableState.activeColumn) { + order = `${columns[tableState.activeColumn].name} desc`; + } + switch (action) { + case 'changePage': + setPage(tableState.page); + break; + case 'changeRowsPerPage': + setPageSize(tableState.rowsPerPage); + break; + case 'sort': + if ( + sortInfo.length === 2 && + tableState.activeColumn !== undefined && + Array.isArray(columns) + ) { + if (sortInfo[1] === 'ascending') { + order = `${columns[tableState.activeColumn].name} asc`; + } else { + order = `${columns[tableState.activeColumn].name} desc`; + } + } + if (order !== sortOrder) { + setSortOrder(order); + } + break; + } + } + }; + + if (_.isNil(filter)) { + options.customToolbarSelect = (selected: any) => ( + handleBulkpatternsDataUnpublishModal(selected, patterns, modalRef)} + iconType="publish" + id={'unpublish-button'} + > + + + ); + } else { + options.onRowsDelete = (rowsDeleted: any) => { + const selectedPatterns = rowsDeleted.data.map(({ dataIndex }: any) => patterns[dataIndex]); + handleBulkDeleteModal(selectedPatterns, modalRef); + return false; + }; + } + + if (!Array.isArray(tableCols) || tableCols.length === 0) { + return null; + } + + return ( + <> + + + + ); +}; + +export default CatalogDesignsTable; diff --git a/src/custom/CatalogDesignTable/TableVisibilityControl.tsx b/src/custom/CatalogDesignTable/TableVisibilityControl.tsx new file mode 100644 index 00000000..a153aa0c --- /dev/null +++ b/src/custom/CatalogDesignTable/TableVisibilityControl.tsx @@ -0,0 +1,41 @@ +import React, { Dispatch, SetStateAction } from 'react'; +import { CustomColumnVisibilityControl } from '../CustomColumnVisibilityControl'; +import { CustomColumn } from '../CustomColumnVisibilityControl/CustomColumnVisibilityControl'; +import { ViewSwitch } from './ViewSwitch'; + +type TypeView = 'grid' | 'table'; + +interface TableVisibilityControlProps { + viewType: TypeView; + setViewType: (view: TypeView) => void; + filteredColumns: CustomColumn[]; + columnVisibility: Record; + setColumnVisibility: Dispatch>>; + viewSwitchDisabled?: boolean; +} + +export const TableVisibilityControl: React.FC = ({ + viewType, + setViewType, + filteredColumns, + columnVisibility, + setColumnVisibility, + viewSwitchDisabled = false +}) => { + return ( +
+ {viewType !== 'grid' && ( + + )} + +
+ ); +}; diff --git a/src/custom/CatalogDesignTable/UnpublishTooltipIcon.tsx b/src/custom/CatalogDesignTable/UnpublishTooltipIcon.tsx new file mode 100644 index 00000000..69b9deb2 --- /dev/null +++ b/src/custom/CatalogDesignTable/UnpublishTooltipIcon.tsx @@ -0,0 +1,53 @@ +import { ReactNode } from 'react'; +import { IconButton } from '../../base'; +import { useTheme } from '../../theme'; +import { HOVER_DELETE } from '../../theme/colors/colors'; +import { CustomTooltip } from '../CustomTooltip'; +import { IconWrapper } from '../ResponsiveDataTable'; + +interface UnpublishTooltipIconProps { + children: ReactNode; + onClick: () => void; + title: string; + iconType: 'delete' | 'publish'; + id: string; + style?: object; + placement?: 'bottom' | 'top' | 'left' | 'right'; + disabled?: boolean; +} + +function UnpublishTooltipIcon({ + children, + onClick, + title, + iconType, + id, + style, + placement, + disabled = false +}: UnpublishTooltipIconProps) { + const theme = useTheme(); + return ( + + + + {children} + + + + ); +} + +export default UnpublishTooltipIcon; diff --git a/src/custom/CatalogDesignTable/ViewSwitch.tsx b/src/custom/CatalogDesignTable/ViewSwitch.tsx new file mode 100644 index 00000000..7ac8a1a2 --- /dev/null +++ b/src/custom/CatalogDesignTable/ViewSwitch.tsx @@ -0,0 +1,66 @@ +/** + * Renders a switch component for toggling between grid and table view. + * + * @typedef {("grid" | "table")} TypeView + * @typedef {object} Props + * @prop {TypeView} props.view - The current view type ("grid" or "table"). + * @prop {Function} props.changeView - The function to change the view type. + */ + +import { IconButton } from '@mui/material'; +import { GridViewIcon, TableViewIcon } from '../../icons'; +import { useTheme } from '../../theme'; +import { CustomTooltip } from '../CustomTooltip'; + +type TypeView = 'grid' | 'table'; + +interface ViewSwitchProps { + view: TypeView; + changeView: (view: TypeView) => void; + height?: string; + style?: React.CSSProperties; + disabled?: boolean; +} + +export const ViewSwitch: React.FC = ({ + view, + changeView, + height = '3rem', + style = {}, + disabled = false +}) => { + const handleClick = () => { + changeView(view === 'grid' ? 'table' : 'grid'); + }; + + const Icon = view === 'grid' ? TableViewIcon : GridViewIcon; + const label = view === 'grid' ? 'Table View' : 'Grid View'; + const theme = useTheme(); + + return ( + + + + + + + + ); +}; diff --git a/src/custom/CatalogDesignTable/columnConfig.tsx b/src/custom/CatalogDesignTable/columnConfig.tsx new file mode 100644 index 00000000..d46f4404 --- /dev/null +++ b/src/custom/CatalogDesignTable/columnConfig.tsx @@ -0,0 +1,372 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { MUIDataTableColumn, MUIDataTableMeta } from 'mui-datatables'; +import { FacebookShareButton, LinkedinShareButton, TwitterShareButton } from 'react-share'; +import { Avatar, Box, Grid, Typography } from '../../base'; +import { iconMedium } from '../../constants/iconsSizes'; +import { + ChainIcon, + CopyIcon, + DownloadIcon, + FacebookIcon, + KanvasIcon, + LinkedinIcon, + PersonIcon, + PublishIcon, + TwitterIcon +} from '../../icons'; +import { downloadFilter, downloadYaml } from '../CatalogDetail/helper'; +import { RESOURCE_TYPES } from '../CatalogDetail/types'; +import { Pattern } from '../CustomCatalog/CustomCard'; +import { CustomTooltip } from '../CustomTooltip'; +import { ConditionalTooltip } from '../Helpers/CondtionalTooltip'; +import { DataTableEllipsisMenu } from '../ResponsiveDataTable'; +import { NameDiv } from './style'; + +export type ColView = [string, 'na' | 'xs' | 'l']; + +export const colViews: ColView[] = [ + ['id', 'na'], + ['name', 'xs'], + ['first_name', 'xs'], + ['last_name', 'na'], + ['created_at', 'na'], + ['updated_at', 'l'], + ['design_type', 'xs'], + ['class', 'l'], + ['view_count', 'na'], + ['download_count', 'na'], + ['clone_count', 'na'], + ['deployment_count', 'na'], + ['share_count', 'na'], + ['actions', 'xs'] +]; + +interface ColumnConfigProps { + handleShowDetails: (design: Pattern) => void; + handleCloneClick: (design: Pattern) => void; + handleCopyUrl: (design: Pattern) => void; + handleOpenPlayground: (design: Pattern) => void; + handleUnpublish?: (design: Pattern) => void; + maxWidth?: boolean; + getCatalogUrl: (type: string, name: string) => string; + type?: string; + theme?: any; + showUnpublish?: boolean; + currentUserId?: string; + isCloneDisabled?: boolean; + isUnpublishDisabled?: boolean; +} + +interface ActionItem { + title: string; + icon?: JSX.Element; + onClick?: () => void; + disabled?: boolean; + customComponent?: JSX.Element; + type?: string; +} + +export const createDesignColumns = ({ + handleShowDetails, + handleCloneClick, + handleCopyUrl, + handleOpenPlayground, + handleUnpublish = () => {}, + maxWidth = true, + getCatalogUrl, + type, + theme, + showUnpublish, + currentUserId, + isCloneDisabled, + isUnpublishDisabled +}: ColumnConfigProps): MUIDataTableColumn[] => { + const cleanedType = type?.replace('my-', '').replace(/s$/, ''); + const getColumnValue = (tableMeta: MUIDataTableMeta, targetColumn: string): any => { + //@ts-ignore + const rowData = tableMeta.tableData[tableMeta.rowIndex] as Pattern; + return (rowData as any)[targetColumn] || ''; + }; + + return [ + { + name: 'id', + label: 'ID', + options: { + filter: false, + customBodyRender: (value: string) => + } + }, + { + name: 'name', + label: 'Name', + options: { + filter: false, + sort: true, + searchable: true, + customBodyRender: (value: string, tableMeta: MUIDataTableMeta) => { + //@ts-ignore + const design = tableMeta.tableData[tableMeta.rowIndex] as Pattern; + return handleShowDetails(design)}>{value}; + } + } + }, + { + name: 'avatar_url', + label: 'Avatar', + options: { + display: false + } + }, + { + name: 'user_id', + label: 'User ID', + options: { + display: false + } + }, + { + name: 'first_name', + label: 'Author', + options: { + filter: false, + sort: true, + searchable: true, + customBodyRender: (_: string, tableMeta: MUIDataTableMeta) => { + const firstName = getColumnValue(tableMeta, 'first_name'); + const lastName = getColumnValue(tableMeta, 'last_name'); + const avatar_url = getColumnValue(tableMeta, 'avatar_url'); + const user_id = getColumnValue(tableMeta, 'user_id'); + const displayName = + firstName && lastName + ? `${firstName} ${lastName}` + : firstName + ? firstName + : lastName + ? lastName + : ''; + + return ( + img': { mr: 2, flexShrink: 0 } }}> + + + + +
+ { + window.location.href = `/user/${user_id}`; + }} + > + {!avatar_url && } + +
+
+
+
+ {maxWidth && ( + + {displayName} + + )} +
+
+ ); + } + } + }, + { + name: 'last_name', + label: 'Last Name', + options: { + display: false + } + }, + { + name: 'created_at', + label: 'Created At', + options: { + filter: false, + sort: true, + searchable: true + } + }, + { + name: 'updated_at', + label: 'Updated At', + options: { + filter: false, + sort: true, + searchable: true + } + }, + { + name: 'design_type', + label: 'Type', + options: { + filter: true, + sort: false, + searchable: true + } + }, + { + name: 'class', + label: 'Class', + options: { + filter: true, + sort: false, + searchable: true + } + }, + { + name: 'view_count', + label: 'Opens', + options: { + filter: false, + sort: true + } + }, + { + name: 'download_count', + label: 'Downloads', + options: { + filter: false, + sort: true + } + }, + { + name: 'clone_count', + label: 'Clones', + options: { + filter: false, + sort: true + } + }, + { + name: 'deployment_count', + label: 'Deploys', + options: { + filter: false, + sort: true + } + }, + { + name: 'share_count', + label: 'Shares', + options: { + filter: false, + sort: true + } + }, + { + name: 'actions', + label: 'Actions', + options: { + filter: false, + sort: false, + searchable: false, + setCellHeaderProps: () => ({ align: 'center' }), + setCellProps: () => ({ align: 'center' }), + customBodyRender: (_: any, tableMeta: MUIDataTableMeta) => { + //@ts-ignore + const rowData = tableMeta.tableData[tableMeta.rowIndex] as Pattern; + + function constructMessage() { + const currentUser = rowData?.user_id === currentUserId; + if (currentUser) { + return `Check out my design "${rowData?.name}" on Layer5's Catalog`; + } else { + return `Check out ${ + rowData?.first_name + ' ' + rowData.last_name + }'s design "${rowData?.name}" on Layer5's Catalog`; + } + } + const baseActions: ActionItem[] = [ + { + title: 'Clone', + onClick: () => handleCloneClick(rowData), + disabled: isCloneDisabled, + icon: + }, + { + title: 'Download', + onClick: () => { + cleanedType === RESOURCE_TYPES.FILTERS + ? downloadFilter(rowData.id, rowData.name) + : downloadYaml(rowData.pattern_file, rowData.name); + }, + icon: + }, + { + title: 'Copy Link', + onClick: () => handleCopyUrl(rowData), + icon: + }, + { + title: 'Share Design via Socials', + type: 'share-social', + customComponent: ( +
+ + + + + + + + + +
+ ) + }, + { + title: 'Open in playground', + onClick: () => handleOpenPlayground(rowData), + icon: + } + ]; + // TODO: make this unbpublish action work for playgroud + const actionsList = showUnpublish + ? [ + ...baseActions.slice(0, 2), + { + title: 'Unpublish', + onClick: () => handleUnpublish(rowData), + disabled: isUnpublishDisabled, + icon: + }, + ...baseActions.slice(2) + ] + : baseActions; + + //@ts-ignore + return ; + } + } + } + ]; +}; diff --git a/src/custom/CatalogDesignTable/index.ts b/src/custom/CatalogDesignTable/index.ts new file mode 100644 index 00000000..fcab0208 --- /dev/null +++ b/src/custom/CatalogDesignTable/index.ts @@ -0,0 +1,5 @@ +import CatalogDesignsTable from './CatalogDesignTable'; +import { colViews, createDesignColumns } from './columnConfig'; +export { TableVisibilityControl } from './TableVisibilityControl'; +export { ViewSwitch } from './ViewSwitch'; +export { CatalogDesignsTable, colViews, createDesignColumns }; diff --git a/src/custom/CatalogDesignTable/style.tsx b/src/custom/CatalogDesignTable/style.tsx new file mode 100644 index 00000000..502129fe --- /dev/null +++ b/src/custom/CatalogDesignTable/style.tsx @@ -0,0 +1,11 @@ +import { styled } from '@mui/material'; + +export const NameDiv = styled('div')({ + cursor: 'pointer', + fontWeight: 'bold', + textDecoration: 'none', + + '&:hover': { + textDecoration: 'underline' + } +}); diff --git a/src/custom/CustomCatalog/CustomCard.tsx b/src/custom/CustomCatalog/CustomCard.tsx index 0a105755..13233bb6 100644 --- a/src/custom/CustomCatalog/CustomCard.tsx +++ b/src/custom/CustomCatalog/CustomCard.tsx @@ -45,6 +45,9 @@ export interface Pattern { first_name: string; last_name: string; }; + first_name?: string; + last_name?: string; + avatar_url: string; name: string; download_count: number; clone_count: number; diff --git a/src/custom/CustomColumnVisibilityControl/CustomColumnVisibilityControl.tsx b/src/custom/CustomColumnVisibilityControl/CustomColumnVisibilityControl.tsx index 36bfc06b..7541891d 100644 --- a/src/custom/CustomColumnVisibilityControl/CustomColumnVisibilityControl.tsx +++ b/src/custom/CustomColumnVisibilityControl/CustomColumnVisibilityControl.tsx @@ -27,7 +27,8 @@ export interface CustomColumn { export function CustomColumnVisibilityControl({ columns, id, - customToolsProps + customToolsProps, + style }: CustomColumnVisibilityControlProps): JSX.Element { const [open, setOpen] = React.useState(false); const [anchorEl, setAnchorEl] = React.useState(null); @@ -60,6 +61,7 @@ export function CustomColumnVisibilityControl({ arrow /> - {children} + {children} ); } diff --git a/src/custom/Prompt/promt-component.tsx b/src/custom/Prompt/promt-component.tsx index 9d1cba78..44022d73 100644 --- a/src/custom/Prompt/promt-component.tsx +++ b/src/custom/Prompt/promt-component.tsx @@ -35,7 +35,7 @@ interface ShowParams { showInfoIcon?: string; } -interface PromptRef { +export interface PromptRef { show: (params: ShowParams) => Promise; } diff --git a/src/custom/ResponsiveDataTable.tsx b/src/custom/ResponsiveDataTable.tsx index 9216e5bd..235ec5e9 100644 --- a/src/custom/ResponsiveDataTable.tsx +++ b/src/custom/ResponsiveDataTable.tsx @@ -17,7 +17,8 @@ export const IconWrapper = styled('div')<{ disabled?: boolean }>(({ disabled = f export const DataTableEllipsisMenu: React.FC<{ actionsList: NonNullable['actionsList']; -}> = ({ actionsList }) => { + theme?: Theme; +}> = ({ actionsList, theme }) => { const [anchorEl, setAnchorEl] = React.useState(null); const [isSocialShareOpen, setIsSocialShareOpen] = React.useState(false); @@ -43,13 +44,59 @@ export const DataTableEllipsisMenu: React.FC<{ return ( <> - } arrow /> - + } + arrow + /> + {actionsList && - actionsList.map((action, index) => ( - - {action.type === 'share-social' ? ( - <> + actionsList.map((action, index) => { + if (action.type === 'share-social') { + return [ + handleActionClick(action)} + disabled={action.disabled} + > + + + + + {action.title} + + , + + {action.customComponent} + + ]; + } else { + return ( + handleActionClick(action)} disabled={action.disabled} > - - - - {action.title} + {action.icon} + + {action.title} + - - {action.customComponent} - - - ) : ( - <> - - handleActionClick(action)} - disabled={action.disabled} - > - {action.icon} - {action.title} - - - - )} - - ))} + + ); + } + })} ); }; -const dataTableTheme = (theme: Theme) => +const dataTableTheme = (theme: Theme, backgroundColor?: string) => createTheme({ components: { + MUIDataTable: { + styleOverrides: { + paper: { + background: backgroundColor || theme.palette.background.default, + maxWidth: '-moz-available' + } + } + }, MuiTable: { styleOverrides: { root: { @@ -103,7 +138,7 @@ const dataTableTheme = (theme: Theme) => '@media (max-width: 500px)': { wordWrap: 'break-word' }, - background: theme.palette.background.constant?.table, + background: backgroundColor || theme.palette.background.constant?.table, color: theme.palette.text.default } } @@ -118,7 +153,8 @@ const dataTableTheme = (theme: Theme) => root: { fontWeight: 'bold', textTransform: 'uppercase', - color: theme.palette.text.default + color: theme.palette.text.default, + backgroundColor: backgroundColor || theme.palette.background.constant?.table } } }, @@ -187,7 +223,7 @@ const dataTableTheme = (theme: Theme) => MUIDataTableSelectCell: { styleOverrides: { headerCell: { - background: theme.palette.background.constant?.table + background: backgroundColor || theme.palette.background.constant?.table } } }, @@ -249,8 +285,8 @@ export interface ResponsiveDataTableProps { theme?: object; colViews?: Record | undefined; rowsPerPageOptions?: number[] | undefined; + backgroundColor?: string; } - const ResponsiveDataTable = ({ data, columns, @@ -258,7 +294,9 @@ const ResponsiveDataTable = ({ tableCols, updateCols, columnVisibility, - rowsPerPageOptions = [10, 25, 50, 100], // Default and standard page size options + rowsPerPageOptions = [10, 25, 50, 100], + theme, + backgroundColor, ...props }: ResponsiveDataTableProps): JSX.Element => { const formatDate = (date: Date): string => { @@ -347,9 +385,7 @@ const ResponsiveDataTable = ({ } }); updateCols && updateCols([...columns]); - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [columnVisibility, updateCols]); + }, [columnVisibility, updateCols, columns]); React.useEffect(() => { updateColumnsEffect(); @@ -360,8 +396,16 @@ const ResponsiveDataTable = ({ Checkbox: Checkbox }; + const finalTheme = (baseTheme: Theme) => { + const defaultTheme = dataTableTheme(baseTheme, backgroundColor); + if (theme) { + return createTheme(defaultTheme, typeof theme === 'function' ? theme(baseTheme) : theme); + } + return defaultTheme; + }; + return ( - + = ({ + width = '24', + height = '28.8', + fill, + opacity, + style = {} +}) => ( + + + +); + +export default GridViewIcon; diff --git a/src/icons/GridView/index.ts b/src/icons/GridView/index.ts new file mode 100644 index 00000000..43f10c66 --- /dev/null +++ b/src/icons/GridView/index.ts @@ -0,0 +1 @@ +export { default as GridViewIcon } from './GridViewIcon'; diff --git a/src/icons/TableView/TableViewIcon.tsx b/src/icons/TableView/TableViewIcon.tsx new file mode 100644 index 00000000..6bd33018 --- /dev/null +++ b/src/icons/TableView/TableViewIcon.tsx @@ -0,0 +1,31 @@ +import React from 'react'; + +interface TableViewIconProps { + width?: string; + height?: string; + fill?: string; + opacity?: number; + style?: React.CSSProperties; +} + +export const TableViewIcon: React.FC = ({ + width = '24', + height = '28.8', + fill, + opacity, + style = {} +}) => ( + + + +); + +export default TableViewIcon; diff --git a/src/icons/TableView/index.ts b/src/icons/TableView/index.ts new file mode 100644 index 00000000..742c6658 --- /dev/null +++ b/src/icons/TableView/index.ts @@ -0,0 +1 @@ +export { default as TableViewIcon } from './TableViewIcon'; diff --git a/src/icons/index.ts b/src/icons/index.ts index 6a4bc8df..22a3a721 100644 --- a/src/icons/index.ts +++ b/src/icons/index.ts @@ -53,6 +53,7 @@ export * from './EmptyStyle'; export * from './Environment'; export * from './ExternalLink'; export * from './Feedback'; +export * from './GridView'; export * from './HelpIcon'; export * from './Idea'; export * from './InfoOutlined'; @@ -88,6 +89,7 @@ export * from './Share'; export * from './SocialMedial'; export * from './Star'; export * from './Success'; +export * from './TableView'; export * from './TerminalIcon'; export * from './Toolkit'; export * from './Touch';