diff --git a/src/layouts/CoreItemsListView/CoreItemsListViewContent.tsx b/src/layouts/CoreItemsListView/CoreItemsListViewContent.tsx new file mode 100644 index 00000000..67a70c07 --- /dev/null +++ b/src/layouts/CoreItemsListView/CoreItemsListViewContent.tsx @@ -0,0 +1,240 @@ +import { Fragment } from "react"; +import { useNavigate } from "react-router-dom"; +import { styled } from "@mui/material/styles"; +import Box from "@mui/material/Box"; +import Divider from "@mui/material/Divider"; +import { usePageLayoutContext } from "@/app/PageLayoutContext/usePageLayoutContext"; +import { DataGrid, dataGridClassNames, type DataGridProps } from "@/components/DataGrid"; +import { + VirtualizedList, + ListFooter, + listClassNames, + type VirtualizedListProps, +} from "@/components/List"; +import { getTabPanelA11yProps } from "@/components/Tabs/helpers"; +import { + CoreContentViewLayout, + coreContentViewLayoutClassNames, + type CoreContentViewLayoutProps, +} from "@/layouts/CoreContentViewLayout"; +import { + listViewSettingsStore, + type ListViewSettingsStoreKey, +} from "@/stores/listviewSettingsStore"; +import { ListHeader, MobileListHeaderTabs } from "./ListHeader"; +import { ListViewHeaderToggleButtons } from "./ListViewHeaderToggleButtons"; +import { coreItemsListViewClassNames as listViewClassNames } from "./classNames"; +import { LIST_VIEW_MODES, type ListViewListName } from "./types"; +import type { ListViewAppPath } from "@/routes/appPaths"; +import type { GridEventListener, GridValidRowModel } from "@mui/x-data-grid"; + +/** + * Provides common styles/props/logic to all core-item list views. + */ +export const CoreItemsListViewContent = < + ListItemType extends Record, + DataGridItemType extends GridValidRowModel = ListItemType, +>({ + // TABLE/DataGrid PROPS: + tableProps, + // LIST/VirtualizedList PROPS: + lists, + renderItem, + virtualizedListProps = {}, + // HEADER-RELATED PROPS: + headerLabel, + itemViewBasePath, + headerComponents, + listViewSettingsStoreKey, + ...containerProps +}: CoreItemsListViewContentProps) => { + const nav = useNavigate(); + const { isMobilePageLayout } = usePageLayoutContext(); + const { viewMode, listVisibility } = + listViewSettingsStore[listViewSettingsStoreKey].useSubToStore(); + + const tryNavToItemView = ({ itemID }: { itemID?: string | number }) => { + if (itemID) nav(`${itemViewBasePath}/${encodeURIComponent(itemID)}`); + }; + + const handleClickDataGridRow: GridEventListener<"rowClick"> = ({ id }) => { + tryNavToItemView({ itemID: id }); + }; + + const handleClickListItem = (event: React.MouseEvent) => { + const { itemId: itemID } = event.currentTarget.dataset; + tryNavToItemView({ itemID }); + }; + + const numVisibleLists = listVisibility + ? Object.values(listVisibility).filter((isVisible) => isVisible).length + : 1; + + const showMobileListHeaderTabs = + isMobilePageLayout && + viewMode === LIST_VIEW_MODES.LIST && + lists.length > 1 && + listViewSettingsStoreKey !== "contacts"; /* <-- This last condition is implied by + `lists.length > 1`, but is included to help TS narrow `listViewSettingsStoreKey`. + Without this, TS complains when the key is passed to MobileListHeaderTabs. */ + + return ( + + + {headerComponents} + + } + {...containerProps} + > + + + rowDataItemName={headerLabel} + onRowClick={handleClickDataGridRow} + style={{ display: viewMode === LIST_VIEW_MODES.TABLE ? "flex" : "none" }} + logLevel={false} // silences "invalid height/width" err msg caused by display: none + {...tableProps} + /> + {showMobileListHeaderTabs && ( + + )} + {lists.map(({ listName, items, listComponentProps = {} }, index) => ( + + {index === 1 && ( + + )} + + {listName && !showMobileListHeaderTabs && } + + renderItem({ + item: items[index], + onClick: handleClickListItem, + ...(!!listName && { listName }), + }) + } + {...virtualizedListProps} + /> + + + ))} + + + ); +}; + +const StyledCoreContentViewLayout = styled(CoreContentViewLayout)(({ theme: { variables } }) => ({ + [`& .${coreContentViewLayoutClassNames.childrenContainer}`]: { + paddingBottom: 0, + }, + + "& *": { + whiteSpace: "nowrap", + }, + + // CONTENT CONTAINER: + [`& .${listViewClassNames.contentContainer}`]: { + position: "relative", + height: "100%", + minHeight: "100%", + width: "100%", + display: "flex", + flexDirection: "row", + justifyContent: "space-between", + + [`& .${dataGridClassNames.root}`]: { + height: variables.isMobilePageLayout ? "calc( 100% - 5rem )" : "calc( 100% - 7rem )", + marginBottom: "1rem !important", + }, + + [`& > hr.${listViewClassNames.listsDivider}`]: { + height: "auto", + alignSelf: "stretch", + minWidth: "1px", + margin: "0 clamp(0.5rem, 1.5%, 1rem)", + }, + + [`& > .${listViewClassNames.listContainer}`]: { + flexGrow: 1, + width: variables.isMobilePageLayout ? "100%" : "50%", + flexDirection: "column", + minHeight: "50vh", + + [`& .${listClassNames.virtualizedList.emptyPlaceHolderComponent.root}`]: { + height: "95%", // prevents scrollbar from appearing + }, + }, + }, +})); + +export type CoreItemsListViewContentProps< + ListItemType extends Record, + DataGridItemType extends GridValidRowModel = ListItemType, +> = { + // TABLE/DataGrid PROPS: + tableProps: Omit< + DataGridProps, + "rowDataItemName" | "onRowClick" | "style" | "logLevel" + >; + // LIST/VirtualizedList PROPS: + lists: Array<{ + listName?: ListViewListName; + items: Array; + listComponentProps?: VirtualizedListProps["componentProps"]; + }>; + renderItem: ListViewRenderItemFn; + virtualizedListProps?: Omit< + VirtualizedListProps, + "components" | "componentProps" | "totalCount" | "itemContent" + >; + // HEADER-RELATED PROPS: + headerLabel: Exclude; // made required + headerComponents?: CoreContentViewLayoutProps["headerComponents"]; // made optional + itemViewBasePath: ListViewAppPath; + listViewSettingsStoreKey: ListViewSettingsStoreKey; +} & Pick, "style" | "sx">; + +/** + * A function used by `VirtualizedList` to render a list item. + */ +export type ListViewRenderItemFn> = ({ + item, + onClick, + listName, +}: { + item: ListItemType; + onClick?: React.MouseEventHandler; + listName?: ListViewListName; +}) => React.ReactNode;