mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-29 00:16:39 +01:00
Merge pull request #10 from rowyio/feat/table-information
Feat: Table Information Drawer
This commit is contained in:
@@ -103,6 +103,9 @@ export { FileTableBoxOutline as Project };
|
||||
import { TableColumn } from "mdi-material-ui";
|
||||
export { TableColumn };
|
||||
|
||||
import { InformationOutline } from "mdi-material-ui";
|
||||
export { InformationOutline as TableInformation };
|
||||
|
||||
export * from "./AddRow";
|
||||
export * from "./AddRowTop";
|
||||
export * from "./ChevronDown";
|
||||
|
||||
@@ -121,6 +121,12 @@ export const importAirtableAtom = atom<{
|
||||
tableId: string;
|
||||
}>({ airtableData: null, apiKey: "", baseId: "", tableId: "" });
|
||||
|
||||
/** Store side drawer open state */
|
||||
export const sideDrawerAtom = atomWithHash<"table-information" | null>(
|
||||
"sideDrawer",
|
||||
null,
|
||||
{ replaceState: true }
|
||||
);
|
||||
/** Store side drawer open state */
|
||||
export const sideDrawerOpenAtom = atom(false);
|
||||
|
||||
|
||||
178
src/components/TableInformationDrawer/Details.tsx
Normal file
178
src/components/TableInformationDrawer/Details.tsx
Normal file
@@ -0,0 +1,178 @@
|
||||
import { useMemo } from "react";
|
||||
import { format } from "date-fns";
|
||||
import MDEditor from "@uiw/react-md-editor";
|
||||
|
||||
import {
|
||||
Box,
|
||||
Grid,
|
||||
IconButton,
|
||||
Stack,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
|
||||
import EditIcon from "@mui/icons-material/EditOutlined";
|
||||
|
||||
import { tableScope, tableSettingsAtom } from "@src/atoms/tableScope";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import {
|
||||
projectScope,
|
||||
tablesAtom,
|
||||
tableSettingsDialogAtom,
|
||||
userRolesAtom,
|
||||
} from "@src/atoms/projectScope";
|
||||
import { find } from "lodash-es";
|
||||
|
||||
export interface IDetailsProps {
|
||||
handleOpenTemplate?: any;
|
||||
}
|
||||
|
||||
export default function Details({ handleOpenTemplate }: IDetailsProps) {
|
||||
const [userRoles] = useAtom(userRolesAtom, projectScope);
|
||||
const [tableSettings] = useAtom(tableSettingsAtom, tableScope);
|
||||
const [tables] = useAtom(tablesAtom, projectScope);
|
||||
const openTableSettingsDialog = useSetAtom(
|
||||
tableSettingsDialogAtom,
|
||||
projectScope
|
||||
);
|
||||
|
||||
const settings = useMemo(
|
||||
() => find(tables, ["id", tableSettings.id]),
|
||||
[tables, tableSettings.id]
|
||||
);
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
if (!settings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const editButton = userRoles.includes("ADMIN") && (
|
||||
<IconButton
|
||||
sx={{
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
}}
|
||||
onClick={() =>
|
||||
openTableSettingsDialog({
|
||||
mode: "update",
|
||||
data: settings,
|
||||
})
|
||||
}
|
||||
disabled={!openTableSettingsDialog || settings.id.includes("/")}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
);
|
||||
|
||||
const { description, details, _createdBy } = settings;
|
||||
|
||||
return (
|
||||
<Grid
|
||||
container
|
||||
flexDirection="column"
|
||||
gap={3}
|
||||
sx={{
|
||||
paddingTop: theme.spacing(3),
|
||||
paddingRight: theme.spacing(3),
|
||||
paddingBottom: theme.spacing(5),
|
||||
"& > .MuiGrid-root": {
|
||||
position: "relative",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* Description */}
|
||||
<Grid container direction="column" gap={1}>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography variant="body1" sx={{ fontWeight: 500 }}>
|
||||
Description
|
||||
</Typography>
|
||||
{editButton}
|
||||
</Stack>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{description ? description : "No description"}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
{/* Details */}
|
||||
<Grid container direction="column" gap={1}>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography variant="body1" sx={{ fontWeight: 500 }}>
|
||||
Details
|
||||
</Typography>
|
||||
{editButton}
|
||||
</Stack>
|
||||
{!details ? (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
No details
|
||||
</Typography>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
color: "text.secondary",
|
||||
}}
|
||||
>
|
||||
<MDEditor.Markdown source={details} />
|
||||
</Box>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
{/* Table Audits */}
|
||||
{_createdBy && (
|
||||
<Grid
|
||||
container
|
||||
sx={{
|
||||
fontSize: theme.typography.body2,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
component="div"
|
||||
style={{ whiteSpace: "normal" }}
|
||||
>
|
||||
Created by{" "}
|
||||
<Typography variant="caption" color="text.primary">
|
||||
{_createdBy.displayName}
|
||||
</Typography>{" "}
|
||||
at{" "}
|
||||
<Typography variant="caption" color="text.primary">
|
||||
{format(_createdBy.timestamp.toDate(), "LLL d, yyyy · p")}
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
{/* Template Settings */}
|
||||
{/* {handleOpenTemplate && (
|
||||
<Divider sx={{ my: 1.5 }} />
|
||||
<Button
|
||||
sx={{ width: "100%", boxShadow: "none", padding: 0 }}
|
||||
onClick={handleOpenTemplate}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
justify-content="flex-start"
|
||||
alignItems="center"
|
||||
sx={{ width: "100%" }}
|
||||
>
|
||||
<ChevronLeft />
|
||||
<ListItemText
|
||||
primary="Template - Roadmap"
|
||||
secondary={<StepsProgress steps={5} value={3} />}
|
||||
sx={{ maxWidth: "188px" }}
|
||||
/>
|
||||
</Stack
|
||||
</Button>
|
||||
)} */}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
134
src/components/TableInformationDrawer/SideDrawer.tsx
Normal file
134
src/components/TableInformationDrawer/SideDrawer.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import { useAtom } from "jotai";
|
||||
import { RESET } from "jotai/utils";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import clsx from "clsx";
|
||||
|
||||
import {
|
||||
Box,
|
||||
Drawer,
|
||||
drawerClasses,
|
||||
IconButton,
|
||||
Stack,
|
||||
styled,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
|
||||
import { sideDrawerAtom, tableScope } from "@src/atoms/tableScope";
|
||||
|
||||
import { TOP_BAR_HEIGHT } from "@src/layouts/Navigation/TopBar";
|
||||
import { TABLE_TOOLBAR_HEIGHT } from "@src/components/TableToolbar/TableToolbar";
|
||||
import ErrorFallback from "@src/components/ErrorFallback";
|
||||
import Details from "./Details";
|
||||
|
||||
export const DRAWER_WIDTH = 450;
|
||||
|
||||
export const StyledDrawer = styled(Drawer)(({ theme }) => ({
|
||||
[`.${drawerClasses.root}`]: {
|
||||
width: DRAWER_WIDTH,
|
||||
flexShrink: 0,
|
||||
whiteSpace: "nowrap",
|
||||
},
|
||||
|
||||
[`.${drawerClasses.paper}`]: {
|
||||
border: "none",
|
||||
boxShadow: theme.shadows[4].replace(/, 0 (\d+px)/g, ", -$1 0"),
|
||||
borderTopLeftRadius: `${(theme.shape.borderRadius as number) * 3}px`,
|
||||
borderBottomLeftRadius: `${(theme.shape.borderRadius as number) * 3}px`,
|
||||
|
||||
width: DRAWER_WIDTH,
|
||||
maxWidth: `calc(100% - 28px - ${theme.spacing(1)})`,
|
||||
|
||||
boxSizing: "content-box",
|
||||
|
||||
top: TOP_BAR_HEIGHT + TABLE_TOOLBAR_HEIGHT,
|
||||
height: `calc(100% - ${TOP_BAR_HEIGHT + TABLE_TOOLBAR_HEIGHT}px)`,
|
||||
".MuiDialog-paperFullScreen &": {
|
||||
top:
|
||||
TOP_BAR_HEIGHT +
|
||||
TABLE_TOOLBAR_HEIGHT +
|
||||
Number(theme.spacing(2).replace("px", "")),
|
||||
height: `calc(100% - ${
|
||||
TOP_BAR_HEIGHT + TABLE_TOOLBAR_HEIGHT
|
||||
}px - ${theme.spacing(2)})`,
|
||||
},
|
||||
|
||||
transition: theme.transitions.create("transform", {
|
||||
easing: theme.transitions.easing.easeInOut,
|
||||
duration: theme.transitions.duration.standard,
|
||||
}),
|
||||
|
||||
zIndex: theme.zIndex.drawer - 1,
|
||||
},
|
||||
[`:not(.sidedrawer-open) .${drawerClasses.paper}`]: {
|
||||
transform: `translateX(calc(100% - env(safe-area-inset-right)))`,
|
||||
},
|
||||
|
||||
".sidedrawer-contents": {
|
||||
height: "100%",
|
||||
overflow: "hidden",
|
||||
marginLeft: theme.spacing(5),
|
||||
marginRight: `max(env(safe-area-inset-right), ${theme.spacing(1)})`,
|
||||
marginTop: theme.spacing(2),
|
||||
paddingBottom: theme.spacing(5),
|
||||
},
|
||||
}));
|
||||
|
||||
export default function SideDrawer() {
|
||||
const [sideDrawer, setSideDrawer] = useAtom(sideDrawerAtom, tableScope);
|
||||
|
||||
// const DetailsComponent =
|
||||
// userRoles.includes("ADMIN") && tableSettings.templateSettings
|
||||
// ? withTemplate(Details)
|
||||
// : Details;
|
||||
// const DetailsComponent = Details;
|
||||
const open = sideDrawer === "table-information";
|
||||
|
||||
return (
|
||||
<StyledDrawer
|
||||
className={clsx(open && "sidedrawer-open")}
|
||||
open={open}
|
||||
variant="permanent"
|
||||
anchor="right"
|
||||
PaperProps={{ elevation: 4, component: "aside" } as any}
|
||||
>
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||
{open && (
|
||||
<div className="sidedrawer-contents">
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
pr={3}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
py={1}
|
||||
>
|
||||
<Typography variant="h5" sx={{ fontWeight: "bold" }}>
|
||||
Information
|
||||
</Typography>
|
||||
</Stack>
|
||||
<IconButton
|
||||
onClick={() => setSideDrawer(RESET)}
|
||||
aria-label="Close"
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
<Box
|
||||
sx={{
|
||||
height: "100%",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
<Details />
|
||||
</Box>
|
||||
</div>
|
||||
)}
|
||||
</ErrorBoundary>
|
||||
</StyledDrawer>
|
||||
);
|
||||
}
|
||||
2
src/components/TableInformationDrawer/index.ts
Normal file
2
src/components/TableInformationDrawer/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "../TableToolbar/TableInformation";
|
||||
export { default } from "../TableToolbar/TableInformation";
|
||||
61
src/components/TableInformationDrawer/withTemplate.tsx
Normal file
61
src/components/TableInformationDrawer/withTemplate.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
// import {
|
||||
// projectScope,
|
||||
// tablesAtom,
|
||||
// templateSettingsDialogAtom,
|
||||
// } from "@src/atoms/projectScope";
|
||||
// import { TemplateSettings } from "@src/components/Tables/Templates";
|
||||
// import { getTemplateById } from "@src/components/Tables/Templates/utills";
|
||||
// import { useAtom, useSetAtom } from "jotai";
|
||||
// import { useEffect, useState } from "react";
|
||||
import Details from "./Details";
|
||||
|
||||
export default function withTemplate(DetailsComponent: typeof Details) {
|
||||
// const [tables] = useAtom(tablesAtom, projectScope);
|
||||
// const setTemplateSettingsDialog = useSetAtom(
|
||||
// templateSettingsDialogAtom,
|
||||
// projectScope
|
||||
// );
|
||||
// const [templateData, setTemplateData] = useState<TemplateSettings | null>(
|
||||
// null
|
||||
// );
|
||||
// const { templateSettings } = tableSettings;
|
||||
// const { draftId, templateId } = templateSettings || {};
|
||||
// useEffect(() => {
|
||||
// if (!templateId) {
|
||||
// throw Error("Template not found");
|
||||
// }
|
||||
// getTemplateById(templateId).then((template) => setTemplateData(template));
|
||||
// }, [templateId]);
|
||||
// const dialogStateInitializer = () => {
|
||||
// const matchingTables =
|
||||
// tables.filter((table) => table.templateSettings?.draftId === draftId) ||
|
||||
// [];
|
||||
// const templateTables = templateData!.tables.map((templateTable) => ({
|
||||
// ...templateTable,
|
||||
// ...matchingTables.find(
|
||||
// (matchingTable) =>
|
||||
// matchingTable.templateSettings?.tableId === templateTable.id
|
||||
// ),
|
||||
// }));
|
||||
// const steps = templateData?.steps.map((step) =>
|
||||
// step.type === "create_table" ? { ...step, completed: true } : step
|
||||
// );
|
||||
// return {
|
||||
// ...templateData,
|
||||
// tables: templateTables,
|
||||
// steps,
|
||||
// } as TemplateSettings;
|
||||
// };
|
||||
// const handleOpenTemplate = () => {
|
||||
// setTemplateSettingsDialog({
|
||||
// type: "set_open",
|
||||
// data: dialogStateInitializer(),
|
||||
// });
|
||||
// };
|
||||
// if (!templateData) {
|
||||
// return null;
|
||||
// }
|
||||
return <DetailsComponent handleOpenTemplate={() => {}} />;
|
||||
}
|
||||
|
||||
// export const DetailsWithTemplate = withTemplate(Details);
|
||||
43
src/components/TableSettingsDialog/TableDetails.tsx
Normal file
43
src/components/TableSettingsDialog/TableDetails.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Box, InputLabel, useTheme } from "@mui/material";
|
||||
import MDEditor from "@uiw/react-md-editor";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function TableDetails({ ...props }) {
|
||||
const {
|
||||
field: { value, onChange },
|
||||
} = props;
|
||||
const theme = useTheme();
|
||||
const [focused, setFocused] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<InputLabel htmlFor="table-details__md-text-area" focused={focused}>
|
||||
{props.label ?? ""}
|
||||
</InputLabel>
|
||||
<Box
|
||||
sx={{
|
||||
"& .w-md-editor": {
|
||||
backgroundColor: `${theme.palette.action.input} !important`,
|
||||
},
|
||||
"& .w-md-editor-fullscreen": {
|
||||
backgroundColor: `${theme.palette.background.paper} !important`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MDEditor
|
||||
style={{ margin: 1 }}
|
||||
preview="live"
|
||||
toolbarHeight={52}
|
||||
height={150}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
textareaProps={{
|
||||
id: "table-details__md-text-area",
|
||||
onFocus: () => setFocused(true),
|
||||
onBlur: () => setFocused(false),
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -38,6 +38,10 @@ import {
|
||||
getTableSchemaPath,
|
||||
getTableBuildFunctionPathname,
|
||||
} from "@src/utils/table";
|
||||
import { firebaseStorageAtom } from "@src/sources/ProjectSourceFirebase";
|
||||
import { uploadTableThumbnail } from "./utils";
|
||||
import TableThumbnail from "./TableThumbnail";
|
||||
import TableDetails from "./TableDetails";
|
||||
|
||||
const customComponents = {
|
||||
tableName: {
|
||||
@@ -55,6 +59,12 @@ const customComponents = {
|
||||
defaultValue: "",
|
||||
validation: [["string"]],
|
||||
},
|
||||
tableThumbnail: {
|
||||
component: TableThumbnail,
|
||||
},
|
||||
tableDetails: {
|
||||
component: TableDetails,
|
||||
},
|
||||
};
|
||||
|
||||
export default function TableSettingsDialog() {
|
||||
@@ -67,6 +77,7 @@ export default function TableSettingsDialog() {
|
||||
const [projectRoles] = useAtom(projectRolesAtom, projectScope);
|
||||
const [tables] = useAtom(tablesAtom, projectScope);
|
||||
const [rowyRun] = useAtom(rowyRunAtom, projectScope);
|
||||
const [firebaseStorage] = useAtom(firebaseStorageAtom, projectScope);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const confirm = useSetAtom(confirmDialogAtom, projectScope);
|
||||
@@ -96,15 +107,26 @@ export default function TableSettingsDialog() {
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const handleSubmit = async (v: TableSettings & AdditionalTableSettings) => {
|
||||
const handleSubmit = async (
|
||||
v: TableSettings & AdditionalTableSettings & { thumbnailFile: File }
|
||||
) => {
|
||||
const {
|
||||
_schemaSource,
|
||||
_initialColumns,
|
||||
_schema,
|
||||
_suggestedRules,
|
||||
thumbnailFile,
|
||||
...values
|
||||
} = v;
|
||||
const data = { ...values };
|
||||
|
||||
let thumbnailURL = values.thumbnailURL;
|
||||
if (thumbnailFile) {
|
||||
thumbnailURL = await uploadTableThumbnail(firebaseStorage)(
|
||||
values.id,
|
||||
thumbnailFile
|
||||
);
|
||||
}
|
||||
const data = { ...values, thumbnailURL };
|
||||
|
||||
const hasExtensions = !isEmpty(get(data, "_schema.extensionObjects"));
|
||||
const hasWebhooks = !isEmpty(get(data, "_schema.webhooks"));
|
||||
|
||||
126
src/components/TableSettingsDialog/TableThumbnail.tsx
Normal file
126
src/components/TableSettingsDialog/TableThumbnail.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { IFieldComponentProps } from "@rowy/form-builder";
|
||||
|
||||
import { Button, Grid, IconButton, InputLabel, useTheme } from "@mui/material";
|
||||
|
||||
import { Upload as UploadImageIcon } from "@src/assets/icons";
|
||||
import {
|
||||
OpenInFull as ExpandIcon,
|
||||
CloseFullscreen as CollapseIcon,
|
||||
AddPhotoAlternateOutlined as NoImageIcon,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
import { IMAGE_MIME_TYPES } from "@src/components/fields/Image";
|
||||
|
||||
export default function TableThumbnail({ ...props }: IFieldComponentProps) {
|
||||
const {
|
||||
name,
|
||||
useFormMethods: { setValue, getValues },
|
||||
} = props;
|
||||
|
||||
const theme = useTheme();
|
||||
const [localImage, setLocalImage] = useState<string | undefined>(
|
||||
() => getValues().thumbnailURL
|
||||
);
|
||||
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles: File[]) => {
|
||||
const imageFile = acceptedFiles[0];
|
||||
|
||||
if (imageFile) {
|
||||
setLocalImage(URL.createObjectURL(imageFile));
|
||||
setValue(name, imageFile);
|
||||
}
|
||||
},
|
||||
[name, setLocalImage, setValue]
|
||||
);
|
||||
|
||||
const { getInputProps } = useDropzone({
|
||||
onDrop,
|
||||
multiple: false,
|
||||
accept: IMAGE_MIME_TYPES,
|
||||
});
|
||||
|
||||
return (
|
||||
<Grid container>
|
||||
<Grid
|
||||
container
|
||||
alignItems="center"
|
||||
xs={expanded ? 12 : 10.5}
|
||||
sx={{
|
||||
marginRight: "auto",
|
||||
transition: "all 0.1s",
|
||||
}}
|
||||
>
|
||||
<InputLabel htmlFor="thumbnail-image__input">{props.label}</InputLabel>
|
||||
<IconButton
|
||||
component="label"
|
||||
sx={{
|
||||
marginLeft: "auto",
|
||||
marginRight: expanded ? 0 : theme.spacing(0.5),
|
||||
}}
|
||||
>
|
||||
<UploadImageIcon />
|
||||
<input
|
||||
id="thumbnail-image__input"
|
||||
type="file"
|
||||
hidden
|
||||
{...getInputProps()}
|
||||
/>
|
||||
</IconButton>
|
||||
</Grid>
|
||||
<Grid
|
||||
item
|
||||
xs={expanded ? 12 : 1.5}
|
||||
sx={{
|
||||
marginLeft: "auto",
|
||||
marginTop: expanded ? theme.spacing(1) : 0,
|
||||
transition: "all 0.5s",
|
||||
}}
|
||||
>
|
||||
<Grid
|
||||
container
|
||||
sx={{
|
||||
position: "relative",
|
||||
// 16:9 ratio
|
||||
paddingBottom: "56.25%",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
disabled={!localImage}
|
||||
sx={{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundImage: `url("${localImage}")`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
"& > svg": {
|
||||
display: localImage ? "none" : "block",
|
||||
},
|
||||
"&:hover": {
|
||||
opacity: 0.75,
|
||||
},
|
||||
"&:hover > svg": {
|
||||
display: "block",
|
||||
},
|
||||
}}
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
>
|
||||
{!localImage ? (
|
||||
<NoImageIcon />
|
||||
) : expanded ? (
|
||||
<CollapseIcon />
|
||||
) : (
|
||||
<ExpandIcon />
|
||||
)}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
@@ -244,7 +244,23 @@ export const tableSettings = (
|
||||
type: FieldType.paragraph,
|
||||
name: "description",
|
||||
label: "Description (optional)",
|
||||
minRows: 2,
|
||||
},
|
||||
{
|
||||
step: "display",
|
||||
type: "tableDetails",
|
||||
name: "details",
|
||||
label: "Details (optional)",
|
||||
},
|
||||
{
|
||||
step: "display",
|
||||
type: "tableThumbnail",
|
||||
name: "thumbnailFile",
|
||||
label: "Thumbnail Image (optional)",
|
||||
},
|
||||
{
|
||||
step: "display",
|
||||
type: FieldType.hidden,
|
||||
name: "thumbnailURL",
|
||||
},
|
||||
|
||||
// Step 3: Access controls
|
||||
|
||||
14
src/components/TableSettingsDialog/utils.ts
Normal file
14
src/components/TableSettingsDialog/utils.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import {
|
||||
FirebaseStorage,
|
||||
getDownloadURL,
|
||||
ref,
|
||||
uploadBytes,
|
||||
} from "firebase/storage";
|
||||
|
||||
export const uploadTableThumbnail =
|
||||
(storage: FirebaseStorage) => (tableId: string, imageFile: File) => {
|
||||
const storageRef = ref(storage, `__thumbnails__/${tableId}`);
|
||||
return uploadBytes(storageRef, imageFile).then(({ ref }) =>
|
||||
getDownloadURL(ref)
|
||||
);
|
||||
};
|
||||
25
src/components/TableToolbar/TableInformation.tsx
Normal file
25
src/components/TableToolbar/TableInformation.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useAtom } from "jotai";
|
||||
import { RESET } from "jotai/utils";
|
||||
|
||||
import {
|
||||
sideDrawerAtom,
|
||||
tableScope,
|
||||
tableSettingsAtom,
|
||||
} from "@src/atoms/tableScope";
|
||||
|
||||
import TableToolbarButton from "@src/components/TableToolbar/TableToolbarButton";
|
||||
import { TableInformation as TableInformationIcon } from "@src/assets/icons";
|
||||
|
||||
export default function TableInformation() {
|
||||
const [tableSettings] = useAtom(tableSettingsAtom, tableScope);
|
||||
const [sideDrawer, setSideDrawer] = useAtom(sideDrawerAtom, tableScope);
|
||||
|
||||
return (
|
||||
<TableToolbarButton
|
||||
title="Table Information"
|
||||
icon={<TableInformationIcon />}
|
||||
onClick={() => setSideDrawer(sideDrawer ? RESET : "table-information")}
|
||||
disabled={!setSideDrawer || tableSettings.id.includes("/")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -36,6 +36,9 @@ import { FieldType } from "@src/constants/fields";
|
||||
const Filters = lazy(() => import("./Filters" /* webpackChunkName: "Filters" */));
|
||||
// prettier-ignore
|
||||
const ImportData = lazy(() => import("./ImportData/ImportData" /* webpackChunkName: "ImportData" */));
|
||||
// prettier-ignore
|
||||
const TableInformation = lazy(() => import("./TableInformation" /* webpackChunkName: "TableInformation" */));
|
||||
|
||||
// prettier-ignore
|
||||
const ReExecute = lazy(() => import("./ReExecute" /* webpackChunkName: "ReExecute" */));
|
||||
|
||||
@@ -147,6 +150,9 @@ export default function TableToolbar() {
|
||||
<TableSettings />
|
||||
</>
|
||||
)}
|
||||
<Suspense fallback={<ButtonSkeleton />}>
|
||||
<TableInformation />
|
||||
</Suspense>
|
||||
<div className="end-spacer" />
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -7,10 +7,9 @@ import {
|
||||
Typography,
|
||||
CardActions,
|
||||
Button,
|
||||
Box,
|
||||
} from "@mui/material";
|
||||
import { Go as GoIcon } from "@src/assets/icons";
|
||||
|
||||
import RenderedMarkdown from "@src/components/RenderedMarkdown";
|
||||
import { TableSettings } from "@src/types/table";
|
||||
|
||||
export interface ITableCardProps extends TableSettings {
|
||||
@@ -19,6 +18,7 @@ export interface ITableCardProps extends TableSettings {
|
||||
}
|
||||
|
||||
export default function TableCard({
|
||||
thumbnailURL,
|
||||
section,
|
||||
name,
|
||||
description,
|
||||
@@ -37,27 +37,46 @@ export default function TableCard({
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
|
||||
<CardContent style={{ flexGrow: 1, paddingTop: 0 }}>
|
||||
<Typography
|
||||
color="textSecondary"
|
||||
sx={{
|
||||
minHeight: (theme) =>
|
||||
(theme.typography.body2.lineHeight as number) * 2 + "em",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 1,
|
||||
}}
|
||||
component="div"
|
||||
>
|
||||
{description && (
|
||||
<RenderedMarkdown
|
||||
children={description}
|
||||
//restrictionPreset="singleLine"
|
||||
{thumbnailURL && (
|
||||
<CardContent style={{ flexGrow: 1, paddingTop: 0 }}>
|
||||
<Box
|
||||
sx={{
|
||||
paddingBottom: "56.25%",
|
||||
position: "relative",
|
||||
backgroundColor: "action.input",
|
||||
borderRadius: 1,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundImage: `url("${thumbnailURL}")`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Box>
|
||||
</CardContent>
|
||||
)}
|
||||
{description && (
|
||||
<CardContent style={{ flexGrow: 1, paddingTop: 0, paddingBottom: 0 }}>
|
||||
<Typography
|
||||
color="textSecondary"
|
||||
sx={{
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
}}
|
||||
component="div"
|
||||
>
|
||||
{description}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
)}
|
||||
|
||||
<CardActions>
|
||||
<Button
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Fade } from "@mui/material";
|
||||
import ErrorFallback, {
|
||||
InlineErrorFallback,
|
||||
} from "@src/components/ErrorFallback";
|
||||
import TableInformationDrawer from "@src/components/TableInformationDrawer/SideDrawer";
|
||||
import TableToolbarSkeleton from "@src/components/TableToolbar/TableToolbarSkeleton";
|
||||
import TableSkeleton from "@src/components/Table/TableSkeleton";
|
||||
import EmptyTable from "@src/components/Table/EmptyTable";
|
||||
@@ -103,6 +104,12 @@ export default function TablePage({
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
|
||||
<ErrorBoundary FallbackComponent={InlineErrorFallback}>
|
||||
<Suspense fallback={null}>
|
||||
<TableInformationDrawer />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
|
||||
{!disableModals && (
|
||||
<ErrorBoundary FallbackComponent={InlineErrorFallback}>
|
||||
<Suspense fallback={null}>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import { find, groupBy, sortBy } from "lodash-es";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import {
|
||||
Container,
|
||||
@@ -13,12 +14,14 @@ import {
|
||||
IconButton,
|
||||
Zoom,
|
||||
} from "@mui/material";
|
||||
|
||||
import ViewListIcon from "@mui/icons-material/ViewListOutlined";
|
||||
import ViewGridIcon from "@mui/icons-material/ViewModuleOutlined";
|
||||
import FavoriteBorderIcon from "@mui/icons-material/FavoriteBorder";
|
||||
import FavoriteIcon from "@mui/icons-material/Favorite";
|
||||
import EditIcon from "@mui/icons-material/EditOutlined";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import { TableInformation as TableInformationIcon } from "@src/assets/icons";
|
||||
|
||||
import FloatingSearch from "@src/components/FloatingSearch";
|
||||
import SlideTransition from "@src/components/Modal/SlideTransition";
|
||||
@@ -54,7 +57,7 @@ export default function TablesPage() {
|
||||
tableSettingsDialogAtom,
|
||||
projectScope
|
||||
);
|
||||
|
||||
const navigate = useNavigate();
|
||||
useScrollToHash();
|
||||
|
||||
const [results, query, handleQuery] = useBasicSearch(
|
||||
@@ -159,6 +162,16 @@ export default function TablesPage() {
|
||||
sx={view === "list" ? { p: 1.5 } : undefined}
|
||||
color="secondary"
|
||||
/>
|
||||
<IconButton
|
||||
aria-label="Table Information"
|
||||
size={view === "list" ? "large" : undefined}
|
||||
onClick={() => {
|
||||
navigate(`${getLink(table)}#sideDrawer="table-information"`);
|
||||
}}
|
||||
style={{ marginLeft: 0 }}
|
||||
>
|
||||
<TableInformationIcon />
|
||||
</IconButton>
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
getTableSchemaAtom,
|
||||
AdditionalTableSettings,
|
||||
MinimumTableSettings,
|
||||
currentUserAtom,
|
||||
} from "@src/atoms/projectScope";
|
||||
import { firebaseDbAtom } from "./init";
|
||||
|
||||
@@ -21,12 +22,14 @@ import {
|
||||
TABLE_SCHEMAS,
|
||||
TABLE_GROUP_SCHEMAS,
|
||||
} from "@src/config/dbPaths";
|
||||
import { rowyUser } from "@src/utils/table";
|
||||
import { TableSettings, TableSchema } from "@src/types/table";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { getFieldProp } from "@src/components/fields";
|
||||
|
||||
export function useTableFunctions() {
|
||||
const [firebaseDb] = useAtom(firebaseDbAtom, projectScope);
|
||||
const [currentUser] = useAtom(currentUserAtom, projectScope);
|
||||
|
||||
// Create a function to get the latest tables from project settings,
|
||||
// so we don’t create new functions when tables change
|
||||
@@ -93,10 +96,11 @@ export function useTableFunctions() {
|
||||
};
|
||||
}
|
||||
|
||||
const _createdBy = currentUser && rowyUser(currentUser);
|
||||
// Appends table to settings doc
|
||||
const promiseUpdateSettings = setDoc(
|
||||
doc(firebaseDb, SETTINGS),
|
||||
{ tables: [...tables, settings] },
|
||||
{ tables: [...tables, { ...settings, _createdBy }] },
|
||||
{ merge: true }
|
||||
);
|
||||
|
||||
@@ -120,7 +124,7 @@ export function useTableFunctions() {
|
||||
await Promise.all([promiseUpdateSettings, promiseAddSchema]);
|
||||
}
|
||||
);
|
||||
}, [firebaseDb, readTables, setCreateTable]);
|
||||
}, [currentUser, firebaseDb, readTables, setCreateTable]);
|
||||
|
||||
// Set the createTable function
|
||||
const setUpdateTable = useSetAtom(updateTableAtom, projectScope);
|
||||
|
||||
12
src/types/table.d.ts
vendored
12
src/types/table.d.ts
vendored
@@ -70,6 +70,18 @@ export type TableSettings = {
|
||||
|
||||
section: string;
|
||||
description?: string;
|
||||
details?: string;
|
||||
thumbnailURL?: string;
|
||||
|
||||
_createdBy?: {
|
||||
displayName?: string;
|
||||
email?: string;
|
||||
emailVerified: boolean;
|
||||
isAnonymous: boolean;
|
||||
photoURL?: string;
|
||||
uid: string;
|
||||
timestamp: firebase.firestore.Timestamp;
|
||||
};
|
||||
|
||||
tableType: "primaryCollection" | "collectionGroup";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user