mirror of
https://github.com/rowyio/rowy.git
synced 2026-02-24 04:01:17 +01:00
@@ -1,2 +1,2 @@
|
||||
REACT_APP_FIREBASE_PROJECT_ID=
|
||||
REACT_APP_FIREBASE_PROJECT_WEB_API_KEY=
|
||||
VITE_APP_FIREBASE_PROJECT_ID=
|
||||
VITE_APP_FIREBASE_PROJECT_WEB_API_KEY=
|
||||
@@ -84,7 +84,7 @@
|
||||
"startWithEmulators": "VITE_APP_FIREBASE_EMULATORS=true vite --port 7699",
|
||||
"emulators": "firebase emulators:start --only firestore,auth --import ./emulators/ --export-on-exit",
|
||||
"test": "vitest",
|
||||
"build": "tsc && vite build",
|
||||
"build": "tsc && cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build",
|
||||
"preview": "vite preview --port 7699",
|
||||
"analyze": "source-map-explorer ./build/static/js/*.js",
|
||||
"prepare": "husky install",
|
||||
|
||||
@@ -51,8 +51,6 @@ const ProvidedSubTablePage = lazy(() => import("@src/pages/Table/ProvidedSubTabl
|
||||
// prettier-ignore
|
||||
const TableTutorialPage = lazy(() => import("@src/pages/Table/TableTutorialPage" /* webpackChunkName: "TableTutorialPage" */));
|
||||
|
||||
// prettier-ignore
|
||||
const FunctionPage = lazy(() => import("@src/pages/FunctionPage" /* webpackChunkName: "FunctionPage" */));
|
||||
// prettier-ignore
|
||||
const UserSettingsPage = lazy(() => import("@src/pages/Settings/UserSettingsPage" /* webpackChunkName: "UserSettingsPage" */));
|
||||
// prettier-ignore
|
||||
@@ -169,13 +167,6 @@ export default function App() {
|
||||
element={<TableTutorialPage />}
|
||||
/>
|
||||
|
||||
<Route path={ROUTES.function}>
|
||||
<Route
|
||||
index
|
||||
element={<Navigate to={ROUTES.functions} replace />}
|
||||
/>
|
||||
<Route path=":id" element={<FunctionPage />} />
|
||||
</Route>
|
||||
<Route
|
||||
path={ROUTES.settings}
|
||||
element={<Navigate to={ROUTES.userSettings} replace />}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { AutoTypings, LocalStorageCache } from "monaco-editor-auto-typings";
|
||||
import Editor, { OnMount } from "@monaco-editor/react";
|
||||
|
||||
const defaultCode = `import React from "react";
|
||||
function App() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Hello World!</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
`;
|
||||
const handleEditorMount: OnMount = (monacoEditor, monaco) => {
|
||||
console.log("handleEditorMount");
|
||||
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
|
||||
target: monaco.languages.typescript.ScriptTarget.ES2016,
|
||||
allowNonTsExtensions: true,
|
||||
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
||||
module: monaco.languages.typescript.ModuleKind.CommonJS,
|
||||
noEmit: true,
|
||||
typeRoots: ["node_modules/@types"],
|
||||
});
|
||||
|
||||
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
|
||||
noSemanticValidation: false,
|
||||
noSyntaxValidation: false,
|
||||
});
|
||||
|
||||
const autoTypings = AutoTypings.create(monacoEditor, {
|
||||
sourceCache: new LocalStorageCache(), // Cache loaded sources in localStorage. May be omitted
|
||||
monaco: monaco,
|
||||
onError: (error) => {
|
||||
console.log(error);
|
||||
},
|
||||
onUpdate: (update, textual) => {
|
||||
console.log(textual);
|
||||
},
|
||||
});
|
||||
};
|
||||
export default function Function() {
|
||||
const onChange = (value: string | undefined, ev: any) => {
|
||||
//console.log(value)
|
||||
};
|
||||
return (
|
||||
<Editor
|
||||
height="100vh"
|
||||
theme="vs-dark"
|
||||
defaultPath="app.tsx"
|
||||
defaultLanguage="typescript"
|
||||
defaultValue={defaultCode}
|
||||
onChange={onChange}
|
||||
onMount={handleEditorMount}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
//export * from "./Function";
|
||||
export { default } from "./Function";
|
||||
@@ -1,79 +0,0 @@
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardActionArea,
|
||||
CardContent,
|
||||
Typography,
|
||||
CardActions,
|
||||
Button,
|
||||
} 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 {
|
||||
link: string;
|
||||
actions?: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function TableCard({
|
||||
section,
|
||||
name,
|
||||
description,
|
||||
link,
|
||||
actions,
|
||||
}: ITableCardProps) {
|
||||
return (
|
||||
<Card style={{ height: "100%", display: "flex", flexDirection: "column" }}>
|
||||
<CardActionArea component={Link} to={link}>
|
||||
<CardContent style={{ paddingBottom: 0 }}>
|
||||
<Typography variant="overline" component="p">
|
||||
{section}
|
||||
</Typography>
|
||||
<Typography variant="h6" component="h3" gutterBottom>
|
||||
{name}
|
||||
</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"
|
||||
/>
|
||||
)}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
|
||||
<CardActions>
|
||||
<Button
|
||||
variant="text"
|
||||
color="primary"
|
||||
endIcon={<GoIcon />}
|
||||
component={Link}
|
||||
to={link}
|
||||
>
|
||||
Open
|
||||
</Button>
|
||||
|
||||
<div style={{ flexGrow: 1 }} />
|
||||
|
||||
{actions}
|
||||
</CardActions>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
CardActions,
|
||||
Skeleton,
|
||||
} from "@mui/material";
|
||||
|
||||
export default function TableCardSkeleton() {
|
||||
return (
|
||||
<Card style={{ height: "100%", display: "flex", flexDirection: "column" }}>
|
||||
<CardContent style={{ flexGrow: 1 }}>
|
||||
<Typography variant="overline">
|
||||
<Skeleton width={80} />
|
||||
</Typography>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<Skeleton width={180} />
|
||||
</Typography>
|
||||
<Typography
|
||||
color="textSecondary"
|
||||
sx={{
|
||||
minHeight: (theme) =>
|
||||
(theme.typography.body2.lineHeight as number) * 2 + "em",
|
||||
}}
|
||||
>
|
||||
<Skeleton width={120} />
|
||||
</Typography>
|
||||
</CardContent>
|
||||
|
||||
<CardActions sx={{ mb: 1, mx: 1 }}>
|
||||
<Skeleton
|
||||
width={60}
|
||||
height={20}
|
||||
variant="rectangular"
|
||||
sx={{ borderRadius: 1, mr: "auto" }}
|
||||
/>
|
||||
|
||||
<Skeleton variant="circular" width={24} height={24} />
|
||||
</CardActions>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import { TransitionGroup } from "react-transition-group";
|
||||
|
||||
import { Box, Grid, Collapse } from "@mui/material";
|
||||
|
||||
import SectionHeading from "@src/components/SectionHeading";
|
||||
import TableCard from "./TableCard";
|
||||
import SlideTransition from "@src/components/Modal/SlideTransition";
|
||||
|
||||
import { TableSettings } from "@src/types/table";
|
||||
|
||||
export interface ITableGridProps {
|
||||
sections: Record<string, TableSettings[]>;
|
||||
getLink: (table: TableSettings) => string;
|
||||
getActions?: (table: TableSettings) => React.ReactNode;
|
||||
}
|
||||
|
||||
export default function TableGrid({
|
||||
sections,
|
||||
getLink,
|
||||
getActions,
|
||||
}: ITableGridProps) {
|
||||
return (
|
||||
<TransitionGroup>
|
||||
{Object.entries(sections).map(
|
||||
([sectionName, sectionTables], sectionIndex) => {
|
||||
const tableItems = sectionTables
|
||||
.map((table, tableIndex) => {
|
||||
if (!table) return null;
|
||||
|
||||
return (
|
||||
<SlideTransition
|
||||
key={table.id}
|
||||
appear
|
||||
timeout={(sectionIndex + 1) * 100 + tableIndex * 50}
|
||||
>
|
||||
<Grid item xs={12} sm={6} md={4} lg={3}>
|
||||
<TableCard
|
||||
{...table}
|
||||
link={getLink(table)}
|
||||
actions={getActions ? getActions(table) : null}
|
||||
/>
|
||||
</Grid>
|
||||
</SlideTransition>
|
||||
);
|
||||
})
|
||||
.filter((item) => item !== null);
|
||||
|
||||
if (tableItems.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Collapse key={sectionName}>
|
||||
<Box component="section" sx={{ mt: 4 }}>
|
||||
<SlideTransition
|
||||
key={"grid-section-" + sectionName}
|
||||
in
|
||||
timeout={(sectionIndex + 1) * 100}
|
||||
>
|
||||
<SectionHeading sx={{ pl: 2, pr: 1.5 }}>
|
||||
{sectionName}
|
||||
</SectionHeading>
|
||||
</SlideTransition>
|
||||
|
||||
<Grid component={TransitionGroup} container spacing={2}>
|
||||
{tableItems}
|
||||
</Grid>
|
||||
</Box>
|
||||
</Collapse>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</TransitionGroup>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./TableGrid";
|
||||
export { default } from "./TableGrid";
|
||||
@@ -1,71 +0,0 @@
|
||||
import { TransitionGroup } from "react-transition-group";
|
||||
|
||||
import { Box, Paper, Collapse, List } from "@mui/material";
|
||||
|
||||
import SectionHeading from "@src/components/SectionHeading";
|
||||
import TableListItem from "./TableListItem";
|
||||
import SlideTransition from "@src/components/Modal/SlideTransition";
|
||||
|
||||
import { TableSettings } from "@src/types/table";
|
||||
|
||||
export interface ITableListProps {
|
||||
sections: Record<string, TableSettings[]>;
|
||||
getLink: (table: TableSettings) => string;
|
||||
getActions?: (table: TableSettings) => React.ReactNode;
|
||||
}
|
||||
|
||||
export default function TableList({
|
||||
sections,
|
||||
getLink,
|
||||
getActions,
|
||||
}: ITableListProps) {
|
||||
return (
|
||||
<TransitionGroup>
|
||||
{Object.entries(sections).map(
|
||||
([sectionName, sectionTables], sectionIndex) => {
|
||||
const tableItems = sectionTables
|
||||
.map((table) => {
|
||||
if (!table) return null;
|
||||
|
||||
return (
|
||||
<Collapse key={table.id}>
|
||||
<TableListItem
|
||||
{...table}
|
||||
link={getLink(table)}
|
||||
actions={getActions ? getActions(table) : null}
|
||||
/>
|
||||
</Collapse>
|
||||
);
|
||||
})
|
||||
.filter((item) => item !== null);
|
||||
|
||||
if (tableItems.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Collapse key={sectionName}>
|
||||
<Box component="section" sx={{ mt: 4 }}>
|
||||
<SlideTransition
|
||||
key={"list-section-" + sectionName}
|
||||
in
|
||||
timeout={(sectionIndex + 1) * 100}
|
||||
>
|
||||
<SectionHeading sx={{ pl: 2, pr: 1 }}>
|
||||
{sectionName}
|
||||
</SectionHeading>
|
||||
</SlideTransition>
|
||||
|
||||
<SlideTransition in timeout={(sectionIndex + 1) * 100}>
|
||||
<Paper>
|
||||
<List disablePadding>
|
||||
<TransitionGroup>{tableItems}</TransitionGroup>
|
||||
</List>
|
||||
</Paper>
|
||||
</SlideTransition>
|
||||
</Box>
|
||||
</Collapse>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</TransitionGroup>
|
||||
);
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import {
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
Typography,
|
||||
IconButton,
|
||||
} from "@mui/material";
|
||||
import GoIcon from "@mui/icons-material/ArrowForward";
|
||||
|
||||
import RenderedMarkdown from "@src/components/RenderedMarkdown";
|
||||
import { TableSettings } from "@src/types/table";
|
||||
|
||||
export interface ITableListItemProps extends TableSettings {
|
||||
link: string;
|
||||
actions?: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function TableListItem({
|
||||
// section,
|
||||
name,
|
||||
description,
|
||||
link,
|
||||
actions,
|
||||
}: ITableListItemProps) {
|
||||
return (
|
||||
<ListItem disableGutters disablePadding>
|
||||
<ListItemButton
|
||||
component={Link}
|
||||
to={link}
|
||||
sx={{
|
||||
alignItems: "baseline",
|
||||
height: 48,
|
||||
py: 0,
|
||||
pr: 0,
|
||||
borderRadius: 2,
|
||||
"& > *": { lineHeight: "48px !important" },
|
||||
flexWrap: "nowrap",
|
||||
overflow: "hidden",
|
||||
|
||||
flexBasis: 160 + 16,
|
||||
flexGrow: 0,
|
||||
flexShrink: 0,
|
||||
mr: 2,
|
||||
}}
|
||||
>
|
||||
<Typography component="h3" variant="button" noWrap>
|
||||
{name}
|
||||
</Typography>
|
||||
</ListItemButton>
|
||||
|
||||
<Typography
|
||||
color="textSecondary"
|
||||
component="div"
|
||||
noWrap
|
||||
sx={{ flexGrow: 1, "& *": { display: "inline" } }}
|
||||
>
|
||||
{description && (
|
||||
<RenderedMarkdown
|
||||
children={description}
|
||||
restrictionPreset="singleLine"
|
||||
/>
|
||||
)}
|
||||
</Typography>
|
||||
|
||||
<div style={{ flexShrink: 0 }}>
|
||||
{actions}
|
||||
|
||||
<IconButton
|
||||
size="large"
|
||||
color="primary"
|
||||
component={Link}
|
||||
to={link}
|
||||
sx={{ display: { xs: "none", sm: "inline-flex" } }}
|
||||
>
|
||||
<GoIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { ListItem, Skeleton } from "@mui/material";
|
||||
|
||||
export default function TableListItemSkeleton() {
|
||||
return (
|
||||
<ListItem disableGutters disablePadding style={{ height: 48 }}>
|
||||
<Skeleton width={160} sx={{ mx: 2, flexShrink: 0 }} />
|
||||
<Skeleton sx={{ mr: 2, flexBasis: 240, flexShrink: 1 }} />
|
||||
|
||||
<Skeleton
|
||||
variant="circular"
|
||||
width={24}
|
||||
height={24}
|
||||
sx={{ ml: "auto", mr: 3, flexShrink: 0 }}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="circular"
|
||||
width={24}
|
||||
height={24}
|
||||
sx={{ mr: 1.5, flexShrink: 0 }}
|
||||
/>
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./TableList";
|
||||
export { default } from "./TableList";
|
||||
@@ -1,33 +0,0 @@
|
||||
import { Zoom, Stack, Typography } from "@mui/material";
|
||||
|
||||
export default function HomeWelcomePrompt() {
|
||||
return (
|
||||
<Zoom in style={{ transformOrigin: `${320 - 52}px ${320 - 52}px` }}>
|
||||
<Stack
|
||||
justifyContent="center"
|
||||
sx={{
|
||||
bgcolor: "primary.main",
|
||||
color: "primary.contrastText",
|
||||
boxShadow: 24,
|
||||
|
||||
width: 320,
|
||||
height: 320,
|
||||
p: 5,
|
||||
borderRadius: "50% 50% 0 50%",
|
||||
|
||||
position: "fixed",
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
}}
|
||||
>
|
||||
<Typography variant="overline" component="h1" gutterBottom>
|
||||
Get started
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h5" component="p">
|
||||
Create a function
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Zoom>
|
||||
);
|
||||
}
|
||||
@@ -33,7 +33,7 @@ export default function EmptyTable() {
|
||||
: false;
|
||||
let contents = <></>;
|
||||
|
||||
if (tableSettings.isCollection !== false && hasData) {
|
||||
if (hasData) {
|
||||
contents = (
|
||||
<>
|
||||
<div>
|
||||
@@ -41,9 +41,15 @@ export default function EmptyTable() {
|
||||
Get started
|
||||
</Typography>
|
||||
<Typography>
|
||||
There is existing data in the Firestore collection:
|
||||
{tableSettings.isCollection === false
|
||||
? "There is existing data in the Array Sub Table:"
|
||||
: "There is existing data in the Firestore collection:"}
|
||||
<br />
|
||||
<code>{tableSettings.collection}</code>
|
||||
<code>
|
||||
{tableSettings.collection}
|
||||
{tableSettings.subTableKey?.length &&
|
||||
`.${tableSettings.subTableKey}`}
|
||||
</code>
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -19,6 +19,9 @@ import { format } from "date-fns";
|
||||
import { DATE_FORMAT, DATE_TIME_FORMAT } from "@src/constants/dates";
|
||||
import { isDate, isFunction } from "lodash-es";
|
||||
import { getDurationString } from "@src/components/fields/Duration/utils";
|
||||
import { doc } from "firebase/firestore";
|
||||
import { firebaseDbAtom } from "@src/sources/ProjectSourceFirebase";
|
||||
import { projectScope } from "@src/atoms/projectScope";
|
||||
|
||||
export const SUPPORTED_TYPES_COPY = new Set([
|
||||
// TEXT
|
||||
@@ -56,6 +59,8 @@ export const SUPPORTED_TYPES_COPY = new Set([
|
||||
FieldType.updatedBy,
|
||||
FieldType.createdAt,
|
||||
FieldType.updatedAt,
|
||||
// CONNECTION
|
||||
FieldType.reference,
|
||||
]);
|
||||
|
||||
export const SUPPORTED_TYPES_PASTE = new Set([
|
||||
@@ -75,6 +80,8 @@ export const SUPPORTED_TYPES_PASTE = new Set([
|
||||
FieldType.json,
|
||||
FieldType.code,
|
||||
FieldType.markdown,
|
||||
// CONNECTION
|
||||
FieldType.reference,
|
||||
]);
|
||||
|
||||
export function useMenuAction(
|
||||
@@ -87,6 +94,7 @@ export function useMenuAction(
|
||||
const updateField = useSetAtom(updateFieldAtom, tableScope);
|
||||
const [cellValue, setCellValue] = useState<any>();
|
||||
const [selectedCol, setSelectedCol] = useState<ColumnConfig>();
|
||||
const [firebaseDb] = useAtom(firebaseDbAtom, projectScope);
|
||||
|
||||
const handleCopy = useCallback(async () => {
|
||||
try {
|
||||
@@ -165,6 +173,13 @@ export function useMenuAction(
|
||||
case "string":
|
||||
parsed = text;
|
||||
break;
|
||||
case "reference":
|
||||
try {
|
||||
parsed = doc(firebaseDb, text);
|
||||
} catch (e: any) {
|
||||
enqueueSnackbar(`Invalid reference.`, { variant: "error" });
|
||||
}
|
||||
break;
|
||||
default:
|
||||
parsed = JSON.parse(text);
|
||||
break;
|
||||
@@ -319,6 +334,8 @@ export function useMenuAction(
|
||||
case FieldType.createdBy:
|
||||
case FieldType.updatedBy:
|
||||
return cellValue.displayName;
|
||||
case FieldType.reference:
|
||||
return cellValue.path;
|
||||
default:
|
||||
return cellValue;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,17 @@ import { FieldType } from "@src/constants/fields";
|
||||
import { getFieldProp } from "@src/components/fields";
|
||||
import { suggestType } from "@src/components/TableModals/ImportAirtableWizard/utils";
|
||||
|
||||
function getFieldKeys(records: any[]) {
|
||||
let fieldKeys = new Set<string>();
|
||||
for (let i = 0; i < records.length; i++) {
|
||||
const keys = Object.keys(records[i].fields);
|
||||
for (let j = 0; j < keys.length; j++) {
|
||||
fieldKeys.add(keys[j]);
|
||||
}
|
||||
}
|
||||
return [...fieldKeys];
|
||||
}
|
||||
|
||||
export default function Step1Columns({
|
||||
airtableData,
|
||||
config,
|
||||
@@ -57,8 +68,7 @@ export default function Step1Columns({
|
||||
config.pairs.map((pair) => pair.fieldKey)
|
||||
);
|
||||
|
||||
const fieldKeys = Object.keys(airtableData.records[0].fields);
|
||||
|
||||
const fieldKeys = getFieldKeys(airtableData.records);
|
||||
// When a field is selected to be imported
|
||||
const handleSelect =
|
||||
(field: string) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
|
||||
@@ -20,10 +20,14 @@ export default function TableName({ watchedField, ...props }: ITableNameProps) {
|
||||
const watchedValue = useWatch({ control, name: watchedField } as any);
|
||||
useEffect(() => {
|
||||
if (!disabled) {
|
||||
if (typeof value === "string" && value.trim() !== "") {
|
||||
onChange(value);
|
||||
} else if (typeof watchedValue === "string" && !!watchedValue) {
|
||||
const touched = control.getFieldState(props.name).isTouched;
|
||||
|
||||
if (!touched && typeof watchedValue === "string" && !!watchedValue) {
|
||||
// if table name field is not touched, and watched value is valid, set table name to watched value
|
||||
onChange(startCase(watchedValue));
|
||||
} else if (typeof value === "string") {
|
||||
// otherwise if table name is valid, set watched value to table name
|
||||
onChange(value.trim());
|
||||
}
|
||||
}
|
||||
}, [watchedValue, disabled, onChange, value]);
|
||||
|
||||
@@ -149,6 +149,7 @@ export const tableSettings = (
|
||||
// https://firebase.google.com/docs/firestore/quotas#collections_documents_and_fields
|
||||
validation: [
|
||||
["matches", /^[^\s]+$/, "Collection name cannot have spaces"],
|
||||
["matches", /^[^.]+$/, "Collection name cannot have dots"],
|
||||
["notOneOf", [".", ".."], "Collection name cannot be . or .."],
|
||||
[
|
||||
"test",
|
||||
@@ -194,6 +195,7 @@ export const tableSettings = (
|
||||
// https://firebase.google.com/docs/firestore/quotas#collections_documents_and_fields
|
||||
validation: [
|
||||
["matches", /^[^\s]+$/, "Collection name cannot have spaces"],
|
||||
["matches", /^[^.]+$/, "Collection name cannot have dots"],
|
||||
["notOneOf", [".", ".."], "Collection name cannot be . or .."],
|
||||
[
|
||||
"test",
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { useState, useEffect } from "react";
|
||||
import { useAtom } from "jotai";
|
||||
import useMemoValue from "use-memo-value";
|
||||
import { isEmpty, isDate } from "lodash-es";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { useSnackbar } from "notistack";
|
||||
|
||||
import {
|
||||
Tab,
|
||||
@@ -19,6 +22,7 @@ import TabPanel from "@mui/lab/TabPanel";
|
||||
|
||||
import FiltersPopover from "./FiltersPopover";
|
||||
import FilterInputs from "./FilterInputs";
|
||||
import { changePageUrl, separateOperands } from "./utils";
|
||||
|
||||
import {
|
||||
projectScope,
|
||||
@@ -62,12 +66,17 @@ export default function Filters() {
|
||||
const [, setTableSorts] = useAtom(tableSortsAtom, tableScope);
|
||||
const [updateTableSchema] = useAtom(updateTableSchemaAtom, tableScope);
|
||||
const [{ defaultQuery }] = useAtom(tableFiltersPopoverAtom, tableScope);
|
||||
|
||||
const tableFilterInputs = useFilterInputs(tableColumnsOrdered);
|
||||
const setTableQuery = tableFilterInputs.setQuery;
|
||||
const userFilterInputs = useFilterInputs(tableColumnsOrdered, defaultQuery);
|
||||
const setUserQuery = userFilterInputs.setQuery;
|
||||
const { availableFilters } = userFilterInputs;
|
||||
const { availableFilters, filterColumns } = userFilterInputs;
|
||||
const [searchParams] = useSearchParams();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
useEffect(() => {
|
||||
let isFiltered = searchParams.get("filter");
|
||||
if (isFiltered) updateUserFilter(isFiltered);
|
||||
}, [searchParams]);
|
||||
|
||||
// Get table filters & user filters from config documents
|
||||
const tableFilters = useMemoValue(
|
||||
@@ -82,6 +91,44 @@ export default function Filters() {
|
||||
const hasTableFilters =
|
||||
Array.isArray(tableFilters) && tableFilters.length > 0;
|
||||
const hasUserFilters = Array.isArray(userFilters) && userFilters.length > 0;
|
||||
function updateUserFilter(str: string) {
|
||||
let { operators, operands = [] } = separateOperands(str);
|
||||
if (!operators.length) return;
|
||||
if (operators.length) {
|
||||
let appliedFilter: TableFilter[] = [];
|
||||
appliedFilter = [
|
||||
{
|
||||
key: operands[0],
|
||||
operator: operators[0],
|
||||
value: Number(operands[1]),
|
||||
},
|
||||
];
|
||||
let isValidFilter = checkFilterValidation(appliedFilter[0]);
|
||||
if (isValidFilter) {
|
||||
setOverrideTableFilters(true);
|
||||
setUserFilters(appliedFilter);
|
||||
} else {
|
||||
enqueueSnackbar("Oops, Invalid filter!!!", { variant: "error" });
|
||||
setUserFilters([]);
|
||||
setOverrideTableFilters(false);
|
||||
userFilterInputs.resetQuery();
|
||||
}
|
||||
}
|
||||
}
|
||||
function checkFilterValidation(filter: TableFilter): boolean {
|
||||
let isFilterableColumn = filterColumns?.filter(
|
||||
(item) =>
|
||||
item.key === filter.key ||
|
||||
item.label === filter.key ||
|
||||
item.type === filter.key
|
||||
);
|
||||
if (!isFilterableColumn?.length) return false;
|
||||
filter.key = isFilterableColumn?.[0]?.value;
|
||||
filter.operator = filter.operator === "-is-" ? "id-equal" : filter.operator;
|
||||
filter.value =
|
||||
filter.operator === "id-equal" ? filter.value.toString() : filter.value;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Set the local table filter
|
||||
useEffect(() => {
|
||||
@@ -109,7 +156,7 @@ export default function Filters() {
|
||||
} else if (hasUserFilters) {
|
||||
filtersToApply = userFilters;
|
||||
}
|
||||
|
||||
updatePageURL(filtersToApply);
|
||||
setLocalFilters(filtersToApply);
|
||||
// Reset order so we don’t have to make a new index
|
||||
if (filtersToApply.length) {
|
||||
@@ -120,7 +167,6 @@ export default function Filters() {
|
||||
hasUserFilters,
|
||||
setLocalFilters,
|
||||
setTableSorts,
|
||||
setTableQuery,
|
||||
tableFilters,
|
||||
tableFiltersOverridable,
|
||||
setUserQuery,
|
||||
@@ -173,7 +219,21 @@ export default function Filters() {
|
||||
if (updateUserSettings && filters)
|
||||
updateUserSettings({ tables: { [`${tableId}`]: { filters } } });
|
||||
};
|
||||
|
||||
function updatePageURL(filters: TableFilter[]) {
|
||||
if (!filters.length) {
|
||||
changePageUrl();
|
||||
} else {
|
||||
const [filter] = filters;
|
||||
const fieldName = filter.key === "_rowy_ref.id" ? "ID" : filter.key;
|
||||
const operator =
|
||||
filter.operator === "id-equal" ? "-is-" : filter.operator;
|
||||
const formattedValue = availableFilters?.valueFormatter
|
||||
? availableFilters.valueFormatter(filter.value, filter.operator)
|
||||
: filter.value.toString();
|
||||
const queryParams = `?filter=${fieldName}${operator}${formattedValue}`;
|
||||
changePageUrl(queryParams);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<FiltersPopover
|
||||
appliedFilters={appliedFilters}
|
||||
|
||||
27
src/components/TableToolbar/Filters/utils.tsx
Normal file
27
src/components/TableToolbar/Filters/utils.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
export const URL =
|
||||
window.location.protocol +
|
||||
"//" +
|
||||
window.location.host +
|
||||
window.location.pathname;
|
||||
export function separateOperands(str: string): {
|
||||
operators: any[];
|
||||
operands: string[];
|
||||
} {
|
||||
const operators = findOperators(str);
|
||||
const operands = str.split(
|
||||
new RegExp(operators.map((op) => `\\${op}`).join("|"), "g")
|
||||
);
|
||||
return { operators, operands };
|
||||
}
|
||||
export function changePageUrl(newURL: string | undefined = URL) {
|
||||
if (newURL !== URL) {
|
||||
newURL = URL + newURL;
|
||||
}
|
||||
window.history.pushState({ path: newURL }, "", newURL);
|
||||
}
|
||||
|
||||
function findOperators(str: string) {
|
||||
const operators = [">=", "<=", ">", "<", "==", "!=", "=", "-is-"];
|
||||
const regex = new RegExp(operators.map((op) => `\\${op}`).join("|"), "g");
|
||||
return str.match(regex) || [];
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import DisplayCell from "./DisplayCell";
|
||||
import EditorCell from "./EditorCell";
|
||||
import { filterOperators } from "@src/components/fields/ShortText/Filter";
|
||||
import { valueFormatter } from "./filters";
|
||||
import BasicContextMenuActions from "@src/components/Table/ContextMenu/BasicCellContextMenuActions";
|
||||
|
||||
const SideDrawerField = lazy(
|
||||
() =>
|
||||
@@ -24,6 +25,7 @@ export const config: IFieldConfig = {
|
||||
initializable: true,
|
||||
icon: <Reference />,
|
||||
description: "Firestore document reference",
|
||||
contextMenuActions: BasicContextMenuActions,
|
||||
TableCell: withRenderTableCell(DisplayCell, EditorCell, "focus", {
|
||||
disablePadding: true,
|
||||
}),
|
||||
|
||||
@@ -47,7 +47,7 @@ export const getColors = (
|
||||
option: string
|
||||
): SelectColorThemeOptions => {
|
||||
const defaultColor = paletteToMui(palette.aGray);
|
||||
const key = option.toLocaleLowerCase().replace(" ", "_").trim();
|
||||
const key = option.toLocaleLowerCase?.().replace(" ", "_").trim();
|
||||
const color = list.find((opt: IColors) => opt.name === key);
|
||||
// Null check in return
|
||||
return color || defaultColor;
|
||||
@@ -113,7 +113,7 @@ export default function Settings({ onChange, config }: ISettingsProps) {
|
||||
color?: SelectColorThemeOptions,
|
||||
newKey?: string
|
||||
) => {
|
||||
const _key = key.toLocaleLowerCase().replace(" ", "_").trim();
|
||||
const _key = key.toLocaleLowerCase?.().replace(" ", "_").trim();
|
||||
const exists = colors.findIndex((option: IColors) => option.name === _key);
|
||||
|
||||
// If saving Check if object with the `color.name` is equal to `_key` and replace value at the index of `exists`
|
||||
@@ -135,7 +135,7 @@ export default function Settings({ onChange, config }: ISettingsProps) {
|
||||
}
|
||||
|
||||
if (type === "update" && newKey) {
|
||||
const _newKey = newKey.toLocaleLowerCase().replace(" ", "_").trim();
|
||||
const _newKey = newKey.toLocaleLowerCase?.().replace(" ", "_").trim();
|
||||
const updatedColors = colors.map((option: IColors) =>
|
||||
option.name === _key ? { ...option, name: _newKey } : option
|
||||
);
|
||||
|
||||
@@ -16,10 +16,6 @@ export enum ROUTES {
|
||||
pageNotFound = "/404",
|
||||
|
||||
tables = "/tables",
|
||||
automations = "/automations",
|
||||
functions = "/functions",
|
||||
function = "/function",
|
||||
functionWithId = "/function/:id",
|
||||
|
||||
table = "/table",
|
||||
tableWithId = "/table/:id",
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import { Suspense } from "react";
|
||||
import { useAtom, Provider } from "jotai";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { isEmpty } from "lodash-es";
|
||||
|
||||
import { Fade } from "@mui/material";
|
||||
|
||||
//import TableHeaderSkeleton from "@src/components/Table/Skeleton/TableHeaderSkeleton";
|
||||
//import TableSkeleton from "@src/components/Table/Skeleton/TableSkeleton";
|
||||
//import EmptyTable from "@src/components/Table/EmptyTable";
|
||||
import Function from "@src/components/Function";
|
||||
|
||||
import { currentUserAtom, projectScope } from "@src/atoms/projectScope";
|
||||
// import TableSourceFirestore from "@src/sources/TableSourceFirestore";
|
||||
// import {
|
||||
// tableScope,
|
||||
// tableIdAtom,
|
||||
// tableSettingsAtom,
|
||||
// tableSchemaAtom,
|
||||
// } from "@src/atoms/tableScope";
|
||||
|
||||
export default function FunctionPage() {
|
||||
// const [tableId] = useAtom(tableIdAtom, tableScope);
|
||||
// const [tableSettings] = useAtom(tableSettingsAtom, tableScope);
|
||||
// const [tableSchema] = useAtom(tableSchemaAtom, tableScope);
|
||||
|
||||
// console.log(tableSchema);
|
||||
|
||||
// if (isEmpty(tableSchema.columns))
|
||||
// return (
|
||||
// <Fade style={{ transitionDelay: "500ms" }}>
|
||||
// <div>
|
||||
// <EmptyTable />
|
||||
// </div>
|
||||
// </Fade>
|
||||
// );
|
||||
|
||||
return <Function key="function" />;
|
||||
}
|
||||
|
||||
function ProvidedFunctionPage() {
|
||||
const { id } = useParams();
|
||||
const [currentUser] = useAtom(currentUserAtom, projectScope);
|
||||
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<>
|
||||
{/* <TableHeaderSkeleton />
|
||||
<TableSkeleton /> */}
|
||||
</>
|
||||
}
|
||||
>
|
||||
{/* <Provider
|
||||
key={id}
|
||||
scope={tableScope}
|
||||
initialValues={[
|
||||
[tableIdAtom, id],
|
||||
[currentUserAtom, currentUser],
|
||||
]}
|
||||
> */}
|
||||
{/* <TableSourceFirestore /> */}
|
||||
<FunctionPage />
|
||||
{/* </Provider> */}
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -1,226 +0,0 @@
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import { find, groupBy } from "lodash-es";
|
||||
|
||||
import {
|
||||
Container,
|
||||
Stack,
|
||||
Typography,
|
||||
ToggleButtonGroup,
|
||||
ToggleButton,
|
||||
Tooltip,
|
||||
Fab,
|
||||
Checkbox,
|
||||
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 FloatingSearch from "@src/components/FloatingSearch";
|
||||
import SlideTransition from "@src/components/Modal/SlideTransition";
|
||||
import FunctionGrid from "@src/components/Functions/FunctionGrid";
|
||||
import FunctionList from "@src/components/Functions/FunctionList";
|
||||
import HomeWelcomePrompt from "@src/components/Functions/HomeWelcomePrompt";
|
||||
import EmptyState from "@src/components/EmptyState";
|
||||
|
||||
import {
|
||||
projectScope,
|
||||
userRolesAtom,
|
||||
userSettingsAtom,
|
||||
updateUserSettingsAtom,
|
||||
tablesAtom,
|
||||
tablesViewAtom,
|
||||
tableSettingsDialogAtom,
|
||||
} from "@src/atoms/projectScope";
|
||||
import { TableSettings } from "@src/types/table";
|
||||
import { ROUTES } from "@src/constants/routes";
|
||||
import useBasicSearch from "@src/hooks/useBasicSearch";
|
||||
import { TOP_BAR_HEIGHT } from "@src/layouts/Navigation/TopBar";
|
||||
|
||||
const SEARCH_KEYS = ["id", "name", "section", "description"];
|
||||
|
||||
export default function HomePage() {
|
||||
const [userRoles] = useAtom(userRolesAtom, projectScope);
|
||||
const [userSettings] = useAtom(userSettingsAtom, projectScope);
|
||||
const [updateUserSettings] = useAtom(updateUserSettingsAtom, projectScope);
|
||||
const [tables] = useAtom(tablesAtom, projectScope);
|
||||
const [view, setView] = useAtom(tablesViewAtom, projectScope);
|
||||
const openTableSettingsDialog = useSetAtom(
|
||||
tableSettingsDialogAtom,
|
||||
projectScope
|
||||
);
|
||||
|
||||
const [results, query, handleQuery] = useBasicSearch(
|
||||
tables ?? [],
|
||||
SEARCH_KEYS
|
||||
);
|
||||
|
||||
const favorites = Array.isArray(userSettings.favoriteTables)
|
||||
? userSettings.favoriteTables
|
||||
: [];
|
||||
const sections: Record<string, TableSettings[]> = {
|
||||
Favorites: favorites.map((id) => find(results, { id })) as TableSettings[],
|
||||
...groupBy(results, "section"),
|
||||
};
|
||||
|
||||
if (!Array.isArray(tables))
|
||||
throw new Error(
|
||||
"Project settings are not configured correctly. `tables` is not an array."
|
||||
);
|
||||
|
||||
const createFunctionFab = (
|
||||
<Tooltip title="Create function">
|
||||
<Zoom in>
|
||||
<Fab
|
||||
color="secondary"
|
||||
aria-label="Create table"
|
||||
onClick={() => openTableSettingsDialog({ mode: "create" })}
|
||||
sx={{
|
||||
zIndex: "speedDial",
|
||||
position: "fixed",
|
||||
bottom: (theme) => ({
|
||||
xs: `max(${theme.spacing(2)}, env(safe-area-inset-bottom))`,
|
||||
sm: `max(${theme.spacing(3)}, env(safe-area-inset-bottom))`,
|
||||
}),
|
||||
right: (theme) => ({
|
||||
xs: `max(${theme.spacing(2)}, env(safe-area-inset-right))`,
|
||||
sm: `max(${theme.spacing(3)}, env(safe-area-inset-right))`,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<AddIcon />
|
||||
</Fab>
|
||||
</Zoom>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
if (tables.length === 0) {
|
||||
if (userRoles.includes("ADMIN"))
|
||||
return (
|
||||
<>
|
||||
<HomeWelcomePrompt />
|
||||
{createFunctionFab}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<EmptyState
|
||||
message="No tables"
|
||||
description="There are no tables in this project. Sign in with an ADMIN account to create tables."
|
||||
fullScreen
|
||||
style={{ marginTop: -TOP_BAR_HEIGHT }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const getLink = (table: TableSettings) =>
|
||||
`${ROUTES.table}/${table.id.replace(/\//g, "~2F")}`;
|
||||
|
||||
const handleFavorite =
|
||||
(id: string) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const favoriteTables = e.target.checked
|
||||
? [...favorites, id]
|
||||
: favorites.filter((f) => f !== id);
|
||||
|
||||
if (updateUserSettings) updateUserSettings({ favoriteTables });
|
||||
};
|
||||
|
||||
const getActions = (table: TableSettings) => (
|
||||
<>
|
||||
{userRoles.includes("ADMIN") && (
|
||||
<IconButton
|
||||
aria-label="Edit table"
|
||||
onClick={() =>
|
||||
openTableSettingsDialog({ mode: "update", data: table })
|
||||
}
|
||||
size={view === "list" ? "large" : undefined}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
<Checkbox
|
||||
onChange={handleFavorite(table.id)}
|
||||
checked={favorites.includes(table.id)}
|
||||
icon={<FavoriteBorderIcon />}
|
||||
checkedIcon={
|
||||
<Zoom in>
|
||||
<FavoriteIcon />
|
||||
</Zoom>
|
||||
}
|
||||
name={`favorite-${table.id}`}
|
||||
inputProps={{ "aria-label": "Favorite" }}
|
||||
sx={view === "list" ? { p: 1.5 } : undefined}
|
||||
color="secondary"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Container component="main" sx={{ px: 1, pt: 1, pb: 7 + 3 + 3 }}>
|
||||
<FloatingSearch
|
||||
label="Search tables"
|
||||
onChange={(e) => handleQuery(e.target.value)}
|
||||
paperSx={{
|
||||
maxWidth: (theme) => ({ md: theme.breakpoints.values.sm - 48 }),
|
||||
mb: { xs: 2, md: -6 },
|
||||
}}
|
||||
/>
|
||||
|
||||
<SlideTransition in timeout={50}>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
component="h1"
|
||||
sx={{ pl: 2, cursor: "default" }}
|
||||
>
|
||||
{query ? `${results.length} of ${tables.length}` : tables.length}{" "}
|
||||
tables
|
||||
</Typography>
|
||||
|
||||
<ToggleButtonGroup
|
||||
value={view}
|
||||
size="large"
|
||||
exclusive
|
||||
onChange={(_, v) => {
|
||||
if (v !== null) setView(v);
|
||||
}}
|
||||
aria-label="Table view"
|
||||
sx={{ "& .MuiToggleButton-root": { borderRadius: 2 } }}
|
||||
>
|
||||
<ToggleButton value="list" aria-label="List view">
|
||||
<ViewListIcon style={{ transform: "rotate(180deg)" }} />
|
||||
</ToggleButton>
|
||||
<ToggleButton value="grid" aria-label="Grid view">
|
||||
<ViewGridIcon />
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Stack>
|
||||
</SlideTransition>
|
||||
|
||||
{view === "list" ? (
|
||||
<FunctionList
|
||||
sections={sections}
|
||||
getLink={getLink}
|
||||
getActions={getActions}
|
||||
/>
|
||||
) : (
|
||||
<FunctionGrid
|
||||
sections={sections}
|
||||
getLink={getLink}
|
||||
getActions={getActions}
|
||||
/>
|
||||
)}
|
||||
|
||||
{userRoles.includes("ADMIN") && createFunctionFab}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
3
src/types/table.d.ts
vendored
3
src/types/table.d.ts
vendored
@@ -193,7 +193,8 @@ export type TableFilter = {
|
||||
| "time-minute-equal"
|
||||
| "id-equal"
|
||||
| "color-equal"
|
||||
| "color-not-equal";
|
||||
| "color-not-equal"
|
||||
| "-is-";
|
||||
value: any;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user