mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-29 00:16:39 +01:00
start work on table data
This commit is contained in:
@@ -1 +1 @@
|
||||
{"kind":"identitytoolkit#DownloadAccountResponse","users":[{"localId":"26CJMrwlouNRwkiLofNK07DNgKhw","createdAt":"1651022832613","lastLoginAt":"1651297974462","displayName":"Admin User","photoUrl":"","customAttributes":"{\"roles\": [\"ADMIN\"]}","providerUserInfo":[{"providerId":"google.com","rawId":"abc123","federatedId":"abc123","displayName":"Admin User","email":"admin@example.com"}],"validSince":"1651195467","email":"admin@example.com","emailVerified":true,"disabled":false,"lastRefreshAt":"2022-04-30T08:44:58.158Z"},{"localId":"3xTRVPnJGT2GE6lkiWKZp1jShuXj","createdAt":"1651023059442","lastLoginAt":"1651223181908","displayName":"Editor User","providerUserInfo":[{"providerId":"google.com","rawId":"1535779573397289142795231390488730790451","federatedId":"1535779573397289142795231390488730790451","displayName":"Editor User","email":"editor@example.com"}],"validSince":"1651195467","email":"editor@example.com","emailVerified":true,"disabled":false,"lastRefreshAt":"2022-04-30T08:44:53.855Z"}]}
|
||||
{"kind":"identitytoolkit#DownloadAccountResponse","users":[{"localId":"26CJMrwlouNRwkiLofNK07DNgKhw","createdAt":"1651022832613","lastLoginAt":"1651630548960","displayName":"Admin User","photoUrl":"","customAttributes":"{\"roles\": [\"ADMIN\"]}","providerUserInfo":[{"providerId":"google.com","rawId":"abc123","federatedId":"abc123","displayName":"Admin User","email":"admin@example.com"}],"validSince":"1651630530","email":"admin@example.com","emailVerified":true,"disabled":false,"lastRefreshAt":"2022-05-04T02:15:48.960Z"},{"localId":"3xTRVPnJGT2GE6lkiWKZp1jShuXj","createdAt":"1651023059442","lastLoginAt":"1651223181908","displayName":"Editor User","providerUserInfo":[{"providerId":"google.com","rawId":"1535779573397289142795231390488730790451","federatedId":"1535779573397289142795231390488730790451","displayName":"Editor User","email":"editor@example.com"}],"validSince":"1651630530","email":"editor@example.com","emailVerified":true,"disabled":false}]}
|
||||
Binary file not shown.
Binary file not shown.
@@ -31,6 +31,8 @@ const SetupPage = lazy(() => import("@src/pages/Setup" /* webpackChunkName: "Set
|
||||
|
||||
// prettier-ignore
|
||||
const TablesPage = lazy(() => import("@src/pages/Tables" /* webpackChunkName: "TablesPage" */));
|
||||
// prettier-ignore
|
||||
const TablePage = lazy(() => import("@src/pages/TableTest" /* webpackChunkName: "TablePage" */));
|
||||
|
||||
// prettier-ignore
|
||||
const UserSettingsPage = lazy(() => import("@src/pages/Settings/UserSettings" /* webpackChunkName: "UserSettingsPage" */));
|
||||
@@ -86,6 +88,11 @@ export default function App() {
|
||||
/>
|
||||
<Route path={ROUTES.tables} element={<TablesPage />} />
|
||||
|
||||
<Route path={ROUTES.table}>
|
||||
<Route index element={<Navigate to={ROUTES.tables} replace />} />
|
||||
<Route path=":id" element={<TablePage />} />
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
path={ROUTES.settings}
|
||||
element={<Navigate to={ROUTES.userSettings} replace />}
|
||||
|
||||
@@ -3,8 +3,12 @@ import { sortBy } from "lodash-es";
|
||||
import { ThemeOptions } from "@mui/material";
|
||||
|
||||
import { userRolesAtom } from "./auth";
|
||||
import { UpdateDocFunction, UpdateCollectionFunction } from "@src/atoms/types";
|
||||
import { UserSettings } from "./user";
|
||||
import {
|
||||
UpdateDocFunction,
|
||||
UpdateCollectionFunction,
|
||||
TableSettings,
|
||||
} from "@src/types/table";
|
||||
|
||||
export const projectIdAtom = atom<string>("");
|
||||
|
||||
@@ -51,23 +55,6 @@ export const projectSettingsAtom = atom<ProjectSettings>({});
|
||||
export const updateProjectSettingsAtom =
|
||||
atom<UpdateDocFunction<ProjectSettings> | null>(null);
|
||||
|
||||
/** Table settings stored in project settings */
|
||||
export type TableSettings = {
|
||||
id: string;
|
||||
collection: string;
|
||||
name: string;
|
||||
roles: string[];
|
||||
|
||||
description: string;
|
||||
section: string;
|
||||
|
||||
tableType: "primaryCollection" | "collectionGroup";
|
||||
|
||||
audit?: boolean;
|
||||
auditFieldCreatedBy?: string;
|
||||
auditFieldUpdatedBy?: string;
|
||||
readOnly?: boolean;
|
||||
};
|
||||
/** Tables visible to the signed-in user based on roles */
|
||||
export const tablesAtom = atom<TableSettings[]>((get) => {
|
||||
const userRoles = get(userRolesAtom);
|
||||
|
||||
@@ -72,7 +72,7 @@ export const rowyRunAtom = atom((get) => {
|
||||
handleNotSetUp,
|
||||
}: IRowyRunRequestProps): Promise<Response | any | false> => {
|
||||
if (!currentUser) {
|
||||
console.log("Rowy Run: Not signed in");
|
||||
console.log("Rowy Run: Not signed in", route.path);
|
||||
if (handleNotSetUp) handleNotSetUp();
|
||||
return false;
|
||||
}
|
||||
@@ -84,7 +84,7 @@ export const rowyRunAtom = atom((get) => {
|
||||
? rowyRunServices?.[service]
|
||||
: rowyRunUrl;
|
||||
if (!serviceUrl) {
|
||||
console.log("Rowy Run: Not set up");
|
||||
console.log("Rowy Run: Not set up", route.path);
|
||||
if (handleNotSetUp) handleNotSetUp();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { atom } from "jotai";
|
||||
import { atomWithStorage } from "jotai/utils";
|
||||
|
||||
import { DialogProps, ButtonProps } from "@mui/material";
|
||||
import { TableSettings } from "./project";
|
||||
import { TableSettings } from "@src/types/table";
|
||||
|
||||
/** Nav open state stored in local storage. */
|
||||
export const navOpenAtom = atomWithStorage("__ROWY__NAV_OPEN", false);
|
||||
|
||||
@@ -5,8 +5,7 @@ import { ThemeOptions } from "@mui/material";
|
||||
|
||||
import themes from "@src/theme";
|
||||
import { publicSettingsAtom } from "./project";
|
||||
import { TableFilter } from "@src/atoms/tableScope/table";
|
||||
import { UpdateDocFunction } from "@src/atoms/types";
|
||||
import { UpdateDocFunction, TableFilter } from "@src/types/table";
|
||||
|
||||
/** User info and settings */
|
||||
export type UserSettings = Partial<{
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
import { where } from "firebase/firestore";
|
||||
import { atom } from "jotai";
|
||||
import {
|
||||
TableSettings,
|
||||
TableSchema,
|
||||
TableFilter,
|
||||
TableOrder,
|
||||
} from "@src/types/table";
|
||||
|
||||
export type TableFilter = {
|
||||
key: Parameters<typeof where>[0];
|
||||
operator: Parameters<typeof where>[1];
|
||||
value: Parameters<typeof where>[2];
|
||||
};
|
||||
export const tableIdAtom = atom<string | undefined>(undefined);
|
||||
export const tableSettingsAtom = atom<TableSettings | undefined>(undefined);
|
||||
export const tableSchemaAtom = atom<TableSchema | undefined>(undefined);
|
||||
|
||||
export const tableFiltersAtom = atom<TableFilter[]>([]);
|
||||
export const tableOrdersAtom = atom<TableOrder[]>([]);
|
||||
export const tablePageAtom = atom(0);
|
||||
|
||||
export const tableRowsAtom = atom<Record<string, any>[]>([]);
|
||||
export const tableLoadingMoreAtom = atom(false);
|
||||
|
||||
6
src/atoms/types.d.ts
vendored
6
src/atoms/types.d.ts
vendored
@@ -1,6 +0,0 @@
|
||||
export type UpdateDocFunction<T> = (update: Partial<T>) => Promise<void>;
|
||||
|
||||
export type UpdateCollectionFunction<T> = (
|
||||
path: string,
|
||||
update: Partial<T>
|
||||
) => Promise<void>;
|
||||
48
src/components/Table/Skeleton/HeaderRowSkeleton.tsx
Normal file
48
src/components/Table/Skeleton/HeaderRowSkeleton.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Fade, Stack, Skeleton, Button } from "@mui/material";
|
||||
import AddColumnIcon from "@src/assets/icons/AddColumn";
|
||||
|
||||
const NUM_CELLS = 5;
|
||||
|
||||
export default function HeaderRowSkeleton() {
|
||||
return (
|
||||
<Fade in timeout={1000} style={{ transitionDelay: "1s" }} unmountOnExit>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
sx={{
|
||||
marginLeft: (theme) =>
|
||||
`max(env(safe-area-inset-left), ${theme.spacing(2)})`,
|
||||
marginRight: `env(safe-area-inset-right)`,
|
||||
}}
|
||||
>
|
||||
{new Array(NUM_CELLS + 1).fill(undefined).map((_, i) => (
|
||||
<Skeleton
|
||||
key={i}
|
||||
variant="rectangular"
|
||||
sx={{
|
||||
bgcolor: "background.default",
|
||||
border: "1px solid",
|
||||
borderColor: "divider",
|
||||
borderLeftWidth: i === 0 ? 1 : 0,
|
||||
width: i === NUM_CELLS ? 46 : 150,
|
||||
height: 42,
|
||||
borderRadius: i === NUM_CELLS ? 1 : 0,
|
||||
borderTopLeftRadius:
|
||||
i === 0 ? (theme) => theme.shape.borderRadius : 0,
|
||||
borderBottomLeftRadius:
|
||||
i === 0 ? (theme) => theme.shape.borderRadius : 0,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Skeleton
|
||||
sx={{ transform: "none", ml: (-46 + 6) / 8, borderRadius: 1 }}
|
||||
>
|
||||
<Button variant="contained" startIcon={<AddColumnIcon />}>
|
||||
Add column
|
||||
</Button>
|
||||
</Skeleton>
|
||||
</Stack>
|
||||
</Fade>
|
||||
);
|
||||
}
|
||||
60
src/components/Table/Skeleton/TableHeaderSkeleton.tsx
Normal file
60
src/components/Table/Skeleton/TableHeaderSkeleton.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Fade, Stack, Button, Skeleton, SkeletonProps } from "@mui/material";
|
||||
import AddRowIcon from "@src/assets/icons/AddRow";
|
||||
|
||||
// TODO:
|
||||
// import { TABLE_HEADER_HEIGHT } from "@src/components/TableHeader";
|
||||
const TABLE_HEADER_HEIGHT = 44;
|
||||
|
||||
const ButtonSkeleton = (props: Partial<SkeletonProps>) => (
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
{...props}
|
||||
sx={{ borderRadius: 1, ...props.sx }}
|
||||
/>
|
||||
);
|
||||
|
||||
export default function TableHeaderSkeleton() {
|
||||
return (
|
||||
<Fade in timeout={1000} style={{ transitionDelay: "1s" }} unmountOnExit>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={1}
|
||||
sx={{
|
||||
ml: "env(safe-area-inset-left)",
|
||||
mr: "env(safe-area-inset-right)",
|
||||
pl: 2,
|
||||
pr: 2,
|
||||
pb: 1.5,
|
||||
height: TABLE_HEADER_HEIGHT,
|
||||
}}
|
||||
>
|
||||
<ButtonSkeleton>
|
||||
<Button variant="contained" startIcon={<AddRowIcon />}>
|
||||
Add row
|
||||
</Button>
|
||||
</ButtonSkeleton>
|
||||
|
||||
<div />
|
||||
|
||||
<ButtonSkeleton>
|
||||
<Button variant="contained" startIcon={<AddRowIcon />}>
|
||||
Hide
|
||||
</Button>
|
||||
</ButtonSkeleton>
|
||||
<ButtonSkeleton>
|
||||
<Button variant="contained" startIcon={<AddRowIcon />}>
|
||||
Filter
|
||||
</Button>
|
||||
</ButtonSkeleton>
|
||||
|
||||
<div style={{ flexGrow: 1 }} />
|
||||
|
||||
<ButtonSkeleton style={{ width: 40, height: 32 }} />
|
||||
<div />
|
||||
<ButtonSkeleton style={{ width: 40, height: 32 }} />
|
||||
<ButtonSkeleton style={{ width: 40, height: 32 }} />
|
||||
</Stack>
|
||||
</Fade>
|
||||
);
|
||||
}
|
||||
@@ -6,11 +6,8 @@ import { useSnackbar } from "notistack";
|
||||
import { IconButton, Menu, MenuItem, DialogContentText } from "@mui/material";
|
||||
import DeleteIcon from "@mui/icons-material/DeleteOutlined";
|
||||
|
||||
import {
|
||||
globalScope,
|
||||
confirmDialogAtom,
|
||||
TableSettings,
|
||||
} from "@src/atoms/globalScope";
|
||||
import { globalScope, confirmDialogAtom } from "@src/atoms/globalScope";
|
||||
import { TableSettings } from "@src/types/table";
|
||||
import { ROUTES } from "@src/constants/routes";
|
||||
import { analytics, logEvent } from "@src/analytics";
|
||||
|
||||
|
||||
@@ -23,8 +23,8 @@ import {
|
||||
rolesAtom,
|
||||
rowyRunAtom,
|
||||
confirmDialogAtom,
|
||||
TableSettings,
|
||||
} from "@src/atoms/globalScope";
|
||||
import { TableSettings } from "@src/types/table";
|
||||
import { analytics, logEvent } from "@src/analytics";
|
||||
|
||||
// TODO:
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import GoIcon from "@src/assets/icons/Go";
|
||||
|
||||
import RenderedMarkdown from "@src/components/RenderedMarkdown";
|
||||
import { TableSettings } from "@src/atoms/globalScope";
|
||||
import { TableSettings } from "@src/types/table";
|
||||
|
||||
export interface ITableCardProps extends TableSettings {
|
||||
link: string;
|
||||
|
||||
@@ -6,7 +6,7 @@ import SectionHeading from "@src/components/SectionHeading";
|
||||
import TableCard from "./TableCard";
|
||||
import SlideTransition from "@src/components/Modal/SlideTransition";
|
||||
|
||||
import { TableSettings } from "@src/atoms/globalScope";
|
||||
import { TableSettings } from "@src/types/table";
|
||||
|
||||
export interface ITableGridProps {
|
||||
sections: Record<string, TableSettings[]>;
|
||||
|
||||
@@ -6,7 +6,7 @@ import SectionHeading from "@src/components/SectionHeading";
|
||||
import TableListItem from "./TableListItem";
|
||||
import SlideTransition from "@src/components/Modal/SlideTransition";
|
||||
|
||||
import { TableSettings } from "@src/atoms/globalScope";
|
||||
import { TableSettings } from "@src/types/table";
|
||||
|
||||
export interface ITableListProps {
|
||||
sections: Record<string, TableSettings[]>;
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import GoIcon from "@mui/icons-material/ArrowForward";
|
||||
|
||||
import RenderedMarkdown from "@src/components/RenderedMarkdown";
|
||||
import { TableSettings } from "@src/atoms/globalScope";
|
||||
import { TableSettings } from "@src/types/table";
|
||||
|
||||
export interface ITableListItemProps extends TableSettings {
|
||||
link: string;
|
||||
|
||||
@@ -43,6 +43,8 @@ export const ROUTE_TITLES = {
|
||||
),
|
||||
},
|
||||
|
||||
[ROUTES.table]: "Table Test",
|
||||
|
||||
[ROUTES.settings]: "Settings",
|
||||
[ROUTES.userSettings]: "Settings",
|
||||
[ROUTES.projectSettings]: "Project Settings",
|
||||
|
||||
@@ -17,7 +17,11 @@ import {
|
||||
import { useErrorHandler } from "react-error-boundary";
|
||||
|
||||
import { globalScope } from "@src/atoms/globalScope";
|
||||
import { UpdateCollectionFunction } from "@src/atoms/types";
|
||||
import {
|
||||
UpdateCollectionFunction,
|
||||
TableFilter,
|
||||
TableOrder,
|
||||
} from "@src/types/table";
|
||||
import { firebaseDbAtom } from "@src/sources/ProjectSourceFirebase";
|
||||
|
||||
/** Options for {@link useFirestoreCollectionWithAtom} */
|
||||
@@ -25,9 +29,9 @@ interface IUseFirestoreCollectionWithAtomOptions<T> {
|
||||
/** Additional path segments appended to the path. If any are undefined, the listener isn’t created at all. */
|
||||
pathSegments?: Array<string | undefined>;
|
||||
/** Attach filters to the query */
|
||||
filters?: Parameters<typeof where>[];
|
||||
filters?: TableFilter[];
|
||||
/** Attach orders to the query */
|
||||
orders?: Parameters<typeof orderBy>[];
|
||||
orders?: TableOrder[];
|
||||
/** Called when an error occurs. Make sure to wrap in useCallback! If not provided, errors trigger the nearest ErrorBoundary. */
|
||||
onError?: (error: FirestoreError) => void;
|
||||
/** Optionally disable Suspense */
|
||||
@@ -91,8 +95,10 @@ export function useFirestoreCollectionWithAtom<T = DocumentData>(
|
||||
// Create the query with filters and orders
|
||||
const _query = query<T>(
|
||||
collectionRef,
|
||||
...(filters?.map((filter) => where(...filter)) || []),
|
||||
...(orders?.map((order) => orderBy(...order)) || [])
|
||||
...(filters?.map((filter) =>
|
||||
where(filter.key, filter.operator, filter.value)
|
||||
) || []),
|
||||
...(orders?.map((order) => orderBy(order.key, order.direction)) || [])
|
||||
);
|
||||
|
||||
const unsubscribe = onSnapshot(
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import { useErrorHandler } from "react-error-boundary";
|
||||
|
||||
import { globalScope } from "@src/atoms/globalScope";
|
||||
import { UpdateDocFunction } from "@src/atoms/types";
|
||||
import { UpdateDocFunction } from "@src/types/table";
|
||||
import { firebaseDbAtom } from "@src/sources/ProjectSourceFirebase";
|
||||
|
||||
/** Options for {@link useFirestoreDocWithAtom} */
|
||||
|
||||
@@ -31,15 +31,14 @@ import {
|
||||
userRolesAtom,
|
||||
userSettingsAtom,
|
||||
tablesAtom,
|
||||
TableSettings,
|
||||
tableSettingsDialogAtom,
|
||||
} from "@src/atoms/globalScope";
|
||||
import { TableSettings } from "@src/types/table";
|
||||
import { ROUTES } from "@src/constants/routes";
|
||||
|
||||
export const NAV_DRAWER_WIDTH = 256;
|
||||
|
||||
export interface INavDrawerProps extends DrawerProps {
|
||||
currentSection?: string;
|
||||
onClose: NonNullable<DrawerProps["onClose"]>;
|
||||
pinned: boolean;
|
||||
setPinned: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
@@ -142,7 +141,7 @@ export default function NavDrawer({
|
||||
<nav>
|
||||
<List disablePadding>
|
||||
<li>
|
||||
<NavItem to={ROUTES.home} onClick={closeDrawer}>
|
||||
<NavItem to={ROUTES.tables} onClick={closeDrawer}>
|
||||
<ListItemIcon>
|
||||
<HomeIcon />
|
||||
</ListItemIcon>
|
||||
|
||||
@@ -5,32 +5,35 @@ import { List, ListItemText, Collapse } from "@mui/material";
|
||||
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
|
||||
import NavItem from "./NavItem";
|
||||
|
||||
import { TableSettings } from "@src/atoms/globalScope";
|
||||
import { TableSettings } from "@src/types/table";
|
||||
import { ROUTES } from "@src/constants/routes";
|
||||
|
||||
export interface INavDrawerItemProps {
|
||||
open?: boolean;
|
||||
const getTableRoute = (table: TableSettings) =>
|
||||
table.tableType === "collectionGroup"
|
||||
? `${ROUTES.tableGroup}/${table.id}`
|
||||
: `${ROUTES.table}/${table.id.replace(/\//g, "~2F")}`;
|
||||
|
||||
export interface INavTableSectionProps {
|
||||
section: string;
|
||||
tables: TableSettings[];
|
||||
currentSection?: string;
|
||||
closeDrawer?: (e: {}) => void;
|
||||
}
|
||||
|
||||
export default function NavDrawerItem({
|
||||
open: openProp,
|
||||
export default function NavTableSection({
|
||||
section,
|
||||
tables,
|
||||
currentSection,
|
||||
closeDrawer,
|
||||
}: INavDrawerItemProps) {
|
||||
}: INavTableSectionProps) {
|
||||
const { pathname } = useLocation();
|
||||
const [open, setOpen] = useState(openProp || section === currentSection);
|
||||
const hasMatch = tables.map(getTableRoute).includes(pathname);
|
||||
|
||||
const [open, setOpen] = useState(hasMatch);
|
||||
|
||||
return (
|
||||
<li>
|
||||
<NavItem
|
||||
{...({ component: "button" } as any)}
|
||||
selected={!open && currentSection === section}
|
||||
selected={hasMatch && !open}
|
||||
onClick={() => setOpen((o) => !o)}
|
||||
>
|
||||
<ListItemText primary={section} style={{ textAlign: "left" }} />
|
||||
@@ -46,31 +49,25 @@ export default function NavDrawerItem({
|
||||
|
||||
<Collapse in={open}>
|
||||
<List disablePadding>
|
||||
{tables
|
||||
.filter((x) => x)
|
||||
.map((table) => {
|
||||
const route =
|
||||
table.tableType === "collectionGroup"
|
||||
? `${ROUTES.tableGroup}/${table.id}`
|
||||
: `${ROUTES.table}/${table.id.replace(/\//g, "~2F")}`;
|
||||
{tables.map((table) => {
|
||||
const route = getTableRoute(table);
|
||||
|
||||
return (
|
||||
<li key={table.id}>
|
||||
<NavItem
|
||||
to={route}
|
||||
selected={pathname.split("%2F")[0] === route}
|
||||
onClick={closeDrawer}
|
||||
sx={{
|
||||
ml: 2,
|
||||
width: (theme) =>
|
||||
`calc(100% - ${theme.spacing(2 + 0.5)})`,
|
||||
}}
|
||||
>
|
||||
<ListItemText primary={table.name} />
|
||||
</NavItem>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
return (
|
||||
<li key={table.id}>
|
||||
<NavItem
|
||||
to={route}
|
||||
selected={pathname.split("%2F")[0] === route}
|
||||
onClick={closeDrawer}
|
||||
sx={{
|
||||
ml: 2,
|
||||
width: (theme) => `calc(100% - ${theme.spacing(2 + 0.5)})`,
|
||||
}}
|
||||
>
|
||||
<ListItemText primary={table.name} />
|
||||
</NavItem>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</Collapse>
|
||||
</li>
|
||||
|
||||
@@ -49,7 +49,11 @@ export default function Navigation({ children }: React.PropsWithChildren<{}>) {
|
||||
const canPin = !useMediaQuery((theme: any) => theme.breakpoints.down("lg"));
|
||||
|
||||
const { pathname } = useLocation();
|
||||
const routeTitle = ROUTE_TITLES[pathname as keyof typeof ROUTE_TITLES] || "";
|
||||
const basePath = ("/" + pathname.split("/")[1]) as keyof typeof ROUTE_TITLES;
|
||||
const routeTitle =
|
||||
ROUTE_TITLES[pathname as keyof typeof ROUTE_TITLES] ||
|
||||
ROUTE_TITLES[basePath] ||
|
||||
"";
|
||||
const title = typeof routeTitle === "string" ? routeTitle : routeTitle.title;
|
||||
useDocumentTitle(projectId, title);
|
||||
|
||||
|
||||
@@ -45,11 +45,7 @@ export default function UserMenu(props: IconButtonProps) {
|
||||
const avatarUrl = userSettings.user?.photoURL;
|
||||
const email = userSettings.user?.email;
|
||||
|
||||
const avatar = avatarUrl ? (
|
||||
<Avatar src={avatarUrl} />
|
||||
) : (
|
||||
<AccountCircleIcon color="secondary" />
|
||||
);
|
||||
const avatar = avatarUrl ? <Avatar src={avatarUrl} /> : <AccountCircleIcon />;
|
||||
|
||||
const changeTheme = (option: "system" | "light" | "dark") => {
|
||||
if (option === "system") {
|
||||
|
||||
49
src/pages/TableTest.tsx
Normal file
49
src/pages/TableTest.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { Suspense } from "react";
|
||||
import { useAtom, Provider } from "jotai";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
import {
|
||||
tableScope,
|
||||
tableIdAtom,
|
||||
tableSettingsAtom,
|
||||
tableSchemaAtom,
|
||||
} from "@src/atoms/tableScope";
|
||||
|
||||
import TableSourceFirestore from "@src/sources/TableSourceFirestore";
|
||||
import TableHeaderSkeleton from "@src/components/Table/Skeleton/TableHeaderSkeleton";
|
||||
import HeaderRowSkeleton from "@src/components/Table/Skeleton/HeaderRowSkeleton";
|
||||
|
||||
function TableTestPage() {
|
||||
const [tableId] = useAtom(tableIdAtom, tableScope);
|
||||
const [tableSettings] = useAtom(tableSettingsAtom, tableScope);
|
||||
const [tableSchema] = useAtom(tableSchemaAtom, tableScope);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>Table ID: {tableId}</p>
|
||||
|
||||
<pre>{JSON.stringify(tableSettings, undefined, 2)}</pre>
|
||||
<pre>{JSON.stringify(tableSchema, undefined, 2)}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ProvidedTableTestPage() {
|
||||
const { id } = useParams();
|
||||
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<>
|
||||
<TableHeaderSkeleton />
|
||||
<HeaderRowSkeleton />
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Provider key={id} scope={tableScope} initialValues={[[tableIdAtom, id]]}>
|
||||
<TableSourceFirestore />
|
||||
<TableTestPage />
|
||||
</Provider>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -35,8 +35,8 @@ import {
|
||||
tablesAtom,
|
||||
tablesViewAtom,
|
||||
tableSettingsDialogAtom,
|
||||
TableSettings,
|
||||
} from "@src/atoms/globalScope";
|
||||
import { TableSettings } from "@src/types/table";
|
||||
import { ROUTES } from "@src/constants/routes";
|
||||
import useBasicSearch from "@src/hooks/useBasicSearch";
|
||||
import { APP_BAR_HEIGHT } from "@src/layouts/Navigation";
|
||||
|
||||
@@ -41,7 +41,7 @@ const envConnectEmulators =
|
||||
/**
|
||||
* Store Firebase config here so it can be set programmatically.
|
||||
* This lets us switch between Firebase projects.
|
||||
* Then app, auth, db, storage need to be derived atoms.
|
||||
* Root atom from which app, auth, db, storage are derived.
|
||||
*/
|
||||
export const firebaseConfigAtom = atom<FirebaseOptions>(envConfig);
|
||||
|
||||
|
||||
54
src/sources/TableSourceFirestore.tsx
Normal file
54
src/sources/TableSourceFirestore.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { memo, useMemo, useEffect } from "react";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import { find } from "lodash-es";
|
||||
|
||||
import { globalScope, tablesAtom } from "@src/atoms/globalScope";
|
||||
import {
|
||||
tableScope,
|
||||
tableIdAtom,
|
||||
tableSettingsAtom,
|
||||
tableSchemaAtom,
|
||||
} from "@src/atoms/tableScope";
|
||||
import { firebaseDbAtom } from "@src/sources/ProjectSourceFirebase";
|
||||
|
||||
import useFirestoreDocWithAtom from "@src/hooks/useFirestoreDocWithAtom";
|
||||
|
||||
// import useFirestoreCollectionWithAtom from "@src/hooks/useFirestoreCollectionWithAtom";
|
||||
// import {
|
||||
// globalScope,
|
||||
// allUsersAtom,
|
||||
// updateUserAtom,
|
||||
// } from "@src/atoms/globalScope";
|
||||
import { TABLE_SCHEMAS, TABLE_GROUP_SCHEMAS } from "@src/config/dbPaths";
|
||||
|
||||
const TableSourceFirestore = memo(function TableSourceFirestore() {
|
||||
const [tables] = useAtom(tablesAtom, globalScope);
|
||||
const [firebaseDb] = useAtom(firebaseDbAtom, globalScope);
|
||||
|
||||
// Get tableSettings from tableId and tables in globalScope
|
||||
const [tableId] = useAtom(tableIdAtom, tableScope);
|
||||
const setTableSettings = useSetAtom(tableSettingsAtom, tableScope);
|
||||
// Store tableSettings as local const so we don’t re-render
|
||||
// when tableSettingsAtom is set
|
||||
const tableSettings = useMemo(
|
||||
() => find(tables, ["id", tableId]),
|
||||
[tables, tableId]
|
||||
);
|
||||
// Store in tableSettingsAtom
|
||||
useEffect(() => {
|
||||
setTableSettings(tableSettings);
|
||||
}, [tableSettings, setTableSettings]);
|
||||
|
||||
useFirestoreDocWithAtom(
|
||||
tableSchemaAtom,
|
||||
tableScope,
|
||||
tableSettings?.tableType === "collectionGroup"
|
||||
? TABLE_GROUP_SCHEMAS
|
||||
: TABLE_SCHEMAS,
|
||||
{ pathSegments: [tableId] }
|
||||
);
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
export default TableSourceFirestore;
|
||||
61
src/types/table.d.ts
vendored
Normal file
61
src/types/table.d.ts
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { where, orderBy } from "firebase/firestore";
|
||||
|
||||
export type UpdateDocFunction<T> = (update: Partial<T>) => Promise<void>;
|
||||
|
||||
export type UpdateCollectionFunction<T> = (
|
||||
path: string,
|
||||
update: Partial<T>
|
||||
) => Promise<void>;
|
||||
|
||||
/** Table settings stored in project settings */
|
||||
export type TableSettings = {
|
||||
id: string;
|
||||
collection: string;
|
||||
name: string;
|
||||
roles: string[];
|
||||
|
||||
description: string;
|
||||
section: string;
|
||||
|
||||
tableType: "primaryCollection" | "collectionGroup";
|
||||
|
||||
audit?: boolean;
|
||||
auditFieldCreatedBy?: string;
|
||||
auditFieldUpdatedBy?: string;
|
||||
readOnly?: boolean;
|
||||
};
|
||||
|
||||
/** Table schema document loaded when table or table settings dialog is open */
|
||||
export type TableSchema = {
|
||||
columns?: Record<string, ColumnConfig>;
|
||||
rowHeight?: number;
|
||||
filters?: TableFilter[];
|
||||
|
||||
functionConfigPath?: string;
|
||||
|
||||
extensionObjects?: any[];
|
||||
webhooks?: any[];
|
||||
};
|
||||
|
||||
export type ColumnConfig = {
|
||||
fieldName: string;
|
||||
key: string;
|
||||
name: string;
|
||||
type: FieldType;
|
||||
index: number;
|
||||
width?: number;
|
||||
editable?: boolean;
|
||||
config: { [key: string]: any };
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
export type TableFilter = {
|
||||
key: Parameters<typeof where>[0];
|
||||
operator: Parameters<typeof where>[1];
|
||||
value: Parameters<typeof where>[2];
|
||||
};
|
||||
|
||||
export type TableOrder = {
|
||||
key: Parameters<typeof orderBy>[0];
|
||||
direction: Parameters<typeof orderBy>[1];
|
||||
};
|
||||
Reference in New Issue
Block a user