mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-28 16:06:41 +01:00
add sub-table breadcrumbs
This commit is contained in:
@@ -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 it’s 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 it’s 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>
|
||||
);
|
||||
}
|
||||
92
src/components/Table/BreadcrumbsSubTable.tsx
Normal file
92
src/components/Table/BreadcrumbsSubTable.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
110
src/components/Table/BreadcrumbsTableRoot.tsx
Normal file
110
src/components/Table/BreadcrumbsTableRoot.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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%" } },
|
||||
},
|
||||
|
||||
@@ -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 can’t 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
|
||||
|
||||
Reference in New Issue
Block a user