add sub-table breadcrumbs

This commit is contained in:
Sidney Alcantara
2022-06-09 15:55:47 +10:00
parent 45de389f47
commit 54d9cfc462
6 changed files with 220 additions and 191 deletions

View File

@@ -1,180 +0,0 @@
import { useAtom } from "jotai";
import {
useLocation,
useSearchParams,
Link as RouterLink,
} from "react-router-dom";
import { find, camelCase, uniq } from "lodash-es";
import {
Breadcrumbs as MuiBreadcrumbs,
BreadcrumbsProps,
Link,
Typography,
Tooltip,
} from "@mui/material";
import ReadOnlyIcon from "@mui/icons-material/EditOffOutlined";
import InfoTooltip from "@src/components/InfoTooltip";
import RenderedMarkdown from "@src/components/RenderedMarkdown";
import {
globalScope,
userRolesAtom,
tableDescriptionDismissedAtom,
tablesAtom,
} from "@src/atoms/globalScope";
import { ROUTES } from "@src/constants/routes";
import { spreadSx } from "@src/utils/ui";
export default function Breadcrumbs({ sx = [], ...props }: BreadcrumbsProps) {
const [userRoles] = useAtom(userRolesAtom, globalScope);
const [dismissed, setDismissed] = useAtom(
tableDescriptionDismissedAtom,
globalScope
);
const [tables] = useAtom(tablesAtom, globalScope);
const { pathname } = useLocation();
const id = pathname.replace(ROUTES.table + "/", "");
const [searchParams] = useSearchParams();
const tableSettings = find(tables, ["id", id]);
if (!tableSettings) return null;
const collection = id || tableSettings.collection;
const parentLabel = decodeURIComponent(searchParams.get("parentLabel") || "");
const breadcrumbs = collection.split("/");
const section = tableSettings.section;
const getLabel = (id: string) => find(tables, ["id", id])?.name || id;
return (
<MuiBreadcrumbs
aria-label="Table breadcrumbs"
{...props}
sx={[
{
fontSize: (theme) => theme.typography.h6.fontSize,
fontWeight: "medium",
color: "text.disabled",
"& .MuiBreadcrumbs-ol": {
userSelect: "none",
flexWrap: "nowrap",
whiteSpace: "nowrap",
},
"& .MuiBreadcrumbs-li": { display: "flex" },
},
...spreadSx(sx),
]}
>
{/* Section name */}
{section && (
<Link
component={RouterLink}
to={`${ROUTES.home}#${camelCase(section)}`}
variant="h6"
color="textSecondary"
underline="hover"
>
{section}
</Link>
)}
{breadcrumbs.map((crumb: string, index) => {
// If its the first breadcrumb, show with specific style
const crumbProps = {
key: index,
variant: "h6",
component: index === 0 ? "h1" : "div",
color:
index === breadcrumbs.length - 1 ? "textPrimary" : "textSecondary",
} as const;
// If its the last crumb, just show the label without linking
if (index === breadcrumbs.length - 1)
return (
<div
key={crumb || index}
style={{
display: "flex",
alignItems: "center",
gap: 8,
}}
>
<Typography {...crumbProps}>
{getLabel(crumb) || crumb.replace(/([A-Z])/g, " $1")}
</Typography>
{crumb === tableSettings.id && tableSettings.readOnly && (
<Tooltip
title={
userRoles.includes("ADMIN")
? "Table is read-only for non-ADMIN users"
: "Table is read-only"
}
>
<ReadOnlyIcon fontSize="small" sx={{ ml: 0.5 }} />
</Tooltip>
)}
{crumb === tableSettings.id && tableSettings.description && (
<InfoTooltip
description={
<div>
<RenderedMarkdown
children={tableSettings.description}
restrictionPreset="singleLine"
/>
</div>
}
buttonLabel="Table info"
tooltipProps={{
componentsProps: {
popper: { sx: { zIndex: "appBar" } },
tooltip: { sx: { maxWidth: "75vw" } },
} as any,
}}
defaultOpen={!dismissed.includes(tableSettings.id)}
onClose={() =>
setDismissed((d) => uniq([...d, tableSettings.id]))
}
/>
)}
</div>
);
// If odd: breadcrumb points to a document — link to rowRef
// FUTURE: show a picker here to switch between sub tables
if (index % 2 === 1)
return (
<Link
{...crumbProps}
component={RouterLink}
to={`${ROUTES.table}/${encodeURIComponent(
breadcrumbs.slice(0, index).join("/")
)}?rowRef=${breadcrumbs.slice(0, index + 1).join("%2F")}`}
underline="hover"
>
{getLabel(
parentLabel.split(",")[Math.ceil(index / 2) - 1] || crumb
)}
</Link>
);
// Otherwise, even: breadcrumb points to a Firestore collection
return (
<Link
{...crumbProps}
component={RouterLink}
to={`${ROUTES.table}/${encodeURIComponent(
breadcrumbs.slice(0, index + 1).join("/")
)}`}
underline="hover"
>
{getLabel(crumb) || crumb.replace(/([A-Z])/g, " $1")}
</Link>
);
})}
</MuiBreadcrumbs>
);
}

View File

@@ -0,0 +1,92 @@
import { useAtom } from "jotai";
import { Link as RouterLink, useSearchParams } from "react-router-dom";
import { camelCase } from "lodash-es";
import { Stack, Breadcrumbs, Link, Typography, Tooltip } from "@mui/material";
import MoreHorizIcon from "@mui/icons-material/MoreHoriz";
import ReadOnlyIcon from "@mui/icons-material/EditOffOutlined";
import { globalScope, userRolesAtom } from "@src/atoms/globalScope";
import { ROUTES } from "@src/constants/routes";
import { TableSettings } from "@src/types/table";
export interface IBreadcrumbsSubTableProps {
rootTableSettings: TableSettings;
subTableSettings: TableSettings;
rootTableLink: string;
parentLabel?: string;
}
export default function BreadcrumbsSubTable({
rootTableSettings,
subTableSettings,
rootTableLink,
parentLabel,
}: IBreadcrumbsSubTableProps) {
const [userRoles] = useAtom(userRolesAtom, globalScope);
const [searchParams] = useSearchParams();
const splitSubTableId = subTableSettings.id.split("/");
return (
<Stack direction="row" alignItems="center" spacing={1}>
<Breadcrumbs
sx={{
typography: "button",
fontSize: (theme) => theme.typography.h6.fontSize,
color: "text.disabled",
"& .MuiBreadcrumbs-ol": {
userSelect: "none",
flexWrap: "nowrap",
whiteSpace: "nowrap",
},
"& .MuiBreadcrumbs-li": { display: "flex" },
"& .MuiTypography-inherit": { typography: "h6" },
}}
>
<Link
component={RouterLink}
to={`${ROUTES.home}#${camelCase(rootTableSettings.section)}`}
color="text.secondary"
underline="hover"
>
{rootTableSettings.section}
</Link>
<Link
component={RouterLink}
to={rootTableLink}
color="text.secondary"
underline="hover"
>
{rootTableSettings.name}
</Link>
{splitSubTableId.length > 3 && (
<MoreHorizIcon style={{ position: "relative", top: 1 }} />
)}
<Typography variant="inherit" color="text.secondary">
{searchParams.get("parentLabel") ||
splitSubTableId[splitSubTableId.length - 2]}
</Typography>
<Typography variant="inherit" color="text.primary">
{subTableSettings.name}
</Typography>
</Breadcrumbs>
{rootTableSettings.readOnly && (
<Tooltip
title={
userRoles.includes("ADMIN")
? "Table is read-only for non-ADMIN users"
: "Table is read-only"
}
>
<ReadOnlyIcon fontSize="small" sx={{ ml: 0.5 }} />
</Tooltip>
)}
</Stack>
);
}

View File

@@ -0,0 +1,110 @@
import { useAtom } from "jotai";
import { useLocation, useParams, Link as RouterLink } from "react-router-dom";
import { find, camelCase, uniq } from "lodash-es";
import {
Stack,
StackProps,
Breadcrumbs,
Link,
Typography,
Tooltip,
} from "@mui/material";
import ReadOnlyIcon from "@mui/icons-material/EditOffOutlined";
import InfoTooltip from "@src/components/InfoTooltip";
import RenderedMarkdown from "@src/components/RenderedMarkdown";
import {
globalScope,
userRolesAtom,
tableDescriptionDismissedAtom,
tablesAtom,
} from "@src/atoms/globalScope";
import { ROUTES } from "@src/constants/routes";
/**
* Breadcrumbs is rendered by the Navigation component,
* so it does not have access to tableScope
*/
export default function BreadcrumbsTableRoot(props: StackProps) {
const { id } = useParams();
const [userRoles] = useAtom(userRolesAtom, globalScope);
const [dismissed, setDismissed] = useAtom(
tableDescriptionDismissedAtom,
globalScope
);
const [tables] = useAtom(tablesAtom, globalScope);
const tableSettings = find(tables, ["id", id]);
if (!tableSettings) return null;
return (
<Stack direction="row" alignItems="center" spacing={1} {...props}>
<Breadcrumbs
aria-label="Table breadcrumbs"
sx={{
typography: "button",
fontSize: (theme) => theme.typography.h6.fontSize,
color: "text.disabled",
"& .MuiBreadcrumbs-ol": {
userSelect: "none",
flexWrap: "nowrap",
whiteSpace: "nowrap",
},
"& .MuiBreadcrumbs-li": { display: "flex" },
"& .MuiTypography-inherit": { typography: "h6" },
}}
>
<Link
component={RouterLink}
to={`${ROUTES.home}#${camelCase(tableSettings.section)}`}
color="text.secondary"
underline="hover"
>
{tableSettings.section}
</Link>
<Typography variant="inherit" color="text.primary">
{tableSettings.name}
</Typography>
</Breadcrumbs>
{tableSettings.readOnly && (
<Tooltip
title={
userRoles.includes("ADMIN")
? "Table is read-only for non-ADMIN users"
: "Table is read-only"
}
>
<ReadOnlyIcon fontSize="small" sx={{ ml: 0.5 }} />
</Tooltip>
)}
{tableSettings.description && (
<InfoTooltip
description={
<div>
<RenderedMarkdown
children={tableSettings.description}
restrictionPreset="singleLine"
/>
</div>
}
buttonLabel="Table info"
tooltipProps={{
componentsProps: {
popper: { sx: { zIndex: "appBar" } },
tooltip: { sx: { maxWidth: "75vw" } },
} as any,
}}
defaultOpen={!dismissed.includes(tableSettings.id)}
onClose={() => setDismissed((d) => uniq([...d, tableSettings.id]))}
/>
)}
</Stack>
);
}

View File

@@ -30,7 +30,8 @@ export const useSubTableData = (
].join("/");
// if (parentLabels) subTablePath += `${parentLabels ?? ""},${label ?? ""}`;
// else subTablePath += encodeURIComponent(label ?? "");
// else
subTablePath += "?parentLabel=" + encodeURIComponent(label ?? "");
return { documentCount, label, subTablePath };
};

View File

@@ -1,5 +1,5 @@
import Logo from "@src/assets/Logo";
import Breadcrumbs from "@src/components/Table/Breadcrumbs";
import BreadcrumbsTableRoot from "@src/components/Table/BreadcrumbsTableRoot";
import { GrowProps } from "@mui/material";
export enum ROUTES {
@@ -59,7 +59,7 @@ export const ROUTE_TITLES = {
[ROUTES.table]: {
title: "Table",
titleComponent: (open, pinned) => (
<Breadcrumbs sx={{ ml: open && pinned ? -48 / 8 : 2 }} />
<BreadcrumbsTableRoot sx={{ ml: open && pinned ? -48 / 8 : 2 }} />
),
titleTransitionProps: { style: { transformOrigin: "0 50%" } },
},

View File

@@ -7,7 +7,7 @@ import { useLocation, useNavigate, useParams } from "react-router-dom";
import { find, isEqual } from "lodash-es";
import Modal from "@src/components/Modal";
import Breadcrumbs from "@src/components/Table/Breadcrumbs";
import BreadcrumbsSubTable from "@src/components/Table/BreadcrumbsSubTable";
import ErrorFallback from "@src/components/ErrorFallback";
import TableSourceFirestore from "@src/sources/TableSourceFirestore";
import TablePage from "./TablePage";
@@ -36,7 +36,7 @@ export default function ProvidedSubTablePage() {
const [currentUser] = useAtom(currentUserAtom, globalScope);
// Get table settings and the source column from root table
const [tableSettings] = useAtom(tableSettingsAtom, tableScope);
const [rootTableSettings] = useAtom(tableSettingsAtom, tableScope);
const [sourceColumn] = useAtom(
useMemo(
() =>
@@ -57,25 +57,31 @@ export default function ProvidedSubTablePage() {
// Must be compatible with `getTableSchemaPath`: tableId/rowId/subTableKey
// This is why we cant have a sub-table column fieldName !== key
const subTableId =
docPath?.replace(tableSettings.collection, tableSettings.id) +
docPath?.replace(rootTableSettings.collection, rootTableSettings.id) +
"/" +
subTableKey;
// Write fake tableSettings
const subTableSettings = {
...tableSettings,
...rootTableSettings,
collection: subTableCollection,
id: subTableId,
tableType: "primaryCollection" as "primaryCollection",
name: sourceColumn?.name || subTableKey,
name: sourceColumn?.name || subTableKey || "",
};
const rootTableLink = location.pathname.split("/" + ROUTES.subTable)[0];
return (
<Modal
title={<>Sub-table: {subTableCollection}</>}
onClose={() =>
navigate(location.pathname.split("/" + ROUTES.subTable)[0])
title={
<BreadcrumbsSubTable
rootTableSettings={rootTableSettings}
subTableSettings={subTableSettings}
rootTableLink={rootTableLink}
/>
}
onClose={() => navigate(rootTableLink)}
disableBackdropClick
disableEscapeKeyDown
fullScreen