From 355648ee37c70ddef13958a78902af38429bb5d2 Mon Sep 17 00:00:00 2001 From: Han Tuerker Date: Mon, 26 Sep 2022 15:25:03 +0300 Subject: [PATCH 001/114] feat(formula-field): add formula field draft --- src/components/fields/Formula/Settings.tsx | 154 +++++++++++++++++++ src/components/fields/Formula/TableCell.tsx | 17 ++ src/components/fields/Formula/index.tsx | 28 ++++ src/components/fields/Formula/useFormula.tsx | 71 +++++++++ src/components/fields/Formula/util.tsx | 54 +++++++ src/components/fields/Formula/worker.ts | 19 +++ src/components/fields/index.ts | 3 + src/constants/externalLinks.ts | 2 + src/constants/fields.ts | 2 + 9 files changed, 350 insertions(+) create mode 100644 src/components/fields/Formula/Settings.tsx create mode 100644 src/components/fields/Formula/TableCell.tsx create mode 100644 src/components/fields/Formula/index.tsx create mode 100644 src/components/fields/Formula/useFormula.tsx create mode 100644 src/components/fields/Formula/util.tsx create mode 100644 src/components/fields/Formula/worker.ts diff --git a/src/components/fields/Formula/Settings.tsx b/src/components/fields/Formula/Settings.tsx new file mode 100644 index 000000000..7cf8f5d36 --- /dev/null +++ b/src/components/fields/Formula/Settings.tsx @@ -0,0 +1,154 @@ +import { lazy, Suspense } from "react"; +import { useDebouncedCallback } from "use-debounce"; +import { FieldType, ISettingsProps } from "@src/components/fields/types"; +import { + styled, + Grid, + InputLabel, + Typography, + Stack, + Tooltip, +} from "@mui/material"; +import FieldSkeleton from "@src/components/SideDrawer/FieldSkeleton"; + +import { useAtom } from "jotai"; +import { + tableColumnsOrderedAtom, + tableRowsAtom, + tableScope, +} from "@src/atoms/tableScope"; +import Cell from "@src/components/Table/Cell"; +import Column from "@src/components/Table/Column"; +import { useFormula } from "./useFormula"; +import { ignoredColumns, typeDefs } from "./util"; + +const CodeEditor = lazy( + () => + import("@src/components/CodeEditor" /* webpackChunkName: "CodeEditor" */) +); + +const ColumnWrapper = styled(Grid)(() => ({ + width: 200, + flexShrink: 0, + marginLeft: -1, + "&:first-of-type": { marginLeft: 0 }, +})); + +export default function Settings({ + config, + onChange, + fieldName, + onBlur, + errors, +}: ISettingsProps) { + const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); + const [tableRows] = useAtom(tableRowsAtom, tableScope); + const { error, result, loading } = useFormula({ + row: tableRows[0], + formulaFn: config.formulaFn, + }); + + if (error && !config?.error) { + onChange("error")(error.message); + } + if (!error && config?.error) { + onChange("error")(undefined); + } + + if (loading) { + console.log("loading"); + } + + if (error) { + console.log(error); + } + + const previewColumns = tableColumnsOrdered.filter( + (c) => !ignoredColumns.includes(c.type) + ); + + const formulaFn = + config?.formulaFn ?? + `// Write your formula code here +// for example: +// return column1 + column2; +// checkout the documentation for more info: https://docs.rowy.io/field-types/formula`; + + const defs = previewColumns + .map((c) => `declare const ${c.key}: ${typeDefs(c.type)};`) + .join("\n"); + + return ( + + Formula script +
+ + + Available: + + + {Object.values(previewColumns).map((column) => ( + + + {column.fieldName} + + + ))} + + + }> + + +
+ + Preview: + + + + + + {Object.entries(previewColumns).map( + ([field, { name, type, key }]) => ( + + + + + ) + )} + + +
+ ); +} + +export const settingsValidator = (config: any) => { + const errors: Record = {}; + if (config.error) errors.error = config.error; + return errors; +}; diff --git a/src/components/fields/Formula/TableCell.tsx b/src/components/fields/Formula/TableCell.tsx new file mode 100644 index 000000000..fc1f1cdbd --- /dev/null +++ b/src/components/fields/Formula/TableCell.tsx @@ -0,0 +1,17 @@ +import { IHeavyCellProps } from "@src/components/fields/types"; + +import { Grid } from "@mui/material"; +import { useFormula } from "./useFormula"; + +export default function Formula({ row, column, onSubmit }: IHeavyCellProps) { + const { result, error } = useFormula({ + row, + formulaFn: column.config?.formulaFn, + }); + + return ( + + {error ? error.message : result} + + ); +} diff --git a/src/components/fields/Formula/index.tsx b/src/components/fields/Formula/index.tsx new file mode 100644 index 000000000..bfc532334 --- /dev/null +++ b/src/components/fields/Formula/index.tsx @@ -0,0 +1,28 @@ +import { lazy } from "react"; +import { IFieldConfig, FieldType } from "@src/components/fields/types"; +import { Derivative as DerivativeIcon } from "@src/assets/icons"; +import BasicCell from "@src/components/fields/_BasicCell/BasicCellNull"; +import NullEditor from "@src/components/Table/editors/NullEditor"; +import Settings, { settingsValidator } from "./Settings"; +import withHeavyCell from "@src/components/fields/_withTableCell/withHeavyCell"; + +const TableCell = lazy( + () => import("./TableCell" /* webpackChunkName: "TableCell-Formula" */) +); + +export const config: IFieldConfig = { + type: FieldType.formula, + name: "Formula", + group: "Client Function", + dataType: "any", + initialValue: undefined, + icon: , + description: "Client Function (Alpha)", + TableCell: withHeavyCell(BasicCell, TableCell), + TableEditor: NullEditor as any, + SideDrawerField: BasicCell as any, + settings: Settings, + settingsValidator: settingsValidator, + requireConfiguration: true, +}; +export default config; diff --git a/src/components/fields/Formula/useFormula.tsx b/src/components/fields/Formula/useFormula.tsx new file mode 100644 index 000000000..e4b4d1cfe --- /dev/null +++ b/src/components/fields/Formula/useFormula.tsx @@ -0,0 +1,71 @@ +import { tableColumnsOrderedAtom, tableScope } from "@src/atoms/tableScope"; +import { TableRow } from "@src/types/table"; +import { useAtom } from "jotai"; +import { pick, zipObject } from "lodash-es"; +import { useEffect, useMemo, useState } from "react"; +import { ignoredColumns, useDeepCompareMemoize } from "./util"; + +export const useFormula = ({ + row, + formulaFn, +}: { + row: TableRow; + formulaFn: string; +}) => { + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); + + const availableColumns = tableColumnsOrdered + .filter((column) => !ignoredColumns.includes(column.type)) + .map((c) => c.key); + + const availableFields = useMemo( + () => ({ + ...zipObject( + availableColumns, + Array(availableColumns.length).fill(undefined) + ), + ...pick(row, availableColumns), + }), + [row, availableColumns] + ); + + useEffect(() => { + setResult(null); + setError(null); + setLoading(true); + + const worker = new Worker(new URL("./worker.ts", import.meta.url)); + const timeout = setTimeout(() => { + setError(new Error("Timeout")); + setLoading(false); + worker.terminate(); + }, 1000); + + worker.onmessage = ({ data: { result, error } }: any) => { + worker.terminate(); + if (error) { + setError(error); + } else { + setResult(result); + } + setLoading(false); + clearInterval(timeout); + }; + + worker.postMessage({ + formulaFn: formulaFn, + fields: availableFields, + }); + + return () => { + worker.terminate(); + clearInterval(timeout); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [useDeepCompareMemoize(availableFields), formulaFn]); + + return { result, error, loading }; +}; diff --git a/src/components/fields/Formula/util.tsx b/src/components/fields/Formula/util.tsx new file mode 100644 index 000000000..ec9b9e491 --- /dev/null +++ b/src/components/fields/Formula/util.tsx @@ -0,0 +1,54 @@ +import { FieldType } from "@src/constants/fields"; +import { isEqual } from "lodash-es"; +import { useMemo, useRef } from "react"; + +export function useDeepCompareMemoize(value: T) { + const ref = useRef(value); + const signalRef = useRef(0); + + if (!isEqual(value, ref.current)) { + ref.current = value; + signalRef.current += 1; + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + return useMemo(() => ref.current, [signalRef.current]); +} + +export const ignoredColumns = [ + FieldType.subTable, + FieldType.derivative, + FieldType.formula, + FieldType.connector, + FieldType.subTable, + FieldType.reference, + FieldType.connectTable, + FieldType.connectService, +]; + +export const typeDefs = (type: FieldType) => { + switch (type) { + case FieldType.shortText: + case FieldType.longText: + case FieldType.richText: + case FieldType.email: + case FieldType.phone: + case FieldType.url: + case FieldType.singleSelect: + return "string | undefined"; + case FieldType.multiSelect: + return "Array | undefined"; + case FieldType.number: + case FieldType.rating: + case FieldType.slider: + case FieldType.percentage: + return "number | undefined"; + case FieldType.checkbox: + return "boolean | undefined"; + case FieldType.date: + case FieldType.dateTime: + case FieldType.duration: + return "{ seconds: number; nanoseconds: number; }"; + } + return "any"; +}; diff --git a/src/components/fields/Formula/worker.ts b/src/components/fields/Formula/worker.ts new file mode 100644 index 000000000..586adfe57 --- /dev/null +++ b/src/components/fields/Formula/worker.ts @@ -0,0 +1,19 @@ +onmessage = ({ data }) => { + try { + const { formulaFn, fields } = data; + // eslint-disable-next-line no-new-func + const result = new Function(...Object.keys(fields), formulaFn).call( + {}, + ...Object.values(fields) + ); + postMessage({ result }); + } catch (error: any) { + console.error(error); + postMessage({ error: new Error(error) }); + } finally { + // eslint-disable-next-line no-restricted-globals + self.close(); + } +}; + +export {}; diff --git a/src/components/fields/index.ts b/src/components/fields/index.ts index d5288ef0d..0d54b0a57 100644 --- a/src/components/fields/index.ts +++ b/src/components/fields/index.ts @@ -33,6 +33,7 @@ import Json from "./Json"; import Code from "./Code"; import Action from "./Action"; import Derivative from "./Derivative"; +import Formula from "./Formula"; import Markdown from "./Markdown"; // // import Aggregate from "./Aggregate"; import Status from "./Status"; @@ -86,6 +87,8 @@ export const FIELDS: IFieldConfig[] = [ Derivative, // // Aggregate, Status, + /** CLIENT FUNCTION */ + Formula, /** AUDITING */ CreatedBy, UpdatedBy, diff --git a/src/constants/externalLinks.ts b/src/constants/externalLinks.ts index bf2f41abd..01c829384 100644 --- a/src/constants/externalLinks.ts +++ b/src/constants/externalLinks.ts @@ -52,6 +52,8 @@ const WIKI_PATHS = { fieldTypesAction: "/field-types/action", fieldTypesAdd: "/field-types/add", + fieldTypesFormula: "/field-types/formula", + rowyRun: "/rowy-run", extensions: "/extensions", diff --git a/src/constants/fields.ts b/src/constants/fields.ts index 4f6919864..e5cbe1d45 100644 --- a/src/constants/fields.ts +++ b/src/constants/fields.ts @@ -40,6 +40,8 @@ export enum FieldType { derivative = "DERIVATIVE", aggregate = "AGGREGATE", status = "STATUS", + // CLIENT FUNCTION + formula = "FORMULA", // AUDIT createdBy = "CREATED_BY", updatedBy = "UPDATED_BY", From d4343ce35615a351479baeee28bb23bfd4ed519d Mon Sep 17 00:00:00 2001 From: Han Tuerker Date: Wed, 16 Nov 2022 03:50:28 +0300 Subject: [PATCH 002/114] update to new table system --- src/components/Table/withTableCell.tsx | 1 + .../{TableCell.tsx => DisplayCell.tsx} | 5 +- src/components/fields/Formula/Settings.tsx | 79 ++++++++++++++++--- src/components/fields/Formula/index.tsx | 23 +++--- src/components/fields/Formula/useFormula.tsx | 4 +- 5 files changed, 81 insertions(+), 31 deletions(-) rename src/components/fields/Formula/{TableCell.tsx => DisplayCell.tsx} (68%) diff --git a/src/components/Table/withTableCell.tsx b/src/components/Table/withTableCell.tsx index d959e408c..dac750593 100644 --- a/src/components/Table/withTableCell.tsx +++ b/src/components/Table/withTableCell.tsx @@ -132,6 +132,7 @@ export default function withTableCell( ); + if (disabled || (editorMode !== "inline" && !focusInsideCell)) return displayCell; diff --git a/src/components/fields/Formula/TableCell.tsx b/src/components/fields/Formula/DisplayCell.tsx similarity index 68% rename from src/components/fields/Formula/TableCell.tsx rename to src/components/fields/Formula/DisplayCell.tsx index fc1f1cdbd..0d28f4073 100644 --- a/src/components/fields/Formula/TableCell.tsx +++ b/src/components/fields/Formula/DisplayCell.tsx @@ -1,9 +1,8 @@ -import { IHeavyCellProps } from "@src/components/fields/types"; - import { Grid } from "@mui/material"; +import { IDisplayCellProps } from "@src/components/fields/types"; import { useFormula } from "./useFormula"; -export default function Formula({ row, column, onSubmit }: IHeavyCellProps) { +export default function Formula({ row, column }: IDisplayCellProps) { const { result, error } = useFormula({ row, formulaFn: column.config?.formulaFn, diff --git a/src/components/fields/Formula/Settings.tsx b/src/components/fields/Formula/Settings.tsx index 7cf8f5d36..13c4b2195 100644 --- a/src/components/fields/Formula/Settings.tsx +++ b/src/components/fields/Formula/Settings.tsx @@ -2,12 +2,12 @@ import { lazy, Suspense } from "react"; import { useDebouncedCallback } from "use-debounce"; import { FieldType, ISettingsProps } from "@src/components/fields/types"; import { - styled, Grid, InputLabel, Typography, Stack, Tooltip, + FormHelperText, } from "@mui/material"; import FieldSkeleton from "@src/components/SideDrawer/FieldSkeleton"; @@ -17,27 +17,20 @@ import { tableRowsAtom, tableScope, } from "@src/atoms/tableScope"; -import Cell from "@src/components/Table/Cell"; -import Column from "@src/components/Table/Column"; import { useFormula } from "./useFormula"; import { ignoredColumns, typeDefs } from "./util"; +import MultiSelect from "@rowy/multiselect"; +import FieldsDropdown from "@src/components/ColumnModals/FieldsDropdown"; const CodeEditor = lazy( () => import("@src/components/CodeEditor" /* webpackChunkName: "CodeEditor" */) ); -const ColumnWrapper = styled(Grid)(() => ({ - width: 200, - flexShrink: 0, - marginLeft: -1, - "&:first-of-type": { marginLeft: 0 }, -})); - export default function Settings({ config, - onChange, fieldName, + onChange, onBlur, errors, }: ISettingsProps) { @@ -67,6 +60,11 @@ export default function Settings({ (c) => !ignoredColumns.includes(c.type) ); + const columnOptions = tableColumnsOrdered + .filter((column) => column.fieldName !== fieldName) + .filter((column) => !ignoredColumns.includes(column.type)) + .map((c) => ({ label: c.name, value: c.key })); + const formulaFn = config?.formulaFn ?? `// Write your formula code here @@ -80,6 +78,61 @@ export default function Settings({ return ( + + + + {errors.listenerFields && ( + + {errors.listenerFields} + + )} + + Changes to these fields will trigger the evaluation of the + column. + + + ), + FormHelperTextProps: { component: "div" } as any, + required: true, + error: errors.listenerFields, + onBlur, + }} + /> + + + + + ![ + FieldType.derivative, + FieldType.aggregate, + FieldType.subTable, + FieldType.action, + ].includes(f) + )} + onChange={(value) => { + onChange("renderFieldType")(value); + }} + TextFieldProps={{ + required: true, + error: errors.renderFieldType, + helperText: errors.renderFieldType, + onBlur, + }} + /> + + + Formula script
- + {/* Preview: @@ -142,7 +195,7 @@ export default function Settings({ ) )} - + */}
); } diff --git a/src/components/fields/Formula/index.tsx b/src/components/fields/Formula/index.tsx index bfc532334..69e00104f 100644 --- a/src/components/fields/Formula/index.tsx +++ b/src/components/fields/Formula/index.tsx @@ -1,26 +1,23 @@ -import { lazy } from "react"; -import { IFieldConfig, FieldType } from "@src/components/fields/types"; import { Derivative as DerivativeIcon } from "@src/assets/icons"; -import BasicCell from "@src/components/fields/_BasicCell/BasicCellNull"; -import NullEditor from "@src/components/Table/editors/NullEditor"; -import Settings, { settingsValidator } from "./Settings"; -import withHeavyCell from "@src/components/fields/_withTableCell/withHeavyCell"; -const TableCell = lazy( - () => import("./TableCell" /* webpackChunkName: "TableCell-Formula" */) -); +import { IFieldConfig, FieldType } from "@src/components/fields/types"; +import withTableCell from "@src/components/Table/withTableCell"; + +import Settings, { settingsValidator } from "./Settings"; +import DisplayCell from "./DisplayCell"; export const config: IFieldConfig = { type: FieldType.formula, name: "Formula", group: "Client Function", dataType: "any", - initialValue: undefined, + initialValue: "", icon: , description: "Client Function (Alpha)", - TableCell: withHeavyCell(BasicCell, TableCell), - TableEditor: NullEditor as any, - SideDrawerField: BasicCell as any, + TableCell: withTableCell(DisplayCell, DisplayCell, "inline", { + usesRowData: true, + }), + SideDrawerField: () => null as any, settings: Settings, settingsValidator: settingsValidator, requireConfiguration: true, diff --git a/src/components/fields/Formula/useFormula.tsx b/src/components/fields/Formula/useFormula.tsx index e4b4d1cfe..bec88b271 100644 --- a/src/components/fields/Formula/useFormula.tsx +++ b/src/components/fields/Formula/useFormula.tsx @@ -33,8 +33,8 @@ export const useFormula = ({ ); useEffect(() => { - setResult(null); - setError(null); + console.log("useFormula calculation: ", row._rowy_ref.path); + console.log("availableFields: ", availableFields); setLoading(true); const worker = new Worker(new URL("./worker.ts", import.meta.url)); From 8e899dad7802b676ab57212cf16ddbe96057c546 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Tue, 22 Nov 2022 03:04:26 +0800 Subject: [PATCH 003/114] enable derivatives logging in code editor --- src/components/CodeEditor/CodeEditorHelper.tsx | 4 ++++ src/components/fields/Derivative/Settings.tsx | 4 ++-- src/components/fields/Derivative/derivative.d.ts | 5 +++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/components/CodeEditor/CodeEditorHelper.tsx b/src/components/CodeEditor/CodeEditorHelper.tsx index 299c21347..04326e3c9 100644 --- a/src/components/CodeEditor/CodeEditorHelper.tsx +++ b/src/components/CodeEditor/CodeEditorHelper.tsx @@ -38,6 +38,10 @@ export default function CodeEditorHelper({ key: "rowy", description: `rowy provides a set of functions that are commonly used, such as easy file uploads & access to GCP Secret Manager`, }, + { + key: "logging", + description: `logging.log is encouraged to replace console.log`, + }, ]; return ( diff --git a/src/components/fields/Derivative/Settings.tsx b/src/components/fields/Derivative/Settings.tsx index 32ce65dc0..7fe368694 100644 --- a/src/components/fields/Derivative/Settings.tsx +++ b/src/components/fields/Derivative/Settings.tsx @@ -65,10 +65,10 @@ export default function Settings({ : config.derivativeFn ? config.derivativeFn : config?.script - ? `const derivative:Derivative = async ({row,ref,db,storage,auth})=>{ + ? `const derivative:Derivative = async ({row,ref,db,storage,auth,logging})=>{ ${config.script.replace(/utilFns.getSecret/g, "rowy.secrets.get")} }` - : `const derivative:Derivative = async ({row,ref,db,storage,auth})=>{ + : `const derivative:Derivative = async ({row,ref,db,storage,auth,logging})=>{ // Write your derivative code here // for example: // const sum = row.a + row.b; diff --git a/src/components/fields/Derivative/derivative.d.ts b/src/components/fields/Derivative/derivative.d.ts index 3af58e11e..9d01f97c0 100644 --- a/src/components/fields/Derivative/derivative.d.ts +++ b/src/components/fields/Derivative/derivative.d.ts @@ -5,6 +5,11 @@ type DerivativeContext = { db: FirebaseFirestore.Firestore; auth: firebaseauth.BaseAuth; change: any; + logging: { + log: (payload: any) => void; + warn: (payload: any) => void; + error: (payload: any) => void; + }; }; type Derivative = (context: DerivativeContext) => "PLACEHOLDER_OUTPUT_TYPE"; From 5baa6386917360273d2424280dd301ecf45821e8 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Wed, 23 Nov 2022 21:26:36 +0800 Subject: [PATCH 004/114] enable actions logging in code editor --- src/components/fields/Action/Settings.tsx | 82 ++++++++++--------- src/components/fields/Action/action.d.ts | 3 + src/components/fields/Action/templates.ts | 4 +- .../fields/Derivative/derivative.d.ts | 8 +- src/components/fields/types.ts | 9 ++ 5 files changed, 59 insertions(+), 47 deletions(-) diff --git a/src/components/fields/Action/Settings.tsx b/src/components/fields/Action/Settings.tsx index 9c2562a1f..735423efc 100644 --- a/src/components/fields/Action/Settings.tsx +++ b/src/components/fields/Action/Settings.tsx @@ -130,7 +130,7 @@ const Settings = ({ config, onChange, fieldName }: ISettingsProps) => { : config?.runFn ? config.runFn : config?.script - ? `const action:Action = async ({row,ref,db,storage,auth,actionParams,user}) => { + ? `const action:Action = async ({row,ref,db,storage,auth,actionParams,user,logging}) => { ${config.script.replace(/utilFns.getSecret/g, "rowy.secrets.get")} }` : RUN_ACTION_TEMPLATE; @@ -140,7 +140,7 @@ const Settings = ({ config, onChange, fieldName }: ISettingsProps) => { : config.undoFn ? config.undoFn : get(config, "undo.script") - ? `const action : Action = async ({row,ref,db,storage,auth,actionParams,user}) => { + ? `const action : Action = async ({row,ref,db,storage,auth,actionParams,user,logging}) => { ${get(config, "undo.script")} }` : UNDO_ACTION_TEMPLATE; @@ -303,7 +303,9 @@ const Settings = ({ config, onChange, fieldName }: ISettingsProps) => { aria-label="Action will run" name="isActionScript" value={ - config.isActionScript !== false ? "actionScript" : "cloudFunction" + config.isActionScript !== false + ? "actionScript" + : "cloudFunction" } onChange={(e) => onChange("isActionScript")( @@ -559,45 +561,45 @@ const Settings = ({ config, onChange, fieldName }: ISettingsProps) => { title: "Customization", content: ( <> - - - onChange("customName.enabled")(e.target.checked) - } - name="customName.enabled" - /> - } - label="Customize label for action" - style={{ marginLeft: -11 }} - /> - {config.customName?.enabled && ( - - onChange("customName.actionName")(e.target.value) - } - label="Action name:" - className="labelHorizontal" - inputProps={{ style: { width: "10ch" } }} - > - )} - + + onChange("customName.enabled")(e.target.checked) + } + name="customName.enabled" + /> + } + label="Customize label for action" + style={{ marginLeft: -11 }} + /> + {config.customName?.enabled && ( + - onChange("customIcons.enabled")(e.target.checked) + onChange("customName.actionName")(e.target.value) } - name="customIcons.enabled" - /> - } - label="Customize button icons with emoji" - style={{ marginLeft: -11 }} - /> + label="Action name:" + className="labelHorizontal" + inputProps={{ style: { width: "10ch" } }} + > + )} + + onChange("customIcons.enabled")(e.target.checked) + } + name="customIcons.enabled" + /> + } + label="Customize button icons with emoji" + style={{ marginLeft: -11 }} + /> {config.customIcons?.enabled && ( diff --git a/src/components/fields/Action/action.d.ts b/src/components/fields/Action/action.d.ts index a43399782..933e7dbc6 100644 --- a/src/components/fields/Action/action.d.ts +++ b/src/components/fields/Action/action.d.ts @@ -1,3 +1,5 @@ +import { RowyLogging } from "@src/components/fields/types"; + type ActionUser = { timestamp: Date; displayName: string; @@ -15,6 +17,7 @@ type ActionContext = { auth: firebaseauth.BaseAuth; actionParams: actionParams; user: ActionUser; + logging: RowyLogging; }; type ActionResult = { diff --git a/src/components/fields/Action/templates.ts b/src/components/fields/Action/templates.ts index 5701ef244..04b3ab233 100644 --- a/src/components/fields/Action/templates.ts +++ b/src/components/fields/Action/templates.ts @@ -1,4 +1,4 @@ -export const RUN_ACTION_TEMPLATE = `const action:Action = async ({row,ref,db,storage,auth,actionParams,user}) => { +export const RUN_ACTION_TEMPLATE = `const action:Action = async ({row,ref,db,storage,auth,actionParams,user,logging}) => { // Write your action code here // for example: // const authToken = await rowy.secrets.get("service") @@ -26,7 +26,7 @@ export const RUN_ACTION_TEMPLATE = `const action:Action = async ({row,ref,db,sto // checkout the documentation for more info: https://docs.rowy.io/field-types/action#script }`; -export const UNDO_ACTION_TEMPLATE = `const action : Action = async ({row,ref,db,storage,auth,actionParams,user}) => { +export const UNDO_ACTION_TEMPLATE = `const action : Action = async ({row,ref,db,storage,auth,actionParams,user,logging}) => { // Write your undo code here // for example: // const authToken = await rowy.secrets.get("service") diff --git a/src/components/fields/Derivative/derivative.d.ts b/src/components/fields/Derivative/derivative.d.ts index 9d01f97c0..d210d4973 100644 --- a/src/components/fields/Derivative/derivative.d.ts +++ b/src/components/fields/Derivative/derivative.d.ts @@ -1,3 +1,5 @@ +import { RowyLogging } from "@src/components/fields/types"; + type DerivativeContext = { row: Row; ref: FirebaseFirestore.DocumentReference; @@ -5,11 +7,7 @@ type DerivativeContext = { db: FirebaseFirestore.Firestore; auth: firebaseauth.BaseAuth; change: any; - logging: { - log: (payload: any) => void; - warn: (payload: any) => void; - error: (payload: any) => void; - }; + logging: RowyLogging; }; type Derivative = (context: DerivativeContext) => "PLACEHOLDER_OUTPUT_TYPE"; diff --git a/src/components/fields/types.ts b/src/components/fields/types.ts index 668904725..a0ee73d97 100644 --- a/src/components/fields/types.ts +++ b/src/components/fields/types.ts @@ -49,6 +49,7 @@ export interface IBasicCellProps { type: FieldType; name: string; } + export interface IHeavyCellProps extends IBasicCellProps, FormatterProps { @@ -61,6 +62,7 @@ export interface IHeavyCellProps export interface IPopoverInlineCellProps extends IHeavyCellProps { showPopoverCell: React.Dispatch>; } + export interface IPopoverCellProps extends IPopoverInlineCellProps { parentRef: PopoverProps["anchorEl"]; } @@ -110,5 +112,12 @@ export interface IFilterOperator { export interface IFilterCustomInputProps { onChange: (value: any) => void; operator: TableFilter["operator"]; + [key: string]: any; } + +export interface RowyLogging { + log: (payload: any) => void; + warn: (payload: any) => void; + error: (payload: any) => void; +} From 74852a8a3dc008f01cfa94ca23603c9b3ef9d542 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Wed, 23 Nov 2022 21:32:20 +0800 Subject: [PATCH 005/114] fix action/derivative type hint issue --- src/components/fields/Action/action.d.ts | 8 +++++--- src/components/fields/Derivative/derivative.d.ts | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/components/fields/Action/action.d.ts b/src/components/fields/Action/action.d.ts index 933e7dbc6..33fc74893 100644 --- a/src/components/fields/Action/action.d.ts +++ b/src/components/fields/Action/action.d.ts @@ -1,5 +1,3 @@ -import { RowyLogging } from "@src/components/fields/types"; - type ActionUser = { timestamp: Date; displayName: string; @@ -17,7 +15,11 @@ type ActionContext = { auth: firebaseauth.BaseAuth; actionParams: actionParams; user: ActionUser; - logging: RowyLogging; + logging: { + log: (payload: any) => void; + warn: (payload: any) => void; + error: (payload: any) => void; + }; }; type ActionResult = { diff --git a/src/components/fields/Derivative/derivative.d.ts b/src/components/fields/Derivative/derivative.d.ts index d210d4973..9d01f97c0 100644 --- a/src/components/fields/Derivative/derivative.d.ts +++ b/src/components/fields/Derivative/derivative.d.ts @@ -1,5 +1,3 @@ -import { RowyLogging } from "@src/components/fields/types"; - type DerivativeContext = { row: Row; ref: FirebaseFirestore.DocumentReference; @@ -7,7 +5,11 @@ type DerivativeContext = { db: FirebaseFirestore.Firestore; auth: firebaseauth.BaseAuth; change: any; - logging: RowyLogging; + logging: { + log: (payload: any) => void; + warn: (payload: any) => void; + error: (payload: any) => void; + }; }; type Derivative = (context: DerivativeContext) => "PLACEHOLDER_OUTPUT_TYPE"; From cc2b86027d0cb413c257eb136d2fc40c22158a72 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Wed, 23 Nov 2022 22:08:35 +0800 Subject: [PATCH 006/114] enable connectors logging in code editor --- src/components/fields/Connector/connector.d.ts | 5 +++++ src/components/fields/Connector/utils.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/fields/Connector/connector.d.ts b/src/components/fields/Connector/connector.d.ts index d031d64bd..d12041d4e 100644 --- a/src/components/fields/Connector/connector.d.ts +++ b/src/components/fields/Connector/connector.d.ts @@ -15,6 +15,11 @@ type ConnectorContext = { auth: firebaseauth.BaseAuth; query: string; user: ConnectorUser; + logging: { + log: (payload: any) => void; + warn: (payload: any) => void; + error: (payload: any) => void; + }; }; type ConnectorResult = any[]; type Connector = ( diff --git a/src/components/fields/Connector/utils.ts b/src/components/fields/Connector/utils.ts index 17687fe78..889598951 100644 --- a/src/components/fields/Connector/utils.ts +++ b/src/components/fields/Connector/utils.ts @@ -11,7 +11,7 @@ export const replacer = (data: any) => (m: string, key: string) => { return get(data, objKey, defaultValue); }; -export const baseFunction = `const connectorFn: Connector = async ({query, row, user}) => { +export const baseFunction = `const connectorFn: Connector = async ({query, row, user, logging}) => { // TODO: Implement your service function here return []; };`; From 7fb8860151125b1e18bdd76d81dd290ecfed2f13 Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Wed, 23 Nov 2022 17:59:24 +0000 Subject: [PATCH 007/114] worked on issue #562 --- src/components/TableToolbar/ManageColumns.tsx | 200 ++++++++++++++++++ src/components/TableToolbar/TableToolbar.tsx | 4 +- 2 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 src/components/TableToolbar/ManageColumns.tsx diff --git a/src/components/TableToolbar/ManageColumns.tsx b/src/components/TableToolbar/ManageColumns.tsx new file mode 100644 index 000000000..c67e689bb --- /dev/null +++ b/src/components/TableToolbar/ManageColumns.tsx @@ -0,0 +1,200 @@ +import { useEffect, useRef, useMemo, useState } from "react"; +import { useAtom, useSetAtom } from "jotai"; +import { isEqual } from "lodash-es"; +import { colord } from "colord"; +import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"; + +import { Box, AutocompleteProps, Theme } from "@mui/material"; +import VisibilityIcon from "@mui/icons-material/VisibilityOutlined"; +// import VisibilityOffIcon from "@mui/icons-material/VisibilityOffOutlined"; +import ViewColumnOutlinedIcon from "@mui/icons-material/ViewColumnOutlined"; +import IconSlash from "@src/components/IconSlash"; +import DragIndicatorOutlinedIcon from "@mui/icons-material/DragIndicatorOutlined"; + +import ColumnSelect, { ColumnItem } from "@src/components/Table/ColumnSelect"; +import ButtonWithStatus from "@src/components/ButtonWithStatus"; + +import { + globalScope, + userSettingsAtom, + updateUserSettingsAtom, +} from "@src/atoms/globalScope"; +import { + tableScope, + tableIdAtom, + updateColumnAtom, +} from "@src/atoms/tableScope"; +import { formatSubTableName } from "@src/utils/table"; + +export default function ManageColumns2() { + const buttonRef = useRef(null); + + const [userSettings] = useAtom(userSettingsAtom, globalScope); + const [tableId] = useAtom(tableIdAtom, tableScope); + + const [open, setOpen] = useState(false); + + // Store local selection here + // Initialize hiddenFields from user doc + const userDocHiddenFields = useMemo( + () => + userSettings.tables?.[formatSubTableName(tableId)]?.hiddenFields ?? [], + [userSettings.tables, tableId] + ); + + const [hiddenFields, setHiddenFields] = + useState(userDocHiddenFields); + useEffect(() => { + setHiddenFields(userDocHiddenFields); + }, [userDocHiddenFields]); + + // const tableColumns = tableColumnsOrdered.map(({ key, name }) => ({ + // value: key, + // label: name, + // })); + + // Save when MultiSelect closes + const [updateUserSettings] = useAtom(updateUserSettingsAtom, globalScope); + const handleSave = () => { + // Only update if there were any changes because it’s slow to update + if (!isEqual(hiddenFields, userDocHiddenFields) && updateUserSettings) { + updateUserSettings({ + tables: { [formatSubTableName(tableId)]: { hiddenFields } }, + }); + } + + setOpen(false); + }; + + const renderOption: AutocompleteProps< + any, + true, + false, + any + >["renderOption"] = (props, option, { selected }) => { + const slashColor = (theme: Theme) => + colord(theme.palette.background.paper) + .mix("#fff", theme.palette.mode === "dark" ? 0.16 : 0) + .alpha(1); + return ( + + {(provided) => ( +
  • + + + + + + + slashColor(theme).toHslString(), + }, + ".Mui-focused & .icon-slash-mask": { + stroke: (theme) => + slashColor(theme) + .mix( + theme.palette.primary.main, + theme.palette.action.selectedOpacity + + theme.palette.action.hoverOpacity + ) + .alpha(1) + .toHslString(), + }, + }, + selected ? { strokeDashoffset: 0 } : {}, + ]} + /> + + +
  • + )} +
    + ); + }; + + const updateColumn = useSetAtom(updateColumnAtom, tableScope); + function handleOnDragEnd(result: any) { + if (!result.destination) return; + updateColumn({ + key: result.draggableId, + config: {}, + index: result.destination.index, + }); + } + + return ( + <> + } + onClick={() => setOpen((o) => !o)} + active={hiddenFields.length > 0} + ref={buttonRef} + > + {"Columns"} + + + + {(provided) => ( + <> + + {/*
    + {provided.placeholder} +
    */} + + )} +
    +
    + + ); +} diff --git a/src/components/TableToolbar/TableToolbar.tsx b/src/components/TableToolbar/TableToolbar.tsx index b7858176f..715353f31 100644 --- a/src/components/TableToolbar/TableToolbar.tsx +++ b/src/components/TableToolbar/TableToolbar.tsx @@ -15,6 +15,7 @@ import AddRow from "./AddRow"; import LoadedRowsStatus from "./LoadedRowsStatus"; import TableSettings from "./TableSettings"; import HiddenFields from "./HiddenFields"; +import ManageColumns from "./ManageColumns"; import RowHeight from "./RowHeight"; import { @@ -85,7 +86,8 @@ export default function TableToolbar() { >
    {/* Spacer */} - + {/* */} + }> From 35a173fba0e29926a9cd247d032f3d6cf105135d Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Thu, 24 Nov 2022 06:25:23 +0800 Subject: [PATCH 008/114] enable webhooks logging in code editor --- .../WebhooksModal/Schemas/basic.tsx | 89 ++++++++++++------- .../WebhooksModal/Schemas/sendgrid.tsx | 4 +- .../WebhooksModal/Schemas/stripe.tsx | 4 +- .../WebhooksModal/Schemas/typeform.tsx | 4 +- .../WebhooksModal/Schemas/webform.tsx | 4 +- .../TableModals/WebhooksModal/utils.tsx | 40 +++++++-- .../TableModals/WebhooksModal/webhooks.d.ts | 10 +++ 7 files changed, 106 insertions(+), 49 deletions(-) diff --git a/src/components/TableModals/WebhooksModal/Schemas/basic.tsx b/src/components/TableModals/WebhooksModal/Schemas/basic.tsx index 7f9e4d119..54075d434 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/basic.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/basic.tsx @@ -21,17 +21,41 @@ const requestType = [ export const parserExtraLibs = [ requestType, - `type Parser = (args:{req:WebHookRequest,db: FirebaseFirestore.Firestore,ref: FirebaseFirestore.CollectionReference,res:{ - send:(v:any)=>void - sendStatus:(status:number)=>void - }}) => Promise;`, + `type Parser = ( + args: { + req: WebHookRequest; + db: FirebaseFirestore.Firestore; + ref: FirebaseFirestore.CollectionReference; + res: { + send: (v:any)=>void; + sendStatus: (status:number)=>void + }; + logging: { + log: (payload: any) => void; + warn: (payload: any) => void; + error: (payload: any) => void; + }; + } + ) => Promise;`, ]; export const conditionExtraLibs = [ requestType, - `type Condition = (args:{req:WebHookRequest,db: FirebaseFirestore.Firestore,ref: FirebaseFirestore.CollectionReference,res:{ - send:(v:any)=>void - sendStatus:(status:number)=>void - }}) => Promise;`, + `type Condition = ( + args: { + req: WebHookRequest; + db: FirebaseFirestore.Firestore; + ref: FirebaseFirestore.CollectionReference; + res: { + send: (v:any)=>void; + sendStatus: (status:number)=>void; + }; + logging: { + log: (payload: any) => void; + warn: (payload: any) => void; + error: (payload: any) => void; + }; + } + ) => Promise;`, ]; const additionalVariables = [ @@ -48,30 +72,29 @@ export const webhookBasic = { extraLibs: parserExtraLibs, template: ( table: TableSettings - ) => `const basicParser: Parser = async({req, db,ref}) => { - // request is the request object from the webhook - // db is the database object - // ref is the reference to collection of the table - // the returned object will be added as a new row to the table - // eg: adding the webhook body as row - const {body} = req; - ${ - table.audit !== false - ? ` - // auditField - const ${ - table.auditFieldCreatedBy ?? "_createdBy" - } = await rowy.metadata.serviceAccountUser() - return { - ...body, - ${table.auditFieldCreatedBy ?? "_createdBy"} - } - ` - : ` - return body; - ` - } - + ) => `const basicParser: Parser = async({req, db, ref, logging}) => { + // request is the request object from the webhook + // db is the database object + // ref is the reference to collection of the table + // the returned object will be added as a new row to the table + // eg: adding the webhook body as row + const {body} = req; + ${ + table.audit !== false + ? ` + // auditField + const ${ + table.auditFieldCreatedBy ?? "_createdBy" + } = await rowy.metadata.serviceAccountUser() + return { + ...body, + ${table.auditFieldCreatedBy ?? "_createdBy"} + } + ` + : ` + return body; + ` + } }`, }, condition: { @@ -79,7 +102,7 @@ export const webhookBasic = { extraLibs: conditionExtraLibs, template: ( table: TableSettings - ) => `const condition: Condition = async({ref,req,db}) => { + ) => `const condition: Condition = async({ref, req, db, logging}) => { // feel free to add your own code logic here return true; }`, diff --git a/src/components/TableModals/WebhooksModal/Schemas/sendgrid.tsx b/src/components/TableModals/WebhooksModal/Schemas/sendgrid.tsx index b557dd786..b251697fe 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/sendgrid.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/sendgrid.tsx @@ -13,7 +13,7 @@ export const webhookSendgrid = { extraLibs: null, template: ( table: TableSettings - ) => `const sendgridParser: Parser = async ({ req, db, ref }) => { + ) => `const sendgridParser: Parser = async ({ req, db, ref, logging }) => { const { body } = req const eventHandler = async (sgEvent) => { // Event handlers can be modiefed to preform different actions based on the sendgrid event @@ -35,7 +35,7 @@ export const webhookSendgrid = { extraLibs: null, template: ( table: TableSettings - ) => `const condition: Condition = async({ref,req,db}) => { + ) => `const condition: Condition = async({ref, req, db, logging}) => { // feel free to add your own code logic here return true; }`, diff --git a/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx b/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx index 31eb4aff0..acfb2dfe3 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx @@ -17,7 +17,7 @@ export const webhookStripe = { extraLibs: null, template: ( table: TableSettings - ) => `const sendgridParser: Parser = async ({ req, db, ref }) => { + ) => `const sendgridParser: Parser = async ({ req, db, ref, logging }) => { const event = req.body switch (event.type) { case "payment_intent.succeeded": @@ -34,7 +34,7 @@ export const webhookStripe = { extraLibs: null, template: ( table: TableSettings - ) => `const condition: Condition = async({ref,req,db}) => { + ) => `const condition: Condition = async({ref, req, db, logging}) => { // feel free to add your own code logic here return true; }`, diff --git a/src/components/TableModals/WebhooksModal/Schemas/typeform.tsx b/src/components/TableModals/WebhooksModal/Schemas/typeform.tsx index 5fd4ce7dd..9c509efd6 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/typeform.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/typeform.tsx @@ -13,7 +13,7 @@ export const webhookTypeform = { extraLibs: null, template: ( table: TableSettings - ) => `const typeformParser: Parser = async({req, db,ref}) =>{ + ) => `const typeformParser: Parser = async({req, db, ref, logging}) =>{ // this reduces the form submission into a single object of key value pairs // eg: {name: "John", age: 20} // ⚠️ ensure that you have assigned ref values of the fields @@ -73,7 +73,7 @@ export const webhookTypeform = { extraLibs: null, template: ( table: TableSettings - ) => `const condition: Condition = async({ref,req,db}) => { + ) => `const condition: Condition = async({ref, req, db, logging}) => { // feel free to add your own code logic here return true; }`, diff --git a/src/components/TableModals/WebhooksModal/Schemas/webform.tsx b/src/components/TableModals/WebhooksModal/Schemas/webform.tsx index bf5e8cdad..7bed061de 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/webform.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/webform.tsx @@ -14,7 +14,7 @@ export const webhook = { extraLibs: null, template: ( table: TableSettings - ) => `const formParser: Parser = async({req, db,ref}) => { + ) => `const formParser: Parser = async({req, db, ref, logging}) => { // request is the request object from the webhook // db is the database object // ref is the reference to collection of the table @@ -45,7 +45,7 @@ export const webhook = { extraLibs: null, template: ( table: TableSettings - ) => `const condition: Condition = async({ref,req,db}) => { + ) => `const condition: Condition = async({ref, req, db, logging}) => { // feel free to add your own code logic here return true; }`, diff --git a/src/components/TableModals/WebhooksModal/utils.tsx b/src/components/TableModals/WebhooksModal/utils.tsx index 7c338babd..b1ba86014 100644 --- a/src/components/TableModals/WebhooksModal/utils.tsx +++ b/src/components/TableModals/WebhooksModal/utils.tsx @@ -26,17 +26,41 @@ const requestType = [ export const parserExtraLibs = [ requestType, - `type Parser = (args:{req:WebHookRequest,db: FirebaseFirestore.Firestore,ref: FirebaseFirestore.CollectionReference,res:{ - send:(v:any)=>void - sendStatus:(status:number)=>void - }}) => Promise;`, + `type Parser = ( + args: { + req: WebHookRequest; + db: FirebaseFirestore.Firestore; + ref: FirebaseFirestore.CollectionReference; + res: { + send: (v:any)=>void; + sendStatus: (status:number)=>void + }; + logging: { + log: (payload: any) => void; + warn: (payload: any) => void; + error: (payload: any) => void; + }; + } + ) => Promise;`, ]; export const conditionExtraLibs = [ requestType, - `type Condition = (args:{req:WebHookRequest,db: FirebaseFirestore.Firestore,ref: FirebaseFirestore.CollectionReference,res:{ - send:(v:any)=>void - sendStatus:(status:number)=>void - }}) => Promise;`, + `type Condition = ( + args: { + req:WebHookRequest, + db: FirebaseFirestore.Firestore, + ref: FirebaseFirestore.CollectionReference, + res: { + send: (v:any)=>void + sendStatus: (status:number)=>void + }; + logging: { + log: (payload: any) => void; + warn: (payload: any) => void; + error: (payload: any) => void; + }; + } + ) => Promise;`, ]; const additionalVariables = [ diff --git a/src/components/TableModals/WebhooksModal/webhooks.d.ts b/src/components/TableModals/WebhooksModal/webhooks.d.ts index 4a8715f8d..fa98341fb 100644 --- a/src/components/TableModals/WebhooksModal/webhooks.d.ts +++ b/src/components/TableModals/WebhooksModal/webhooks.d.ts @@ -3,10 +3,20 @@ type Condition = (args: { db: FirebaseFirestore.Firestore; ref: FirebaseFirestore.CollectionReference; res: Response; + logging: { + log: (payload: any) => void; + warn: (payload: any) => void; + error: (payload: any) => void; + }; }) => Promise; type Parser = (args: { req: WebHookRequest; db: FirebaseFirestore.Firestore; ref: FirebaseFirestore.CollectionReference; + logging: { + log: (payload: any) => void; + warn: (payload: any) => void; + error: (payload: any) => void; + }; }) => Promise; From 8c490a44c68d10c65a0019c88c7405bd02166dcf Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Thu, 24 Nov 2022 16:11:45 +0000 Subject: [PATCH 009/114] worked on issue #562 --- src/components/TableToolbar/ManageColumns.tsx | 158 ++++++++++++------ src/components/TableToolbar/TableToolbar.tsx | 2 +- 2 files changed, 106 insertions(+), 54 deletions(-) diff --git a/src/components/TableToolbar/ManageColumns.tsx b/src/components/TableToolbar/ManageColumns.tsx index c67e689bb..7be2d690d 100644 --- a/src/components/TableToolbar/ManageColumns.tsx +++ b/src/components/TableToolbar/ManageColumns.tsx @@ -1,8 +1,20 @@ -import { useEffect, useRef, useMemo, useState } from "react"; +import { + useEffect, + useRef, + useMemo, + useState, + ChangeEvent, + forwardRef, +} from "react"; import { useAtom, useSetAtom } from "jotai"; import { isEqual } from "lodash-es"; import { colord } from "colord"; -import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"; +import { + DragDropContext, + Droppable, + Draggable, + DropResult, +} from "react-beautiful-dnd"; import { Box, AutocompleteProps, Theme } from "@mui/material"; import VisibilityIcon from "@mui/icons-material/VisibilityOutlined"; @@ -26,7 +38,7 @@ import { } from "@src/atoms/tableScope"; import { formatSubTableName } from "@src/utils/table"; -export default function ManageColumns2() { +export default function ManageColumns() { const buttonRef = useRef(null); const [userSettings] = useAtom(userSettingsAtom, globalScope); @@ -66,6 +78,9 @@ export default function ManageColumns2() { setOpen(false); }; + // disable drag if search box is not empty + const [disableDrag, setDisableDrag] = useState(false); + const renderOption: AutocompleteProps< any, true, @@ -77,7 +92,11 @@ export default function ManageColumns2() { .mix("#fff", theme.palette.mode === "dark" ? 0.16 : 0) .alpha(1); return ( - + {(provided) => (
  • @@ -95,7 +114,12 @@ export default function ManageColumns2() { { position: "relative", height: "1.5rem" }, selected ? { color: "primary.main" } - : { color: "primary.gray", opacity: 0.6 }, + : { + opacity: 0, + ".MuiAutocomplete-option.Mui-focused &": { + opacity: 0.5, + }, + }, ]} > @@ -129,7 +153,7 @@ export default function ManageColumns2() { }; const updateColumn = useSetAtom(updateColumnAtom, tableScope); - function handleOnDragEnd(result: any) { + function handleOnDragEnd(result: DropResult) { if (!result.destination) return; updateColumn({ key: result.draggableId, @@ -138,6 +162,39 @@ export default function ManageColumns2() { }); } + function checkToDisableDrag(e: ChangeEvent) { + setDisableDrag(e.target.value !== ""); + } + + const ListboxComponent = forwardRef(function ListboxComponent( + props: React.HTMLAttributes, + ulRef: any /*React.ForwardedRef*/ + ) { + const { children, ...other } = props; + + return ( + + + {(provided) => ( +
      { + provided.innerRef(ref); + if (ulRef !== null) { + ulRef(ref); + } + }} + > + {props.children} + {provided.placeholder} +
    + )} +
    +
    + ); + }); + return ( <> {"Columns"} - - - {(provided) => ( - <> - - {/*
    - {provided.placeholder} -
    */} - - )} -
    -
    + }, + }, + }, + }} + {...{ + AutocompleteProps: { + renderOption, + ListboxComponent, + // ListboxProps: { + // ...provided.droppableProps, + // ref: provided.innerRef, + // }, + }, + }} + label="Hidden fields" + labelPlural="fields" + value={hiddenFields ?? []} + onChange={(updates: string[]) => { + setHiddenFields(updates); + setDisableDrag(false); + }} + onClose={handleSave} + clearText="Show all" + selectAllText="Hide all" + doneText="Apply" + /> ); } diff --git a/src/components/TableToolbar/TableToolbar.tsx b/src/components/TableToolbar/TableToolbar.tsx index 715353f31..2079a20d2 100644 --- a/src/components/TableToolbar/TableToolbar.tsx +++ b/src/components/TableToolbar/TableToolbar.tsx @@ -14,7 +14,7 @@ import { ButtonSkeleton } from "./TableToolbarSkeleton"; import AddRow from "./AddRow"; import LoadedRowsStatus from "./LoadedRowsStatus"; import TableSettings from "./TableSettings"; -import HiddenFields from "./HiddenFields"; +// import HiddenFields from "./HiddenFields"; import ManageColumns from "./ManageColumns"; import RowHeight from "./RowHeight"; From 3a57d6e28a914b4579f7a73f77b7c10bd9a0a6f8 Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Fri, 25 Nov 2022 15:19:51 +0000 Subject: [PATCH 010/114] added reordering feature to the hidden fields menu --- src/components/TableToolbar/HiddenFields.tsx | 183 ++++++++++++++----- 1 file changed, 138 insertions(+), 45 deletions(-) diff --git a/src/components/TableToolbar/HiddenFields.tsx b/src/components/TableToolbar/HiddenFields.tsx index 8015f6f52..a761c4de8 100644 --- a/src/components/TableToolbar/HiddenFields.tsx +++ b/src/components/TableToolbar/HiddenFields.tsx @@ -1,12 +1,26 @@ -import { useEffect, useRef, useMemo, useState } from "react"; -import { useAtom } from "jotai"; +import { + useEffect, + useRef, + useMemo, + useState, + forwardRef, + ChangeEvent, +} from "react"; +import { useAtom, useSetAtom } from "jotai"; import { isEqual } from "lodash-es"; import { colord } from "colord"; +import { + DragDropContext, + Droppable, + Draggable, + DropResult, +} from "react-beautiful-dnd"; import { Box, AutocompleteProps, Theme } from "@mui/material"; import VisibilityIcon from "@mui/icons-material/VisibilityOutlined"; import VisibilityOffIcon from "@mui/icons-material/VisibilityOffOutlined"; import IconSlash from "@src/components/IconSlash"; +import DragIndicatorOutlinedIcon from "@mui/icons-material/DragIndicatorOutlined"; import ColumnSelect, { ColumnItem } from "@src/components/Table/ColumnSelect"; import ButtonWithStatus from "@src/components/ButtonWithStatus"; @@ -16,10 +30,14 @@ import { userSettingsAtom, updateUserSettingsAtom, } from "@src/atoms/projectScope"; -import { tableScope, tableIdAtom } from "@src/atoms/tableScope"; +import { + tableScope, + tableIdAtom, + updateColumnAtom, +} from "@src/atoms/tableScope"; import { formatSubTableName } from "@src/utils/table"; -export default function HiddenFields() { +export default function HiddenFields2() { const buttonRef = useRef(null); const [userSettings] = useAtom(userSettingsAtom, projectScope); @@ -57,6 +75,9 @@ export default function HiddenFields() { } setOpen(false); }; + + // disable drag if search box is not empty + const [disableDrag, setDisableDrag] = useState(false); const renderOption: AutocompleteProps< any, true, @@ -67,49 +88,112 @@ export default function HiddenFields() { colord(theme.palette.background.paper) .mix("#fff", theme.palette.mode === "dark" ? 0.16 : 0) .alpha(1); - return ( -
  • - - - - slashColor(theme).toHslString(), - }, - ".Mui-focused & .icon-slash-mask": { - stroke: (theme) => - slashColor(theme) - .mix( - theme.palette.primary.main, - theme.palette.action.selectedOpacity + - theme.palette.action.hoverOpacity - ) - .alpha(1) - .toHslString(), - }, - }, - selected ? { strokeDashoffset: 0 } : {}, - ]} - /> - - -
  • + + {(provided) => ( +
  • + + + + + + + slashColor(theme).toHslString(), + }, + ".Mui-focused & .icon-slash-mask": { + stroke: (theme) => + slashColor(theme) + .mix( + theme.palette.primary.main, + theme.palette.action.selectedOpacity + + theme.palette.action.hoverOpacity + ) + .alpha(1) + .toHslString(), + }, + }, + selected ? { strokeDashoffset: 0 } : {}, + ]} + /> + + +
  • + )} +
    ); }; + const updateColumn = useSetAtom(updateColumnAtom, tableScope); + + // updates column on drag end + function handleOnDragEnd(result: DropResult) { + if (!result.destination) return; + updateColumn({ + key: result.draggableId, + config: {}, + index: result.destination.index, + }); + } + + function checkToDisableDrag(e: ChangeEvent) { + setDisableDrag(e.target.value !== ""); + } + + const ListboxComponent = forwardRef(function ListboxComponent( + props: React.HTMLAttributes, + ulRef: any /*React.ForwardedRef*/ + ) { + const { children, ...other } = props; + + return ( + + + {(provided) => ( +
      { + provided.innerRef(ref); + if (ulRef !== null) { + ulRef(ref); + } + }} + > + {props.children} + {provided.placeholder} +
    + )} +
    +
    + ); + }); + return ( <> { + setHiddenFields(updates); + setDisableDrag(false); + }} onClose={handleSave} clearText="Show all" selectAllText="Hide all" From 9c312bb53b9ab6c94c569e338c9caa5e0ee7bfdf Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Fri, 25 Nov 2022 15:21:38 +0000 Subject: [PATCH 011/114] added comment --- src/components/TableToolbar/HiddenFields.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/TableToolbar/HiddenFields.tsx b/src/components/TableToolbar/HiddenFields.tsx index a761c4de8..e147accb3 100644 --- a/src/components/TableToolbar/HiddenFields.tsx +++ b/src/components/TableToolbar/HiddenFields.tsx @@ -161,6 +161,7 @@ export default function HiddenFields2() { }); } + // checks whether to disable reordering when search filter is applied function checkToDisableDrag(e: ChangeEvent) { setDisableDrag(e.target.value !== ""); } From 9bfe324362b9a8173c684984b313f610a357f8ae Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Fri, 25 Nov 2022 16:44:45 +0000 Subject: [PATCH 012/114] HiddenFields2 to HiddenFields --- src/components/TableToolbar/HiddenFields.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TableToolbar/HiddenFields.tsx b/src/components/TableToolbar/HiddenFields.tsx index e147accb3..8a33d3ed5 100644 --- a/src/components/TableToolbar/HiddenFields.tsx +++ b/src/components/TableToolbar/HiddenFields.tsx @@ -37,7 +37,7 @@ import { } from "@src/atoms/tableScope"; import { formatSubTableName } from "@src/utils/table"; -export default function HiddenFields2() { +export default function HiddenFields() { const buttonRef = useRef(null); const [userSettings] = useAtom(userSettingsAtom, projectScope); From 2c206d2e721da9a603fdfb9171b2bf2933f10d84 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Sun, 27 Nov 2022 20:45:12 +0800 Subject: [PATCH 013/114] enable logging in extensions code editor --- src/components/CodeEditor/extensions.d.ts | 5 ++++ .../TableModals/ExtensionsModal/utils.ts | 25 ++++++++++--------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/components/CodeEditor/extensions.d.ts b/src/components/CodeEditor/extensions.d.ts index dd592d051..3da39ae9a 100644 --- a/src/components/CodeEditor/extensions.d.ts +++ b/src/components/CodeEditor/extensions.d.ts @@ -26,6 +26,11 @@ type ExtensionContext = { extensionBody: any; }; RULES_UTILS: any; + logging: { + log: (payload: any) => void; + warn: (payload: any) => void; + error: (payload: any) => void; + }; }; // extension body definition diff --git a/src/components/TableModals/ExtensionsModal/utils.ts b/src/components/TableModals/ExtensionsModal/utils.ts index 28d24fda1..48de1a920 100644 --- a/src/components/TableModals/ExtensionsModal/utils.ts +++ b/src/components/TableModals/ExtensionsModal/utils.ts @@ -61,7 +61,7 @@ export interface IRuntimeOptions { export const triggerTypes: ExtensionTrigger[] = ["create", "update", "delete"]; const extensionBodyTemplate = { - task: `const extensionBody: TaskBody = async({row, db, change, ref}) => { + task: `const extensionBody: TaskBody = async({row, db, change, ref, logging}) => { // task extensions are very flexible you can do anything from updating other documents in your database, to making an api request to 3rd party service. // example: @@ -87,7 +87,7 @@ const extensionBodyTemplate = { }) */ }`, - docSync: `const extensionBody: DocSyncBody = async({row, db, change, ref}) => { + docSync: `const extensionBody: DocSyncBody = async({row, db, change, ref, logging}) => { // feel free to add your own code logic here return ({ @@ -96,7 +96,7 @@ const extensionBodyTemplate = { targetPath: "", // fill in the path here }) }`, - historySnapshot: `const extensionBody: HistorySnapshotBody = async({row, db, change, ref}) => { + historySnapshot: `const extensionBody: HistorySnapshotBody = async({row, db, change, ref, logging}) => { // feel free to add your own code logic here return ({ @@ -104,7 +104,7 @@ const extensionBodyTemplate = { collectionId: "historySnapshots", // optionally change the sub-collection id of where the history snapshots are stored }) }`, - algoliaIndex: `const extensionBody: AlgoliaIndexBody = async({row, db, change, ref}) => { + algoliaIndex: `const extensionBody: AlgoliaIndexBody = async({row, db, change, ref, logging}) => { // feel free to add your own code logic here return ({ @@ -114,7 +114,7 @@ const extensionBodyTemplate = { objectID: ref.id, // algolia object ID, ref.id is one possible choice }) }`, - meiliIndex: `const extensionBody: MeiliIndexBody = async({row, db, change, ref}) => { + meiliIndex: `const extensionBody: MeiliIndexBody = async({row, db, change, ref, logging}) => { // feel free to add your own code logic here return({ @@ -124,7 +124,7 @@ const extensionBodyTemplate = { objectID: ref.id, // algolia object ID, ref.id is one possible choice }) }`, - bigqueryIndex: `const extensionBody: BigqueryIndexBody = async({row, db, change, ref}) => { + bigqueryIndex: `const extensionBody: BigqueryIndexBody = async({row, db, change, ref, logging}) => { // feel free to add your own code logic here return ({ @@ -134,7 +134,7 @@ const extensionBodyTemplate = { objectID: ref.id, // algolia object ID, ref.id is one possible choice }) }`, - slackMessage: `const extensionBody: SlackMessageBody = async({row, db, change, ref}) => { + slackMessage: `const extensionBody: SlackMessageBody = async({row, db, change, ref, logging}) => { // feel free to add your own code logic here return ({ @@ -144,7 +144,7 @@ const extensionBodyTemplate = { attachments: [], // the attachments parameter to pass in to slack api }) }`, - sendgridEmail: `const extensionBody: SendgridEmailBody = async({row, db, change, ref}) => { + sendgridEmail: `const extensionBody: SendgridEmailBody = async({row, db, change, ref, logging}) => { // feel free to add your own code logic here return ({ @@ -164,7 +164,7 @@ const extensionBodyTemplate = { }, }) }`, - apiCall: `const extensionBody: ApiCallBody = async({row, db, change, ref}) => { + apiCall: `const extensionBody: ApiCallBody = async({row, db, change, ref, logging}) => { // feel free to add your own code logic here return ({ @@ -174,7 +174,7 @@ const extensionBodyTemplate = { callback: ()=>{}, }) }`, - twilioMessage: `const extensionBody: TwilioMessageBody = async({row, db, change, ref}) => { + twilioMessage: `const extensionBody: TwilioMessageBody = async({row, db, change, ref, logging}) => { /** * * Setup twilio secret key: https://docs.rowy.io/extensions/twilio-message#secret-manager-setup @@ -190,7 +190,7 @@ const extensionBodyTemplate = { body: "Hi there!" // message text }) }`, - pushNotification: `const extensionBody: PushNotificationBody = async({row, db, change, ref}) => { + pushNotification: `const extensionBody: PushNotificationBody = async({row, db, change, ref, logging}) => { // you can FCM token from the row or from the user document in the database // const FCMtoken = row.FCMtoken // or push through topic @@ -238,13 +238,14 @@ export function emptyExtensionObject( extensionBody: extensionBodyTemplate[type] ?? extensionBodyTemplate["task"], requiredFields: [], trackedFields: [], - conditions: `const condition: Condition = async({row, change}) => { + conditions: `const condition: Condition = async({row, change, logging}) => { // feel free to add your own code logic here return true; }`, lastEditor: user, }; } + export function sparkToExtensionObjects( sparkConfig: string, user: IExtensionEditor From 2fc323ecd5958796cb263c2a877088c80c6cf093 Mon Sep 17 00:00:00 2001 From: Sidney Alcantara Date: Mon, 28 Nov 2022 16:29:30 +1100 Subject: [PATCH 014/114] Update src/components/TableToolbar/HiddenFields.tsx --- src/components/TableToolbar/HiddenFields.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/TableToolbar/HiddenFields.tsx b/src/components/TableToolbar/HiddenFields.tsx index 8a33d3ed5..0a5377132 100644 --- a/src/components/TableToolbar/HiddenFields.tsx +++ b/src/components/TableToolbar/HiddenFields.tsx @@ -102,7 +102,13 @@ export default function HiddenFields() { > + disableDrag ? theme.palette.action.disabledOpacity : 1, + }, + ]} /> From d7801a1946049bd08907de8249952cbf889a9495 Mon Sep 17 00:00:00 2001 From: Han Tuerker Date: Thu, 8 Dec 2022 18:58:26 +0800 Subject: [PATCH 015/114] add output field to formula field --- .../ColumnConfigModal/ColumnConfig.tsx | 5 +- src/components/fields/Formula/DisplayCell.tsx | 17 ++- src/components/fields/Formula/Settings.tsx | 62 ++++------ src/components/fields/Formula/index.tsx | 6 +- src/components/fields/Formula/useFormula.tsx | 14 +-- src/components/fields/Formula/util.tsx | 116 ++++++++++++++++-- src/components/fields/Formula/worker.ts | 18 +-- src/constants/fields.ts | 1 - 8 files changed, 156 insertions(+), 83 deletions(-) diff --git a/src/components/ColumnModals/ColumnConfigModal/ColumnConfig.tsx b/src/components/ColumnModals/ColumnConfigModal/ColumnConfig.tsx index 072c22e9f..276656068 100644 --- a/src/components/ColumnModals/ColumnConfigModal/ColumnConfig.tsx +++ b/src/components/ColumnModals/ColumnConfigModal/ColumnConfig.tsx @@ -50,8 +50,9 @@ export default function ColumnConfigModal({ const rendedFieldSettings = useMemo( () => - [FieldType.derivative, FieldType.aggregate].includes(column.type) && - newConfig.renderFieldType + [FieldType.derivative, FieldType.aggregate, FieldType.formula].includes( + column.type + ) && newConfig.renderFieldType ? getFieldProp("settings", newConfig.renderFieldType) : null, [newConfig.renderFieldType, column.type] diff --git a/src/components/fields/Formula/DisplayCell.tsx b/src/components/fields/Formula/DisplayCell.tsx index 0d28f4073..ddea15d32 100644 --- a/src/components/fields/Formula/DisplayCell.tsx +++ b/src/components/fields/Formula/DisplayCell.tsx @@ -1,16 +1,15 @@ -import { Grid } from "@mui/material"; import { IDisplayCellProps } from "@src/components/fields/types"; import { useFormula } from "./useFormula"; +import { getDisplayCell } from "./util"; -export default function Formula({ row, column }: IDisplayCellProps) { +export default function Formula(props: IDisplayCellProps) { const { result, error } = useFormula({ - row, - formulaFn: column.config?.formulaFn, + row: props.row, + formulaFn: props.column.config?.formulaFn, }); - return ( - - {error ? error.message : result} - - ); + const type = props.column.config?.renderFieldType; + const DisplayCell = getDisplayCell(type); + + return ; } diff --git a/src/components/fields/Formula/Settings.tsx b/src/components/fields/Formula/Settings.tsx index 13c4b2195..36d21e912 100644 --- a/src/components/fields/Formula/Settings.tsx +++ b/src/components/fields/Formula/Settings.tsx @@ -1,12 +1,11 @@ import { lazy, Suspense } from "react"; import { useDebouncedCallback } from "use-debounce"; -import { FieldType, ISettingsProps } from "@src/components/fields/types"; +import { ISettingsProps } from "@src/components/fields/types"; import { Grid, InputLabel, Typography, Stack, - Tooltip, FormHelperText, } from "@mui/material"; import FieldSkeleton from "@src/components/SideDrawer/FieldSkeleton"; @@ -18,7 +17,7 @@ import { tableScope, } from "@src/atoms/tableScope"; import { useFormula } from "./useFormula"; -import { ignoredColumns, typeDefs } from "./util"; +import { listenerFieldTypes, outputFieldTypes, typeDefs } from "./util"; import MultiSelect from "@rowy/multiselect"; import FieldsDropdown from "@src/components/ColumnModals/FieldsDropdown"; @@ -41,29 +40,20 @@ export default function Settings({ formulaFn: config.formulaFn, }); - if (error && !config?.error) { - onChange("error")(error.message); - } - if (!error && config?.error) { - onChange("error")(undefined); - } + // if (error && !config?.error) { + // onChange("error")(error.message); + // } + // if (!error && config?.error) { + // onChange("error")(undefined); + // } - if (loading) { - console.log("loading"); - } + // if (loading) { + // console.log("loading"); + // } - if (error) { - console.log(error); - } - - const previewColumns = tableColumnsOrdered.filter( - (c) => !ignoredColumns.includes(c.type) - ); - - const columnOptions = tableColumnsOrdered - .filter((column) => column.fieldName !== fieldName) - .filter((column) => !ignoredColumns.includes(column.type)) - .map((c) => ({ label: c.name, value: c.key })); + // if (error) { + // console.log(error); + // } const formulaFn = config?.formulaFn ?? @@ -72,17 +62,15 @@ export default function Settings({ // return column1 + column2; // checkout the documentation for more info: https://docs.rowy.io/field-types/formula`; - const defs = previewColumns - .map((c) => `declare const ${c.key}: ${typeDefs(c.type)};`) - .join("\n"); - return ( listenerFieldTypes.includes(c.type)) + .map((c) => c.name)} value={config.listenerFields ?? []} onChange={onChange("listenerFields")} TextFieldProps={{ @@ -111,15 +99,7 @@ export default function Settings({ - ![ - FieldType.derivative, - FieldType.aggregate, - FieldType.subTable, - FieldType.action, - ].includes(f) - )} + options={outputFieldTypes} onChange={(value) => { onChange("renderFieldType")(value); }} @@ -150,19 +130,19 @@ export default function Settings({ spacing={1} style={{ flexGrow: 1, marginTop: -8, marginLeft: 0 }} > - {Object.values(previewColumns).map((column) => ( + {/* {Object.values(previewColumns).map((column) => ( {column.fieldName} - ))} + ))} */} }> diff --git a/src/components/fields/Formula/index.tsx b/src/components/fields/Formula/index.tsx index 69e00104f..0e55e24d0 100644 --- a/src/components/fields/Formula/index.tsx +++ b/src/components/fields/Formula/index.tsx @@ -1,10 +1,10 @@ import { Derivative as DerivativeIcon } from "@src/assets/icons"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; -import withTableCell from "@src/components/Table/withTableCell"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; +import DisplayCell from "./DisplayCell"; import Settings, { settingsValidator } from "./Settings"; -import DisplayCell from "./DisplayCell"; export const config: IFieldConfig = { type: FieldType.formula, @@ -14,7 +14,7 @@ export const config: IFieldConfig = { initialValue: "", icon: , description: "Client Function (Alpha)", - TableCell: withTableCell(DisplayCell, DisplayCell, "inline", { + TableCell: withRenderTableCell(DisplayCell as any, null, undefined, { usesRowData: true, }), SideDrawerField: () => null as any, diff --git a/src/components/fields/Formula/useFormula.tsx b/src/components/fields/Formula/useFormula.tsx index bec88b271..48a2d9e80 100644 --- a/src/components/fields/Formula/useFormula.tsx +++ b/src/components/fields/Formula/useFormula.tsx @@ -3,7 +3,7 @@ import { TableRow } from "@src/types/table"; import { useAtom } from "jotai"; import { pick, zipObject } from "lodash-es"; import { useEffect, useMemo, useState } from "react"; -import { ignoredColumns, useDeepCompareMemoize } from "./util"; +import { useDeepCompareMemoize } from "./util"; export const useFormula = ({ row, @@ -17,9 +17,7 @@ export const useFormula = ({ const [loading, setLoading] = useState(false); const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); - const availableColumns = tableColumnsOrdered - .filter((column) => !ignoredColumns.includes(column.type)) - .map((c) => c.key); + const availableColumns = tableColumnsOrdered.map((c) => c.key); const availableFields = useMemo( () => ({ @@ -33,11 +31,11 @@ export const useFormula = ({ ); useEffect(() => { - console.log("useFormula calculation: ", row._rowy_ref.path); - console.log("availableFields: ", availableFields); setLoading(true); - const worker = new Worker(new URL("./worker.ts", import.meta.url)); + const worker = new Worker(new URL("./worker.ts", import.meta.url), { + type: "classic", + }); const timeout = setTimeout(() => { setError(new Error("Timeout")); setLoading(false); @@ -57,7 +55,7 @@ export const useFormula = ({ worker.postMessage({ formulaFn: formulaFn, - fields: availableFields, + row: availableFields, }); return () => { diff --git a/src/components/fields/Formula/util.tsx b/src/components/fields/Formula/util.tsx index ec9b9e491..6dd04db41 100644 --- a/src/components/fields/Formula/util.tsx +++ b/src/components/fields/Formula/util.tsx @@ -1,6 +1,27 @@ -import { FieldType } from "@src/constants/fields"; -import { isEqual } from "lodash-es"; import { useMemo, useRef } from "react"; +import { isEqual } from "lodash-es"; + +import { FieldType } from "@src/constants/fields"; + +import ShortTextDisplayCell from "@src/components/fields/ShortText/DisplayCell"; +import LongTextDisplayCell from "@src/components/fields/LongText/DisplayCell"; +import RichTextDisplayCell from "@src/components/fields/RichText/DisplayCell"; +import UrlDisplayCell from "@src/components/fields/Url/DisplayCell"; +import NumberDisplayCell from "@src/components/fields/Number/DisplayCell"; +import CheckboxDisplayCell from "@src/components/fields/Checkbox/DisplayCell"; +import PercentageDisplayCell from "@src/components/fields/Percentage/DisplayCell"; +import RatingDisplayCell from "@src/components/fields/Rating/DisplayCell"; +import SliderDisplayCell from "@src/components/fields/Slider/DisplayCell"; +import ColorDisplayCell from "@src/components/fields/Color/DisplayCell"; +import GeoPointDisplayCell from "@src/components/fields/GeoPoint/DisplayCell"; +import DateDisplayCell from "@src/components/fields/Date/DisplayCell"; +import DateTimeDisplayCell from "@src/components/fields/DateTime/DisplayCell"; +import ImageDisplayCell from "@src/components/fields/Image/DisplayCell"; +import FileDisplayCell from "@src/components/fields/File/DisplayCell"; +import JsonDisplayCell from "@src/components/fields/Json/DisplayCell"; +import CodeDisplayCell from "@src/components/fields/Code/DisplayCell"; +import MarkdownDisplayCell from "@src/components/fields/Markdown/DisplayCell"; +import CreatedByDisplayCell from "@src/components/fields/CreatedBy/DisplayCell"; export function useDeepCompareMemoize(value: T) { const ref = useRef(value); @@ -15,16 +36,43 @@ export function useDeepCompareMemoize(value: T) { return useMemo(() => ref.current, [signalRef.current]); } -export const ignoredColumns = [ - FieldType.subTable, - FieldType.derivative, - FieldType.formula, - FieldType.connector, - FieldType.subTable, - FieldType.reference, - FieldType.connectTable, - FieldType.connectService, -]; +export const listenerFieldTypes = Object.values(FieldType).filter( + (type) => + ![ + FieldType.action, + FieldType.status, + FieldType.formula, + FieldType.aggregate, + FieldType.connectService, + FieldType.connectTable, + FieldType.connector, + FieldType.subTable, + FieldType.reference, + FieldType.last, + ].includes(type) +); + +export const outputFieldTypes = Object.values(FieldType).filter( + (type) => + ![ + FieldType.formula, + FieldType.derivative, + FieldType.action, + FieldType.status, + FieldType.aggregate, + FieldType.connectService, + FieldType.connectTable, + FieldType.connector, + FieldType.duration, + FieldType.subTable, + FieldType.reference, + FieldType.createdAt, + FieldType.createdBy, + FieldType.updatedAt, + FieldType.updatedBy, + FieldType.last, + ].includes(type) +); export const typeDefs = (type: FieldType) => { switch (type) { @@ -52,3 +100,47 @@ export const typeDefs = (type: FieldType) => { } return "any"; }; + +export const getDisplayCell = (type: FieldType) => { + switch (type) { + case FieldType.longText: + return LongTextDisplayCell; + case FieldType.richText: + return RichTextDisplayCell; + case FieldType.url: + return UrlDisplayCell; + case FieldType.number: + return NumberDisplayCell; + case FieldType.checkbox: + return CheckboxDisplayCell; + case FieldType.percentage: + return PercentageDisplayCell; + case FieldType.rating: + return RatingDisplayCell; + case FieldType.slider: + return SliderDisplayCell; + case FieldType.color: + return ColorDisplayCell; + case FieldType.geoPoint: + return GeoPointDisplayCell; + case FieldType.date: + return DateDisplayCell; + case FieldType.dateTime: + return DateTimeDisplayCell; + case FieldType.image: + return ImageDisplayCell; + case FieldType.file: + return FileDisplayCell; + case FieldType.json: + return JsonDisplayCell; + case FieldType.code: + return CodeDisplayCell; + case FieldType.markdown: + return MarkdownDisplayCell; + case FieldType.createdBy: + return CreatedByDisplayCell; + default: + // FieldType url, email, phone, singleSelect, multiSelect + return ShortTextDisplayCell; + } +}; diff --git a/src/components/fields/Formula/worker.ts b/src/components/fields/Formula/worker.ts index 586adfe57..1dab169c2 100644 --- a/src/components/fields/Formula/worker.ts +++ b/src/components/fields/Formula/worker.ts @@ -1,14 +1,18 @@ -onmessage = ({ data }) => { +import { ContentSaveCogOutline } from "mdi-material-ui"; + +onmessage = async ({ data }) => { try { - const { formulaFn, fields } = data; + const { formulaFn, row } = data; + console.dir(row); + const AsyncFunction = (async (x: any) => x).constructor as any; // eslint-disable-next-line no-new-func - const result = new Function(...Object.keys(fields), formulaFn).call( - {}, - ...Object.values(fields) - ); + // const result = await new AsyncFunction("row", formulaFn)(row); + const fn = new AsyncFunction("row", formulaFn); + const result = await fn(row); + console.log(result); postMessage({ result }); } catch (error: any) { - console.error(error); + console.error("error: ", error); postMessage({ error: new Error(error) }); } finally { // eslint-disable-next-line no-restricted-globals diff --git a/src/constants/fields.ts b/src/constants/fields.ts index e5cbe1d45..900b88db6 100644 --- a/src/constants/fields.ts +++ b/src/constants/fields.ts @@ -48,7 +48,6 @@ export enum FieldType { createdAt = "CREATED_AT", updatedAt = "UPDATED_AT", // METADATA - user = "USER", id = "ID", last = "LAST", From 30a563015a45c53178b5d56489270016d8f7bc11 Mon Sep 17 00:00:00 2001 From: Han Tuerker Date: Thu, 8 Dec 2022 22:30:20 +0800 Subject: [PATCH 016/114] add listener fields --- src/components/fields/Formula/DisplayCell.tsx | 5 + src/components/fields/Formula/Settings.tsx | 94 +++++-------------- src/components/fields/Formula/formula.d.ts | 8 ++ src/components/fields/Formula/useFormula.tsx | 22 +++-- src/components/fields/Formula/util.tsx | 34 ++----- src/components/fields/Formula/worker.ts | 15 +-- 6 files changed, 65 insertions(+), 113 deletions(-) create mode 100644 src/components/fields/Formula/formula.d.ts diff --git a/src/components/fields/Formula/DisplayCell.tsx b/src/components/fields/Formula/DisplayCell.tsx index ddea15d32..79760e322 100644 --- a/src/components/fields/Formula/DisplayCell.tsx +++ b/src/components/fields/Formula/DisplayCell.tsx @@ -5,11 +5,16 @@ import { getDisplayCell } from "./util"; export default function Formula(props: IDisplayCellProps) { const { result, error } = useFormula({ row: props.row, + listenerFields: props.column.config?.listenerFields || [], formulaFn: props.column.config?.formulaFn, }); const type = props.column.config?.renderFieldType; const DisplayCell = getDisplayCell(type); + if (error) { + return <>Error; + } + return ; } diff --git a/src/components/fields/Formula/Settings.tsx b/src/components/fields/Formula/Settings.tsx index 36d21e912..cb907a8c2 100644 --- a/src/components/fields/Formula/Settings.tsx +++ b/src/components/fields/Formula/Settings.tsx @@ -7,19 +7,19 @@ import { Typography, Stack, FormHelperText, + Tooltip, } from "@mui/material"; import FieldSkeleton from "@src/components/SideDrawer/FieldSkeleton"; import { useAtom } from "jotai"; -import { - tableColumnsOrderedAtom, - tableRowsAtom, - tableScope, -} from "@src/atoms/tableScope"; -import { useFormula } from "./useFormula"; -import { listenerFieldTypes, outputFieldTypes, typeDefs } from "./util"; +import { tableColumnsOrderedAtom, tableScope } from "@src/atoms/tableScope"; +import { defaultFn, listenerFieldTypes, outputFieldTypes } from "./util"; import MultiSelect from "@rowy/multiselect"; import FieldsDropdown from "@src/components/ColumnModals/FieldsDropdown"; +import { getFieldProp } from ".."; + +/* eslint-disable import/no-webpack-loader-syntax */ +import formulaDefs from "!!raw-loader!./formula.d.ts"; const CodeEditor = lazy( () => @@ -28,39 +28,13 @@ const CodeEditor = lazy( export default function Settings({ config, - fieldName, onChange, onBlur, errors, }: ISettingsProps) { const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); - const [tableRows] = useAtom(tableRowsAtom, tableScope); - const { error, result, loading } = useFormula({ - row: tableRows[0], - formulaFn: config.formulaFn, - }); - - // if (error && !config?.error) { - // onChange("error")(error.message); - // } - // if (!error && config?.error) { - // onChange("error")(undefined); - // } - - // if (loading) { - // console.log("loading"); - // } - - // if (error) { - // console.log(error); - // } - - const formulaFn = - config?.formulaFn ?? - `// Write your formula code here -// for example: -// return column1 + column2; -// checkout the documentation for more info: https://docs.rowy.io/field-types/formula`; + const returnType = getFieldProp("dataType", config.renderFieldType) ?? "any"; + const formulaFn = config?.formulaFn ? config.formulaFn : defaultFn; return ( @@ -70,7 +44,7 @@ export default function Settings({ label="Listener fields" options={tableColumnsOrdered .filter((c) => listenerFieldTypes.includes(c.type)) - .map((c) => c.name)} + .map((c) => ({ label: c.name, value: c.key }))} value={config.listenerFields ?? []} onChange={onChange("listenerFields")} TextFieldProps={{ @@ -130,52 +104,26 @@ export default function Settings({ spacing={1} style={{ flexGrow: 1, marginTop: -8, marginLeft: 0 }} > - {/* {Object.values(previewColumns).map((column) => ( - - - {column.fieldName} - - - ))} */} + + + row + + }> ` + ), + ]} onChange={useDebouncedCallback(onChange("formulaFn"), 300)} />
    - {/* - Preview: - - - - - - {Object.entries(previewColumns).map( - ([field, { name, type, key }]) => ( - - - - - ) - )} - - */} ); } diff --git a/src/components/fields/Formula/formula.d.ts b/src/components/fields/Formula/formula.d.ts new file mode 100644 index 000000000..90f9351d1 --- /dev/null +++ b/src/components/fields/Formula/formula.d.ts @@ -0,0 +1,8 @@ +type FormulaContext = { + row: Row; + // ref: FirebaseFirestore.DocumentReference; + // storage: firebasestorage.Storage; + // db: FirebaseFirestore.Firestore; +}; + +type Formula = (context: FormulaContext) => "PLACEHOLDER_OUTPUT_TYPE"; diff --git a/src/components/fields/Formula/useFormula.tsx b/src/components/fields/Formula/useFormula.tsx index 48a2d9e80..f33bab43a 100644 --- a/src/components/fields/Formula/useFormula.tsx +++ b/src/components/fields/Formula/useFormula.tsx @@ -3,13 +3,15 @@ import { TableRow } from "@src/types/table"; import { useAtom } from "jotai"; import { pick, zipObject } from "lodash-es"; import { useEffect, useMemo, useState } from "react"; -import { useDeepCompareMemoize } from "./util"; +import { listenerFieldTypes, useDeepCompareMemoize } from "./util"; export const useFormula = ({ row, + listenerFields, formulaFn, }: { row: TableRow; + listenerFields: string[]; formulaFn: string; }) => { const [result, setResult] = useState(null); @@ -17,7 +19,9 @@ export const useFormula = ({ const [loading, setLoading] = useState(false); const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); - const availableColumns = tableColumnsOrdered.map((c) => c.key); + const availableColumns = tableColumnsOrdered + .filter((c) => listenerFieldTypes.includes(c.type)) + .map((c) => c.key); const availableFields = useMemo( () => ({ @@ -30,17 +34,22 @@ export const useFormula = ({ [row, availableColumns] ); + const listeners = useMemo( + () => pick(availableFields, listenerFields), + [availableFields, listenerFields] + ); + useEffect(() => { setLoading(true); const worker = new Worker(new URL("./worker.ts", import.meta.url), { - type: "classic", + type: "module", }); const timeout = setTimeout(() => { setError(new Error("Timeout")); setLoading(false); worker.terminate(); - }, 1000); + }, 10000); worker.onmessage = ({ data: { result, error } }: any) => { worker.terminate(); @@ -53,8 +62,9 @@ export const useFormula = ({ clearInterval(timeout); }; + const functionBody = formulaFn.replace(/^.*=>\s*{/, "").replace(/}$/, ""); worker.postMessage({ - formulaFn: formulaFn, + formulaFn: functionBody, row: availableFields, }); @@ -63,7 +73,7 @@ export const useFormula = ({ clearInterval(timeout); }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [useDeepCompareMemoize(availableFields), formulaFn]); + }, [useDeepCompareMemoize(listeners), formulaFn]); return { result, error, loading }; }; diff --git a/src/components/fields/Formula/util.tsx b/src/components/fields/Formula/util.tsx index 6dd04db41..8fda4b07c 100644 --- a/src/components/fields/Formula/util.tsx +++ b/src/components/fields/Formula/util.tsx @@ -47,7 +47,6 @@ export const listenerFieldTypes = Object.values(FieldType).filter( FieldType.connectTable, FieldType.connector, FieldType.subTable, - FieldType.reference, FieldType.last, ].includes(type) ); @@ -74,32 +73,13 @@ export const outputFieldTypes = Object.values(FieldType).filter( ].includes(type) ); -export const typeDefs = (type: FieldType) => { - switch (type) { - case FieldType.shortText: - case FieldType.longText: - case FieldType.richText: - case FieldType.email: - case FieldType.phone: - case FieldType.url: - case FieldType.singleSelect: - return "string | undefined"; - case FieldType.multiSelect: - return "Array | undefined"; - case FieldType.number: - case FieldType.rating: - case FieldType.slider: - case FieldType.percentage: - return "number | undefined"; - case FieldType.checkbox: - return "boolean | undefined"; - case FieldType.date: - case FieldType.dateTime: - case FieldType.duration: - return "{ seconds: number; nanoseconds: number; }"; - } - return "any"; -}; +export const defaultFn = `const formula:Formula = async ({ row })=> { + // Write your formula code here + // for example: + // return row.a + row.b; + // checkout the documentation for more info: https://docs.rowy.io/field-types/formula +} +`; export const getDisplayCell = (type: FieldType) => { switch (type) { diff --git a/src/components/fields/Formula/worker.ts b/src/components/fields/Formula/worker.ts index 1dab169c2..57c03265f 100644 --- a/src/components/fields/Formula/worker.ts +++ b/src/components/fields/Formula/worker.ts @@ -1,19 +1,20 @@ -import { ContentSaveCogOutline } from "mdi-material-ui"; - onmessage = async ({ data }) => { try { const { formulaFn, row } = data; - console.dir(row); - const AsyncFunction = (async (x: any) => x).constructor as any; - // eslint-disable-next-line no-new-func - // const result = await new AsyncFunction("row", formulaFn)(row); + + const AsyncFunction = async function () {}.constructor as any; const fn = new AsyncFunction("row", formulaFn); const result = await fn(row); + await new Promise((resolve) => setTimeout(() => resolve(50), 3000)).then( + (res) => console.log(res) + ); console.log(result); postMessage({ result }); } catch (error: any) { console.error("error: ", error); - postMessage({ error: new Error(error) }); + postMessage({ + error: new Error("Something went wrong. Check console logs."), + }); } finally { // eslint-disable-next-line no-restricted-globals self.close(); From 065484aeb21384bb6e0f15fa02a5240988d23f26 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Fri, 9 Dec 2022 03:45:58 +0800 Subject: [PATCH 017/114] add rowy loggin tab --- src/atoms/tableScope/ui.ts | 2 +- .../ColumnModals/ColumnConfigModal/DefaultValueInput.tsx | 4 ++-- .../ColumnModals/ColumnConfigModal/defaultValue.d.ts | 5 +++++ src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx | 2 ++ src/components/TableModals/CloudLogsModal/utils.ts | 3 +++ 5 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/atoms/tableScope/ui.ts b/src/atoms/tableScope/ui.ts index 3d11a081e..47bdd8286 100644 --- a/src/atoms/tableScope/ui.ts +++ b/src/atoms/tableScope/ui.ts @@ -142,7 +142,7 @@ export const selectedCellAtom = atom(null); export const contextMenuTargetAtom = atom(null); export type CloudLogFilters = { - type: "webhook" | "functions" | "audit" | "build"; + type: "rowy" | "webhook" | "functions" | "audit" | "build"; timeRange: | { type: "seconds" | "minutes" | "hours" | "days"; value: number } | { type: "range"; start: Date; end: Date }; diff --git a/src/components/ColumnModals/ColumnConfigModal/DefaultValueInput.tsx b/src/components/ColumnModals/ColumnConfigModal/DefaultValueInput.tsx index 5213eb8a7..3cc78e2af 100644 --- a/src/components/ColumnModals/ColumnConfigModal/DefaultValueInput.tsx +++ b/src/components/ColumnModals/ColumnConfigModal/DefaultValueInput.tsx @@ -52,11 +52,11 @@ function CodeEditor({ type, column, handleChange }: ICodeEditorProps) { } else if (column.config?.defaultValue?.dynamicValueFn) { dynamicValueFn = column.config?.defaultValue?.dynamicValueFn; } else if (column.config?.defaultValue?.script) { - dynamicValueFn = `const dynamicValueFn : DefaultValue = async ({row,ref,db,storage,auth})=>{ + dynamicValueFn = `const dynamicValueFn : DefaultValue = async ({row,ref,db,storage,auth,logging})=>{ ${column.config?.defaultValue.script} }`; } else { - dynamicValueFn = `const dynamicValueFn : DefaultValue = async ({row,ref,db,storage,auth})=>{ + dynamicValueFn = `const dynamicValueFn : DefaultValue = async ({row,ref,db,storage,auth,logging})=>{ // Write your default value code here // for example: // generate random hex color diff --git a/src/components/ColumnModals/ColumnConfigModal/defaultValue.d.ts b/src/components/ColumnModals/ColumnConfigModal/defaultValue.d.ts index 0e50bb2d5..e3b90e138 100644 --- a/src/components/ColumnModals/ColumnConfigModal/defaultValue.d.ts +++ b/src/components/ColumnModals/ColumnConfigModal/defaultValue.d.ts @@ -4,5 +4,10 @@ type DefaultValueContext = { storage: firebasestorage.Storage; db: FirebaseFirestore.Firestore; auth: firebaseauth.BaseAuth; + logging: { + log: (payload: any) => void; + warn: (payload: any) => void; + error: (payload: any) => void; + }; }; type DefaultValue = (context: DefaultValueContext) => "PLACEHOLDER_OUTPUT_TYPE"; diff --git a/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx b/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx index e69acc446..217b59298 100644 --- a/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx +++ b/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx @@ -118,6 +118,7 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { } aria-label="Filter by log type" > + Rowy Logging Webhooks Functions Audit @@ -139,6 +140,7 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { )} + {cloudLogFilters.type === "rowy" && <>} {cloudLogFilters.type === "webhook" && ( Date: Sat, 10 Dec 2022 11:42:08 +0800 Subject: [PATCH 018/114] fix function body extracter --- src/components/fields/Formula/DisplayCell.tsx | 2 +- src/components/fields/Formula/useFormula.tsx | 25 +++++++++++-------- src/components/fields/Formula/util.tsx | 16 ++++++++++++ src/components/fields/Formula/worker.ts | 5 ---- 4 files changed, 31 insertions(+), 17 deletions(-) diff --git a/src/components/fields/Formula/DisplayCell.tsx b/src/components/fields/Formula/DisplayCell.tsx index 79760e322..1a5b538d4 100644 --- a/src/components/fields/Formula/DisplayCell.tsx +++ b/src/components/fields/Formula/DisplayCell.tsx @@ -13,7 +13,7 @@ export default function Formula(props: IDisplayCellProps) { const DisplayCell = getDisplayCell(type); if (error) { - return <>Error; + return <>Error ${error}; } return ; diff --git a/src/components/fields/Formula/useFormula.tsx b/src/components/fields/Formula/useFormula.tsx index f33bab43a..c0113c3b6 100644 --- a/src/components/fields/Formula/useFormula.tsx +++ b/src/components/fields/Formula/useFormula.tsx @@ -3,7 +3,11 @@ import { TableRow } from "@src/types/table"; import { useAtom } from "jotai"; import { pick, zipObject } from "lodash-es"; import { useEffect, useMemo, useState } from "react"; -import { listenerFieldTypes, useDeepCompareMemoize } from "./util"; +import { + getFunctionBody, + listenerFieldTypes, + useDeepCompareMemoize, +} from "./util"; export const useFormula = ({ row, @@ -40,17 +44,17 @@ export const useFormula = ({ ); useEffect(() => { + setError(null); setLoading(true); const worker = new Worker(new URL("./worker.ts", import.meta.url), { type: "module", }); - const timeout = setTimeout(() => { - setError(new Error("Timeout")); - setLoading(false); - worker.terminate(); - }, 10000); - + // const timeout = setTimeout(() => { + // setError(new Error("timeout")); + // setLoading(false); + // worker.terminate(); + // }, 1000); worker.onmessage = ({ data: { result, error } }: any) => { worker.terminate(); if (error) { @@ -59,18 +63,17 @@ export const useFormula = ({ setResult(result); } setLoading(false); - clearInterval(timeout); + // clearInterval(timeout); }; - const functionBody = formulaFn.replace(/^.*=>\s*{/, "").replace(/}$/, ""); worker.postMessage({ - formulaFn: functionBody, + formulaFn: getFunctionBody(formulaFn), row: availableFields, }); return () => { worker.terminate(); - clearInterval(timeout); + // clearInterval(timeout); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [useDeepCompareMemoize(listeners), formulaFn]); diff --git a/src/components/fields/Formula/util.tsx b/src/components/fields/Formula/util.tsx index 8fda4b07c..8b74b4931 100644 --- a/src/components/fields/Formula/util.tsx +++ b/src/components/fields/Formula/util.tsx @@ -124,3 +124,19 @@ export const getDisplayCell = (type: FieldType) => { return ShortTextDisplayCell; } }; + +export const getFunctionBody = (fn: string) => { + const sanitizedFn = fn.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, ""); + const matches = sanitizedFn.match(/=>\s*({?[\s\S]*}?)$/); + + if (!matches) { + return null; + } + + const body = matches[1].trim(); + const isOneLiner = body[0] !== "{" && body[body.length - 1] !== "}"; + + if (isOneLiner) return `return ${body}`; + + return body.slice(1, body.length - 1); +}; diff --git a/src/components/fields/Formula/worker.ts b/src/components/fields/Formula/worker.ts index 57c03265f..6c4fd21d0 100644 --- a/src/components/fields/Formula/worker.ts +++ b/src/components/fields/Formula/worker.ts @@ -1,14 +1,9 @@ onmessage = async ({ data }) => { try { const { formulaFn, row } = data; - const AsyncFunction = async function () {}.constructor as any; const fn = new AsyncFunction("row", formulaFn); const result = await fn(row); - await new Promise((resolve) => setTimeout(() => resolve(50), 3000)).then( - (res) => console.log(res) - ); - console.log(result); postMessage({ result }); } catch (error: any) { console.error("error: ", error); From 8747ffd144feeb6562bae4c866d376703d506360 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Sat, 10 Dec 2022 17:19:53 +0800 Subject: [PATCH 019/114] rowy logging ui implementation --- src/atoms/tableScope/ui.ts | 14 +- src/components/ColumnMenu/ColumnMenu.tsx | 18 + .../CloudLogsModal/CloudLogItem.tsx | 40 +- .../CloudLogsModal/CloudLogList.tsx | 7 + .../CloudLogsModal/CloudLogSeverityIcon.tsx | 6 + .../CloudLogsModal/CloudLogsModal.tsx | 477 +++++++++++++----- .../TableModals/CloudLogsModal/utils.ts | 58 ++- .../ExtensionsModal/ExtensionList.tsx | 26 + .../TableModals/WebhooksModal/WebhookList.tsx | 3 +- 9 files changed, 503 insertions(+), 146 deletions(-) diff --git a/src/atoms/tableScope/ui.ts b/src/atoms/tableScope/ui.ts index 47bdd8286..44afee102 100644 --- a/src/atoms/tableScope/ui.ts +++ b/src/atoms/tableScope/ui.ts @@ -142,14 +142,26 @@ export const selectedCellAtom = atom(null); export const contextMenuTargetAtom = atom(null); export type CloudLogFilters = { - type: "rowy" | "webhook" | "functions" | "audit" | "build"; + type: "rowy" | "audit" | "build"; timeRange: | { type: "seconds" | "minutes" | "hours" | "days"; value: number } | { type: "range"; start: Date; end: Date }; severity?: Array; webhook?: string[]; + extension?: string[]; + column?: string[]; auditRowId?: string; buildLogExpanded?: number; + functionType?: ( + | "connector" + | "derivative-script" + | "action" + | "derivative-function" + | "extension" + | "defaultValue" + | "hooks" + )[]; + loggingSource?: ("backend-scripts" | "backend-function" | "hooks")[]; }; /** Store cloud log modal filters in URL */ export const cloudLogFiltersAtom = atomWithHash( diff --git a/src/components/ColumnMenu/ColumnMenu.tsx b/src/components/ColumnMenu/ColumnMenu.tsx index cf0252171..a5f76a67d 100644 --- a/src/components/ColumnMenu/ColumnMenu.tsx +++ b/src/components/ColumnMenu/ColumnMenu.tsx @@ -20,6 +20,7 @@ import { ColumnPlusBefore as ColumnPlusBeforeIcon, ColumnPlusAfter as ColumnPlusAfterIcon, ColumnRemove as ColumnRemoveIcon, + CloudLogs as LogsIcon, } from "@src/assets/icons"; import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward"; @@ -51,6 +52,8 @@ import { tableFiltersPopoverAtom, tableNextPageAtom, tableSchemaAtom, + cloudLogFiltersAtom, + tableModalAtom, } from "@src/atoms/tableScope"; import { FieldType } from "@src/constants/fields"; import { getFieldProp } from "@src/components/fields"; @@ -107,6 +110,8 @@ export default function ColumnMenu({ ); const [tableNextPage] = useAtom(tableNextPageAtom, tableScope); const [tableSchema] = useAtom(tableSchemaAtom, tableScope); + const setModal = useSetAtom(tableModalAtom, tableScope); + const setCloudLogFilters = useSetAtom(cloudLogFiltersAtom, tableScope); const snackLogContext = useSnackLogContext(); const [altPress] = useAtom(altPressAtom, projectScope); @@ -383,6 +388,19 @@ export default function ColumnMenu({ confirm: "Evaluate", }), }, + { + key: "logs", + label: altPress ? "Logs" : "Logs…", + icon: , + onClick: () => { + setModal("cloudLogs"); + setCloudLogFilters({ + type: "rowy", + timeRange: { type: "days", value: 7 }, + column: [column.key], + }); + }, + }, ]; const columnActions: IMenuContentsProps["menuItems"] = [ diff --git a/src/components/TableModals/CloudLogsModal/CloudLogItem.tsx b/src/components/TableModals/CloudLogsModal/CloudLogItem.tsx index f82a75f17..9e0887f35 100644 --- a/src/components/TableModals/CloudLogsModal/CloudLogItem.tsx +++ b/src/components/TableModals/CloudLogsModal/CloudLogItem.tsx @@ -187,22 +187,32 @@ export default function CloudLogItem({ )} - {data.payload === "textPayload" && data.textPayload} - {get(data, "httpRequest.requestUrl")?.split(".run.app").pop()} - {data.payload === "jsonPayload" && ( - - {data.jsonPayload.error}{" "} - + {data.logName.endsWith("rowy-logging") && data.jsonPayload.payload ? ( + <> + {typeof data.jsonPayload.payload === "string" + ? data.jsonPayload.payload + : JSON.stringify(data.jsonPayload.payload)} + + ) : ( + <> + {data.payload === "textPayload" && data.textPayload} + {get(data, "httpRequest.requestUrl")?.split(".run.app").pop()} + {data.payload === "jsonPayload" && ( + + {data.jsonPayload.error}{" "} + + )} + {data.payload === "jsonPayload" && + stringify(data.jsonPayload.body ?? data.jsonPayload, { + space: 2, + })} + )} - {data.payload === "jsonPayload" && - stringify(data.jsonPayload.body ?? data.jsonPayload, { - space: 2, - })} diff --git a/src/components/TableModals/CloudLogsModal/CloudLogList.tsx b/src/components/TableModals/CloudLogsModal/CloudLogList.tsx index 5e5382eec..93093d38a 100644 --- a/src/components/TableModals/CloudLogsModal/CloudLogList.tsx +++ b/src/components/TableModals/CloudLogsModal/CloudLogList.tsx @@ -70,6 +70,13 @@ export default function CloudLogList({ items, ...props }: ICloudLogListProps) { "jsonPayload.rowyUser.displayName", // Webhook event "jsonPayload.params.endpoint", + // Rowy Logging + "jsonPayload.functionType", + "jsonPayload.loggingSource", + "jsonPayload.extensionName", + "jsonPayload.extensionType", + "jsonPayload.webhookName", + "jsonPayload.fieldName", ]} /> diff --git a/src/components/TableModals/CloudLogsModal/CloudLogSeverityIcon.tsx b/src/components/TableModals/CloudLogsModal/CloudLogSeverityIcon.tsx index 0cf0dc6ea..f8413f659 100644 --- a/src/components/TableModals/CloudLogsModal/CloudLogSeverityIcon.tsx +++ b/src/components/TableModals/CloudLogsModal/CloudLogSeverityIcon.tsx @@ -22,6 +22,12 @@ export const SEVERITY_LEVELS = { EMERGENCY: "One or more systems are unusable.", }; +export const SEVERITY_LEVELS_ROWY = { + DEFAULT: "The log entry has no assigned severity level.", + WARNING: "Warning events might cause problems.", + ERROR: "Error events are likely to cause problems.", +}; + export interface ICloudLogSeverityIconProps extends SvgIconProps { severity: keyof typeof SEVERITY_LEVELS; } diff --git a/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx b/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx index 217b59298..95af37f17 100644 --- a/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx +++ b/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx @@ -1,6 +1,6 @@ import useSWR from "swr"; import { useAtom } from "jotai"; -import { startCase } from "lodash-es"; +import { startCase, upperCase } from "lodash-es"; import { ITableModalProps } from "@src/components/TableModals"; import { @@ -12,6 +12,7 @@ import { TextField, InputAdornment, Button, + Box, } from "@mui/material"; import RefreshIcon from "@mui/icons-material/Refresh"; import { CloudLogs as LogsIcon } from "@src/assets/icons"; @@ -23,7 +24,10 @@ import TimeRangeSelect from "./TimeRangeSelect"; import CloudLogList from "./CloudLogList"; import BuildLogs from "./BuildLogs"; import EmptyState from "@src/components/EmptyState"; -import CloudLogSeverityIcon, { SEVERITY_LEVELS } from "./CloudLogSeverityIcon"; +import CloudLogSeverityIcon, { + SEVERITY_LEVELS, + SEVERITY_LEVELS_ROWY, +} from "./CloudLogSeverityIcon"; import { projectScope, @@ -119,8 +123,6 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { aria-label="Filter by log type" > Rowy Logging - Webhooks - Functions Audit Build @@ -141,36 +143,6 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { )} {cloudLogFilters.type === "rowy" && <>} - {cloudLogFilters.type === "webhook" && ( - ({ - label: x.name, - value: x.endpoint, - })) - : [] - } - value={cloudLogFilters.webhook ?? []} - onChange={(v) => - setCloudLogFilters((prev) => ({ ...prev, webhook: v })) - } - TextFieldProps={{ - id: "webhook", - className: "labelHorizontal", - sx: { "& .MuiInputBase-root": { width: 180 } }, - fullWidth: false, - }} - itemRenderer={(option) => ( - <> - {option.label} {option.value} - - )} - /> - )} {cloudLogFilters.type === "audit" && ( )} - {/* Spacer */}
    {cloudLogFilters.type !== "build" && ( @@ -220,10 +191,311 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { )} + {cloudLogFilters.type !== "rowy" && ( + + setCloudLogFilters((prev) => ({ ...prev, severity })) + } + TextFieldProps={{ + style: { width: 130 }, + placeholder: "Severity", + SelectProps: { + renderValue: () => { + if ( + !Array.isArray(cloudLogFilters.severity) || + cloudLogFilters.severity.length === 0 + ) + return `Severity`; + + if (cloudLogFilters.severity.length === 1) + return ( + <> + Severity{" "} + + + ); + + return `Severity (${cloudLogFilters.severity.length})`; + }, + }, + }} + itemRenderer={(option) => ( + <> + + {startCase(option.value.toLowerCase())} + + )} + /> + )} + + setCloudLogFilters((c) => ({ ...c, timeRange: value })) + } + /> + mutate()} + title="Refresh" + icon={} + disabled={isValidating} + /> + + )} + + + {isValidating && ( + + )} + + } + > + {cloudLogFilters.type === "build" ? ( + + ) : ( + + + {cloudLogFilters.type === "rowy" ? ( + + + { + setCloudLogFilters((prev) => ({ + ...prev, + functionType: v, + })); + }} + TextFieldProps={{ + id: "functionType", + className: "labelHorizontal", + sx: { "& .MuiInputBase-root": { width: 200 } }, + fullWidth: false, + SelectProps: { + renderValue: () => { + if (cloudLogFilters?.functionType?.length === 1) { + return `Type (${cloudLogFilters.functionType[0]})`; + } else if (cloudLogFilters?.functionType?.length) { + return `Type (${cloudLogFilters.functionType.length})`; + } else { + return `Type`; + } + }, + }, + }} + itemRenderer={(option) => <>{upperCase(option.value)}} + /> + { + setCloudLogFilters((prev) => ({ + ...prev, + loggingSource: v, + })); + }} + TextFieldProps={{ + id: "loggingSource", + className: "labelHorizontal", + sx: { "& .MuiInputBase-root": { width: 200 } }, + fullWidth: false, + SelectProps: { + renderValue: () => { + if (cloudLogFilters?.loggingSource?.length === 1) { + return `Source (${cloudLogFilters.loggingSource[0]})`; + } else if (cloudLogFilters?.loggingSource?.length) { + return `Source (${cloudLogFilters.loggingSource.length})`; + } else { + return `Source`; + } + }, + }, + }} + itemRenderer={(option) => <>{upperCase(option.value)}} + /> + ({ + label: x.name, + value: x.endpoint, + })) + : [] + } + value={cloudLogFilters.webhook ?? []} + onChange={(v) => + setCloudLogFilters((prev) => ({ ...prev, webhook: v })) + } + TextFieldProps={{ + id: "webhook", + className: "labelHorizontal", + sx: { "& .MuiInputBase-root": { width: 180 } }, + fullWidth: false, + SelectProps: { + renderValue: () => { + if (cloudLogFilters?.webhook?.length) { + return `Webhook (${cloudLogFilters.webhook.length})`; + } else { + return `Webhook`; + } + }, + }, + }} + itemRenderer={(option) => ( + <> + {option.label} {option.value} + + )} + /> + ({ + label: x.name, + value: x.name, + type: x.type, + })) + : [] + } + value={cloudLogFilters.extension ?? []} + onChange={(v) => + setCloudLogFilters((prev) => ({ ...prev, extension: v })) + } + TextFieldProps={{ + id: "extension", + className: "labelHorizontal", + sx: { "& .MuiInputBase-root": { width: 180 } }, + fullWidth: false, + placeholder: "Extension", + SelectProps: { + renderValue: () => { + if (cloudLogFilters?.extension?.length === 1) { + return `Extension (${cloudLogFilters.extension[0]})`; + } else if (cloudLogFilters?.extension?.length) { + return `Extension (${cloudLogFilters.extension.length})`; + } else { + return `Extension`; + } + }, + }, + }} + itemRenderer={(option) => ( + <> + {option.label} {option.type} + + )} + /> + ({ + label: config.name, + value: key, + type: config.type, + }) + )} + value={cloudLogFilters.column ?? []} + onChange={(v) => + setCloudLogFilters((prev) => ({ ...prev, column: v })) + } + TextFieldProps={{ + id: "column", + className: "labelHorizontal", + sx: { "& .MuiInputBase-root": { width: 200 } }, + fullWidth: false, + placeholder: "Column", + SelectProps: { + renderValue: () => { + if (cloudLogFilters?.column?.length === 1) { + return `Column (${cloudLogFilters.column[0]})`; + } else if (cloudLogFilters?.column?.length) { + return `Column (${cloudLogFilters.column.length})`; + } else { + return `Column`; + } + }, + }, + }} + itemRenderer={(option) => ( + <> + {option.label} {option.value}  + {option.type} + + )} + /> setCloudLogFilters((prev) => ({ ...prev, severity })) @@ -264,86 +536,61 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { )} /> - - setCloudLogFilters((c) => ({ ...c, timeRange: value })) - } - /> - mutate()} - title="Refresh" - icon={} - disabled={isValidating} - /> - + + ) : null} + + + {Array.isArray(data) && data.length > 0 ? ( + + + {cloudLogFilters.timeRange.type !== "range" && ( + + )} + + ) : isValidating ? ( + + ) : ( + )} - - - {isValidating && ( - - )} - - {/* {logQueryUrl} */} - - } - > - {cloudLogFilters.type === "build" ? ( - - ) : Array.isArray(data) && data.length > 0 ? ( - <> - - {cloudLogFilters.timeRange.type !== "range" && ( - - )} - - ) : isValidating ? ( - - ) : ( - + + )} ); diff --git a/src/components/TableModals/CloudLogsModal/utils.ts b/src/components/TableModals/CloudLogsModal/utils.ts index 3c7fddb0f..306c47942 100644 --- a/src/components/TableModals/CloudLogsModal/utils.ts +++ b/src/components/TableModals/CloudLogsModal/utils.ts @@ -15,21 +15,55 @@ export const cloudLogFetcher = ( switch (cloudLogFilters.type) { case "rowy": logQuery.push(`logName = "projects/${projectId}/logs/rowy-logging"`); - break; - case "webhook": - logQuery.push( - `logName = "projects/${projectId}/logs/rowy-webhook-events"` - ); - logQuery.push(`jsonPayload.url : "${tablePath}"`); - if ( - Array.isArray(cloudLogFilters.webhook) && - cloudLogFilters.webhook.length > 0 - ) + if (cloudLogFilters?.functionType?.length) + logQuery.push( + cloudLogFilters.functionType + .map((functionType) => { + return `jsonPayload.functionType = "${functionType}"`; + }) + .join(encodeURIComponent(" OR ")) + ); + if (cloudLogFilters?.loggingSource?.length) { + logQuery.push( + cloudLogFilters.loggingSource + .map((loggingSource) => { + return `jsonPayload.loggingSource = "${loggingSource}"`; + }) + .join(encodeURIComponent(" OR ")) + ); + } else { + // mandatory filter to remove unwanted gcp diagnostic logs + logQuery.push( + ["backend-scripts", "backend-function", "hooks"] + .map((loggingSource) => { + return `jsonPayload.loggingSource = "${loggingSource}"`; + }) + .join(encodeURIComponent(" OR ")) + ); + } + if (cloudLogFilters?.webhook?.length) { logQuery.push( cloudLogFilters.webhook .map((id) => `jsonPayload.url : "${id}"`) .join(encodeURIComponent(" OR ")) ); + } + if (cloudLogFilters?.extension?.length) + logQuery.push( + cloudLogFilters.extension + .map((extensionName) => { + return `jsonPayload.extensionName = "${extensionName}"`; + }) + .join(encodeURIComponent(" OR ")) + ); + if (cloudLogFilters?.column?.length) + logQuery.push( + cloudLogFilters.column + .map((column) => { + return `jsonPayload.fieldName = "${column}"`; + }) + .join(encodeURIComponent(" OR ")) + ); break; case "audit": @@ -41,10 +75,6 @@ export const cloudLogFetcher = ( ); break; - case "functions": - logQuery.push(`resource.labels.function_name = "R-${tablePath}"`); - break; - default: break; } diff --git a/src/components/TableModals/ExtensionsModal/ExtensionList.tsx b/src/components/TableModals/ExtensionsModal/ExtensionList.tsx index dbd63a9e1..7ed8e6c6e 100644 --- a/src/components/TableModals/ExtensionsModal/ExtensionList.tsx +++ b/src/components/TableModals/ExtensionsModal/ExtensionList.tsx @@ -14,6 +14,7 @@ import { import { Extension as ExtensionIcon, Copy as DuplicateIcon, + CloudLogs as LogsIcon, } from "@src/assets/icons"; import EditIcon from "@mui/icons-material/EditOutlined"; import DeleteIcon from "@mui/icons-material/DeleteOutlined"; @@ -21,6 +22,12 @@ import DeleteIcon from "@mui/icons-material/DeleteOutlined"; import EmptyState from "@src/components/EmptyState"; import { extensionNames, IExtension } from "./utils"; import { DATE_TIME_FORMAT } from "@src/constants/dates"; +import { useSetAtom } from "jotai"; +import { + cloudLogFiltersAtom, + tableModalAtom, + tableScope, +} from "@src/atoms/tableScope"; export interface IExtensionListProps { extensions: IExtension[]; @@ -37,6 +44,9 @@ export default function ExtensionList({ handleEdit, handleDelete, }: IExtensionListProps) { + const setModal = useSetAtom(tableModalAtom, tableScope); + const setCloudLogFilters = useSetAtom(cloudLogFiltersAtom, tableScope); + if (extensions.length === 0) return ( + + { + setModal("cloudLogs"); + setCloudLogFilters({ + type: "rowy", + functionType: ["extension"], + timeRange: { type: "days", value: 7 }, + extension: [extensionObject.name], + }); + }} + > + + + { setModal("cloudLogs"); setCloudLogFilters({ - type: "webhook", + type: "rowy", + functionType: ["hooks"], timeRange: { type: "days", value: 7 }, webhook: [webhook.endpoint], }); From 3b91bf9eaafdae32cfe8b3fc6ae631c3ea2d87e1 Mon Sep 17 00:00:00 2001 From: Han Tuerker Date: Sun, 11 Dec 2022 13:14:31 +0800 Subject: [PATCH 020/114] add editor diagnostics --- src/components/fields/Formula/Settings.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/fields/Formula/Settings.tsx b/src/components/fields/Formula/Settings.tsx index cb907a8c2..ebce8995e 100644 --- a/src/components/fields/Formula/Settings.tsx +++ b/src/components/fields/Formula/Settings.tsx @@ -26,6 +26,12 @@ const CodeEditor = lazy( import("@src/components/CodeEditor" /* webpackChunkName: "CodeEditor" */) ); +const diagnosticsOptions = { + noSemanticValidation: false, + noSyntaxValidation: false, + noSuggestionDiagnostics: true, +}; + export default function Settings({ config, onChange, @@ -113,6 +119,7 @@ export default function Settings({ }> Date: Tue, 13 Dec 2022 12:59:46 +0530 Subject: [PATCH 021/114] Feat: add reordering of Select and Multiselect field option --- .../fields/SingleSelect/Settings.tsx | 89 ++++++++++++++++++- 1 file changed, 86 insertions(+), 3 deletions(-) diff --git a/src/components/fields/SingleSelect/Settings.tsx b/src/components/fields/SingleSelect/Settings.tsx index 2fc908b9a..822697758 100644 --- a/src/components/fields/SingleSelect/Settings.tsx +++ b/src/components/fields/SingleSelect/Settings.tsx @@ -15,6 +15,78 @@ import { import AddIcon from "@mui/icons-material/AddCircle"; import RemoveIcon from "@mui/icons-material/CancelRounded"; +import { HTML5Backend } from "react-dnd-html5-backend"; +import { DndProvider } from "react-dnd"; +import { useDrag, useDrop } from "react-dnd"; + +interface IsettingsDraggableProps { + options: string[]; + option: string; + index: number; + onChange: (key: string) => (value: any) => void; +} + +function DraggableSettingsCard({ + options, + option, + index, + onChange, +}: IsettingsDraggableProps) { + const [{ isDragging }, dragRef] = useDrag({ + type: "SETTING_DRAG", + item: { key: index }, + collect: (monitor) => ({ + isDragging: monitor.isDragging(), + }), + }); + + const [{ isOver }, dropRef] = useDrop({ + accept: "SETTING_DRAG", + drop: ({ key }: { key: number }) => { + const temp = options[key]; + options[key] = options[index]; + options[index] = temp; + onChange("options")([...options]); + }, + collect: (monitor) => ({ + isOver: monitor.isOver(), + canDrop: monitor.canDrop(), + }), + }); + + return ( +
    + { + dragRef(ref); + dropRef(ref); + }} + style={{ cursor: "move" }} + container + direction="row" + key={`option-${option}`} + justifyContent="space-between" + alignItems="center" + > + + {option} + + + + onChange("options")(options.filter((o: string) => o !== option)) + } + > + {} + + + + +
    + ); +} + export default function Settings({ onChange, config }: ISettingsProps) { const listEndRef: any = useRef(null); const options = config.options ?? []; @@ -42,7 +114,18 @@ export default function Settings({ onChange, config }: ISettingsProps) { marginBottom: 5, }} > - {options?.map((option: string) => ( + + {options?.map((option: string, index: number) => ( + + ))} + + + {/* {options?.map((option: string) => ( <> - ))} + ))} */}
    @@ -93,7 +176,7 @@ export default function Settings({ onChange, config }: ISettingsProps) { onChange={(e) => { setNewOption(e.target.value); }} - onKeyDown={(e: any) => { + onKeyPress={(e: any) => { if (e.key === "Enter") { handleAdd(); } From f53c9ef62b20391a6cf3c1ef498f3ecbe308e400 Mon Sep 17 00:00:00 2001 From: Miriam Shams-Rainey Date: Tue, 13 Dec 2022 11:09:04 -0500 Subject: [PATCH 022/114] Modified default/empty hook and extension settings --- src/components/TableModals/ExtensionsModal/utils.ts | 2 +- src/components/TableModals/WebhooksModal/utils.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/TableModals/ExtensionsModal/utils.ts b/src/components/TableModals/ExtensionsModal/utils.ts index e85bd207d..857544fba 100644 --- a/src/components/TableModals/ExtensionsModal/utils.ts +++ b/src/components/TableModals/ExtensionsModal/utils.ts @@ -225,7 +225,7 @@ export function emptyExtensionObject( ): IExtension { return { name: `${type} extension`, - active: false, + active: true, triggers: [], type, extensionBody: extensionBodyTemplate[type] ?? extensionBodyTemplate["task"], diff --git a/src/components/TableModals/WebhooksModal/utils.tsx b/src/components/TableModals/WebhooksModal/utils.tsx index db8338096..15f02a1d1 100644 --- a/src/components/TableModals/WebhooksModal/utils.tsx +++ b/src/components/TableModals/WebhooksModal/utils.tsx @@ -91,7 +91,7 @@ export function emptyWebhookObject( ): IWebhook { return { name: `${type} webhook`, - active: false, + active: true, endpoint: generateId(), type, parser: webhookSchemas[type].parser?.template(table), From 430833c46ebe8165a921de8ab7f10d6f4ec4f692 Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Sat, 17 Dec 2022 06:53:24 +0000 Subject: [PATCH 023/114] bug fix: small blip in the ordering --- src/components/Table/ColumnSelect.tsx | 10 +++++++- src/components/TableToolbar/HiddenFields.tsx | 27 +++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/components/Table/ColumnSelect.tsx b/src/components/Table/ColumnSelect.tsx index d5b3fa154..599b32f11 100644 --- a/src/components/Table/ColumnSelect.tsx +++ b/src/components/Table/ColumnSelect.tsx @@ -22,14 +22,22 @@ export interface IColumnSelectProps { filterColumns?: (column: ColumnConfig) => boolean; showFieldNames?: boolean; options?: ColumnOption[]; + tableColumnsOrdered?: ColumnConfig[]; } export default function ColumnSelect({ filterColumns, showFieldNames, + tableColumnsOrdered: tableColumnsOrdered_, ...props }: IColumnSelectProps & Omit, "options">) { - const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); + // if tableColumnsOrdered is passed(in case of HiddenFields) else traditional way to get tableColumnsOrders (backward compitable) + let [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); + + if (tableColumnsOrdered_) { + tableColumnsOrdered = tableColumnsOrdered_; + } + const options = props.options || (filterColumns diff --git a/src/components/TableToolbar/HiddenFields.tsx b/src/components/TableToolbar/HiddenFields.tsx index 0a5377132..0ec52f78b 100644 --- a/src/components/TableToolbar/HiddenFields.tsx +++ b/src/components/TableToolbar/HiddenFields.tsx @@ -2,6 +2,7 @@ import { useEffect, useRef, useMemo, + useCallback, useState, forwardRef, ChangeEvent, @@ -34,6 +35,7 @@ import { tableScope, tableIdAtom, updateColumnAtom, + tableColumnsOrderedAtom, } from "@src/atoms/tableScope"; import { formatSubTableName } from "@src/utils/table"; @@ -59,6 +61,10 @@ export default function HiddenFields() { setHiddenFields(userDocHiddenFields); }, [userDocHiddenFields]); + const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); + // cashed tableColumnsOrdered from quick updates(reorder). + const [quickAccessTableColumnsOrdered, setQuickAccessTableColumnsOrdered] = + useState(tableColumnsOrdered); // const tableColumns = tableColumnsOrdered.map(({ key, name }) => ({ // value: key, // label: name, @@ -157,9 +163,27 @@ export default function HiddenFields() { const updateColumn = useSetAtom(updateColumnAtom, tableScope); + // function to reorder columns from cashed tableColumnsOrdered + const optimisticReorder = useCallback( + (from: number, to: number) => { + let temp = Array.from(quickAccessTableColumnsOrdered); + const currentColumn = temp.splice(from, 1)[0]; + temp.splice(to, 0, currentColumn); + + // update indexes + for (let i = from < to ? from : to; i < temp.length; i++) { + temp[i].index = i; + } + setQuickAccessTableColumnsOrdered(temp); + }, + [quickAccessTableColumnsOrdered] + ); + // updates column on drag end function handleOnDragEnd(result: DropResult) { - if (!result.destination) return; + if (!result.destination || result.destination.index === result.source.index) + return; + optimisticReorder(result.source.index, result.destination.index); updateColumn({ key: result.draggableId, config: {}, @@ -212,6 +236,7 @@ export default function HiddenFields() { {hiddenFields.length > 0 ? `${hiddenFields.length} hidden` : "Hide"} Date: Sat, 17 Dec 2022 08:49:26 +0000 Subject: [PATCH 024/114] revalidate cache --- src/components/TableToolbar/HiddenFields.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/TableToolbar/HiddenFields.tsx b/src/components/TableToolbar/HiddenFields.tsx index 0ec52f78b..32f9118ab 100644 --- a/src/components/TableToolbar/HiddenFields.tsx +++ b/src/components/TableToolbar/HiddenFields.tsx @@ -163,6 +163,11 @@ export default function HiddenFields() { const updateColumn = useSetAtom(updateColumnAtom, tableScope); + // function to revalidate quickAccessTableColumnsOrdered + function revalidateCache() { + setQuickAccessTableColumnsOrdered(tableColumnsOrdered); + } + // function to reorder columns from cashed tableColumnsOrdered const optimisticReorder = useCallback( (from: number, to: number) => { @@ -229,7 +234,10 @@ export default function HiddenFields() { <> } - onClick={() => setOpen((o) => !o)} + onClick={() => { + revalidateCache(); + setOpen((o) => !o); + }} active={hiddenFields.length > 0} ref={buttonRef} > From 4bde3ce1721f152e45ea135923a1911f26fa5536 Mon Sep 17 00:00:00 2001 From: Han Tuerker Date: Sun, 18 Dec 2022 20:27:41 +0800 Subject: [PATCH 025/114] fix empty formula function --- src/components/fields/Formula/DisplayCell.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/fields/Formula/DisplayCell.tsx b/src/components/fields/Formula/DisplayCell.tsx index 1a5b538d4..e765bad10 100644 --- a/src/components/fields/Formula/DisplayCell.tsx +++ b/src/components/fields/Formula/DisplayCell.tsx @@ -6,7 +6,7 @@ export default function Formula(props: IDisplayCellProps) { const { result, error } = useFormula({ row: props.row, listenerFields: props.column.config?.listenerFields || [], - formulaFn: props.column.config?.formulaFn, + formulaFn: props.column.config?.formulaFn || "", }); const type = props.column.config?.renderFieldType; From f6e53f631215d3fcfc75d47eedd03d468c5e5332 Mon Sep 17 00:00:00 2001 From: Han Tuerker Date: Sun, 18 Dec 2022 20:46:05 +0800 Subject: [PATCH 026/114] update formula field icon --- src/components/fields/Formula/index.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/fields/Formula/index.tsx b/src/components/fields/Formula/index.tsx index 0e55e24d0..2394092bd 100644 --- a/src/components/fields/Formula/index.tsx +++ b/src/components/fields/Formula/index.tsx @@ -1,5 +1,4 @@ -import { Derivative as DerivativeIcon } from "@src/assets/icons"; - +import FormulaIcon from "@mui/icons-material/Functions"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; import DisplayCell from "./DisplayCell"; @@ -12,7 +11,7 @@ export const config: IFieldConfig = { group: "Client Function", dataType: "any", initialValue: "", - icon: , + icon: , description: "Client Function (Alpha)", TableCell: withRenderTableCell(DisplayCell as any, null, undefined, { usesRowData: true, From 2b1795387b0570f485d7320b50b58eadb815a513 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Mon, 19 Dec 2022 04:34:16 +1100 Subject: [PATCH 027/114] add snackbar message to ask for more rowy run memory allocation if evaluation fails due to out of memory --- .../fields/Derivative/ContextMenuActions.tsx | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/components/fields/Derivative/ContextMenuActions.tsx b/src/components/fields/Derivative/ContextMenuActions.tsx index 015e3f7aa..92d781d84 100644 --- a/src/components/fields/Derivative/ContextMenuActions.tsx +++ b/src/components/fields/Derivative/ContextMenuActions.tsx @@ -2,13 +2,17 @@ import { useAtom } from "jotai"; import { find, get } from "lodash-es"; import { useSnackbar } from "notistack"; +import { Button } from "@mui/material"; import ReEvalIcon from "@mui/icons-material/ReplayOutlined"; import EvalIcon from "@mui/icons-material/PlayCircleOutline"; +import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon"; import { projectScope, compatibleRowyRunVersionAtom, rowyRunAtom, + projectIdAtom, + projectSettingsAtom, } from "@src/atoms/projectScope"; import { tableScope, @@ -34,6 +38,8 @@ export const ContextMenuActions: IFieldConfig["contextMenuActions"] = ( const [tableSettings] = useAtom(tableSettingsAtom, tableScope); const [tableSchema] = useAtom(tableSchemaAtom, tableScope); const [tableRows] = useAtom(tableRowsAtom, tableScope); + const [projectId] = useAtom(projectIdAtom, projectScope); + const [projectSettings] = useAtom(projectSettingsAtom, projectScope); const { enqueueSnackbar, closeSnackbar } = useSnackbar(); const [compatibleRowyRunVersion] = useAtom( compatibleRowyRunVersionAtom, @@ -76,8 +82,32 @@ export const ContextMenuActions: IFieldConfig["contextMenuActions"] = ( } else { enqueueSnackbar("Cell evaluated", { variant: "success" }); } - } catch (error) { - enqueueSnackbar(`Failed: ${error}`, { variant: "error" }); + } catch (error: any) { + if (error.message === "Failed to fetch") { + enqueueSnackbar( + "Evaluation failed. Rowy Run is likely out of memory. Please allocate more in GCP console.", + { + variant: "warning", + persist: true, + action: (snackbarId) => ( + + ), + } + ); + } else { + enqueueSnackbar(`Failed: ${error}`, { variant: "error" }); + } } }; const isEmpty = From f89e0e1de36c0b7f60f39a550f636718ae5d0a8d Mon Sep 17 00:00:00 2001 From: shamsmosowi Date: Fri, 23 Dec 2022 17:05:57 +0100 Subject: [PATCH 028/114] add,remove,edit columns --- .../Table/FinalColumn/FinalColumnHeader.tsx | 76 +++--- src/components/TableSettingsDialog/form.tsx | 11 + src/components/TableToolbar/ManageColumns.tsx | 252 ------------------ src/components/TableToolbar/TableToolbar.tsx | 6 +- src/pages/Table/TablePage.tsx | 6 +- .../useTableFunctions.ts | 17 +- src/types/table.d.ts | 1 + 7 files changed, 70 insertions(+), 299 deletions(-) delete mode 100644 src/components/TableToolbar/ManageColumns.tsx diff --git a/src/components/Table/FinalColumn/FinalColumnHeader.tsx b/src/components/Table/FinalColumn/FinalColumnHeader.tsx index a1989f53d..3d65a9a41 100644 --- a/src/components/Table/FinalColumn/FinalColumnHeader.tsx +++ b/src/components/Table/FinalColumn/FinalColumnHeader.tsx @@ -1,9 +1,8 @@ -import { useAtom, useSetAtom } from "jotai"; +import { useSetAtom } from "jotai"; import { Box, BoxProps, Button } from "@mui/material"; import { AddColumn as AddColumnIcon } from "@src/assets/icons"; -import { projectScope, userRolesAtom } from "@src/atoms/projectScope"; import { tableScope, columnModalAtom } from "@src/atoms/tableScope"; import { spreadSx } from "@src/utils/ui"; @@ -17,10 +16,43 @@ export default function FinalColumnHeader({ canAddColumns, ...props }: IFinalColumnHeaderProps) { - const [userRoles] = useAtom(userRolesAtom, projectScope); const openColumnModal = useSetAtom(columnModalAtom, tableScope); - if (!userRoles.includes("ADMIN")) + if (canAddColumns) + return ( + `1px solid ${theme.palette.divider}`, + borderLeft: "none", + borderTopRightRadius: (theme) => theme.shape.borderRadius, + borderBottomRightRadius: (theme) => theme.shape.borderRadius, + display: "flex", + alignItems: "center", + width: 32 * 3 + 4 * 2 + 10 * 2, + overflow: "visible", + px: 0.75, + }, + ...spreadSx(props.sx), + ]} + className="column-header" + > + + + ); + else return ( ); - - return ( - `1px solid ${theme.palette.divider}`, - borderLeft: "none", - borderTopRightRadius: (theme) => theme.shape.borderRadius, - borderBottomRightRadius: (theme) => theme.shape.borderRadius, - - display: "flex", - alignItems: "center", - - width: 32 * 3 + 4 * 2 + 10 * 2, - overflow: "visible", - px: 0.75, - }, - ...spreadSx(props.sx), - ]} - className="column-header" - > - - - ); } diff --git a/src/components/TableSettingsDialog/form.tsx b/src/components/TableSettingsDialog/form.tsx index 0dddff69a..4bd8a7637 100644 --- a/src/components/TableSettingsDialog/form.tsx +++ b/src/components/TableSettingsDialog/form.tsx @@ -311,6 +311,17 @@ export const tableSettings = ( label: "Suggested Firestore Rules", watchedField: "collection", }, + { + step: "accessControls", + type: FieldType.multiSelect, + name: "modifiableBy", + label: "Modifiable by", + labelPlural: "Modifier Roles", + options: roles ?? [], + defaultValue: ["ADMIN"], + required: true, + freeText: true, + }, // Step 4: Auditing { diff --git a/src/components/TableToolbar/ManageColumns.tsx b/src/components/TableToolbar/ManageColumns.tsx deleted file mode 100644 index 7be2d690d..000000000 --- a/src/components/TableToolbar/ManageColumns.tsx +++ /dev/null @@ -1,252 +0,0 @@ -import { - useEffect, - useRef, - useMemo, - useState, - ChangeEvent, - forwardRef, -} from "react"; -import { useAtom, useSetAtom } from "jotai"; -import { isEqual } from "lodash-es"; -import { colord } from "colord"; -import { - DragDropContext, - Droppable, - Draggable, - DropResult, -} from "react-beautiful-dnd"; - -import { Box, AutocompleteProps, Theme } from "@mui/material"; -import VisibilityIcon from "@mui/icons-material/VisibilityOutlined"; -// import VisibilityOffIcon from "@mui/icons-material/VisibilityOffOutlined"; -import ViewColumnOutlinedIcon from "@mui/icons-material/ViewColumnOutlined"; -import IconSlash from "@src/components/IconSlash"; -import DragIndicatorOutlinedIcon from "@mui/icons-material/DragIndicatorOutlined"; - -import ColumnSelect, { ColumnItem } from "@src/components/Table/ColumnSelect"; -import ButtonWithStatus from "@src/components/ButtonWithStatus"; - -import { - globalScope, - userSettingsAtom, - updateUserSettingsAtom, -} from "@src/atoms/globalScope"; -import { - tableScope, - tableIdAtom, - updateColumnAtom, -} from "@src/atoms/tableScope"; -import { formatSubTableName } from "@src/utils/table"; - -export default function ManageColumns() { - const buttonRef = useRef(null); - - const [userSettings] = useAtom(userSettingsAtom, globalScope); - const [tableId] = useAtom(tableIdAtom, tableScope); - - const [open, setOpen] = useState(false); - - // Store local selection here - // Initialize hiddenFields from user doc - const userDocHiddenFields = useMemo( - () => - userSettings.tables?.[formatSubTableName(tableId)]?.hiddenFields ?? [], - [userSettings.tables, tableId] - ); - - const [hiddenFields, setHiddenFields] = - useState(userDocHiddenFields); - useEffect(() => { - setHiddenFields(userDocHiddenFields); - }, [userDocHiddenFields]); - - // const tableColumns = tableColumnsOrdered.map(({ key, name }) => ({ - // value: key, - // label: name, - // })); - - // Save when MultiSelect closes - const [updateUserSettings] = useAtom(updateUserSettingsAtom, globalScope); - const handleSave = () => { - // Only update if there were any changes because it’s slow to update - if (!isEqual(hiddenFields, userDocHiddenFields) && updateUserSettings) { - updateUserSettings({ - tables: { [formatSubTableName(tableId)]: { hiddenFields } }, - }); - } - - setOpen(false); - }; - - // disable drag if search box is not empty - const [disableDrag, setDisableDrag] = useState(false); - - const renderOption: AutocompleteProps< - any, - true, - false, - any - >["renderOption"] = (props, option, { selected }) => { - const slashColor = (theme: Theme) => - colord(theme.palette.background.paper) - .mix("#fff", theme.palette.mode === "dark" ? 0.16 : 0) - .alpha(1); - return ( - - {(provided) => ( -
  • - - - - - - - slashColor(theme).toHslString(), - }, - ".Mui-focused & .icon-slash-mask": { - stroke: (theme) => - slashColor(theme) - .mix( - theme.palette.primary.main, - theme.palette.action.selectedOpacity + - theme.palette.action.hoverOpacity - ) - .alpha(1) - .toHslString(), - }, - }, - selected ? { strokeDashoffset: 0 } : {}, - ]} - /> - - -
  • - )} -
    - ); - }; - - const updateColumn = useSetAtom(updateColumnAtom, tableScope); - function handleOnDragEnd(result: DropResult) { - if (!result.destination) return; - updateColumn({ - key: result.draggableId, - config: {}, - index: result.destination.index, - }); - } - - function checkToDisableDrag(e: ChangeEvent) { - setDisableDrag(e.target.value !== ""); - } - - const ListboxComponent = forwardRef(function ListboxComponent( - props: React.HTMLAttributes, - ulRef: any /*React.ForwardedRef*/ - ) { - const { children, ...other } = props; - - return ( - - - {(provided) => ( -
      { - provided.innerRef(ref); - if (ulRef !== null) { - ulRef(ref); - } - }} - > - {props.children} - {provided.placeholder} -
    - )} -
    -
    - ); - }); - - return ( - <> - } - onClick={() => setOpen((o) => !o)} - active={hiddenFields.length > 0} - ref={buttonRef} - > - {"Columns"} - - - { - setHiddenFields(updates); - setDisableDrag(false); - }} - onClose={handleSave} - clearText="Show all" - selectAllText="Hide all" - doneText="Apply" - /> - - ); -} diff --git a/src/components/TableToolbar/TableToolbar.tsx b/src/components/TableToolbar/TableToolbar.tsx index 187f32f89..7af0f6b9a 100644 --- a/src/components/TableToolbar/TableToolbar.tsx +++ b/src/components/TableToolbar/TableToolbar.tsx @@ -14,8 +14,7 @@ import { ButtonSkeleton } from "./TableToolbarSkeleton"; import AddRow from "./AddRow"; import LoadedRowsStatus from "./LoadedRowsStatus"; import TableSettings from "./TableSettings"; -// import HiddenFields from "./HiddenFields"; -import ManageColumns from "./ManageColumns"; +import HiddenFields from "./HiddenFields"; import RowHeight from "./RowHeight"; import TableInformation from "./TableInformation"; @@ -90,8 +89,7 @@ export default function TableToolbar() { >
    {/* Spacer */} - {/* */} - + }> diff --git a/src/pages/Table/TablePage.tsx b/src/pages/Table/TablePage.tsx index 75f1fc281..04fc0e618 100644 --- a/src/pages/Table/TablePage.tsx +++ b/src/pages/Table/TablePage.tsx @@ -81,8 +81,10 @@ export default function TablePage({ // Set permissions here so we can pass them to the `Table` component, which // shouldn’t access `projectScope` at all, to separate concerns. - const canAddColumns = - userRoles.includes("ADMIN") || userRoles.includes("OPS"); + const canAddColumns = Boolean( + userRoles.includes("ADMIN") || + tableSettings.modifiableBy?.some((r) => userRoles.includes(r)) + ); const canEditColumns = canAddColumns; const canDeleteColumns = canAddColumns; const canEditCells = diff --git a/src/sources/ProjectSourceFirebase/useTableFunctions.ts b/src/sources/ProjectSourceFirebase/useTableFunctions.ts index 95bbdcb5b..ab6fb4efc 100644 --- a/src/sources/ProjectSourceFirebase/useTableFunctions.ts +++ b/src/sources/ProjectSourceFirebase/useTableFunctions.ts @@ -145,10 +145,25 @@ export function useTableFunctions() { // Shallow merge new settings with old tables[tableIndex] = { ...tables[tableIndex], ...settings }; + // Create tablesSettings object from tables array + const tablesSettings = tables.reduce( + (acc, table) => { + if (table.tableType === "primaryCollection") { + acc.pc[table.id] = table; + } else { + acc.cg[table.id] = table; + } + return acc; + }, + { + pc: {}, + cg: {}, + } as Record> + ); // Updates settings doc with new tables array const promiseUpdateSettings = setDoc( doc(firebaseDb, SETTINGS), - { tables }, + { tables, tablesSettings }, { merge: true } ); diff --git a/src/types/table.d.ts b/src/types/table.d.ts index b9753c41b..2e188ea93 100644 --- a/src/types/table.d.ts +++ b/src/types/table.d.ts @@ -75,6 +75,7 @@ export type TableSettings = { description?: string; details?: string; thumbnailURL?: string; + modifiableBy?: string[]; _createdBy?: { displayName?: string; From f9abf4b385fa6d04b54a05d6631eb5418504e7bc Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Sun, 25 Dec 2022 13:58:15 +0000 Subject: [PATCH 029/114] show a snackbar error when firestore pemission rejects a change --- .../Table/TableCell/EditorCellController.tsx | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/components/Table/TableCell/EditorCellController.tsx b/src/components/Table/TableCell/EditorCellController.tsx index a8978c8d9..c80380d40 100644 --- a/src/components/Table/TableCell/EditorCellController.tsx +++ b/src/components/Table/TableCell/EditorCellController.tsx @@ -2,6 +2,7 @@ import { useEffect, useLayoutEffect } from "react"; import useStateRef from "react-usestateref"; import { useSetAtom } from "jotai"; import { isEqual } from "lodash-es"; +import { useSnackbar } from "notistack"; import { tableScope, updateFieldAtom } from "@src/atoms/tableScope"; import type { @@ -45,6 +46,8 @@ export default function EditorCellController({ const [isDirty, setIsDirty, isDirtyRef] = useStateRef(false); const updateField = useSetAtom(updateFieldAtom, tableScope); + const { enqueueSnackbar } = useSnackbar(); + // When this cell’s data has updated, update the local value if // it’s not dirty and the value is different useEffect(() => { @@ -53,17 +56,20 @@ export default function EditorCellController({ }, [isDirty, localValueRef, setLocalValue, value]); // This is where we update the documents - const handleSubmit = () => { + const handleSubmit = async () => { // props.disabled should always be false as withRenderTableCell would // render DisplayCell instead of EditorCell if (props.disabled || !isDirtyRef.current) return; - - updateField({ - path: props._rowy_ref.path, - fieldName: props.column.fieldName, - value: localValueRef.current, - deleteField: localValueRef.current === undefined, - }); + try { + await updateField({ + path: props._rowy_ref.path, + fieldName: props.column.fieldName, + value: localValueRef.current, + deleteField: localValueRef.current === undefined, + }); + } catch (e) { + enqueueSnackbar((e as Error).message, { variant: "error" }); + } }; useLayoutEffect(() => { From 65fabca63081899470082506005b6028f61e60cb Mon Sep 17 00:00:00 2001 From: Han Tuerker Date: Sun, 25 Dec 2022 20:34:54 +0300 Subject: [PATCH 030/114] feat(formula-field): fix error state --- src/components/fields/Formula/DisplayCell.tsx | 10 ++++++++-- src/components/fields/Formula/Settings.tsx | 12 +++++++----- src/components/fields/Formula/formula.d.ts | 6 +++--- src/components/fields/Formula/useFormula.tsx | 17 ++++++----------- src/components/fields/Formula/worker.ts | 2 +- 5 files changed, 25 insertions(+), 22 deletions(-) diff --git a/src/components/fields/Formula/DisplayCell.tsx b/src/components/fields/Formula/DisplayCell.tsx index e765bad10..b0cb61f8b 100644 --- a/src/components/fields/Formula/DisplayCell.tsx +++ b/src/components/fields/Formula/DisplayCell.tsx @@ -1,9 +1,11 @@ +import CircularProgressOptical from "@src/components/CircularProgressOptical"; import { IDisplayCellProps } from "@src/components/fields/types"; + import { useFormula } from "./useFormula"; import { getDisplayCell } from "./util"; export default function Formula(props: IDisplayCellProps) { - const { result, error } = useFormula({ + const { result, error, loading } = useFormula({ row: props.row, listenerFields: props.column.config?.listenerFields || [], formulaFn: props.column.config?.formulaFn || "", @@ -13,7 +15,11 @@ export default function Formula(props: IDisplayCellProps) { const DisplayCell = getDisplayCell(type); if (error) { - return <>Error ${error}; + return <>Error: {error.message}; + } + + if (loading) { + return ; } return ; diff --git a/src/components/fields/Formula/Settings.tsx b/src/components/fields/Formula/Settings.tsx index ebce8995e..ad0eb16fb 100644 --- a/src/components/fields/Formula/Settings.tsx +++ b/src/components/fields/Formula/Settings.tsx @@ -1,6 +1,8 @@ import { lazy, Suspense } from "react"; import { useDebouncedCallback } from "use-debounce"; -import { ISettingsProps } from "@src/components/fields/types"; +import { useAtom } from "jotai"; +import MultiSelect from "@rowy/multiselect"; + import { Grid, InputLabel, @@ -9,13 +11,13 @@ import { FormHelperText, Tooltip, } from "@mui/material"; -import FieldSkeleton from "@src/components/SideDrawer/FieldSkeleton"; -import { useAtom } from "jotai"; +import FieldSkeleton from "@src/components/SideDrawer/FieldSkeleton"; +import { ISettingsProps } from "@src/components/fields/types"; import { tableColumnsOrderedAtom, tableScope } from "@src/atoms/tableScope"; -import { defaultFn, listenerFieldTypes, outputFieldTypes } from "./util"; -import MultiSelect from "@rowy/multiselect"; import FieldsDropdown from "@src/components/ColumnModals/FieldsDropdown"; + +import { defaultFn, listenerFieldTypes, outputFieldTypes } from "./util"; import { getFieldProp } from ".."; /* eslint-disable import/no-webpack-loader-syntax */ diff --git a/src/components/fields/Formula/formula.d.ts b/src/components/fields/Formula/formula.d.ts index 90f9351d1..517b7e570 100644 --- a/src/components/fields/Formula/formula.d.ts +++ b/src/components/fields/Formula/formula.d.ts @@ -1,8 +1,8 @@ type FormulaContext = { row: Row; - // ref: FirebaseFirestore.DocumentReference; - // storage: firebasestorage.Storage; - // db: FirebaseFirestore.Firestore; + // ref: FirebaseFirestore.DocumentReference; + // storage: firebasestorage.Storage; + // db: FirebaseFirestore.Firestore; }; type Formula = (context: FormulaContext) => "PLACEHOLDER_OUTPUT_TYPE"; diff --git a/src/components/fields/Formula/useFormula.tsx b/src/components/fields/Formula/useFormula.tsx index c0113c3b6..b316eccdf 100644 --- a/src/components/fields/Formula/useFormula.tsx +++ b/src/components/fields/Formula/useFormula.tsx @@ -1,8 +1,10 @@ -import { tableColumnsOrderedAtom, tableScope } from "@src/atoms/tableScope"; -import { TableRow } from "@src/types/table"; -import { useAtom } from "jotai"; -import { pick, zipObject } from "lodash-es"; import { useEffect, useMemo, useState } from "react"; +import { pick, zipObject } from "lodash-es"; +import { useAtom } from "jotai"; + +import { TableRow } from "@src/types/table"; +import { tableColumnsOrderedAtom, tableScope } from "@src/atoms/tableScope"; + import { getFunctionBody, listenerFieldTypes, @@ -50,11 +52,6 @@ export const useFormula = ({ const worker = new Worker(new URL("./worker.ts", import.meta.url), { type: "module", }); - // const timeout = setTimeout(() => { - // setError(new Error("timeout")); - // setLoading(false); - // worker.terminate(); - // }, 1000); worker.onmessage = ({ data: { result, error } }: any) => { worker.terminate(); if (error) { @@ -63,7 +60,6 @@ export const useFormula = ({ setResult(result); } setLoading(false); - // clearInterval(timeout); }; worker.postMessage({ @@ -73,7 +69,6 @@ export const useFormula = ({ return () => { worker.terminate(); - // clearInterval(timeout); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [useDeepCompareMemoize(listeners), formulaFn]); diff --git a/src/components/fields/Formula/worker.ts b/src/components/fields/Formula/worker.ts index 6c4fd21d0..4c37bf5ce 100644 --- a/src/components/fields/Formula/worker.ts +++ b/src/components/fields/Formula/worker.ts @@ -8,7 +8,7 @@ onmessage = async ({ data }) => { } catch (error: any) { console.error("error: ", error); postMessage({ - error: new Error("Something went wrong. Check console logs."), + error, }); } finally { // eslint-disable-next-line no-restricted-globals From a5e3303002f1d9b5a978ee2c9c033682cb6058cd Mon Sep 17 00:00:00 2001 From: Han Tuerker Date: Mon, 26 Dec 2022 23:29:51 +0300 Subject: [PATCH 031/114] feat(formula-field): fix function regexp, expand listener fields --- src/components/fields/Formula/DisplayCell.tsx | 4 +-- src/components/fields/Formula/useFormula.tsx | 8 ++--- src/components/fields/Formula/util.tsx | 35 ++++--------------- src/components/fields/Formula/worker.ts | 9 +++-- 4 files changed, 18 insertions(+), 38 deletions(-) diff --git a/src/components/fields/Formula/DisplayCell.tsx b/src/components/fields/Formula/DisplayCell.tsx index b0cb61f8b..24c7e2c66 100644 --- a/src/components/fields/Formula/DisplayCell.tsx +++ b/src/components/fields/Formula/DisplayCell.tsx @@ -2,13 +2,13 @@ import CircularProgressOptical from "@src/components/CircularProgressOptical"; import { IDisplayCellProps } from "@src/components/fields/types"; import { useFormula } from "./useFormula"; -import { getDisplayCell } from "./util"; +import { defaultFn, getDisplayCell } from "./util"; export default function Formula(props: IDisplayCellProps) { const { result, error, loading } = useFormula({ row: props.row, listenerFields: props.column.config?.listenerFields || [], - formulaFn: props.column.config?.formulaFn || "", + formulaFn: props.column.config?.formulaFn || defaultFn, }); const type = props.column.config?.renderFieldType; diff --git a/src/components/fields/Formula/useFormula.tsx b/src/components/fields/Formula/useFormula.tsx index b316eccdf..cfac7ef39 100644 --- a/src/components/fields/Formula/useFormula.tsx +++ b/src/components/fields/Formula/useFormula.tsx @@ -5,11 +5,7 @@ import { useAtom } from "jotai"; import { TableRow } from "@src/types/table"; import { tableColumnsOrderedAtom, tableScope } from "@src/atoms/tableScope"; -import { - getFunctionBody, - listenerFieldTypes, - useDeepCompareMemoize, -} from "./util"; +import { listenerFieldTypes, useDeepCompareMemoize } from "./util"; export const useFormula = ({ row, @@ -63,7 +59,7 @@ export const useFormula = ({ }; worker.postMessage({ - formulaFn: getFunctionBody(formulaFn), + formulaFn, row: availableFields, }); diff --git a/src/components/fields/Formula/util.tsx b/src/components/fields/Formula/util.tsx index 8b74b4931..1378d8d42 100644 --- a/src/components/fields/Formula/util.tsx +++ b/src/components/fields/Formula/util.tsx @@ -12,6 +12,8 @@ import CheckboxDisplayCell from "@src/components/fields/Checkbox/DisplayCell"; import PercentageDisplayCell from "@src/components/fields/Percentage/DisplayCell"; import RatingDisplayCell from "@src/components/fields/Rating/DisplayCell"; import SliderDisplayCell from "@src/components/fields/Slider/DisplayCell"; +import SingleSelectDisplayCell from "@src/components/fields/SingleSelect/DisplayCell"; +import MultiSelectDisplayCell from "@src/components/fields/MultiSelect/DisplayCell"; import ColorDisplayCell from "@src/components/fields/Color/DisplayCell"; import GeoPointDisplayCell from "@src/components/fields/GeoPoint/DisplayCell"; import DateDisplayCell from "@src/components/fields/Date/DisplayCell"; @@ -38,17 +40,7 @@ export function useDeepCompareMemoize(value: T) { export const listenerFieldTypes = Object.values(FieldType).filter( (type) => - ![ - FieldType.action, - FieldType.status, - FieldType.formula, - FieldType.aggregate, - FieldType.connectService, - FieldType.connectTable, - FieldType.connector, - FieldType.subTable, - FieldType.last, - ].includes(type) + ![FieldType.formula, FieldType.subTable, FieldType.last].includes(type) ); export const outputFieldTypes = Object.values(FieldType).filter( @@ -99,6 +91,10 @@ export const getDisplayCell = (type: FieldType) => { return RatingDisplayCell; case FieldType.slider: return SliderDisplayCell; + case FieldType.singleSelect: + return SingleSelectDisplayCell; + case FieldType.multiSelect: + return MultiSelectDisplayCell; case FieldType.color: return ColorDisplayCell; case FieldType.geoPoint: @@ -120,23 +116,6 @@ export const getDisplayCell = (type: FieldType) => { case FieldType.createdBy: return CreatedByDisplayCell; default: - // FieldType url, email, phone, singleSelect, multiSelect return ShortTextDisplayCell; } }; - -export const getFunctionBody = (fn: string) => { - const sanitizedFn = fn.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g, ""); - const matches = sanitizedFn.match(/=>\s*({?[\s\S]*}?)$/); - - if (!matches) { - return null; - } - - const body = matches[1].trim(); - const isOneLiner = body[0] !== "{" && body[body.length - 1] !== "}"; - - if (isOneLiner) return `return ${body}`; - - return body.slice(1, body.length - 1); -}; diff --git a/src/components/fields/Formula/worker.ts b/src/components/fields/Formula/worker.ts index 4c37bf5ce..16295548e 100644 --- a/src/components/fields/Formula/worker.ts +++ b/src/components/fields/Formula/worker.ts @@ -2,11 +2,16 @@ onmessage = async ({ data }) => { try { const { formulaFn, row } = data; const AsyncFunction = async function () {}.constructor as any; - const fn = new AsyncFunction("row", formulaFn); + const [_, fnBody] = formulaFn.match(/=>\s*({?[\s\S]*}?)$/); + if (!fnBody) return; + const fn = new AsyncFunction( + "row", + `const fn = async () => \n${fnBody}\n return fn();` + ); const result = await fn(row); postMessage({ result }); } catch (error: any) { - console.error("error: ", error); + console.error("Error: ", error); postMessage({ error, }); From 072686bb66f3fb1e7cd9cd70b08555b862e1e175 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Wed, 28 Dec 2022 17:01:48 +0930 Subject: [PATCH 032/114] expand rowy logging into tabs: extension, webhook, column(derivative/action/default value/connector) --- src/atoms/tableScope/ui.ts | 2 +- src/components/ColumnMenu/ColumnMenu.tsx | 45 +- .../CloudLogsModal/CloudLogsModal.tsx | 572 ++++++++---------- .../CloudLogsModal/TimeRangeSelect.tsx | 4 +- .../TableModals/CloudLogsModal/utils.ts | 69 ++- .../ExtensionsModal/ExtensionList.tsx | 3 +- .../TableModals/WebhooksModal/WebhookList.tsx | 3 +- 7 files changed, 298 insertions(+), 400 deletions(-) diff --git a/src/atoms/tableScope/ui.ts b/src/atoms/tableScope/ui.ts index 44afee102..177ae9b2e 100644 --- a/src/atoms/tableScope/ui.ts +++ b/src/atoms/tableScope/ui.ts @@ -142,7 +142,7 @@ export const selectedCellAtom = atom(null); export const contextMenuTargetAtom = atom(null); export type CloudLogFilters = { - type: "rowy" | "audit" | "build"; + type: "extension" | "webhook" | "column" | "audit" | "build"; timeRange: | { type: "seconds" | "minutes" | "hours" | "days"; value: number } | { type: "range"; start: Date; end: Date }; diff --git a/src/components/ColumnMenu/ColumnMenu.tsx b/src/components/ColumnMenu/ColumnMenu.tsx index a5f76a67d..99b168aab 100644 --- a/src/components/ColumnMenu/ColumnMenu.tsx +++ b/src/components/ColumnMenu/ColumnMenu.tsx @@ -25,7 +25,6 @@ import { import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward"; import EditIcon from "@mui/icons-material/EditOutlined"; -// import ReorderIcon from "@mui/icons-material/Reorder"; import SettingsIcon from "@mui/icons-material/SettingsOutlined"; import EvalIcon from "@mui/icons-material/PlayCircleOutline"; @@ -319,24 +318,19 @@ export default function ColumnMenu({ }, disabled: !isConfigurable, }, - // { - // label: "Re-order", - // icon: , - // onClick: () => alert("REORDER"), - // }, - - // { - // label: "Hide for everyone", - // activeLabel: "Show", - // icon: , - // activeIcon: , - // onClick: () => { - // actions.update(column.key, { hidden: !column.hidden }); - // handleClose(); - // }, - // active: column.hidden, - // color: "error" as "error", - // }, + { + key: "logs", + label: altPress ? "Logs" : "Logs…", + icon: , + onClick: () => { + setModal("cloudLogs"); + setCloudLogFilters({ + type: "column", + timeRange: { type: "days", value: 7 }, + column: [column.key], + }); + }, + }, ]; // TODO: Generalize @@ -388,19 +382,6 @@ export default function ColumnMenu({ confirm: "Evaluate", }), }, - { - key: "logs", - label: altPress ? "Logs" : "Logs…", - icon: , - onClick: () => { - setModal("cloudLogs"); - setCloudLogFilters({ - type: "rowy", - timeRange: { type: "days", value: 7 }, - column: [column.key], - }); - }, - }, ]; const columnActions: IMenuContentsProps["menuItems"] = [ diff --git a/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx b/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx index 95af37f17..93c2a8afa 100644 --- a/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx +++ b/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx @@ -13,6 +13,7 @@ import { InputAdornment, Button, Box, + CircularProgress, } from "@mui/material"; import RefreshIcon from "@mui/icons-material/Refresh"; import { CloudLogs as LogsIcon } from "@src/assets/icons"; @@ -96,7 +97,7 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { "&, & .MuiTab-root": { minHeight: { md: "var(--dialog-title-height)" }, }, - ml: { md: 18 }, + ml: { md: 20 }, mr: { md: 40 / 8 + 3 }, minHeight: 32, @@ -114,15 +115,17 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { + onChange={(_, v) => { setCloudLogFilters((c) => ({ type: v, timeRange: c.timeRange, - })) - } + })); + }} aria-label="Filter by log type" > - Rowy Logging + Extension + Webhook + Column Audit Build @@ -142,128 +145,48 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { )} - {cloudLogFilters.type === "rowy" && <>} - {cloudLogFilters.type === "audit" && ( - - setCloudLogFilters((prev) => ({ - ...prev, - auditRowId: e.target.value, - })) - } - InputProps={{ - startAdornment: ( - - {tableSettings.collection}/ - - ), - }} - className="labelHorizontal" - sx={{ - "& .MuiInputBase-root, & .MuiInputBase-input": { - typography: "body2", - fontFamily: "mono", - }, - "& .MuiInputAdornment-positionStart": { - m: "0 !important", - pointerEvents: "none", - }, - "& .MuiInputBase-input": { pl: 0 }, - }} - /> - )} -
    {cloudLogFilters.type !== "build" && ( <> - {!isValidating && Array.isArray(data) && ( - - {data.length} entries - - )} - - {cloudLogFilters.type !== "rowy" && ( - - setCloudLogFilters((prev) => ({ ...prev, severity })) - } - TextFieldProps={{ - style: { width: 130 }, - placeholder: "Severity", - SelectProps: { - renderValue: () => { - if ( - !Array.isArray(cloudLogFilters.severity) || - cloudLogFilters.severity.length === 0 - ) - return `Severity`; - - if (cloudLogFilters.severity.length === 1) - return ( - <> - Severity{" "} - - - ); + + {isValidating ? "Loading" : `${data?.length ?? 0} entries`} + + - return `Severity (${cloudLogFilters.severity.length})`; - }, - }, - }} - itemRenderer={(option) => ( - <> - - {startCase(option.value.toLowerCase())} - - )} - /> - )} - - setCloudLogFilters((c) => ({ ...c, timeRange: value })) - } - /> mutate()} title="Refresh" - icon={} + icon={ + isValidating ? ( + + ) : ( + + ) + } disabled={isValidating} /> )} - - {isValidating && ( - - )} } > @@ -278,103 +201,71 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { overflowY: "visible", }} > - - {cloudLogFilters.type === "rowy" ? ( - - - { - setCloudLogFilters((prev) => ({ - ...prev, - functionType: v, - })); - }} - TextFieldProps={{ - id: "functionType", - className: "labelHorizontal", - sx: { "& .MuiInputBase-root": { width: 200 } }, - fullWidth: false, - SelectProps: { - renderValue: () => { - if (cloudLogFilters?.functionType?.length === 1) { - return `Type (${cloudLogFilters.functionType[0]})`; - } else if (cloudLogFilters?.functionType?.length) { - return `Type (${cloudLogFilters.functionType.length})`; - } else { - return `Type`; - } + {["extension", "webhook", "column", "audit"].includes( + cloudLogFilters.type + ) ? ( + + {cloudLogFilters.type === "extension" ? ( + <> + ({ + label: x.name, + value: x.name, + type: x.type, + })) + : [] + } + value={cloudLogFilters.extension ?? []} + onChange={(v) => + setCloudLogFilters((prev) => ({ ...prev, extension: v })) + } + TextFieldProps={{ + id: "extension", + className: "labelHorizontal", + sx: { + width: "100%", + "& .MuiInputBase-root": { width: "100%" }, }, - }, - }} - itemRenderer={(option) => <>{upperCase(option.value)}} - /> - { - setCloudLogFilters((prev) => ({ - ...prev, - loggingSource: v, - })); - }} - TextFieldProps={{ - id: "loggingSource", - className: "labelHorizontal", - sx: { "& .MuiInputBase-root": { width: 200 } }, - fullWidth: false, - SelectProps: { - renderValue: () => { - if (cloudLogFilters?.loggingSource?.length === 1) { - return `Source (${cloudLogFilters.loggingSource[0]})`; - } else if (cloudLogFilters?.loggingSource?.length) { - return `Source (${cloudLogFilters.loggingSource.length})`; - } else { - return `Source`; - } + fullWidth: false, + placeholder: "Extension", + SelectProps: { + renderValue: () => { + if (cloudLogFilters?.extension?.length === 1) { + return `Extension (${cloudLogFilters.extension[0]})`; + } else if (cloudLogFilters?.extension?.length) { + return `Extension (${cloudLogFilters.extension.length})`; + } else { + return `Extension`; + } + }, }, - }, - }} - itemRenderer={(option) => <>{upperCase(option.value)}} - /> + }} + itemRenderer={(option) => ( + <> + {option.label} {option.type} + + )} + /> + + ) : null} + {cloudLogFilters.type === "webhook" ? ( { @@ -412,133 +306,145 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { )} /> - ({ - label: x.name, - value: x.name, - type: x.type, - })) - : [] - } - value={cloudLogFilters.extension ?? []} - onChange={(v) => - setCloudLogFilters((prev) => ({ ...prev, extension: v })) - } - TextFieldProps={{ - id: "extension", - className: "labelHorizontal", - sx: { "& .MuiInputBase-root": { width: 180 } }, - fullWidth: false, - placeholder: "Extension", - SelectProps: { - renderValue: () => { - if (cloudLogFilters?.extension?.length === 1) { - return `Extension (${cloudLogFilters.extension[0]})`; - } else if (cloudLogFilters?.extension?.length) { - return `Extension (${cloudLogFilters.extension.length})`; - } else { - return `Extension`; - } + ) : null} + {cloudLogFilters.type === "column" ? ( + <> + ({ + label: config.name, + value: key, + type: config.type, + }) + )} + value={cloudLogFilters.column ?? []} + onChange={(v) => + setCloudLogFilters((prev) => ({ ...prev, column: v })) + } + TextFieldProps={{ + id: "column", + className: "labelHorizontal", + sx: { + width: "100%", + "& .MuiInputBase-root": { width: "100%" }, }, - }, - }} - itemRenderer={(option) => ( - <> - {option.label} {option.type} - - )} - /> - ({ - label: config.name, - value: key, - type: config.type, - }) - )} - value={cloudLogFilters.column ?? []} - onChange={(v) => - setCloudLogFilters((prev) => ({ ...prev, column: v })) - } - TextFieldProps={{ - id: "column", - className: "labelHorizontal", - sx: { "& .MuiInputBase-root": { width: 200 } }, - fullWidth: false, - placeholder: "Column", - SelectProps: { - renderValue: () => { - if (cloudLogFilters?.column?.length === 1) { - return `Column (${cloudLogFilters.column[0]})`; - } else if (cloudLogFilters?.column?.length) { - return `Column (${cloudLogFilters.column.length})`; - } else { - return `Column`; - } + fullWidth: false, + placeholder: "Column", + SelectProps: { + renderValue: () => { + if (cloudLogFilters?.column?.length === 1) { + return `Column (${cloudLogFilters.column[0]})`; + } else if (cloudLogFilters?.column?.length) { + return `Column (${cloudLogFilters.column.length})`; + } else { + return `Column`; + } + }, }, - }, - }} - itemRenderer={(option) => ( - <> - {option.label} {option.value}  - {option.type} - - )} - /> - - setCloudLogFilters((prev) => ({ ...prev, severity })) - } - TextFieldProps={{ - style: { width: 130 }, - placeholder: "Severity", - SelectProps: { - renderValue: () => { - if ( - !Array.isArray(cloudLogFilters.severity) || - cloudLogFilters.severity.length === 0 - ) - return `Severity`; + }} + itemRenderer={(option) => ( + <> + {option.label} {option.value}  + {option.type} + + )} + /> + + ) : null} + {cloudLogFilters.type === "audit" ? ( + <> + + setCloudLogFilters((prev) => ({ + ...prev, + auditRowId: e.target.value, + })) + } + InputProps={{ + startAdornment: ( + + {tableSettings.collection}/ + + ), + }} + className="labelHorizontal" + sx={{ + width: "100%", + "& .MuiInputBase-root, & .MuiInputBase-input": { + width: "100%", + typography: "body2", + fontFamily: "mono", + }, + "& .MuiInputAdornment-positionStart": { + m: "0 !important", + pointerEvents: "none", + }, + "& .MuiInputBase-input": { pl: 0 }, + "& .MuiFormLabel-root": { + whiteSpace: "nowrap", + }, + }} + /> + + ) : null} + + setCloudLogFilters((prev) => ({ ...prev, severity })) + } + TextFieldProps={{ + style: { width: 200 }, + placeholder: "Severity", + SelectProps: { + renderValue: () => { + if ( + !Array.isArray(cloudLogFilters.severity) || + cloudLogFilters.severity.length === 0 + ) + return `Severity`; - if (cloudLogFilters.severity.length === 1) - return ( - <> - Severity{" "} - - - ); + if (cloudLogFilters.severity.length === 1) + return ( + <> + Severity{" "} + + + ); - return `Severity (${cloudLogFilters.severity.length})`; - }, + return `Severity (${cloudLogFilters.severity.length})`; }, - }} - itemRenderer={(option) => ( - <> - - {startCase(option.value.toLowerCase())} - - )} - /> - - ) : null} - + }, + }} + itemRenderer={(option) => ( + <> + + {startCase(option.value.toLowerCase())} + + )} + /> + + setCloudLogFilters((c) => ({ ...c, timeRange: value })) + } + /> + + ) : null} +
    {value && value.type !== "range" && ( { + return `jsonPayload.loggingSource = "${loggingSource}"`; + }) + .join(encodeURIComponent(" OR ")) + ); + } + switch (cloudLogFilters.type) { - case "rowy": + case "extension": logQuery.push(`logName = "projects/${projectId}/logs/rowy-logging"`); - if (cloudLogFilters?.functionType?.length) + if (cloudLogFilters?.extension?.length) { logQuery.push( - cloudLogFilters.functionType - .map((functionType) => { - return `jsonPayload.functionType = "${functionType}"`; - }) - .join(encodeURIComponent(" OR ")) - ); - if (cloudLogFilters?.loggingSource?.length) { - logQuery.push( - cloudLogFilters.loggingSource - .map((loggingSource) => { - return `jsonPayload.loggingSource = "${loggingSource}"`; + cloudLogFilters.extension + .map((extensionName) => { + return `jsonPayload.extensionName = "${extensionName}"`; }) .join(encodeURIComponent(" OR ")) ); } else { - // mandatory filter to remove unwanted gcp diagnostic logs - logQuery.push( - ["backend-scripts", "backend-function", "hooks"] - .map((loggingSource) => { - return `jsonPayload.loggingSource = "${loggingSource}"`; - }) - .join(encodeURIComponent(" OR ")) - ); + logQuery.push(`jsonPayload.functionType = "extension"`); } + break; + + case "webhook": if (cloudLogFilters?.webhook?.length) { logQuery.push( cloudLogFilters.webhook .map((id) => `jsonPayload.url : "${id}"`) .join(encodeURIComponent(" OR ")) ); + } else { + logQuery.push(`jsonPayload.functionType = "hooks"`); } - if (cloudLogFilters?.extension?.length) + break; + + case "column": + if (cloudLogFilters?.column?.length) { logQuery.push( - cloudLogFilters.extension - .map((extensionName) => { - return `jsonPayload.extensionName = "${extensionName}"`; + cloudLogFilters.column + .map((column) => { + return `jsonPayload.fieldName = "${column}"`; }) .join(encodeURIComponent(" OR ")) ); - if (cloudLogFilters?.column?.length) + } else { logQuery.push( - cloudLogFilters.column - .map((column) => { - return `jsonPayload.fieldName = "${column}"`; + [ + "connector", + "derivative-script", + "action", + "derivative-function", + "defaultValue", + ] + .map((functionType) => { + return `jsonPayload.functionType = "${functionType}"`; }) .join(encodeURIComponent(" OR ")) ); + } break; case "audit": diff --git a/src/components/TableModals/ExtensionsModal/ExtensionList.tsx b/src/components/TableModals/ExtensionsModal/ExtensionList.tsx index 7ed8e6c6e..a0d5b82f4 100644 --- a/src/components/TableModals/ExtensionsModal/ExtensionList.tsx +++ b/src/components/TableModals/ExtensionsModal/ExtensionList.tsx @@ -107,8 +107,7 @@ export default function ExtensionList({ onClick={() => { setModal("cloudLogs"); setCloudLogFilters({ - type: "rowy", - functionType: ["extension"], + type: "extension", timeRange: { type: "days", value: 7 }, extension: [extensionObject.name], }); diff --git a/src/components/TableModals/WebhooksModal/WebhookList.tsx b/src/components/TableModals/WebhooksModal/WebhookList.tsx index 01b67d072..d2f6b6709 100644 --- a/src/components/TableModals/WebhooksModal/WebhookList.tsx +++ b/src/components/TableModals/WebhooksModal/WebhookList.tsx @@ -126,8 +126,7 @@ export default function WebhookList({ onClick={() => { setModal("cloudLogs"); setCloudLogFilters({ - type: "rowy", - functionType: ["hooks"], + type: "webhook", timeRange: { type: "days", value: 7 }, webhook: [webhook.endpoint], }); From 28f6ea519f22589965def134db46794b13ae28bf Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Thu, 29 Dec 2022 20:57:18 +0930 Subject: [PATCH 033/114] column logs: only allow logging view for connector/action/default value/derivative --- src/components/ColumnMenu/ColumnMenu.tsx | 14 +++++++++++--- .../CloudLogsModal/CloudLogsModal.tsx | 17 +++++++++++++---- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/components/ColumnMenu/ColumnMenu.tsx b/src/components/ColumnMenu/ColumnMenu.tsx index 99b168aab..7dff465e0 100644 --- a/src/components/ColumnMenu/ColumnMenu.tsx +++ b/src/components/ColumnMenu/ColumnMenu.tsx @@ -318,7 +318,15 @@ export default function ColumnMenu({ }, disabled: !isConfigurable, }, - { + ]; + + if ( + column?.config?.defaultValue?.type === "dynamic" || + [FieldType.action, FieldType.derivative, FieldType.connector].includes( + column.type + ) + ) { + configActions.push({ key: "logs", label: altPress ? "Logs" : "Logs…", icon: , @@ -330,8 +338,8 @@ export default function ColumnMenu({ column: [column.key], }); }, - }, - ]; + }); + } // TODO: Generalize const handleEvaluateAll = async () => { diff --git a/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx b/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx index 93c2a8afa..2b835e3aa 100644 --- a/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx +++ b/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx @@ -43,6 +43,7 @@ import { cloudLogFiltersAtom, } from "@src/atoms/tableScope"; import { cloudLogFetcher } from "./utils"; +import { FieldType } from "@src/constants/fields"; export default function CloudLogsModal({ onClose }: ITableModalProps) { const [projectId] = useAtom(projectIdAtom, projectScope); @@ -312,13 +313,21 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { ({ + options={Object.entries(tableSchema.columns ?? {}) + .filter( + ([key, config]) => + config?.config?.defaultValue?.type === "dynamic" || + [ + FieldType.action, + FieldType.derivative, + FieldType.connector, + ].includes(config.type) + ) + .map(([key, config]) => ({ label: config.name, value: key, type: config.type, - }) - )} + }))} value={cloudLogFilters.column ?? []} onChange={(v) => setCloudLogFilters((prev) => ({ ...prev, column: v })) From 503dae79d8dba7537b0444280136c8e1a5428598 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Thu, 29 Dec 2022 21:41:06 +0930 Subject: [PATCH 034/114] auto refresh when switching log tabs --- .../TableModals/CloudLogsModal/CloudLogsModal.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx b/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx index 2b835e3aa..2e20a4753 100644 --- a/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx +++ b/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx @@ -116,11 +116,20 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { { + onChange={(_, newType) => { setCloudLogFilters((c) => ({ - type: v, + type: newType, timeRange: c.timeRange, })); + if ( + ["extension", "webhook", "column", "audit"].includes( + newType + ) + ) { + setTimeout(() => { + mutate(); + }, 0); + } }} aria-label="Filter by log type" > @@ -169,6 +178,7 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { severity: undefined, })); }} + disabled={isValidating} > Reset From 295974145acd0dafe4c7faf86ec4172c554ff25e Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Thu, 29 Dec 2022 21:58:36 +0930 Subject: [PATCH 035/114] add back old functions logging --- src/atoms/tableScope/ui.ts | 2 +- .../CloudLogsModal/CloudLogsModal.tsx | 32 ++++++++++++------- .../TableModals/CloudLogsModal/utils.ts | 4 +++ 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/atoms/tableScope/ui.ts b/src/atoms/tableScope/ui.ts index 177ae9b2e..c1de17801 100644 --- a/src/atoms/tableScope/ui.ts +++ b/src/atoms/tableScope/ui.ts @@ -142,7 +142,7 @@ export const selectedCellAtom = atom(null); export const contextMenuTargetAtom = atom(null); export type CloudLogFilters = { - type: "extension" | "webhook" | "column" | "audit" | "build"; + type: "extension" | "webhook" | "column" | "audit" | "build" | "functions"; timeRange: | { type: "seconds" | "minutes" | "hours" | "days"; value: number } | { type: "range"; start: Date; end: Date }; diff --git a/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx b/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx index 2e20a4753..b211dec2e 100644 --- a/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx +++ b/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx @@ -17,6 +17,7 @@ import { } from "@mui/material"; import RefreshIcon from "@mui/icons-material/Refresh"; import { CloudLogs as LogsIcon } from "@src/assets/icons"; +import ClearIcon from "@mui/icons-material/Clear"; import Modal from "@src/components/Modal"; import TableToolbarButton from "@src/components/TableToolbar/TableToolbarButton"; @@ -122,9 +123,13 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { timeRange: c.timeRange, })); if ( - ["extension", "webhook", "column", "audit"].includes( - newType - ) + [ + "extension", + "webhook", + "column", + "audit", + "functions", + ].includes(newType) ) { setTimeout(() => { mutate(); @@ -138,6 +143,9 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { Column Audit Build + + Functions (legacy) + ) : ( - {isValidating ? "Loading" : `${data?.length ?? 0} entries`} + {isValidating ? "" : `${data?.length ?? 0} entries`} - - + /> mutate()} title="Refresh" @@ -212,7 +219,7 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { overflowY: "visible", }} > - {["extension", "webhook", "column", "audit"].includes( + {["extension", "webhook", "column", "audit", "functions"].includes( cloudLogFilters.type ) ? ( + {cloudLogFilters.type === "functions" ? ( + + ) : null} {cloudLogFilters.type === "extension" ? ( <> Date: Thu, 29 Dec 2022 22:03:12 +0930 Subject: [PATCH 036/114] fix load more not working --- .../TableModals/CloudLogsModal/CloudLogsModal.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx b/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx index b211dec2e..76cfe7197 100644 --- a/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx +++ b/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx @@ -489,15 +489,18 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { marginRight: "auto", display: "flex", }} - onClick={() => + onClick={() => { setCloudLogFilters((c) => ({ ...c, timeRange: { ...c.timeRange, value: (c.timeRange as any).value * 2, }, - })) - } + })); + setTimeout(() => { + mutate(); + }, 0); + }} > Load more (last {cloudLogFilters.timeRange.value * 2}{" "} {cloudLogFilters.timeRange.type}) From 9b7a31f029e2a2cc33d046529a010f4e24aca464 Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Fri, 30 Dec 2022 04:35:44 +0000 Subject: [PATCH 037/114] worked on adding "Import JSON feat" --- src/atoms/tableScope/ui.ts | 2 +- .../TableToolbar/ImportData/ImportData.tsx | 2 +- .../TableToolbar/ImportData/ImportFromCsv.tsx | 108 +++++++++++++++--- 3 files changed, 97 insertions(+), 15 deletions(-) diff --git a/src/atoms/tableScope/ui.ts b/src/atoms/tableScope/ui.ts index 3d11a081e..f14c8f126 100644 --- a/src/atoms/tableScope/ui.ts +++ b/src/atoms/tableScope/ui.ts @@ -109,7 +109,7 @@ export type ImportAirtableData = { records: Record[] }; /** Store import CSV popover and wizard state */ export const importCsvAtom = atom<{ - importType: "csv" | "tsv"; + importType: "csv" | "tsv" | "json"; csvData: ImportCsvData | null; }>({ importType: "csv", csvData: null }); diff --git a/src/components/TableToolbar/ImportData/ImportData.tsx b/src/components/TableToolbar/ImportData/ImportData.tsx index 01b2aa4e3..4bdfc216e 100644 --- a/src/components/TableToolbar/ImportData/ImportData.tsx +++ b/src/components/TableToolbar/ImportData/ImportData.tsx @@ -91,7 +91,7 @@ export default function ImportData({ render, PopoverProps }: IImportDataProps) { variant="fullWidth" > (importMethodRef.current = ImportMethod.csv)} /> diff --git a/src/components/TableToolbar/ImportData/ImportFromCsv.tsx b/src/components/TableToolbar/ImportData/ImportFromCsv.tsx index 40ec3f844..d16f50bb9 100644 --- a/src/components/TableToolbar/ImportData/ImportFromCsv.tsx +++ b/src/components/TableToolbar/ImportData/ImportFromCsv.tsx @@ -28,13 +28,73 @@ import { } from "@src/atoms/tableScope"; import { analytics, logEvent } from "@src/analytics"; +import { parse as parseJSON } from "json2csv"; +import { remove as _remove } from "lodash-es"; + export enum ImportMethod { paste = "paste", upload = "upload", url = "url", } -export default function ImportFromCsv() { +enum FileType { + CSV = "text/csv", + TSV = "text/tab-separated-values", + JSON = "application/json", +} +// extract the column names and return the names +function extractFields(data: JSON): string[] { + return _remove(Object.keys(data), (col: string) => col !== "id"); +} + +function convertJSONToCSV(rawData: string): string | false { + let rawDataJSONified: JSON[]; + try { + rawDataJSONified = JSON.parse(rawData); + } catch (e) { + return false; + } + if (rawDataJSONified.length < 1) { + return false; + } + const fields = extractFields(rawDataJSONified[0]); + const opts = { fields }; + + try { + const csv = parseJSON(rawDataJSONified, opts); + return csv; + } catch (err) { + return false; + } +} + +function hasProperJsonStructure(raw: string) { + try { + raw = JSON.parse(raw); + const type = Object.prototype.toString.call(raw); + // we don't want '[object Object]' + return type === "[object Array]"; + } catch (err) { + return false; + } +} + +function checkIsJson(raw: string): boolean { + raw = typeof raw !== "string" ? JSON.stringify(raw) : raw; + + try { + raw = JSON.parse(raw); + } catch (e) { + return false; + } + + if (typeof raw === "object" && raw !== null) { + return true; + } + return false; +} + +export default function ImportFromFile() { const [{ importType: importTypeCsv, csvData }, setImportCsv] = useAtom( importCsvAtom, tableScope @@ -55,6 +115,20 @@ export default function ImportFromCsv() { }; }, [setImportCsv]); + const parseFile = (rawData: string) => { + if (importTypeRef.current === "json") { + if (!hasProperJsonStructure(rawData)) { + return setError("Invalid Structure! It must be an Array"); + } + const converted = convertJSONToCSV(rawData); + if (!converted) { + return setError("No columns detected"); + } + rawData = converted; + } + parseCsv(rawData); + }; + const parseCsv = useCallback( (csvString: string) => parse(csvString, { delimiter: [",", "\t"] }, (err, rows) => { @@ -86,13 +160,17 @@ export default function ImportFromCsv() { async (acceptedFiles: File[]) => { try { const file = acceptedFiles[0]; + importTypeRef.current = + file.type === FileType.TSV + ? "tsv" + : file.type === FileType.JSON + ? "json" + : "csv"; const reader = new FileReader(); - reader.onload = (event: any) => parseCsv(event.target.result); + reader.onload = (event: any) => parseFile(event.target.result); reader.readAsText(file); - importTypeRef.current = - file.type === "text/tab-separated-values" ? "tsv" : "csv"; } catch (error) { - enqueueSnackbar(`Please import a .tsv or .csv file`, { + enqueueSnackbar(`Please import a .tsv or .csv or .json file`, { variant: "error", anchorOrigin: { vertical: "top", @@ -107,10 +185,14 @@ export default function ImportFromCsv() { const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop, multiple: false, - accept: ["text/csv", "text/tab-separated-values"], + accept: [FileType.CSV, FileType.TSV, FileType.JSON], }); function setDataTypeRef(data: string) { + if (checkIsJson(data)) { + return (importTypeRef.current = "json"); + } + const getFirstLine = data?.match(/^(.*)/)?.[0]; /* * Catching edge case with regex @@ -128,8 +210,8 @@ export default function ImportFromCsv() { : (importTypeRef.current = "csv"); } const handlePaste = useDebouncedCallback((value: string) => { - parseCsv(value); setDataTypeRef(value); + parseFile(value); }, 1000); const handleUrl = useDebouncedCallback((value: string) => { @@ -138,8 +220,8 @@ export default function ImportFromCsv() { fetch(value, { mode: "no-cors" }) .then((res) => res.text()) .then((data) => { - parseCsv(data); setDataTypeRef(data); + parseFile(data); setLoading(false); }) .catch((e) => { @@ -217,7 +299,7 @@ export default function ImportFromCsv() { {isDragActive ? ( - Drop CSV or TSV file here… + Drop CSV or TSV or JSON file here… ) : ( <> @@ -227,8 +309,8 @@ export default function ImportFromCsv() { {validCsv - ? "Valid CSV or TSV" - : "Click to upload or drop CSV or TSV file here"} + ? "Valid CSV or TSV or JSON" + : "Click to upload or drop CSV or TSV or JSON file here"} @@ -249,7 +331,7 @@ export default function ImportFromCsv() { inputProps={{ minRows: 3 }} autoFocus fullWidth - label="Paste CSV or TSV text" + label="Paste CSV or TSV or JSON text" placeholder="column, column, …" onChange={(e) => { if (csvData !== null) @@ -279,7 +361,7 @@ export default function ImportFromCsv() { variant="filled" autoFocus fullWidth - label="Paste URL to CSV or TSV file" + label="Paste URL to CSV or TSV or JSON file" placeholder="https://" onChange={(e) => { if (csvData !== null) From 305d47d1a139617ea319d010bb2d718042eb7114 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Fri, 30 Dec 2022 15:29:30 +0930 Subject: [PATCH 038/114] reuse Rowy rowy loggin types --- src/components/CodeEditor/extensions.d.ts | 6 +----- src/components/CodeEditor/rowy.d.ts | 5 +++++ .../ColumnModals/ColumnConfigModal/defaultValue.d.ts | 6 +----- .../TableModals/WebhooksModal/Schemas/basic.tsx | 12 ++---------- src/components/TableModals/WebhooksModal/utils.tsx | 12 ++---------- .../TableModals/WebhooksModal/webhooks.d.ts | 12 ++---------- src/components/fields/Action/action.d.ts | 6 +----- src/components/fields/Connector/connector.d.ts | 6 +----- src/components/fields/Derivative/derivative.d.ts | 6 +----- src/components/fields/types.ts | 6 ------ 10 files changed, 16 insertions(+), 61 deletions(-) diff --git a/src/components/CodeEditor/extensions.d.ts b/src/components/CodeEditor/extensions.d.ts index 3da39ae9a..22af57566 100644 --- a/src/components/CodeEditor/extensions.d.ts +++ b/src/components/CodeEditor/extensions.d.ts @@ -26,11 +26,7 @@ type ExtensionContext = { extensionBody: any; }; RULES_UTILS: any; - logging: { - log: (payload: any) => void; - warn: (payload: any) => void; - error: (payload: any) => void; - }; + logging: RowyLogging; }; // extension body definition diff --git a/src/components/CodeEditor/rowy.d.ts b/src/components/CodeEditor/rowy.d.ts index f6852cce2..42582dd0b 100644 --- a/src/components/CodeEditor/rowy.d.ts +++ b/src/components/CodeEditor/rowy.d.ts @@ -17,6 +17,11 @@ type uploadOptions = { folderPath?: string; fileName?: string; }; +type RowyLogging = { + log: (payload: any) => void; + warn: (payload: any) => void; + error: (payload: any) => void; +}; interface Rowy { metadata: { /** diff --git a/src/components/ColumnModals/ColumnConfigModal/defaultValue.d.ts b/src/components/ColumnModals/ColumnConfigModal/defaultValue.d.ts index e3b90e138..779bd9be2 100644 --- a/src/components/ColumnModals/ColumnConfigModal/defaultValue.d.ts +++ b/src/components/ColumnModals/ColumnConfigModal/defaultValue.d.ts @@ -4,10 +4,6 @@ type DefaultValueContext = { storage: firebasestorage.Storage; db: FirebaseFirestore.Firestore; auth: firebaseauth.BaseAuth; - logging: { - log: (payload: any) => void; - warn: (payload: any) => void; - error: (payload: any) => void; - }; + logging: RowyLogging; }; type DefaultValue = (context: DefaultValueContext) => "PLACEHOLDER_OUTPUT_TYPE"; diff --git a/src/components/TableModals/WebhooksModal/Schemas/basic.tsx b/src/components/TableModals/WebhooksModal/Schemas/basic.tsx index 54075d434..9d8e7b8c7 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/basic.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/basic.tsx @@ -30,11 +30,7 @@ export const parserExtraLibs = [ send: (v:any)=>void; sendStatus: (status:number)=>void }; - logging: { - log: (payload: any) => void; - warn: (payload: any) => void; - error: (payload: any) => void; - }; + logging: RowyLogging; } ) => Promise;`, ]; @@ -49,11 +45,7 @@ export const conditionExtraLibs = [ send: (v:any)=>void; sendStatus: (status:number)=>void; }; - logging: { - log: (payload: any) => void; - warn: (payload: any) => void; - error: (payload: any) => void; - }; + logging: RowyLogging; } ) => Promise;`, ]; diff --git a/src/components/TableModals/WebhooksModal/utils.tsx b/src/components/TableModals/WebhooksModal/utils.tsx index b1ba86014..c7a992efd 100644 --- a/src/components/TableModals/WebhooksModal/utils.tsx +++ b/src/components/TableModals/WebhooksModal/utils.tsx @@ -35,11 +35,7 @@ export const parserExtraLibs = [ send: (v:any)=>void; sendStatus: (status:number)=>void }; - logging: { - log: (payload: any) => void; - warn: (payload: any) => void; - error: (payload: any) => void; - }; + logging: RowyLogging; } ) => Promise;`, ]; @@ -54,11 +50,7 @@ export const conditionExtraLibs = [ send: (v:any)=>void sendStatus: (status:number)=>void }; - logging: { - log: (payload: any) => void; - warn: (payload: any) => void; - error: (payload: any) => void; - }; + logging: RowyLogging; } ) => Promise;`, ]; diff --git a/src/components/TableModals/WebhooksModal/webhooks.d.ts b/src/components/TableModals/WebhooksModal/webhooks.d.ts index fa98341fb..65df05302 100644 --- a/src/components/TableModals/WebhooksModal/webhooks.d.ts +++ b/src/components/TableModals/WebhooksModal/webhooks.d.ts @@ -3,20 +3,12 @@ type Condition = (args: { db: FirebaseFirestore.Firestore; ref: FirebaseFirestore.CollectionReference; res: Response; - logging: { - log: (payload: any) => void; - warn: (payload: any) => void; - error: (payload: any) => void; - }; + logging: RowyLogging; }) => Promise; type Parser = (args: { req: WebHookRequest; db: FirebaseFirestore.Firestore; ref: FirebaseFirestore.CollectionReference; - logging: { - log: (payload: any) => void; - warn: (payload: any) => void; - error: (payload: any) => void; - }; + logging: RowyLogging; }) => Promise; diff --git a/src/components/fields/Action/action.d.ts b/src/components/fields/Action/action.d.ts index 33fc74893..1daab613b 100644 --- a/src/components/fields/Action/action.d.ts +++ b/src/components/fields/Action/action.d.ts @@ -15,11 +15,7 @@ type ActionContext = { auth: firebaseauth.BaseAuth; actionParams: actionParams; user: ActionUser; - logging: { - log: (payload: any) => void; - warn: (payload: any) => void; - error: (payload: any) => void; - }; + logging: RowyLogging; }; type ActionResult = { diff --git a/src/components/fields/Connector/connector.d.ts b/src/components/fields/Connector/connector.d.ts index d12041d4e..c16585ae5 100644 --- a/src/components/fields/Connector/connector.d.ts +++ b/src/components/fields/Connector/connector.d.ts @@ -15,11 +15,7 @@ type ConnectorContext = { auth: firebaseauth.BaseAuth; query: string; user: ConnectorUser; - logging: { - log: (payload: any) => void; - warn: (payload: any) => void; - error: (payload: any) => void; - }; + logging: RowyLogging; }; type ConnectorResult = any[]; type Connector = ( diff --git a/src/components/fields/Derivative/derivative.d.ts b/src/components/fields/Derivative/derivative.d.ts index 9d01f97c0..a56afeba1 100644 --- a/src/components/fields/Derivative/derivative.d.ts +++ b/src/components/fields/Derivative/derivative.d.ts @@ -5,11 +5,7 @@ type DerivativeContext = { db: FirebaseFirestore.Firestore; auth: firebaseauth.BaseAuth; change: any; - logging: { - log: (payload: any) => void; - warn: (payload: any) => void; - error: (payload: any) => void; - }; + logging: RowyLogging; }; type Derivative = (context: DerivativeContext) => "PLACEHOLDER_OUTPUT_TYPE"; diff --git a/src/components/fields/types.ts b/src/components/fields/types.ts index 299336f27..63374d811 100644 --- a/src/components/fields/types.ts +++ b/src/components/fields/types.ts @@ -113,9 +113,3 @@ export interface IFilterCustomInputProps { operator: TableFilter["operator"]; [key: string]: any; } - -export interface RowyLogging { - log: (payload: any) => void; - warn: (payload: any) => void; - error: (payload: any) => void; -} From 5fad8022f0dd0e2e7456ddc75de84a8e1ad6cfba Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Fri, 30 Dec 2022 13:39:04 +0000 Subject: [PATCH 039/114] fix: column names are only determined by the first row --- .../TableToolbar/ImportData/ImportFromCsv.tsx | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/components/TableToolbar/ImportData/ImportFromCsv.tsx b/src/components/TableToolbar/ImportData/ImportFromCsv.tsx index d16f50bb9..3b8e90cea 100644 --- a/src/components/TableToolbar/ImportData/ImportFromCsv.tsx +++ b/src/components/TableToolbar/ImportData/ImportFromCsv.tsx @@ -42,9 +42,14 @@ enum FileType { TSV = "text/tab-separated-values", JSON = "application/json", } + // extract the column names and return the names -function extractFields(data: JSON): string[] { - return _remove(Object.keys(data), (col: string) => col !== "id"); +function extractFields(data: JSON[]): string[] { + let columns = new Set(); + for (let jsonRow of data) { + columns = new Set([...columns, ...Object.keys(jsonRow)]); + } + return _remove([...columns], (col: string) => col !== "id"); } function convertJSONToCSV(rawData: string): string | false { @@ -57,7 +62,7 @@ function convertJSONToCSV(rawData: string): string | false { if (rawDataJSONified.length < 1) { return false; } - const fields = extractFields(rawDataJSONified[0]); + const fields = extractFields(rawDataJSONified); const opts = { fields }; try { @@ -115,7 +120,7 @@ export default function ImportFromFile() { }; }, [setImportCsv]); - const parseFile = (rawData: string) => { + const parseFile = useCallback((rawData: string) => { if (importTypeRef.current === "json") { if (!hasProperJsonStructure(rawData)) { return setError("Invalid Structure! It must be an Array"); @@ -127,7 +132,7 @@ export default function ImportFromFile() { rawData = converted; } parseCsv(rawData); - }; + }, []); const parseCsv = useCallback( (csvString: string) => @@ -145,6 +150,7 @@ export default function ImportFromFile() { {} ) ); + console.log(mappedRows); setImportCsv({ importType: importTypeRef.current, csvData: { columns, rows: mappedRows }, From 58d02c2377084ba75ee649ecd99508ca53e41c96 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Sat, 31 Dec 2022 00:05:39 +0930 Subject: [PATCH 040/114] add table path filter to logging --- src/components/TableModals/CloudLogsModal/utils.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/TableModals/CloudLogsModal/utils.ts b/src/components/TableModals/CloudLogsModal/utils.ts index 79904ccfe..6d210e450 100644 --- a/src/components/TableModals/CloudLogsModal/utils.ts +++ b/src/components/TableModals/CloudLogsModal/utils.ts @@ -26,6 +26,7 @@ export const cloudLogFetcher = ( switch (cloudLogFilters.type) { case "extension": logQuery.push(`logName = "projects/${projectId}/logs/rowy-logging"`); + logQuery.push(`jsonPayload.tablePath : "${tablePath}"`); if (cloudLogFilters?.extension?.length) { logQuery.push( cloudLogFilters.extension @@ -40,6 +41,7 @@ export const cloudLogFetcher = ( break; case "webhook": + logQuery.push(`jsonPayload.tablePath : "${tablePath}"`); if (cloudLogFilters?.webhook?.length) { logQuery.push( cloudLogFilters.webhook @@ -52,6 +54,7 @@ export const cloudLogFetcher = ( break; case "column": + logQuery.push(`jsonPayload.tablePath : "${tablePath}"`); if (cloudLogFilters?.column?.length) { logQuery.push( cloudLogFilters.column From 8c364596d76fdce54e9f9e88f6acccf3c44c200c Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Sat, 31 Dec 2022 05:10:53 +0000 Subject: [PATCH 041/114] ignore id column --- src/components/TableToolbar/ImportData/ImportFromCsv.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/TableToolbar/ImportData/ImportFromCsv.tsx b/src/components/TableToolbar/ImportData/ImportFromCsv.tsx index 3b8e90cea..9739efab7 100644 --- a/src/components/TableToolbar/ImportData/ImportFromCsv.tsx +++ b/src/components/TableToolbar/ImportData/ImportFromCsv.tsx @@ -1,6 +1,7 @@ import { useState, useCallback, useRef, useEffect } from "react"; import { useAtom, useSetAtom } from "jotai"; import { parse } from "csv-parse/browser/esm"; +import { parse as parseJSON } from "json2csv"; import { useDropzone } from "react-dropzone"; import { useDebouncedCallback } from "use-debounce"; import { useSnackbar } from "notistack"; @@ -28,9 +29,6 @@ import { } from "@src/atoms/tableScope"; import { analytics, logEvent } from "@src/analytics"; -import { parse as parseJSON } from "json2csv"; -import { remove as _remove } from "lodash-es"; - export enum ImportMethod { paste = "paste", upload = "upload", @@ -49,7 +47,8 @@ function extractFields(data: JSON[]): string[] { for (let jsonRow of data) { columns = new Set([...columns, ...Object.keys(jsonRow)]); } - return _remove([...columns], (col: string) => col !== "id"); + columns.delete("id"); + return [...columns]; } function convertJSONToCSV(rawData: string): string | false { From 5b49964aac1a27f88977e4d56e9ee18789ad4e64 Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Sat, 31 Dec 2022 05:55:10 +0000 Subject: [PATCH 042/114] optimistic updates with useFirestoreDocWithAtom --- src/components/Table/ColumnSelect.tsx | 7 ----- src/components/TableToolbar/HiddenFields.tsx | 30 -------------------- src/hooks/useFirestoreDocWithAtom.ts | 15 +++++++++- 3 files changed, 14 insertions(+), 38 deletions(-) diff --git a/src/components/Table/ColumnSelect.tsx b/src/components/Table/ColumnSelect.tsx index 599b32f11..3ea281557 100644 --- a/src/components/Table/ColumnSelect.tsx +++ b/src/components/Table/ColumnSelect.tsx @@ -22,22 +22,15 @@ export interface IColumnSelectProps { filterColumns?: (column: ColumnConfig) => boolean; showFieldNames?: boolean; options?: ColumnOption[]; - tableColumnsOrdered?: ColumnConfig[]; } export default function ColumnSelect({ filterColumns, showFieldNames, - tableColumnsOrdered: tableColumnsOrdered_, ...props }: IColumnSelectProps & Omit, "options">) { - // if tableColumnsOrdered is passed(in case of HiddenFields) else traditional way to get tableColumnsOrders (backward compitable) let [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); - if (tableColumnsOrdered_) { - tableColumnsOrdered = tableColumnsOrdered_; - } - const options = props.options || (filterColumns diff --git a/src/components/TableToolbar/HiddenFields.tsx b/src/components/TableToolbar/HiddenFields.tsx index 32f9118ab..1e4d26017 100644 --- a/src/components/TableToolbar/HiddenFields.tsx +++ b/src/components/TableToolbar/HiddenFields.tsx @@ -2,7 +2,6 @@ import { useEffect, useRef, useMemo, - useCallback, useState, forwardRef, ChangeEvent, @@ -35,7 +34,6 @@ import { tableScope, tableIdAtom, updateColumnAtom, - tableColumnsOrderedAtom, } from "@src/atoms/tableScope"; import { formatSubTableName } from "@src/utils/table"; @@ -61,10 +59,6 @@ export default function HiddenFields() { setHiddenFields(userDocHiddenFields); }, [userDocHiddenFields]); - const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); - // cashed tableColumnsOrdered from quick updates(reorder). - const [quickAccessTableColumnsOrdered, setQuickAccessTableColumnsOrdered] = - useState(tableColumnsOrdered); // const tableColumns = tableColumnsOrdered.map(({ key, name }) => ({ // value: key, // label: name, @@ -163,32 +157,10 @@ export default function HiddenFields() { const updateColumn = useSetAtom(updateColumnAtom, tableScope); - // function to revalidate quickAccessTableColumnsOrdered - function revalidateCache() { - setQuickAccessTableColumnsOrdered(tableColumnsOrdered); - } - - // function to reorder columns from cashed tableColumnsOrdered - const optimisticReorder = useCallback( - (from: number, to: number) => { - let temp = Array.from(quickAccessTableColumnsOrdered); - const currentColumn = temp.splice(from, 1)[0]; - temp.splice(to, 0, currentColumn); - - // update indexes - for (let i = from < to ? from : to; i < temp.length; i++) { - temp[i].index = i; - } - setQuickAccessTableColumnsOrdered(temp); - }, - [quickAccessTableColumnsOrdered] - ); - // updates column on drag end function handleOnDragEnd(result: DropResult) { if (!result.destination || result.destination.index === result.source.index) return; - optimisticReorder(result.source.index, result.destination.index); updateColumn({ key: result.draggableId, config: {}, @@ -235,7 +207,6 @@ export default function HiddenFields() { } onClick={() => { - revalidateCache(); setOpen((o) => !o); }} active={hiddenFields.length > 0} @@ -244,7 +215,6 @@ export default function HiddenFields() { {hiddenFields.length > 0 ? `${hiddenFields.length} hidden` : "Hide"} ( dataScope ); const handleError = useErrorHandler(); + const { enqueueSnackbar } = useSnackbar(); // Create the doc ref and memoize using Firestore’s refEqual const memoizedDocRef = useMemoValue( @@ -145,7 +148,17 @@ export function useFirestoreDocWithAtom( } } - return setDoc(memoizedDocRef, updateToDb, { merge: true }); + setDataAtom((prev) => { + return { + ...prev, + ...updateToDb, + }; + }); + return setDoc(memoizedDocRef, updateToDb, { merge: true }).catch( + (e) => { + enqueueSnackbar((e as Error).message, { variant: "error" }); + } + ); }); } From d7a4cdfc74200fd988e92dac7599e5c0c624ce12 Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Sat, 31 Dec 2022 06:15:55 +0000 Subject: [PATCH 043/114] added required dependencies --- src/hooks/useFirestoreDocWithAtom.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/hooks/useFirestoreDocWithAtom.ts b/src/hooks/useFirestoreDocWithAtom.ts index 0ed56899c..b66351df3 100644 --- a/src/hooks/useFirestoreDocWithAtom.ts +++ b/src/hooks/useFirestoreDocWithAtom.ts @@ -167,7 +167,13 @@ export function useFirestoreDocWithAtom( // reset the atom’s value to prevent writes if (updateDataAtom) setUpdateDataAtom(undefined); }; - }, [memoizedDocRef, updateDataAtom, setUpdateDataAtom]); + }, [ + memoizedDocRef, + updateDataAtom, + setUpdateDataAtom, + enqueueSnackbar, + setDataAtom, + ]); } export default useFirestoreDocWithAtom; From e754b251217c7aa29f246e3a4ff7cb66a085689e Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Sat, 31 Dec 2022 06:28:18 +0000 Subject: [PATCH 044/114] disable drag if user does not have permission --- src/components/TableToolbar/HiddenFields.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/components/TableToolbar/HiddenFields.tsx b/src/components/TableToolbar/HiddenFields.tsx index 1e4d26017..ff1fc86a6 100644 --- a/src/components/TableToolbar/HiddenFields.tsx +++ b/src/components/TableToolbar/HiddenFields.tsx @@ -29,6 +29,7 @@ import { projectScope, userSettingsAtom, updateUserSettingsAtom, + userRolesAtom, } from "@src/atoms/projectScope"; import { tableScope, @@ -41,6 +42,9 @@ export default function HiddenFields() { const buttonRef = useRef(null); const [userSettings] = useAtom(userSettingsAtom, projectScope); + const [userRoles] = useAtom(userRolesAtom, projectScope); + const canEditColumns = + userRoles.includes("ADMIN") || userRoles.includes("OPS"); const [tableId] = useAtom(tableIdAtom, tableScope); const [open, setOpen] = useState(false); @@ -76,7 +80,7 @@ export default function HiddenFields() { setOpen(false); }; - // disable drag if search box is not empty + // disable drag if search box is not empty and user does not have permission const [disableDrag, setDisableDrag] = useState(false); const renderOption: AutocompleteProps< any, @@ -92,7 +96,7 @@ export default function HiddenFields() { {(provided) => (
  • @@ -106,7 +110,9 @@ export default function HiddenFields() { { marginRight: "6px", opacity: (theme) => - disableDrag ? theme.palette.action.disabledOpacity : 1, + disableDrag || !canEditColumns + ? theme.palette.action.disabledOpacity + : 1, }, ]} /> @@ -170,7 +176,7 @@ export default function HiddenFields() { // checks whether to disable reordering when search filter is applied function checkToDisableDrag(e: ChangeEvent) { - setDisableDrag(e.target.value !== ""); + setDisableDrag(e.target.value !== "" || !canEditColumns); } const ListboxComponent = forwardRef(function ListboxComponent( From fed06b3e59848f3d97f8d6e5d612c70d63e0168d Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Sat, 31 Dec 2022 09:01:47 +0000 Subject: [PATCH 045/114] let -> const --- src/components/Table/ColumnSelect.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/Table/ColumnSelect.tsx b/src/components/Table/ColumnSelect.tsx index 3ea281557..d5b3fa154 100644 --- a/src/components/Table/ColumnSelect.tsx +++ b/src/components/Table/ColumnSelect.tsx @@ -29,8 +29,7 @@ export default function ColumnSelect({ showFieldNames, ...props }: IColumnSelectProps & Omit, "options">) { - let [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); - + const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); const options = props.options || (filterColumns From c439b35c98fe52b319226f47e718fb0e81bccb04 Mon Sep 17 00:00:00 2001 From: shamsmosowi Date: Sun, 1 Jan 2023 16:15:02 +0100 Subject: [PATCH 046/114] log alert --- .../CloudLogsModal/CloudLogsModal.tsx | 22 ++++++++++++++++++- src/constants/externalLinks.ts | 1 + 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx b/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx index 76cfe7197..cc6775529 100644 --- a/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx +++ b/src/components/TableModals/CloudLogsModal/CloudLogsModal.tsx @@ -14,6 +14,8 @@ import { Button, Box, CircularProgress, + Alert, + Link, } from "@mui/material"; import RefreshIcon from "@mui/icons-material/Refresh"; import { CloudLogs as LogsIcon } from "@src/assets/icons"; @@ -45,6 +47,7 @@ import { } from "@src/atoms/tableScope"; import { cloudLogFetcher } from "./utils"; import { FieldType } from "@src/constants/fields"; +import { WIKI_LINKS } from "@src/constants/externalLinks"; export default function CloudLogsModal({ onClose }: ITableModalProps) { const [projectId] = useAtom(projectIdAtom, projectScope); @@ -164,7 +167,6 @@ export default function CloudLogsModal({ onClose }: ITableModalProps) { )}
    - {cloudLogFilters.type !== "build" && ( <> ) : null} + {["extension", "webhook", "column"].includes( + cloudLogFilters.type + ) && ( + + Remember to use logging functions,{" "} + log,warning,error for them to appear in the logs + bellow{" "} + + Learn more + + + )} Date: Mon, 2 Jan 2023 12:17:52 +0300 Subject: [PATCH 047/114] fix broken atom --- src/hooks/useFirestoreDocWithAtom.ts | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/hooks/useFirestoreDocWithAtom.ts b/src/hooks/useFirestoreDocWithAtom.ts index b66351df3..107a50f1f 100644 --- a/src/hooks/useFirestoreDocWithAtom.ts +++ b/src/hooks/useFirestoreDocWithAtom.ts @@ -148,12 +148,6 @@ export function useFirestoreDocWithAtom( } } - setDataAtom((prev) => { - return { - ...prev, - ...updateToDb, - }; - }); return setDoc(memoizedDocRef, updateToDb, { merge: true }).catch( (e) => { enqueueSnackbar((e as Error).message, { variant: "error" }); @@ -167,13 +161,7 @@ export function useFirestoreDocWithAtom( // reset the atom’s value to prevent writes if (updateDataAtom) setUpdateDataAtom(undefined); }; - }, [ - memoizedDocRef, - updateDataAtom, - setUpdateDataAtom, - enqueueSnackbar, - setDataAtom, - ]); + }, [memoizedDocRef, updateDataAtom, setUpdateDataAtom, enqueueSnackbar]); } export default useFirestoreDocWithAtom; From e59a10c944262e5b6df9b992b855252195c8872e Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Wed, 4 Jan 2023 09:35:40 +0000 Subject: [PATCH 048/114] worked on copy/paste feature --- package.json | 1 + .../BasicCellContextMenuActions.tsx | 87 +-------- src/components/Table/Table.tsx | 21 +- src/contexts/TableKbShortcutContext.tsx | 179 ++++++++++++++++++ yarn.lock | 5 + 5 files changed, 203 insertions(+), 90 deletions(-) create mode 100644 src/contexts/TableKbShortcutContext.tsx diff --git a/package.json b/package.json index 4fac66726..53fb464e5 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "dependencies": { "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", + "@mantine/hooks": "^5.10.0", "@mdi/js": "^6.6.96", "@monaco-editor/react": "^4.4.4", "@mui/icons-material": "^5.10.16", diff --git a/src/components/Table/ContextMenu/BasicCellContextMenuActions.tsx b/src/components/Table/ContextMenu/BasicCellContextMenuActions.tsx index fdaa1557a..a82c6e783 100644 --- a/src/components/Table/ContextMenu/BasicCellContextMenuActions.tsx +++ b/src/components/Table/ContextMenu/BasicCellContextMenuActions.tsx @@ -1,94 +1,19 @@ -import { useAtom, useSetAtom } from "jotai"; -import { useSnackbar } from "notistack"; -import { get, find } from "lodash-es"; - -// import Cut from "@mui/icons-material/ContentCut"; import { Copy as CopyCells } from "@src/assets/icons"; +// import Cut from "@mui/icons-material/ContentCut"; import Paste from "@mui/icons-material/ContentPaste"; - -import { - tableScope, - tableSchemaAtom, - tableRowsAtom, - updateFieldAtom, -} from "@src/atoms/tableScope"; -import { getFieldProp, getFieldType } from "@src/components/fields"; import { IFieldConfig } from "@src/components/fields/types"; +import { useMenuAction } from "@src/contexts/TableKbShortcutContext"; // TODO: Remove this and add `handlePaste` function to column config export const BasicContextMenuActions: IFieldConfig["contextMenuActions"] = ( selectedCell, reset ) => { - const { enqueueSnackbar } = useSnackbar(); - - const [tableSchema] = useAtom(tableSchemaAtom, tableScope); - const [tableRows] = useAtom(tableRowsAtom, tableScope); - const updateField = useSetAtom(updateFieldAtom, tableScope); - - const selectedCol = tableSchema.columns?.[selectedCell.columnKey]; - if (!selectedCol) return []; - - const selectedRow = find(tableRows, ["_rowy_ref.path", selectedCell.path]); - const cellValue = get(selectedRow, selectedCol.fieldName); - const handleClose = async () => await reset?.(); - - const handleCopy = async () => { - try { - await navigator.clipboard.writeText(cellValue); - enqueueSnackbar("Copied"); - } catch (error) { - enqueueSnackbar(`Failed to copy:${error}`, { variant: "error" }); - } - handleClose(); - }; - - // const handleCut = async () => { - // try { - // await navigator.clipboard.writeText(cellValue); - // if (typeof cellValue !== "undefined") - // updateField({ - // path: selectedCell.path, - // fieldName: selectedCol.fieldName, - // value: undefined, - // deleteField: true, - // }); - // } catch (error) { - // enqueueSnackbar(`Failed to cut: ${error}`, { variant: "error" }); - // } - // handleClose(); - // }; - - const handlePaste = async () => { - try { - if (!selectedCol) return; - const text = await navigator.clipboard.readText(); - const cellDataType = getFieldProp("dataType", getFieldType(selectedCol)); - let parsed; - switch (cellDataType) { - case "number": - parsed = Number(text); - if (isNaN(parsed)) throw new Error(`${text} is not a number`); - break; - case "string": - parsed = text; - break; - default: - parsed = JSON.parse(text); - break; - } - updateField({ - path: selectedCell.path, - fieldName: selectedCol.fieldName, - value: parsed, - }); - } catch (error) { - enqueueSnackbar(`Failed to paste: ${error}`, { variant: "error" }); - } - - handleClose(); - }; + const { handleCopy, handlePaste, cellValue } = useMenuAction( + selectedCell, + handleClose + ); const contextMenuActions = [ // { label: "Cut", icon: , onClick: handleCut }, diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 092ff0abd..0b772a2b5 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -35,6 +35,7 @@ import { getFieldType, getFieldProp } from "@src/components/fields"; import { useKeyboardNavigation } from "./useKeyboardNavigation"; import { useSaveColumnSizing } from "./useSaveColumnSizing"; import type { TableRow, ColumnConfig } from "@src/types/table"; +import { TableKbShortcutProvider } from "@src/contexts/TableKbShortcutContext"; export const DEFAULT_ROW_HEIGHT = 41; export const DEFAULT_COL_WIDTH = 150; @@ -267,15 +268,17 @@ export default function Table({ {tableRows.length === 0 ? ( emptyState ?? ) : ( - + + + )} diff --git a/src/contexts/TableKbShortcutContext.tsx b/src/contexts/TableKbShortcutContext.tsx new file mode 100644 index 000000000..507ed5eb7 --- /dev/null +++ b/src/contexts/TableKbShortcutContext.tsx @@ -0,0 +1,179 @@ +import { + createContext, + useContext, + useCallback, + useState, + useEffect, +} from "react"; +import { useAtom, useSetAtom } from "jotai"; +import { useSnackbar } from "notistack"; +import { get, find } from "lodash-es"; +import { useHotkeys } from "@mantine/hooks"; + +import { + tableScope, + tableSchemaAtom, + tableRowsAtom, + updateFieldAtom, + selectedCellAtom, + SelectedCell, +} from "@src/atoms/tableScope"; +import { getFieldProp, getFieldType } from "@src/components/fields"; +import { ColumnConfig } from "@src/types/table"; + +import { FieldType } from "@src/constants/fields"; + +const SUPPORTED_TYPES = new Set([ + FieldType.shortText, + FieldType.longText, + FieldType.number, + FieldType.email, + FieldType.percentage, + FieldType.phone, + FieldType.richText, + FieldType.url, +]); + +export function useMenuAction( + selectedCell: SelectedCell | null, + handleClose?: Function +) { + const { enqueueSnackbar } = useSnackbar(); + const [tableSchema] = useAtom(tableSchemaAtom, tableScope); + const [tableRows] = useAtom(tableRowsAtom, tableScope); + const updateField = useSetAtom(updateFieldAtom, tableScope); + const [cellValue, setCellValue] = useState(); + const [selectedCol, setSelectedCol] = useState(); + const [enableAction, setEnableAction] = useState(false); + + const handleCopy = useCallback(async () => { + try { + if (cellValue !== undefined && cellValue !== null && cellValue !== "") { + await navigator.clipboard.writeText( + typeof cellValue === "object" ? JSON.stringify(cellValue) : cellValue + ); + enqueueSnackbar("Copied"); + } else { + await navigator.clipboard.writeText(""); + } + } catch (error) { + enqueueSnackbar(`Failed to copy:${error}`, { variant: "error" }); + } + if (handleClose) handleClose(); + }, [cellValue, enqueueSnackbar, handleClose]); + + const handleCut = useCallback(async () => { + try { + if (!selectedCell || !selectedCol || !cellValue) return; + if (cellValue !== undefined && cellValue !== null && cellValue !== "") { + await navigator.clipboard.writeText( + typeof cellValue === "object" ? JSON.stringify(cellValue) : cellValue + ); + enqueueSnackbar("Copied"); + } else { + await navigator.clipboard.writeText(""); + } + if (cellValue !== undefined) + updateField({ + path: selectedCell.path, + fieldName: selectedCol.fieldName, + value: undefined, + deleteField: true, + }); + } catch (error) { + enqueueSnackbar(`Failed to cut: ${error}`, { variant: "error" }); + } + if (handleClose) handleClose(); + }, [ + cellValue, + selectedCell, + selectedCol, + updateField, + enqueueSnackbar, + handleClose, + ]); + + const handlePaste = useCallback(async () => { + try { + if (!selectedCell || !selectedCol) return; + const text = await navigator.clipboard.readText(); + const cellDataType = getFieldProp("dataType", getFieldType(selectedCol)); + let parsed; + switch (cellDataType) { + case "number": + parsed = Number(text); + if (isNaN(parsed)) throw new Error(`${text} is not a number`); + break; + case "string": + parsed = text; + break; + default: + parsed = JSON.parse(text); + break; + } + updateField({ + path: selectedCell.path, + fieldName: selectedCol.fieldName, + value: parsed, + }); + } catch (error) { + enqueueSnackbar(`Failed to paste: ${error}`, { variant: "error" }); + } + if (handleClose) handleClose(); + }, [selectedCell, selectedCol, updateField, enqueueSnackbar, handleClose]); + + useEffect(() => { + setEnableAction(SUPPORTED_TYPES.has(selectedCol?.type)); + }, [selectedCol]); + + useEffect(() => { + if (!selectedCell) return setCellValue(""); + const selectedCol = tableSchema.columns?.[selectedCell.columnKey]; + if (!selectedCol) return setCellValue(""); + setSelectedCol(selectedCol); + const selectedRow = find(tableRows, ["_rowy_ref.path", selectedCell.path]); + setCellValue(get(selectedRow, selectedCol.fieldName)); + }, [selectedCell, tableSchema, tableRows]); + + const checkEnabled = (func: Function) => { + return function () { + if (enableAction) { + return func(); + } else { + enqueueSnackbar(`Simple copy not supported with this type.`, { + variant: "info", + }); + } + }; + }; + + return { + handleCopy: checkEnabled(handleCopy), + handleCut: checkEnabled(handleCut), + handlePaste: handlePaste, + cellValue, + }; +} + +const TableKbShortcutContext = createContext(null); + +export function useTableKbShortcut() { + return useContext(TableKbShortcutContext); +} + +export function TableKbShortcutProvider(props: { children: any }) { + const [selectedCell] = useAtom(selectedCellAtom, tableScope); + const { handleCopy, handlePaste, handleCut } = useMenuAction(selectedCell); + + useHotkeys([ + ["mod+C", handleCopy], + ["mod+X", handleCut], + ["mod+V", handlePaste], + ]); + + return ( + + {props.children} + + ); +} diff --git a/yarn.lock b/yarn.lock index 003c37e7e..61ea6da53 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2217,6 +2217,11 @@ resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== +"@mantine/hooks@^5.10.0": + version "5.10.0" + resolved "https://registry.yarnpkg.com/@mantine/hooks/-/hooks-5.10.0.tgz#e7886025a11dfa25f99c8c7fb7186d6a065c9d5c" + integrity sha512-dAefxpvqjFtXNeKse+awkIa4U1XGnMMOqWg1+07Y2Ino2G6EiT8AEnYqQyTXgcPoNaWwG9533Q/DDadmyweqaQ== + "@mark.probst/unicode-properties@~1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@mark.probst/unicode-properties/-/unicode-properties-1.1.0.tgz#5caafeab4737df93163d6d288007df33f9939b80" From c665baf6cc9afb9547acf23b89d7cf52490dc1f8 Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Wed, 4 Jan 2023 10:14:37 +0000 Subject: [PATCH 049/114] added json support --- src/contexts/TableKbShortcutContext.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/contexts/TableKbShortcutContext.tsx b/src/contexts/TableKbShortcutContext.tsx index 507ed5eb7..a64eada29 100644 --- a/src/contexts/TableKbShortcutContext.tsx +++ b/src/contexts/TableKbShortcutContext.tsx @@ -32,6 +32,7 @@ const SUPPORTED_TYPES = new Set([ FieldType.phone, FieldType.richText, FieldType.url, + FieldType.json, ]); export function useMenuAction( From 0675ddcfa45d9aa1e68821d30b5642df1005d2f4 Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Wed, 4 Jan 2023 13:28:08 +0000 Subject: [PATCH 050/114] better error message --- src/contexts/TableKbShortcutContext.tsx | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/contexts/TableKbShortcutContext.tsx b/src/contexts/TableKbShortcutContext.tsx index a64eada29..1576bd002 100644 --- a/src/contexts/TableKbShortcutContext.tsx +++ b/src/contexts/TableKbShortcutContext.tsx @@ -97,7 +97,15 @@ export function useMenuAction( const handlePaste = useCallback(async () => { try { if (!selectedCell || !selectedCol) return; - const text = await navigator.clipboard.readText(); + let text; + try { + text = await navigator.clipboard.readText(); + } catch (e) { + enqueueSnackbar(`Read clilboard permission denied.`, { + variant: "error", + }); + return; + } const cellDataType = getFieldProp("dataType", getFieldType(selectedCol)); let parsed; switch (cellDataType) { @@ -118,7 +126,10 @@ export function useMenuAction( value: parsed, }); } catch (error) { - enqueueSnackbar(`Failed to paste: ${error}`, { variant: "error" }); + enqueueSnackbar( + `${selectedCol?.type} field does not support the data type being pasted`, + { variant: "error" } + ); } if (handleClose) handleClose(); }, [selectedCell, selectedCol, updateField, enqueueSnackbar, handleClose]); @@ -141,9 +152,12 @@ export function useMenuAction( if (enableAction) { return func(); } else { - enqueueSnackbar(`Simple copy not supported with this type.`, { - variant: "info", - }); + enqueueSnackbar( + `${selectedCol?.type} field cannot be copied using keyboard shortcut`, + { + variant: "info", + } + ); } }; }; From d80815f1d837438bbb073b594ef4191ca536a4c6 Mon Sep 17 00:00:00 2001 From: Han Tuerker Date: Tue, 3 Jan 2023 09:47:59 +0300 Subject: [PATCH 051/114] feat(ROWY-831): add preview table --- .../fields/Formula/PreviewTable.tsx | 74 +++++++++++++++++++ src/components/fields/Formula/Settings.tsx | 38 +++++++++- .../fields/Formula/TableSourcePreview.ts | 70 ++++++++++++++++++ 3 files changed, 180 insertions(+), 2 deletions(-) create mode 100644 src/components/fields/Formula/PreviewTable.tsx create mode 100644 src/components/fields/Formula/TableSourcePreview.ts diff --git a/src/components/fields/Formula/PreviewTable.tsx b/src/components/fields/Formula/PreviewTable.tsx new file mode 100644 index 000000000..66113b326 --- /dev/null +++ b/src/components/fields/Formula/PreviewTable.tsx @@ -0,0 +1,74 @@ +import { Provider, useAtom } from "jotai"; + +import { currentUserAtom } from "@src/atoms/projectScope"; +import { + tableRowsDbAtom, + tableScope, + tableSettingsAtom, +} from "@src/atoms/tableScope"; + +import TablePage from "@src/pages/Table/TablePage"; +import { TableSchema } from "@src/types/table"; +import { Box, InputLabel } from "@mui/material"; +import TableSourcePreview from "./TableSourcePreview"; + +const PreviewTable = ({ tableSchema }: { tableSchema: TableSchema }) => { + const [currentUser] = useAtom(currentUserAtom, tableScope); + const [tableSettings] = useAtom(tableSettingsAtom, tableScope); + return ( + + Preview table + + + div:first-child > *:not(:first-child)": { + display: "none", + }, + // table grid + "& > div:nth-child(2)": { + height: "unset", + }, + // emtpy state + "& .empty-state": { + display: "none", + }, + // column actions - add column + '& [data-col-id="_rowy_column_actions"]': { + padding: 0, + width: 0, + }, + '& [data-col-id="_rowy_column_actions"] > button': { + display: "none", + }, + }} + > + + + + + ); +}; + +export default PreviewTable; diff --git a/src/components/fields/Formula/Settings.tsx b/src/components/fields/Formula/Settings.tsx index ad0eb16fb..5a0ac1012 100644 --- a/src/components/fields/Formula/Settings.tsx +++ b/src/components/fields/Formula/Settings.tsx @@ -1,4 +1,4 @@ -import { lazy, Suspense } from "react"; +import { lazy, Suspense, useMemo } from "react"; import { useDebouncedCallback } from "use-debounce"; import { useAtom } from "jotai"; import MultiSelect from "@rowy/multiselect"; @@ -12,12 +12,19 @@ import { Tooltip, } from "@mui/material"; +import { + tableColumnsOrderedAtom, + tableSchemaAtom, + tableScope, +} from "@src/atoms/tableScope"; + import FieldSkeleton from "@src/components/SideDrawer/FieldSkeleton"; import { ISettingsProps } from "@src/components/fields/types"; -import { tableColumnsOrderedAtom, tableScope } from "@src/atoms/tableScope"; import FieldsDropdown from "@src/components/ColumnModals/FieldsDropdown"; +import { ColumnConfig } from "@src/types/table"; import { defaultFn, listenerFieldTypes, outputFieldTypes } from "./util"; +import PreviewTable from "./PreviewTable"; import { getFieldProp } from ".."; /* eslint-disable import/no-webpack-loader-syntax */ @@ -36,14 +43,40 @@ const diagnosticsOptions = { export default function Settings({ config, + fieldName, onChange, onBlur, errors, }: ISettingsProps) { + const [tableSchema] = useAtom(tableSchemaAtom, tableScope); const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); const returnType = getFieldProp("dataType", config.renderFieldType) ?? "any"; const formulaFn = config?.formulaFn ? config.formulaFn : defaultFn; + const previewTableSchema = useMemo(() => { + const columns = tableSchema.columns || {}; + return { + ...tableSchema, + columns: Object.keys(columns).reduce((previewSchema, key) => { + if ((config.listenerFields || []).includes(columns[key].fieldName)) { + previewSchema[key] = { + ...columns[key], + fixed: false, + width: undefined, + }; + } + if (columns[key].fieldName === fieldName) { + previewSchema[key] = { + ...columns[key], + config, + fixed: true, + }; + } + return previewSchema; + }, {} as { [key: string]: ColumnConfig }), + }; + }, [config, fieldName, tableSchema]); + return ( @@ -133,6 +166,7 @@ export default function Settings({ />
    + ); } diff --git a/src/components/fields/Formula/TableSourcePreview.ts b/src/components/fields/Formula/TableSourcePreview.ts new file mode 100644 index 000000000..57deb3d9b --- /dev/null +++ b/src/components/fields/Formula/TableSourcePreview.ts @@ -0,0 +1,70 @@ +import { useCallback } from "react"; +import { useSetAtom } from "jotai"; +import { useAtomCallback } from "jotai/utils"; +import { cloneDeep, findIndex, sortBy } from "lodash-es"; + +import { + _deleteRowDbAtom, + _updateRowDbAtom, + tableNextPageAtom, + tableRowsDbAtom, + tableSchemaAtom, + tableScope, +} from "@src/atoms/tableScope"; + +import { TableRow, TableSchema } from "@src/types/table"; +import { updateRowData } from "@src/utils/table"; + +const TableSourcePreview = ({ tableSchema }: { tableSchema: TableSchema }) => { + const setTableSchemaAtom = useSetAtom(tableSchemaAtom, tableScope); + setTableSchemaAtom(() => ({ + ...tableSchema, + _rowy_ref: "preview", + })); + + const setRows = useSetAtom(tableRowsDbAtom, tableScope); + const readRowsDb = useAtomCallback( + useCallback((get) => get(tableRowsDbAtom), []), + tableScope + ); + + const setUpdateRowDb = useSetAtom(_updateRowDbAtom, tableScope); + setUpdateRowDb(() => async (path: string, update: Partial) => { + const rows = await readRowsDb(); + const index = findIndex(rows, ["_rowy_ref.path", path]); + if (index === -1) { + setRows( + sortBy( + [ + ...rows, + { ...update, _rowy_ref: { id: path.split("/").pop()!, path } }, + ], + ["_rowy_ref.id"] + ) + ); + } else { + const updatedRows = [...rows]; + updatedRows[index] = cloneDeep(rows[index]); + updatedRows[index] = updateRowData(updatedRows[index], update); + setRows(updatedRows); + } + return Promise.resolve(); + }); + + const setDeleteRowDb = useSetAtom(_deleteRowDbAtom, tableScope); + setDeleteRowDb(() => async (path: string) => { + const rows = await readRowsDb(); + const index = findIndex(rows, ["_rowy_ref.path", path]); + if (index > -1) { + setRows(rows.filter((_, idx) => idx !== index)); + } + return Promise.resolve(); + }); + + const setNextPageAtom = useSetAtom(tableNextPageAtom, tableScope); + setNextPageAtom({ loading: false, available: false }); + + return null; +}; + +export default TableSourcePreview; From 6bfaae0e4ba2d6cec392e002356b059f0a188a57 Mon Sep 17 00:00:00 2001 From: Han Tuerker Date: Tue, 3 Jan 2023 12:10:03 +0300 Subject: [PATCH 052/114] fix side effect causing crash --- src/atoms/tableScope/table.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/atoms/tableScope/table.ts b/src/atoms/tableScope/table.ts index fdf8820c9..d9322e289 100644 --- a/src/atoms/tableScope/table.ts +++ b/src/atoms/tableScope/table.ts @@ -164,7 +164,7 @@ export const tableRowsDbAtom = atom([]); /** Combine tableRowsLocal and tableRowsDb */ export const tableRowsAtom = atom((get) => { - const rowsDb = [...get(tableRowsDbAtom)]; + const rowsDb = get(tableRowsDbAtom); const rowsLocal = get(tableRowsLocalAtom); // Optimization: create Map of rowsDb by path to index for faster lookup @@ -178,7 +178,7 @@ export const tableRowsAtom = atom((get) => { if (rowsDbMap.has(row._rowy_ref.path)) { const index = rowsDbMap.get(row._rowy_ref.path)!; const merged = updateRowData({ ...rowsDb[index] }, row); - rowsDb.splice(index, 1); + rowsDbMap.delete(row._rowy_ref.path); return merged; } @@ -186,7 +186,10 @@ export const tableRowsAtom = atom((get) => { }); // Merge the two arrays - return [...rowsLocalToMerge, ...rowsDb]; + return [ + ...rowsLocalToMerge, + ...rowsDb.filter((row) => rowsDbMap.get(row._rowy_ref.path)), + ]; }); /** Store next page state for infinite scroll */ From df2e7d23d416e600cdf72dafffe6d89082279191 Mon Sep 17 00:00:00 2001 From: Han Tuerker Date: Tue, 3 Jan 2023 13:43:56 +0300 Subject: [PATCH 053/114] fix filter condition --- src/atoms/tableScope/table.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/atoms/tableScope/table.ts b/src/atoms/tableScope/table.ts index d9322e289..f8d519c48 100644 --- a/src/atoms/tableScope/table.ts +++ b/src/atoms/tableScope/table.ts @@ -181,14 +181,13 @@ export const tableRowsAtom = atom((get) => { rowsDbMap.delete(row._rowy_ref.path); return merged; } - return row; }); // Merge the two arrays return [ ...rowsLocalToMerge, - ...rowsDb.filter((row) => rowsDbMap.get(row._rowy_ref.path)), + ...rowsDb.filter((row) => rowsDbMap.has(row._rowy_ref.path)), ]; }); From c1d5c21d2af36bfe23bf8d67f39ea914584db7e8 Mon Sep 17 00:00:00 2001 From: Yash Joshi Date: Thu, 5 Jan 2023 08:52:18 +0530 Subject: [PATCH 054/114] fix: prevent redirecting to link when deleting file (#1047) * Bump tinymce from 5.10.4 to 5.10.7 Bumps [tinymce](https://github.com/tinymce/tinymce/tree/HEAD/modules/tinymce) from 5.10.4 to 5.10.7. - [Release notes](https://github.com/tinymce/tinymce/releases) - [Changelog](https://github.com/tinymce/tinymce/blob/5.10.7/modules/tinymce/CHANGELOG.md) - [Commits](https://github.com/tinymce/tinymce/commits/5.10.7/modules/tinymce) --- updated-dependencies: - dependency-name: tinymce dependency-type: direct:production ... Signed-off-by: dependabot[bot] * fix: prevent redirecting to link when deleting file Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Shams Co-authored-by: Han Tuerker --- src/components/fields/File/EditorCell.tsx | 6 ++++-- yarn.lock | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/fields/File/EditorCell.tsx b/src/components/fields/File/EditorCell.tsx index 247c99faa..a520c594d 100644 --- a/src/components/fields/File/EditorCell.tsx +++ b/src/components/fields/File/EditorCell.tsx @@ -88,14 +88,16 @@ export default function File_({ onDelete={ disabled ? undefined - : () => + : (e) => { + e.preventDefault(); confirm({ handleConfirm: () => handleDelete(file), title: "Delete file?", body: "This file cannot be recovered after", confirm: "Delete", confirmColor: "error", - }) + }); + } } tabIndex={tabIndex} style={{ width: "100%", cursor: "pointer" }} diff --git a/yarn.lock b/yarn.lock index 003c37e7e..d669d494d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12043,9 +12043,9 @@ tiny-invariant@^1.0.6: integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== tinymce@^5, tinymce@^5.5.1: - version "5.10.6" - resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-5.10.6.tgz#ea03927e9d20c035619dfd32ec4fd471c55e32c5" - integrity sha512-bnF2LUoycDsoZZLQBNHbOijrmoJuEeR1rQdqgo4s77BedufpOVnDh00OZKbseHeTMCxhVH05wvOqxLsi6vpeZw== + version "5.10.7" + resolved "https://registry.yarnpkg.com/tinymce/-/tinymce-5.10.7.tgz#d89d446f1962f2a1df6b2b70018ce475ec7ffb80" + integrity sha512-9UUjaO0R7FxcFo0oxnd1lMs7H+D0Eh+dDVo5hKbVe1a+VB0nit97vOqlinj+YwgoBDt6/DSCUoWqAYlLI8BLYA== tmp@^0.2.1: version "0.2.1" From a48088c2715f1055b204e87f06a657600c3806b9 Mon Sep 17 00:00:00 2001 From: Miriam Shams-Rainey Date: Thu, 5 Jan 2023 22:05:28 -0600 Subject: [PATCH 055/114] #ROWY-763: Select all option in CSV import --- .../ImportCsvWizard/Step1Columns.tsx | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/src/components/TableModals/ImportCsvWizard/Step1Columns.tsx b/src/components/TableModals/ImportCsvWizard/Step1Columns.tsx index 684584ec9..6da3fbf30 100644 --- a/src/components/TableModals/ImportCsvWizard/Step1Columns.tsx +++ b/src/components/TableModals/ImportCsvWizard/Step1Columns.tsx @@ -64,6 +64,60 @@ export default function Step1Columns({ config.pairs.map((pair) => pair.csvKey) ); + const handleSelectAll = () => { + if (selectedFields.length !== csvData.columns.length) { + setSelectedFields(csvData.columns); + csvData.columns.forEach(field => { + // Try to match the field to a column in the table + const match = + find(tableColumns, (column) => + column.label.toLowerCase().includes(field.toLowerCase()) + )?.value ?? null; + const columnKey = camelCase(field); + const columnConfig: Partial = { pairs: [], newColumns: [] }; + columnConfig.pairs = [{ csvKey: field, columnKey: match ?? columnKey }]; + if (!match) { + columnConfig.newColumns = [ + { + name: field, + fieldName: columnKey, + key: columnKey, + type: suggestType(csvData.rows, field) || FieldType.shortText, + index: -1, + config: {}, + }, + ]; + } + updateConfig(columnConfig); + }) + } else { + setSelectedFields([]); + csvData.columns.forEach(field => { + // Check if this pair was already pushed to main config + const configPair = find(config.pairs, { csvKey: field }); + const configIndex = findIndex(config.pairs, { csvKey: field }); + + // Delete matching newColumn if it was created + if (configPair) { + const newColumnIndex = findIndex(config.newColumns, { + key: configPair.columnKey, + }); + if (newColumnIndex > -1) { + const newColumns = [...config.newColumns]; + newColumns.splice(newColumnIndex, 1); + setConfig((config) => ({ ...config, newColumns })); + } + } + + // Delete pair from main config + if (configIndex > -1) { + const newConfig = [...config.pairs]; + newConfig.splice(configIndex, 1); + setConfig((config) => ({ ...config, pairs: newConfig })); + } + }) + }}; + // When a field is selected to be imported const handleSelect = (field: string) => (e: React.ChangeEvent) => { @@ -187,6 +241,28 @@ export default function Step1Columns({ +
  • + + } + label="Select all" + sx={{ + height: 42, + mr: 0, + alignItems: "center", + "& .MuiFormControlLabel-label": { mt: 0, flex: 1 }, + }} + /> +
  • {csvData.columns.map((field) => { const selected = selectedFields.indexOf(field) > -1; const columnKey = From f803a3a21d9371e2ccac0fa3d06a028feb1044d5 Mon Sep 17 00:00:00 2001 From: shamsmosowi Date: Fri, 6 Jan 2023 10:32:04 +0100 Subject: [PATCH 056/114] fix table crashing, for filtered group collections --- src/sources/TableSourceFirestore/handleFirestoreError.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/sources/TableSourceFirestore/handleFirestoreError.tsx b/src/sources/TableSourceFirestore/handleFirestoreError.tsx index 0dda67cb9..dcaf8b1dd 100644 --- a/src/sources/TableSourceFirestore/handleFirestoreError.tsx +++ b/src/sources/TableSourceFirestore/handleFirestoreError.tsx @@ -22,7 +22,10 @@ export const handleFirestoreError = ( return; } - if (error.message.includes("indexes?create_composite=")) { + if ( + error.message.includes("indexes?create_composite=") || + error.message.includes("/firestore/indexes?") + ) { enqueueSnackbar( "Filtering while having another column sorted requires a new Firestore index", { From d4a41b7af7795994e75f0db514eca965eaa273d7 Mon Sep 17 00:00:00 2001 From: shamsmosowi Date: Fri, 6 Jan 2023 10:34:35 +0100 Subject: [PATCH 057/114] update error message --- .../handleFirestoreError.tsx | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/sources/TableSourceFirestore/handleFirestoreError.tsx b/src/sources/TableSourceFirestore/handleFirestoreError.tsx index dcaf8b1dd..7476c9868 100644 --- a/src/sources/TableSourceFirestore/handleFirestoreError.tsx +++ b/src/sources/TableSourceFirestore/handleFirestoreError.tsx @@ -22,10 +22,7 @@ export const handleFirestoreError = ( return; } - if ( - error.message.includes("indexes?create_composite=") || - error.message.includes("/firestore/indexes?") - ) { + if (error.message.includes("indexes?create_composite=")) { enqueueSnackbar( "Filtering while having another column sorted requires a new Firestore index", { @@ -47,6 +44,28 @@ export const handleFirestoreError = ( return; } + if (error.message.includes("/firestore/indexes?")) { + enqueueSnackbar( + "Filtering on a group collection requires a new Firestore index", + { + variant: "warning", + action: ( + + ), + } + ); + return; + } + if (error.code === "invalid-argument") { enqueueSnackbar("Cannot sort by this column with the current set filters", { variant: "error", From ebc596759483f6923fc9830bc5af66cddcd328c8 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Sat, 7 Jan 2023 02:03:18 +1300 Subject: [PATCH 058/114] null check on .some functions --- src/atoms/projectScope/project.ts | 2 +- .../TableModals/ImportAirtableWizard/ImportAirtableWizard.tsx | 2 +- .../TableModals/ImportCsvWizard/ImportCsvWizard.tsx | 2 +- src/components/TableTutorial/Steps/Step2Add.tsx | 2 +- src/components/fields/Connector/Select/PopupContents.tsx | 4 +++- src/hooks/useFirestoreDocWithAtom.ts | 2 +- src/sources/ProjectSourceFirebase/useTableFunctions.ts | 2 +- 7 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/atoms/projectScope/project.ts b/src/atoms/projectScope/project.ts index 0d9518ac5..1e9cf1bdb 100644 --- a/src/atoms/projectScope/project.ts +++ b/src/atoms/projectScope/project.ts @@ -48,7 +48,7 @@ export const tablesAtom = atom((get) => { return sortBy(tables, "name") .filter((table) => userRoles.includes("ADMIN") || Array.isArray(table.roles) - ? table.roles.some((role) => userRoles.includes(role)) + ? table.roles?.some((role) => userRoles.includes(role)) : false ) .map((table) => ({ diff --git a/src/components/TableModals/ImportAirtableWizard/ImportAirtableWizard.tsx b/src/components/TableModals/ImportAirtableWizard/ImportAirtableWizard.tsx index afe95479d..7a6cee3f3 100644 --- a/src/components/TableModals/ImportAirtableWizard/ImportAirtableWizard.tsx +++ b/src/components/TableModals/ImportAirtableWizard/ImportAirtableWizard.tsx @@ -72,7 +72,7 @@ export default function ImportAirtableWizard({ onClose }: ITableModalProps) { const newColumns = uniqBy( [...prev.newColumns, ...(value.newColumns ?? [])], "key" - ).filter((col) => pairs.some((pair) => pair.columnKey === col.key)); + ).filter((col) => pairs?.some((pair) => pair.columnKey === col.key)); return { ...prev, pairs, newColumns }; }); }, []); diff --git a/src/components/TableModals/ImportCsvWizard/ImportCsvWizard.tsx b/src/components/TableModals/ImportCsvWizard/ImportCsvWizard.tsx index ac67337ea..b40e0e8c9 100644 --- a/src/components/TableModals/ImportCsvWizard/ImportCsvWizard.tsx +++ b/src/components/TableModals/ImportCsvWizard/ImportCsvWizard.tsx @@ -80,7 +80,7 @@ export default function ImportCsvWizard({ onClose }: ITableModalProps) { const newColumns = uniqBy( [...prev.newColumns, ...(value.newColumns ?? [])], "key" - ).filter((col) => pairs.some((pair) => pair.columnKey === col.key)); + ).filter((col) => pairs?.some((pair) => pair.columnKey === col.key)); return { ...prev, pairs, newColumns }; }); diff --git a/src/components/TableTutorial/Steps/Step2Add.tsx b/src/components/TableTutorial/Steps/Step2Add.tsx index 5e6d9dbe0..9c9024d56 100644 --- a/src/components/TableTutorial/Steps/Step2Add.tsx +++ b/src/components/TableTutorial/Steps/Step2Add.tsx @@ -44,7 +44,7 @@ function StepComponent({ setComplete }: ITableTutorialStepComponentProps) { const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope); useEffect(() => { if ( - tableColumnsOrdered.some( + tableColumnsOrdered?.some( (c) => c.type === FieldType.rating && c.name.toLowerCase().includes("rating") ) diff --git a/src/components/fields/Connector/Select/PopupContents.tsx b/src/components/fields/Connector/Select/PopupContents.tsx index 941418c78..0bd770cbe 100644 --- a/src/components/fields/Connector/Select/PopupContents.tsx +++ b/src/components/fields/Connector/Select/PopupContents.tsx @@ -127,7 +127,9 @@ export default function PopupContents({ {hits.map((hit) => { - const isSelected = selectedValues.some((v) => v === hit[elementId]); + const isSelected = selectedValues?.some( + (v) => v === hit[elementId] + ); return ( ( path: string | undefined, pathSegments?: Array ) => { - if (!path || (Array.isArray(pathSegments) && pathSegments.some((x) => !x))) + if (!path || (Array.isArray(pathSegments) && pathSegments?.some((x) => !x))) return null; return doc( diff --git a/src/sources/ProjectSourceFirebase/useTableFunctions.ts b/src/sources/ProjectSourceFirebase/useTableFunctions.ts index ab6fb4efc..6c6effa6c 100644 --- a/src/sources/ProjectSourceFirebase/useTableFunctions.ts +++ b/src/sources/ProjectSourceFirebase/useTableFunctions.ts @@ -84,7 +84,7 @@ export function useTableFunctions() { if ( checked && // Make sure we don’t have - !Object.values(columns).some((column) => column.type === type) + !Object.values(columns)?.some((column) => column.type === type) ) columns["_" + camelCase(type)] = { type, From f7cce06320112a9dd4145eeca934956833c774c2 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Sat, 7 Jan 2023 04:20:16 +1300 Subject: [PATCH 059/114] requires cloud function setup before creating related columns --- src/atoms/projectScope/rowyRun.ts | 2 - .../ColumnModals/FieldsDropdown.tsx | 46 +++++++++++++++++-- src/components/fields/Action/index.tsx | 1 + src/components/fields/Connector/index.tsx | 1 + src/components/fields/Derivative/index.tsx | 1 + src/components/fields/types.ts | 1 + 6 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/atoms/projectScope/rowyRun.ts b/src/atoms/projectScope/rowyRun.ts index bc6d35cd6..bb66d8f2a 100644 --- a/src/atoms/projectScope/rowyRun.ts +++ b/src/atoms/projectScope/rowyRun.ts @@ -73,7 +73,6 @@ export const rowyRunAtom = atom((get) => { handleNotSetUp, }: IRowyRunRequestProps): Promise => { if (!currentUser) { - console.log("Rowy Run: Not signed in", route.path); if (handleNotSetUp) handleNotSetUp(); return false; } @@ -85,7 +84,6 @@ export const rowyRunAtom = atom((get) => { ? rowyRunServices?.[service] : rowyRunUrl; if (!serviceUrl) { - console.log("Rowy Run: Not set up", route.path); if (handleNotSetUp) handleNotSetUp(); return false; } diff --git a/src/components/ColumnModals/FieldsDropdown.tsx b/src/components/ColumnModals/FieldsDropdown.tsx index 8257292cb..a3fdd4c6f 100644 --- a/src/components/ColumnModals/FieldsDropdown.tsx +++ b/src/components/ColumnModals/FieldsDropdown.tsx @@ -5,12 +5,20 @@ import { FIELDS } from "@src/components/fields"; import { FieldType } from "@src/constants/fields"; import { getFieldProp } from "@src/components/fields"; +import { useSetAtom, useAtom } from "jotai"; +import { + projectScope, + projectSettingsAtom, + rowyRunModalAtom, +} from "@src/atoms/projectScope"; + export interface IFieldsDropdownProps { value: FieldType | ""; onChange: (value: FieldType) => void; hideLabel?: boolean; label?: string; options?: FieldType[]; + [key: string]: any; } @@ -25,13 +33,21 @@ export default function FieldsDropdown({ options: optionsProp, ...props }: IFieldsDropdownProps) { + const [projectSettings] = useAtom(projectSettingsAtom, projectScope); + const openRowyRunModal = useSetAtom(rowyRunModalAtom, projectScope); const fieldTypesToDisplay = optionsProp ? FIELDS.filter((fieldConfig) => optionsProp.indexOf(fieldConfig.type) > -1) : FIELDS; - const options = fieldTypesToDisplay.map((fieldConfig) => ({ - label: fieldConfig.name, - value: fieldConfig.type, - })); + const options = fieldTypesToDisplay.map((fieldConfig) => { + const requireCloudFunctionSetup = + fieldConfig.requireCloudFunction && !projectSettings.rowyRunUrl; + return { + label: fieldConfig.name, + value: fieldConfig.type, + disabled: requireCloudFunctionSetup, + requireCloudFunctionSetup, + }; + }); return ( {getFieldProp("icon", option.value as FieldType)} - {option.label} + {option.label}{" "} + {option.requireCloudFunctionSetup && ( + <> + (requires + { + e.stopPropagation(); + openRowyRunModal({ feature: option.label }); + }} + > + Cloud Function + + ) + + )} )} label={label || "Field type"} diff --git a/src/components/fields/Action/index.tsx b/src/components/fields/Action/index.tsx index e2e8c1bda..7ab899e2d 100644 --- a/src/components/fields/Action/index.tsx +++ b/src/components/fields/Action/index.tsx @@ -30,6 +30,7 @@ export const config: IFieldConfig = { SideDrawerField, settings: Settings, requireConfiguration: true, + requireCloudFunction: true, sortKey: "status", }; export default config; diff --git a/src/components/fields/Connector/index.tsx b/src/components/fields/Connector/index.tsx index a0e239d16..7c72963b6 100644 --- a/src/components/fields/Connector/index.tsx +++ b/src/components/fields/Connector/index.tsx @@ -34,6 +34,7 @@ export const config: IFieldConfig = { }), SideDrawerField, requireConfiguration: true, + requireCloudFunction: true, settings: Settings, }; export default config; diff --git a/src/components/fields/Derivative/index.tsx b/src/components/fields/Derivative/index.tsx index d046b4d31..4c18a0f31 100644 --- a/src/components/fields/Derivative/index.tsx +++ b/src/components/fields/Derivative/index.tsx @@ -21,5 +21,6 @@ export const config: IFieldConfig = { settings: Settings, settingsValidator, requireConfiguration: true, + requireCloudFunction: true, }; export default config; diff --git a/src/components/fields/types.ts b/src/components/fields/types.ts index 63374d811..a11e1e10f 100644 --- a/src/components/fields/types.ts +++ b/src/components/fields/types.ts @@ -19,6 +19,7 @@ export interface IFieldConfig { dataType: string; initializable?: boolean; requireConfiguration?: boolean; + requireCloudFunction?: boolean; initialValue: any; icon?: React.ReactNode; description?: string; From 7be0755b8de70bcbe8cafc6f4ca73d739ca3d7d1 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Sat, 7 Jan 2023 05:34:29 +1300 Subject: [PATCH 060/114] consistent "require cloud function" text on both default values and columns --- .../ColumnConfigModal/DefaultValueInput.tsx | 22 +++++++++++-- .../ColumnModals/FieldsDropdown.tsx | 32 +++++++++++++++---- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/src/components/ColumnModals/ColumnConfigModal/DefaultValueInput.tsx b/src/components/ColumnModals/ColumnConfigModal/DefaultValueInput.tsx index 3cc78e2af..028fc0d61 100644 --- a/src/components/ColumnModals/ColumnConfigModal/DefaultValueInput.tsx +++ b/src/components/ColumnModals/ColumnConfigModal/DefaultValueInput.tsx @@ -1,5 +1,5 @@ import { lazy, Suspense, createElement, useState } from "react"; -import { useAtom } from "jotai"; +import { useAtom, useSetAtom } from "jotai"; import Checkbox from "@mui/material/Checkbox"; import FormControlLabel from "@mui/material/FormControlLabel"; @@ -17,6 +17,7 @@ import { projectScope, compatibleRowyRunVersionAtom, projectSettingsAtom, + rowyRunModalAtom, } from "@src/atoms/projectScope"; import { ColumnConfig } from "@src/types/table"; @@ -93,6 +94,7 @@ export default function DefaultValueInput({ column, }: IDefaultValueInputProps) { const [projectSettings] = useAtom(projectSettingsAtom, projectScope); + const openRowyRunModal = useSetAtom(rowyRunModalAtom, projectScope); const _type = column.type !== FieldType.derivative @@ -152,9 +154,23 @@ export default function DefaultValueInput({ "Dynamic" ) : ( <> - Dynamic —{" "} + Dynamic{" "} - Requires Rowy Run setup + Requires + { + e.stopPropagation(); + openRowyRunModal({ feature: "Dynamic Default Value" }); + }} + > + Cloud Function + ) diff --git a/src/components/ColumnModals/FieldsDropdown.tsx b/src/components/ColumnModals/FieldsDropdown.tsx index a3fdd4c6f..529c3b840 100644 --- a/src/components/ColumnModals/FieldsDropdown.tsx +++ b/src/components/ColumnModals/FieldsDropdown.tsx @@ -1,5 +1,5 @@ import MultiSelect from "@rowy/multiselect"; -import { ListItemIcon } from "@mui/material"; +import { Box, ListItemIcon, Typography } from "@mui/material"; import { FIELDS } from "@src/components/fields"; import { FieldType } from "@src/constants/fields"; @@ -60,6 +60,20 @@ export default function FieldsDropdown({ AutocompleteProps: { groupBy: (option: typeof options[number]) => getFieldProp("group", option.value), + ListboxProps: { + sx: { + '& li.MuiAutocomplete-option[aria-disabled="true"]': { + opacity: 1, + }, + '& li.MuiAutocomplete-option[aria-disabled="true"] > *': { + opacity: 0.4, + }, + '& li.MuiAutocomplete-option[aria-disabled="true"] > .require-cloud-function': + { + opacity: 1, + }, + }, + }, }, } as any)} itemRenderer={(option) => ( @@ -67,10 +81,17 @@ export default function FieldsDropdown({ {getFieldProp("icon", option.value as FieldType)} - {option.label}{" "} + {option.label} {option.requireCloudFunctionSetup && ( - <> - (requires + + {" "} + Requires Cloud Function - ) - + )} )} From f1531a5656c7268887f67659de5d3934a022478f Mon Sep 17 00:00:00 2001 From: Miriam Shams-Rainey Date: Fri, 6 Jan 2023 14:45:07 -0600 Subject: [PATCH 061/114] #ROWY-763: Select all for Airtable wizard, format --- .../ImportAirtableWizard/Step1Columns.tsx | 71 +++++++++++++++- .../ImportCsvWizard/Step1Columns.tsx | 82 +++++++------------ .../ImportExistingWizard/Step1Columns.tsx | 2 +- 3 files changed, 99 insertions(+), 56 deletions(-) diff --git a/src/components/TableModals/ImportAirtableWizard/Step1Columns.tsx b/src/components/TableModals/ImportAirtableWizard/Step1Columns.tsx index 062355458..befaeedbc 100644 --- a/src/components/TableModals/ImportAirtableWizard/Step1Columns.tsx +++ b/src/components/TableModals/ImportAirtableWizard/Step1Columns.tsx @@ -57,6 +57,9 @@ export default function Step1Columns({ config.pairs.map((pair) => pair.fieldKey) ); + + const fieldKeys = Object.keys(airtableData.records[0].fields); + // When a field is selected to be imported const handleSelect = (field: string) => (e: React.ChangeEvent) => { @@ -123,6 +126,47 @@ export default function Step1Columns({ } }; + const handleSelectAll = () => { + if (selectedFields.length !== fieldKeys.length) { + setSelectedFields(fieldKeys) + fieldKeys.forEach(field => { + // Try to match each field to a column in the table + const match = + find(tableColumns, (column) => + column.label.toLowerCase().includes(field.toLowerCase()) + )?.value ?? null; + + const columnKey = camelCase(field); + const columnConfig: Partial = { + pairs: [], + newColumns: [], + }; + columnConfig.pairs = [ + { fieldKey: field, columnKey: match ?? columnKey }, + ]; + if (!match) { + columnConfig.newColumns = [ + { + name: field, + fieldName: columnKey, + key: columnKey, + type: + suggestType(airtableData.records, field) || FieldType.shortText, + index: -1, + config: {}, + }, + ]; + } + updateConfig(columnConfig); + }) + } else { + setSelectedFields([]) + setConfig((config) => ({ ...config, newColumns: [], pairs: [] })) + } + + }; + + // When a field is mapped to a new column const handleChange = (fieldKey: string) => (value: string) => { if (!value) return; @@ -159,7 +203,6 @@ export default function Step1Columns({ } }; - const fieldKeys = Object.keys(airtableData.records[0].fields); return (
    @@ -180,14 +223,36 @@ export default function Step1Columns({ +
  • + + } + label={selectedFields.length == fieldKeys.length ? "Clear all" : "Select all"} + sx={{ + height: 42, + mr: 0, + alignItems: "center", + "& .MuiFormControlLabel-label": { mt: 0, flex: 1 }, + }} + /> +
  • {fieldKeys.map((field) => { const selected = selectedFields.indexOf(field) > -1; const columnKey = find(config.pairs, { fieldKey: field })?.columnKey ?? null; const matchingColumn = columnKey ? tableSchema.columns?.[columnKey] ?? - find(config.newColumns, { key: columnKey }) ?? - null + find(config.newColumns, { key: columnKey }) ?? + null : null; const isNewColumn = !!find(config.newColumns, { key: columnKey }); return ( diff --git a/src/components/TableModals/ImportCsvWizard/Step1Columns.tsx b/src/components/TableModals/ImportCsvWizard/Step1Columns.tsx index 6da3fbf30..c2b04cd8e 100644 --- a/src/components/TableModals/ImportCsvWizard/Step1Columns.tsx +++ b/src/components/TableModals/ImportCsvWizard/Step1Columns.tsx @@ -68,7 +68,7 @@ export default function Step1Columns({ if (selectedFields.length !== csvData.columns.length) { setSelectedFields(csvData.columns); csvData.columns.forEach(field => { - // Try to match the field to a column in the table + // Try to match each field to a column in the table const match = find(tableColumns, (column) => column.label.toLowerCase().includes(field.toLowerCase()) @@ -89,34 +89,12 @@ export default function Step1Columns({ ]; } updateConfig(columnConfig); - }) - } else { - setSelectedFields([]); - csvData.columns.forEach(field => { - // Check if this pair was already pushed to main config - const configPair = find(config.pairs, { csvKey: field }); - const configIndex = findIndex(config.pairs, { csvKey: field }); - - // Delete matching newColumn if it was created - if (configPair) { - const newColumnIndex = findIndex(config.newColumns, { - key: configPair.columnKey, - }); - if (newColumnIndex > -1) { - const newColumns = [...config.newColumns]; - newColumns.splice(newColumnIndex, 1); - setConfig((config) => ({ ...config, newColumns })); - } - } - - // Delete pair from main config - if (configIndex > -1) { - const newConfig = [...config.pairs]; - newConfig.splice(configIndex, 1); - setConfig((config) => ({ ...config, pairs: newConfig })); - } }) - }}; + } else { + setSelectedFields([]) + setConfig((config) => ({ ...config, newColumns: [], pairs: [] })) + } + }; // When a field is selected to be imported const handleSelect = @@ -241,36 +219,36 @@ export default function Step1Columns({ -
  • - - } - label="Select all" - sx={{ - height: 42, - mr: 0, - alignItems: "center", - "& .MuiFormControlLabel-label": { mt: 0, flex: 1 }, - }} - /> -
  • +
  • + + } + label={selectedFields.length == csvData.columns.length ? "Clear all" : "Select all"} + sx={{ + height: 42, + mr: 0, + alignItems: "center", + "& .MuiFormControlLabel-label": { mt: 0, flex: 1 }, + }} + /> +
  • {csvData.columns.map((field) => { const selected = selectedFields.indexOf(field) > -1; const columnKey = find(config.pairs, { csvKey: field })?.columnKey ?? null; const matchingColumn = columnKey ? tableSchema.columns?.[columnKey] ?? - find(config.newColumns, { key: columnKey }) ?? - null + find(config.newColumns, { key: columnKey }) ?? + null : null; const isNewColumn = !!find(config.newColumns, { key: columnKey }); diff --git a/src/components/TableModals/ImportExistingWizard/Step1Columns.tsx b/src/components/TableModals/ImportExistingWizard/Step1Columns.tsx index 29d5903fd..c5d832996 100644 --- a/src/components/TableModals/ImportExistingWizard/Step1Columns.tsx +++ b/src/components/TableModals/ImportExistingWizard/Step1Columns.tsx @@ -117,7 +117,7 @@ export default function Step1Columns({ config, setConfig }: IStepProps) { color="default" /> } - label="Select all" + label={selectedFields.length == allFields.length ? "Clear all" : "Select all"} sx={{ height: 42, mr: 0, From c726e7829b35a3f8f6a5313d88b00d815eedb199 Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Sat, 7 Jan 2023 13:53:45 +0000 Subject: [PATCH 062/114] getting subtable schema --- src/atoms/projectScope/project.ts | 2 +- src/atoms/projectScope/ui.ts | 2 +- .../useTableFunctions.ts | 44 ++++++++++++++++--- src/types/table.d.ts | 5 +++ 4 files changed, 45 insertions(+), 8 deletions(-) diff --git a/src/atoms/projectScope/project.ts b/src/atoms/projectScope/project.ts index 0d9518ac5..5b85fd629 100644 --- a/src/atoms/projectScope/project.ts +++ b/src/atoms/projectScope/project.ts @@ -105,7 +105,7 @@ export const deleteTableAtom = atom< /** Stores a function to get a table’s schema doc (without listener) */ export const getTableSchemaAtom = atom< - ((id: string) => Promise) | undefined + ((id: string, withSubtables?: boolean) => Promise) | undefined >(undefined); /** Roles used in the project based on table settings */ diff --git a/src/atoms/projectScope/ui.ts b/src/atoms/projectScope/ui.ts index 6d1abbc17..6f9d66d8c 100644 --- a/src/atoms/projectScope/ui.ts +++ b/src/atoms/projectScope/ui.ts @@ -141,7 +141,7 @@ export const tableSettingsDialogSchemaAtom = atom(async (get) => { const tableId = get(tableSettingsDialogIdAtom); const getTableSchema = get(getTableSchemaAtom); if (!tableId || !getTableSchema) return {} as TableSchema; - return getTableSchema(tableId); + return getTableSchema(tableId, true); }); /** Open the Get Started checklist from anywhere */ diff --git a/src/sources/ProjectSourceFirebase/useTableFunctions.ts b/src/sources/ProjectSourceFirebase/useTableFunctions.ts index ab6fb4efc..10ce973a2 100644 --- a/src/sources/ProjectSourceFirebase/useTableFunctions.ts +++ b/src/sources/ProjectSourceFirebase/useTableFunctions.ts @@ -1,7 +1,14 @@ import { useEffect, useCallback } from "react"; import { useAtom, useSetAtom } from "jotai"; import { useAtomCallback } from "jotai/utils"; -import { doc, getDoc, setDoc, deleteDoc } from "firebase/firestore"; +import { + doc, + getDoc, + setDoc, + deleteDoc, + collection, + getDocs, +} from "firebase/firestore"; import { camelCase, find, findIndex, isEmpty } from "lodash-es"; import { @@ -23,7 +30,7 @@ import { TABLE_GROUP_SCHEMAS, } from "@src/config/dbPaths"; import { rowyUser } from "@src/utils/table"; -import { TableSettings, TableSchema } from "@src/types/table"; +import { TableSettings, TableSchema, SubTablesSchema } from "@src/types/table"; import { FieldType } from "@src/constants/fields"; import { getFieldProp } from "@src/components/fields"; @@ -220,7 +227,7 @@ export function useTableFunctions() { // Set the getTableSchema function const setGetTableSchema = useSetAtom(getTableSchemaAtom, projectScope); useEffect(() => { - setGetTableSchema(() => async (id: string) => { + setGetTableSchema(() => async (id: string, withSubtables?: boolean) => { // Get latest tables const tables = (await readTables()) || []; const table = find(tables, ["id", id]); @@ -232,9 +239,34 @@ export function useTableFunctions() { : TABLE_SCHEMAS, id ); - return getDoc(tableSchemaDocRef).then( - (doc) => (doc.data() || {}) as TableSchema - ); + + let tableSchema: TableSchema | Promise = getDoc( + tableSchemaDocRef + ).then((doc) => (doc.data() || {}) as TableSchema); + + if (withSubtables) { + let subTables: SubTablesSchema | Promise = getDocs( + collection( + firebaseDb, + `${ + table?.tableType === "collectionGroup" + ? TABLE_GROUP_SCHEMAS + : TABLE_SCHEMAS + }/${id}/subTables` + ) + ).then((querySnapshot) => { + let subTables: SubTablesSchema = {}; + querySnapshot.forEach((doc) => { + subTables[doc.id] = doc.data(); + }); + return subTables; + }); + + [tableSchema, subTables] = await Promise.all([tableSchema, subTables]); + tableSchema.subTables = subTables; + } + + return tableSchema as TableSchema; }); }, [firebaseDb, readTables, setGetTableSchema]); } diff --git a/src/types/table.d.ts b/src/types/table.d.ts index 2e188ea93..e1140d185 100644 --- a/src/types/table.d.ts +++ b/src/types/table.d.ts @@ -110,10 +110,15 @@ export type TableSchema = { webhooks?: IWebhook[]; runtimeOptions?: IRuntimeOptions; + subTables?: SubTablesSchema; /** @deprecated Migrate to Extensions */ sparks?: string; }; +export type SubTablesSchema = { + [key: string]: TablesSchema; +}; + export type ColumnConfig = { /** Unique key for this column. Currently set to the same as fieldName */ key: string; From f28ca3eb0d6cde49b5a93cbad3af4ec230305fc8 Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Sat, 7 Jan 2023 16:38:31 +0000 Subject: [PATCH 063/114] import subtables --- .../useTableFunctions.ts | 67 ++++++++++++++++++- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/src/sources/ProjectSourceFirebase/useTableFunctions.ts b/src/sources/ProjectSourceFirebase/useTableFunctions.ts index 10ce973a2..a9189a473 100644 --- a/src/sources/ProjectSourceFirebase/useTableFunctions.ts +++ b/src/sources/ProjectSourceFirebase/useTableFunctions.ts @@ -8,6 +8,7 @@ import { deleteDoc, collection, getDocs, + writeBatch, } from "firebase/firestore"; import { camelCase, find, findIndex, isEmpty } from "lodash-es"; @@ -111,6 +112,33 @@ export function useTableFunctions() { { merge: true } ); + // adding subtables + const batch = writeBatch(firebaseDb); + + if (_schema?.subTables) { + const subTableCollectionRef = (id: string) => + doc( + firebaseDb, + settings.tableType !== "collectionGroup" + ? TABLE_SCHEMAS + : TABLE_GROUP_SCHEMAS, + settings.id, + "subTables", + id + ); + Object.keys(_schema.subTables).forEach((subTableId: string) => { + if (_schema.subTables) { + console.log(subTableId); + batch.set( + subTableCollectionRef(subTableId), + _schema.subTables[subTableId] + ); + } + }); + + delete _schema.subTables; + } + // Creates schema doc with columns const { functionConfigPath, functionBuilderRef, ...schemaToWrite } = _schema ?? {}; @@ -128,7 +156,11 @@ export function useTableFunctions() { ); // Wait for both to complete - await Promise.all([promiseUpdateSettings, promiseAddSchema]); + await Promise.all([ + promiseUpdateSettings, + promiseAddSchema, + batch.commit(), + ]); } ); }, [currentUser, firebaseDb, readTables, setCreateTable]); @@ -174,6 +206,33 @@ export function useTableFunctions() { { merge: true } ); + // adding subtables + const batch = writeBatch(firebaseDb); + + if (_schema?.subTables) { + const subTableCollectionRef = (id: string) => + doc( + firebaseDb, + settings.tableType !== "collectionGroup" + ? TABLE_SCHEMAS + : TABLE_GROUP_SCHEMAS, + settings.id, + "subTables", + id + ); + Object.keys(_schema.subTables).forEach((subTableId: string) => { + if (_schema.subTables) { + console.log(subTableId); + batch.set( + subTableCollectionRef(subTableId), + _schema.subTables[subTableId] + ); + } + }); + + delete _schema.subTables; + } + // Updates schema doc if param is provided const { functionConfigPath, functionBuilderRef, ...schemaToWrite } = _schema ?? {}; @@ -189,7 +248,11 @@ export function useTableFunctions() { : await setDoc(tableSchemaDocRef, schemaToWrite, { merge: true }); // Wait for both to complete - await Promise.all([promiseUpdateSettings, promiseUpdateSchema]); + await Promise.all([ + promiseUpdateSettings, + promiseUpdateSchema, + batch.commit(), + ]); } ); }, [firebaseDb, readTables, setUpdateTable]); From 818759e082f4c62105c9fac406be0564598ce1c2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Jan 2023 05:44:03 +0000 Subject: [PATCH 064/114] Bump json5 from 1.0.1 to 1.0.2 Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2. - [Release notes](https://github.com/json5/json5/releases) - [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md) - [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2) --- updated-dependencies: - dependency-name: json5 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index d669d494d..8ff8ab683 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8048,9 +8048,9 @@ json2csv@^5.0.7: lodash.get "^4.4.2" json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== dependencies: minimist "^1.2.0" From afbd8f489a9b8ea03213e927fda2b4f5c85fb9e3 Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Sun, 8 Jan 2023 06:36:36 +0000 Subject: [PATCH 065/114] removed logs --- src/sources/ProjectSourceFirebase/useTableFunctions.ts | 2 -- src/types/table.d.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/sources/ProjectSourceFirebase/useTableFunctions.ts b/src/sources/ProjectSourceFirebase/useTableFunctions.ts index a9189a473..3269ccb5e 100644 --- a/src/sources/ProjectSourceFirebase/useTableFunctions.ts +++ b/src/sources/ProjectSourceFirebase/useTableFunctions.ts @@ -128,7 +128,6 @@ export function useTableFunctions() { ); Object.keys(_schema.subTables).forEach((subTableId: string) => { if (_schema.subTables) { - console.log(subTableId); batch.set( subTableCollectionRef(subTableId), _schema.subTables[subTableId] @@ -222,7 +221,6 @@ export function useTableFunctions() { ); Object.keys(_schema.subTables).forEach((subTableId: string) => { if (_schema.subTables) { - console.log(subTableId); batch.set( subTableCollectionRef(subTableId), _schema.subTables[subTableId] diff --git a/src/types/table.d.ts b/src/types/table.d.ts index e1140d185..46a5a939e 100644 --- a/src/types/table.d.ts +++ b/src/types/table.d.ts @@ -116,7 +116,7 @@ export type TableSchema = { }; export type SubTablesSchema = { - [key: string]: TablesSchema; + [key: string]: TableSchema; }; export type ColumnConfig = { From d9a1393a83237bff4be9be99ce1e3d7429efeced Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Mon, 9 Jan 2023 15:57:56 +1300 Subject: [PATCH 066/114] update code template style and indentation --- .../CodeEditor/CodeEditorHelper.tsx | 100 +++++++------ .../ColumnConfigModal/DefaultValueInput.tsx | 21 +-- .../TableModals/ExtensionsModal/utils.ts | 140 ++++++++++-------- .../WebhooksModal/AddWebhookButton.tsx | 2 +- .../WebhooksModal/Schemas/basic.tsx | 49 +++--- .../WebhooksModal/Schemas/sendgrid.tsx | 42 +++--- .../WebhooksModal/Schemas/stripe.tsx | 25 ++-- .../WebhooksModal/Schemas/typeform.tsx | 112 +++++++------- .../WebhooksModal/Schemas/webform.tsx | 50 +++---- .../WebhooksModal/WebhookModal.tsx | 2 +- .../TableModals/WebhooksModal/utils.tsx | 2 +- src/components/fields/Action/templates.ts | 107 ++++++------- src/components/fields/Connector/utils.ts | 4 +- src/components/fields/Derivative/Settings.tsx | 20 ++- src/components/fields/Formula/Settings.tsx | 36 ++--- src/components/fields/Formula/util.tsx | 7 +- 16 files changed, 376 insertions(+), 343 deletions(-) diff --git a/src/components/CodeEditor/CodeEditorHelper.tsx b/src/components/CodeEditor/CodeEditorHelper.tsx index 04326e3c9..caf3fbc05 100644 --- a/src/components/CodeEditor/CodeEditorHelper.tsx +++ b/src/components/CodeEditor/CodeEditorHelper.tsx @@ -9,6 +9,9 @@ import { projectScope, projectIdAtom } from "@src/atoms/projectScope"; export interface ICodeEditorHelperProps { docLink: string; + disableDefaultVariables?: boolean; + disableSecretManagerLink?: boolean; + disableCloudManagerLink?: boolean; additionalVariables?: { key: string; description: string; @@ -17,32 +20,37 @@ export interface ICodeEditorHelperProps { export default function CodeEditorHelper({ docLink, + disableDefaultVariables, + disableSecretManagerLink, + disableCloudManagerLink, additionalVariables, }: ICodeEditorHelperProps) { const [projectId] = useAtom(projectIdAtom, projectScope); - const availableVariables = [ - { - key: "db", - description: `db object provides access to firestore database instance of this project. giving you access to any collection or document in this firestore instance`, - }, - { - key: "auth", - description: `auth provides access to a firebase auth instance, can be used to manage auth users or generate tokens.`, - }, - { - key: "storage", - description: `firebase Storage can be accessed through this, storage.bucket() returns default storage bucket of the firebase project.`, - }, - { - key: "rowy", - description: `rowy provides a set of functions that are commonly used, such as easy file uploads & access to GCP Secret Manager`, - }, - { - key: "logging", - description: `logging.log is encouraged to replace console.log`, - }, - ]; + const availableVariables = disableDefaultVariables + ? [] + : [ + { + key: "db", + description: `db object provides access to firestore database instance of this project. giving you access to any collection or document in this firestore instance`, + }, + { + key: "auth", + description: `auth provides access to a firebase auth instance, can be used to manage auth users or generate tokens.`, + }, + { + key: "storage", + description: `firebase Storage can be accessed through this, storage.bucket() returns default storage bucket of the firebase project.`, + }, + { + key: "rowy", + description: `rowy provides a set of functions that are commonly used, such as easy file uploads & access to GCP Secret Manager`, + }, + { + key: "logging", + description: `logging.log is encouraged to replace console.log`, + }, + ]; return ( - - - - - + {!disableSecretManagerLink && ( + + + + + + )} - - - - - + {!disableCloudManagerLink && ( + + + + + + )} { - ${column.config?.defaultValue.script} - }`; + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + + ${column.config?.defaultValue.script} + // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY +}`; } else { dynamicValueFn = `const dynamicValueFn : DefaultValue = async ({row,ref,db,storage,auth,logging})=>{ - // Write your default value code here - // for example: - // generate random hex color - // const color = "#" + Math.floor(Math.random() * 16777215).toString(16); - // return color; - // checkout the documentation for more info: https://docs.rowy.io/how-to/default-values#dynamic - }`; + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + + // Example: generate random hex color + // const color = "#" + Math.floor(Math.random() * 16777215).toString(16); + // return color; + // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY +}`; } return ( diff --git a/src/components/TableModals/ExtensionsModal/utils.ts b/src/components/TableModals/ExtensionsModal/utils.ts index 50daaf90d..6fe636cf5 100644 --- a/src/components/TableModals/ExtensionsModal/utils.ts +++ b/src/components/TableModals/ExtensionsModal/utils.ts @@ -62,50 +62,53 @@ export const triggerTypes: ExtensionTrigger[] = ["create", "update", "delete"]; const extensionBodyTemplate = { task: `const extensionBody: TaskBody = async({row, db, change, ref, logging}) => { - // task extensions are very flexible you can do anything from updating other documents in your database, to making an api request to 3rd party service. + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY - // example: - // we can post notification to different discord channels based on row data + // Task Extension is very flexible, you can do anything. + // From updating other documents in your database, to making an api request to 3rd party service. + // Example: post notification to different discord channels based on row data /* const topic = row.topic; const channel = await db.collection('discordChannels').doc(topic).get(); const channelUrl = await channel.get("channelUrl"); const content = "Hello discord channel"; - return fetch("https://discord.com/api/webhooks/"+channelUrl, { - { - method: "POST", - headers: { - "Content-Type": "application/json" - }, - body: JSON.stringify({ - content - }) - }).then(async resp => { - const result = await resp.json() - if (resp.ok) console.info(result) - else console.error(result) + return fetch("https://discord.com/api/webhooks/"+channelUrl, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + content }) + }).then(async resp => { + const result = await resp.json() + if (resp.ok) console.info(result) + else console.error(result) + }) */ + // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY }`, docSync: `const extensionBody: DocSyncBody = async({row, db, change, ref, logging}) => { - // feel free to add your own code logic here + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY return ({ fieldsToSync: [], // a list of string of column names row: row, // object of data to sync, usually the row itself targetPath: "", // fill in the path here }) + // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY }`, historySnapshot: `const extensionBody: HistorySnapshotBody = async({row, db, change, ref, logging}) => { - // feel free to add your own code logic here + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY return ({ trackedFields: [], // a list of string of column names collectionId: "historySnapshots", // optionally change the sub-collection id of where the history snapshots are stored }) + // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY }`, algoliaIndex: `const extensionBody: AlgoliaIndexBody = async({row, db, change, ref, logging}) => { - // feel free to add your own code logic here + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY return ({ fieldsToSync: [], // a list of string of column names @@ -113,9 +116,10 @@ const extensionBodyTemplate = { index: "", // algolia index to sync to objectID: ref.id, // algolia object ID, ref.id is one possible choice }) + // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY }`, meiliIndex: `const extensionBody: MeiliIndexBody = async({row, db, change, ref, logging}) => { - // feel free to add your own code logic here + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY return({ fieldsToSync: [], // a list of string of column names @@ -123,9 +127,10 @@ const extensionBodyTemplate = { index: "", // algolia index to sync to objectID: ref.id, // algolia object ID, ref.id is one possible choice }) + // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY }`, bigqueryIndex: `const extensionBody: BigqueryIndexBody = async({row, db, change, ref, logging}) => { - // feel free to add your own code logic here + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY return ({ fieldsToSync: [], // a list of string of column names @@ -133,9 +138,10 @@ const extensionBodyTemplate = { index: "", // algolia index to sync to objectID: ref.id, // algolia object ID, ref.id is one possible choice }) + // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY }`, slackMessage: `const extensionBody: SlackMessageBody = async({row, db, change, ref, logging}) => { - // feel free to add your own code logic here + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY return ({ channels: [], // a list of slack channel IDs in string @@ -143,18 +149,19 @@ const extensionBodyTemplate = { text: "", // the text parameter to pass in to slack api attachments: [], // the attachments parameter to pass in to slack api }) + // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY }`, sendgridEmail: `const extensionBody: SendgridEmailBody = async({row, db, change, ref, logging}) => { - // feel free to add your own code logic here + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY return ({ from: "Name", // send from field personalizations: [ - { - to: [{ name: "", email: "" }], // recipient - dynamic_template_data: { - }, // template parameters - }, + { + to: [{ name: "", email: "" }], // recipient + dynamic_template_data: { + }, // template parameters + }, ], template_id: "", // sendgrid template ID categories: [], // helper info to categorise sendgrid emails @@ -163,9 +170,10 @@ const extensionBodyTemplate = { // add any other custom args you want to pass to sendgrid events here }, }) + // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY }`, apiCall: `const extensionBody: ApiCallBody = async({row, db, change, ref, logging}) => { - // feel free to add your own code logic here + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY return ({ body: "", @@ -173,56 +181,56 @@ const extensionBodyTemplate = { method: "", callback: ()=>{}, }) + // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY }`, twilioMessage: `const extensionBody: TwilioMessageBody = async({row, db, change, ref, logging}) => { - /** - * - * Setup twilio secret key: https://docs.rowy.io/extensions/twilio-message#secret-manager-setup - * - * You can add any code logic here to be able to customize your message - * or dynamically get the from or to numbers - * - **/ + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + // Setup twilio secret key: https://docs.rowy.io/extensions/twilio-message#secret-manager-setup + // Add any code here to customize your message or dynamically get the from/to numbers return ({ from: "", // from phone number registered on twilio to: "", // recipient phone number - eg: row. body: "Hi there!" // message text }) + // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY }`, pushNotification: `const extensionBody: PushNotificationBody = async({row, db, change, ref, logging}) => { - // you can FCM token from the row or from the user document in the database + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + + // You can use FCM token from the row or from the user document in the database // const FCMtoken = row.FCMtoken - // or push through topic + // Or push through topic const topicName = 'industry-tech'; - // you can return single or array of notification payloads - return [{ - notification: { - title: 'Hello!', - }, - android: { + // You can return single or array of notification payloads + return [{ notification: { - imageUrl: 'https://thiscatdoesnotexist.com/' - } - }, - apns: { - payload: { - aps: { - 'mutable-content': 1 + title: 'Hello!', + }, + android: { + notification: { + imageUrl: 'https://thiscatdoesnotexist.com/' } }, - fcm_options: { - image: 'https://thiscatdoesnotexist.com/' - } - }, - webpush: { - headers: { - image: 'https://thiscatdoesnotexist.com/' - } - }, - // topic: topicName, // add topic send to subscribers - // token: FCMtoken // add FCM token to send to specific user -}] + apns: { + payload: { + aps: { + 'mutable-content': 1 + } + }, + fcm_options: { + image: 'https://thiscatdoesnotexist.com/' + } + }, + webpush: { + headers: { + image: 'https://thiscatdoesnotexist.com/' + } + }, + // topic: topicName, // add topic send to subscribers + // token: FCMtoken // add FCM token to send to specific user + }] + // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY }`, }; @@ -239,8 +247,10 @@ export function emptyExtensionObject( requiredFields: [], trackedFields: [], conditions: `const condition: Condition = async({row, change, logging}) => { - // feel free to add your own code logic here + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + return true; + // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY }`, lastEditor: user, }; diff --git a/src/components/TableModals/WebhooksModal/AddWebhookButton.tsx b/src/components/TableModals/WebhooksModal/AddWebhookButton.tsx index 0d8a63d5d..64c2f15d3 100644 --- a/src/components/TableModals/WebhooksModal/AddWebhookButton.tsx +++ b/src/components/TableModals/WebhooksModal/AddWebhookButton.tsx @@ -48,7 +48,7 @@ export default function AddWebhookButton({ }} {...props} > - Add webhook… + Add Webhook… `const basicParser: Parser = async({req, db, ref, logging}) => { - // request is the request object from the webhook - // db is the database object - // ref is the reference to collection of the table - // the returned object will be added as a new row to the table - // eg: adding the webhook body as row - const {body} = req; - ${ - table.audit !== false - ? ` - // auditField - const ${ - table.auditFieldCreatedBy ?? "_createdBy" - } = await rowy.metadata.serviceAccountUser() - return { - ...body, - ${table.auditFieldCreatedBy ?? "_createdBy"} - } - ` - : ` - return body; - ` - } - }`, + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + + // Optionally return an object to be added as a new row to the table + // Example: add the webhook body as row + const {body} = req; + ${ + table.audit !== false + ? `const ${ + table.auditFieldCreatedBy ?? "_createdBy" + } = await rowy.metadata.serviceAccountUser() + return { + ...body, + ${table.auditFieldCreatedBy ?? "_createdBy"} + }` + : `return body;` + } + // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY +}`, }, condition: { additionalVariables, @@ -95,9 +90,11 @@ export const webhookBasic = { template: ( table: TableSettings ) => `const condition: Condition = async({ref, req, db, logging}) => { - // feel free to add your own code logic here - return true; - }`, + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + + return true; + // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY +}`, }, auth: ( webhookObject: IWebhook, diff --git a/src/components/TableModals/WebhooksModal/Schemas/sendgrid.tsx b/src/components/TableModals/WebhooksModal/Schemas/sendgrid.tsx index b251697fe..bafa321ae 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/sendgrid.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/sendgrid.tsx @@ -14,21 +14,25 @@ export const webhookSendgrid = { template: ( table: TableSettings ) => `const sendgridParser: Parser = async ({ req, db, ref, logging }) => { - const { body } = req - const eventHandler = async (sgEvent) => { - // Event handlers can be modiefed to preform different actions based on the sendgrid event - // List of events & docs : https://docs.sendgrid.com/for-developers/tracking-events/event#events - const { event, docPath } = sgEvent - // event param is provided by default - // however docPath or other custom parameter needs be passed in the custom_args variable in Sengrid Extension - return db.doc(docPath).update({ sgStatus: event }) - } - // - if (Array.isArray(body)) { - // when multiple events are passed in one call - await Promise.allSettled(body.map(eventHandler)) - } else eventHandler(body) - };`, + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + + const { body } = req + const eventHandler = async (sgEvent) => { + // Event handlers can be modiefed to preform different actions based on the sendgrid event + // List of events & docs : https://docs.sendgrid.com/for-developers/tracking-events/event#events + const { event, docPath } = sgEvent + // Event param is provided by default + // However docPath or other custom parameter needs be passed in the custom_args variable in Sengrid Extension + return db.doc(docPath).update({ sgStatus: event }) + } + if (Array.isArray(body)) { + // Multiple events are passed in one call + await Promise.allSettled(body.map(eventHandler)) + } else { + eventHandler(body) + } + // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY +};`, }, condition: { additionalVariables: null, @@ -36,9 +40,11 @@ export const webhookSendgrid = { template: ( table: TableSettings ) => `const condition: Condition = async({ref, req, db, logging}) => { - // feel free to add your own code logic here - return true; - }`, + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + + return true; + // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY +}`, }, auth: ( webhookObject: IWebhook, diff --git a/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx b/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx index acfb2dfe3..e77e1df7b 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx @@ -18,15 +18,18 @@ export const webhookStripe = { template: ( table: TableSettings ) => `const sendgridParser: Parser = async ({ req, db, ref, logging }) => { - const event = req.body - switch (event.type) { - case "payment_intent.succeeded": - break; - case "payment_intent.payment_failed": - break; - default: - // all other types - } + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + + const event = req.body + switch (event.type) { + case "payment_intent.succeeded": + break; + case "payment_intent.payment_failed": + break; + default: + // All other types + // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY +} };`, }, condition: { @@ -35,8 +38,10 @@ export const webhookStripe = { template: ( table: TableSettings ) => `const condition: Condition = async({ref, req, db, logging}) => { - // feel free to add your own code logic here + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + return true; + // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY }`, }, auth: ( diff --git a/src/components/TableModals/WebhooksModal/Schemas/typeform.tsx b/src/components/TableModals/WebhooksModal/Schemas/typeform.tsx index 9c509efd6..bf42b7813 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/typeform.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/typeform.tsx @@ -14,59 +14,57 @@ export const webhookTypeform = { template: ( table: TableSettings ) => `const typeformParser: Parser = async({req, db, ref, logging}) =>{ - // this reduces the form submission into a single object of key value pairs - // eg: {name: "John", age: 20} - // ⚠️ ensure that you have assigned ref values of the fields - // set the ref value to field key you would like to sync to - // docs: https://help.typeform.com/hc/en-us/articles/360050447552-Block-reference-format-restrictions - const {submitted_at,hidden,answers} = req.body.form_response - const submission = ({ - _createdAt: submitted_at, - ...hidden, - ...answers.reduce((accRow, currAnswer) => { - switch (currAnswer.type) { - case "date": - return { - ...accRow, - [currAnswer.field.ref]: new Date(currAnswer[currAnswer.type]), - }; - case "choice": - return { - ...accRow, - [currAnswer.field.ref]: currAnswer[currAnswer.type].label, - }; - case "choices": - return { - ...accRow, - [currAnswer.field.ref]: currAnswer[currAnswer.type].labels, - }; - case "file_url": - default: - return { - ...accRow, - [currAnswer.field.ref]: currAnswer[currAnswer.type], - }; - } - }, {}), - }) - - ${ - table.audit !== false - ? ` - // auditField - const ${ - table.auditFieldCreatedBy ?? "_createdBy" - } = await rowy.metadata.serviceAccountUser() - return { - ...submission, - ${table.auditFieldCreatedBy ?? "_createdBy"} - } - ` - : ` - return submission - ` - } - };`, + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + + // This reduces the form submission into a single object of key value pairs + // Example: {name: "John", age: 20} + // ⚠️ Ensure that you have assigned ref values of the fields + // Set the ref value to field key you would like to sync to + // Docs: https://help.typeform.com/hc/en-us/articles/360050447552-Block-reference-format-restrictions + const {submitted_at,hidden,answers} = req.body.form_response + const submission = ({ + _createdAt: submitted_at, + ...hidden, + ...answers.reduce((accRow, currAnswer) => { + switch (currAnswer.type) { + case "date": + return { + ...accRow, + [currAnswer.field.ref]: new Date(currAnswer[currAnswer.type]), + }; + case "choice": + return { + ...accRow, + [currAnswer.field.ref]: currAnswer[currAnswer.type].label, + }; + case "choices": + return { + ...accRow, + [currAnswer.field.ref]: currAnswer[currAnswer.type].labels, + }; + case "file_url": + default: + return { + ...accRow, + [currAnswer.field.ref]: currAnswer[currAnswer.type], + }; + } + }, {}), + }) + + ${ + table.audit !== false + ? `const ${ + table.auditFieldCreatedBy ?? "_createdBy" + } = await rowy.metadata.serviceAccountUser() + return { + ...submission, + ${table.auditFieldCreatedBy ?? "_createdBy"} + }` + : `return submission;` + } + // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY +};`, }, condition: { additionalVariables: null, @@ -74,9 +72,11 @@ export const webhookTypeform = { template: ( table: TableSettings ) => `const condition: Condition = async({ref, req, db, logging}) => { - // feel free to add your own code logic here - return true; - }`, + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + + return true; + // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY +}`, }, auth: ( webhookObject: IWebhook, diff --git a/src/components/TableModals/WebhooksModal/Schemas/webform.tsx b/src/components/TableModals/WebhooksModal/Schemas/webform.tsx index 7bed061de..abad9c292 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/webform.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/webform.tsx @@ -15,30 +15,24 @@ export const webhook = { template: ( table: TableSettings ) => `const formParser: Parser = async({req, db, ref, logging}) => { - // request is the request object from the webhook - // db is the database object - // ref is the reference to collection of the table - // the returned object will be added as a new row to the table - // eg: adding the webhook body as row - const {body} = req; - ${ - table.audit !== false - ? ` - // auditField - const ${ - table.auditFieldCreatedBy ?? "_createdBy" - } = await rowy.metadata.serviceAccountUser() - return { - ...body, - ${table.auditFieldCreatedBy ?? "_createdBy"} - } - ` - : ` - return body; - ` - } - - }`, + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + + // Optionally return an object to be added as a new row to the table + // Example: add the webhook body as row + const {body} = req; + ${ + table.audit !== false + ? `const ${ + table.auditFieldCreatedBy ?? "_createdBy" + } = await rowy.metadata.serviceAccountUser() + return { + ...body, + ${table.auditFieldCreatedBy ?? "_createdBy"} + }` + : `return body;` + } + // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY +}`, }, condition: { additionalVariables: null, @@ -46,9 +40,11 @@ export const webhook = { template: ( table: TableSettings ) => `const condition: Condition = async({ref, req, db, logging}) => { - // feel free to add your own code logic here - return true; - }`, + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + + return true; + // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY +}`, }, auth: ( webhookObject: IWebhook, diff --git a/src/components/TableModals/WebhooksModal/WebhookModal.tsx b/src/components/TableModals/WebhooksModal/WebhookModal.tsx index 8575c0b5c..01c2f2901 100644 --- a/src/components/TableModals/WebhooksModal/WebhookModal.tsx +++ b/src/components/TableModals/WebhooksModal/WebhookModal.tsx @@ -86,7 +86,7 @@ export default function WebhookModal({ disableBackdropClick disableEscapeKeyDown fullWidth - title={`${mode === "add" ? "Add" : "Update"} webhook: ${ + title={`${mode === "add" ? "Add" : "Update"} Webhook: ${ webhookNames[webhookObject.type] }`} sx={{ diff --git a/src/components/TableModals/WebhooksModal/utils.tsx b/src/components/TableModals/WebhooksModal/utils.tsx index c4292604c..e16ca1d9c 100644 --- a/src/components/TableModals/WebhooksModal/utils.tsx +++ b/src/components/TableModals/WebhooksModal/utils.tsx @@ -72,7 +72,7 @@ export const webhookNames: Record = { // twitter: "Twitter", stripe: "Stripe", basic: "Basic", - webform: "Web form", + webform: "Web Form", }; export interface IWebhookEditor { diff --git a/src/components/fields/Action/templates.ts b/src/components/fields/Action/templates.ts index 04b3ab233..9d8779c85 100644 --- a/src/components/fields/Action/templates.ts +++ b/src/components/fields/Action/templates.ts @@ -1,54 +1,59 @@ export const RUN_ACTION_TEMPLATE = `const action:Action = async ({row,ref,db,storage,auth,actionParams,user,logging}) => { - // Write your action code here - // for example: - // const authToken = await rowy.secrets.get("service") - // try { - // const resp = await fetch('https://example.com/api/v1/users/'+ref.id,{ - // method: 'PUT', - // headers: { - // 'Content-Type': 'application/json', - // 'Authorization': authToken - // }, - // body: JSON.stringify(row) - // }) - // - // return { - // success: true, - // message: 'User updated successfully on example service', - // status: "upto date" - // } - // } catch (error) { - // return { - // success: false, - // message: 'User update failed on example service', - // } - // } - // checkout the documentation for more info: https://docs.rowy.io/field-types/action#script - }`; + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + + // Example: + /* + const authToken = await rowy.secrets.get("service") + try { + const resp = await fetch('https://example.com/api/v1/users/'+ref.id,{ + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + 'Authorization': authToken + }, + body: JSON.stringify(row) + }) + return { + success: true, + message: 'User updated successfully on example service', + status: "upto date" + } + } catch (error) { + return { + success: false, + message: 'User update failed on example service', + } + } + */ + // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY +}`; export const UNDO_ACTION_TEMPLATE = `const action : Action = async ({row,ref,db,storage,auth,actionParams,user,logging}) => { - // Write your undo code here - // for example: - // const authToken = await rowy.secrets.get("service") - // try { - // const resp = await fetch('https://example.com/api/v1/users/'+ref.id,{ - // method: 'DELETE', - // headers: { - // 'Content-Type': 'application/json', - // 'Authorization': authToken - // }, - // body: JSON.stringify(row) - // }) - // - // return { - // success: true, - // message: 'User deleted successfully on example service', - // status: null - // } - // } catch (error) { - // return { - // success: false, - // message: 'User delete failed on example service', - // } - // } - }`; + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + + // Example: + /* + const authToken = await rowy.secrets.get("service") + try { + const resp = await fetch('https://example.com/api/v1/users/'+ref.id,{ + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'Authorization': authToken + }, + body: JSON.stringify(row) + }) + return { + success: true, + message: 'User deleted successfully on example service', + status: null + } + } catch (error) { + return { + success: false, + message: 'User delete failed on example service', + } + } + */ + // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY +}`; diff --git a/src/components/fields/Connector/utils.ts b/src/components/fields/Connector/utils.ts index 889598951..a9668ccb8 100644 --- a/src/components/fields/Connector/utils.ts +++ b/src/components/fields/Connector/utils.ts @@ -12,8 +12,10 @@ export const replacer = (data: any) => (m: string, key: string) => { }; export const baseFunction = `const connectorFn: Connector = async ({query, row, user, logging}) => { - // TODO: Implement your service function here + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + return []; + // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY };`; export const getLabel = (config: any, row: TableRow) => { diff --git a/src/components/fields/Derivative/Settings.tsx b/src/components/fields/Derivative/Settings.tsx index 7fe368694..14cbd3932 100644 --- a/src/components/fields/Derivative/Settings.tsx +++ b/src/components/fields/Derivative/Settings.tsx @@ -66,15 +66,19 @@ export default function Settings({ ? config.derivativeFn : config?.script ? `const derivative:Derivative = async ({row,ref,db,storage,auth,logging})=>{ - ${config.script.replace(/utilFns.getSecret/g, "rowy.secrets.get")} - }` + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + + ${config.script.replace(/utilFns.getSecret/g, "rowy.secrets.get")} + // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY +}` : `const derivative:Derivative = async ({row,ref,db,storage,auth,logging})=>{ - // Write your derivative code here - // for example: - // const sum = row.a + row.b; - // return sum; - // checkout the documentation for more info: https://docs.rowy.io/field-types/derivative - }`; + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + + // Example: + // const sum = row.a + row.b; + // return sum; + // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY +}`; return ( <> diff --git a/src/components/fields/Formula/Settings.tsx b/src/components/fields/Formula/Settings.tsx index ad0eb16fb..360c59813 100644 --- a/src/components/fields/Formula/Settings.tsx +++ b/src/components/fields/Formula/Settings.tsx @@ -22,6 +22,8 @@ import { getFieldProp } from ".."; /* eslint-disable import/no-webpack-loader-syntax */ import formulaDefs from "!!raw-loader!./formula.d.ts"; +import { WIKI_LINKS } from "@src/constants/externalLinks"; +import CodeEditorHelper from "@src/components/CodeEditor/CodeEditorHelper"; const CodeEditor = lazy( () => @@ -97,28 +99,18 @@ export default function Settings({ Formula script
    - - - Available: - - - - - row - - - - + }> { - // Write your formula code here - // for example: + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + + // Example: // return row.a + row.b; - // checkout the documentation for more info: https://docs.rowy.io/field-types/formula + // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY } `; From 6ff51fe86c87544843e7aecab18edd0da0a810c9 Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Mon, 9 Jan 2023 20:46:02 +0530 Subject: [PATCH 067/114] fix: optimistic update (#1052) * fix: optimistic update * added guard and changed useUpdateAtom to useSetAtom * removed useUpdateAtom * extract updateTableSchema init * remove unnecessary deps Co-authored-by: Han Tuerker --- src/hooks/useFirestoreDocWithAtom.ts | 2 +- .../TableSourceFirestore.tsx | 77 ++++++++++++++++--- 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/src/hooks/useFirestoreDocWithAtom.ts b/src/hooks/useFirestoreDocWithAtom.ts index 00c51e60a..c5c2b51cc 100644 --- a/src/hooks/useFirestoreDocWithAtom.ts +++ b/src/hooks/useFirestoreDocWithAtom.ts @@ -170,7 +170,7 @@ export default useFirestoreDocWithAtom; * Create the Firestore document reference. * Put code in a function so the results can be compared by useMemoValue. */ -const getDocRef = ( +export const getDocRef = ( firebaseDb: Firestore, path: string | undefined, pathSegments?: Array diff --git a/src/sources/TableSourceFirestore/TableSourceFirestore.tsx b/src/sources/TableSourceFirestore/TableSourceFirestore.tsx index 557fb75e4..9f3e2cedf 100644 --- a/src/sources/TableSourceFirestore/TableSourceFirestore.tsx +++ b/src/sources/TableSourceFirestore/TableSourceFirestore.tsx @@ -1,6 +1,15 @@ -import { memo, useCallback } from "react"; -import { useAtom } from "jotai"; -import { FirestoreError } from "firebase/firestore"; +import { memo, useCallback, useEffect } from "react"; +import { useAtom, useSetAtom } from "jotai"; +import useMemoValue from "use-memo-value"; +import { cloneDeep, set } from "lodash-es"; +import { + FirestoreError, + deleteField, + refEqual, + setDoc, +} from "firebase/firestore"; +import { useSnackbar } from "notistack"; +import { useErrorHandler } from "react-error-boundary"; import { tableScope, @@ -16,29 +25,81 @@ import { tableNextPageAtom, serverDocCountAtom, } from "@src/atoms/tableScope"; -import useFirestoreDocWithAtom from "@src/hooks/useFirestoreDocWithAtom"; + +import useFirestoreDocWithAtom, { + getDocRef, +} from "@src/hooks/useFirestoreDocWithAtom"; import useFirestoreCollectionWithAtom from "@src/hooks/useFirestoreCollectionWithAtom"; import useAuditChange from "./useAuditChange"; import useBulkWriteDb from "./useBulkWriteDb"; import { handleFirestoreError } from "./handleFirestoreError"; -import { useSnackbar } from "notistack"; -import { useErrorHandler } from "react-error-boundary"; import { getTableSchemaPath } from "@src/utils/table"; +import { TableSchema } from "@src/types/table"; +import { firebaseDbAtom } from "@src/sources/ProjectSourceFirebase"; +import { projectScope } from "@src/atoms/projectScope"; /** * When rendered, provides atom values for top-level tables and sub-tables */ export const TableSourceFirestore = memo(function TableSourceFirestore() { - // Get tableSettings from tableId and tables in projectScope + const [firebaseDb] = useAtom(firebaseDbAtom, projectScope); const [tableSettings] = useAtom(tableSettingsAtom, tableScope); + const setTableSchema = useSetAtom(tableSchemaAtom, tableScope); + const setUpdateTableSchema = useSetAtom(updateTableSchemaAtom, tableScope); + + const { enqueueSnackbar } = useSnackbar(); + if (!tableSettings) throw new Error("No table config"); if (!tableSettings.collection) throw new Error("Invalid table config: no collection"); + const tableSchemaDocRef = useMemoValue( + getDocRef(firebaseDb, getTableSchemaPath(tableSettings)), + (next, prev) => refEqual(next as any, prev as any) + ); const isCollectionGroup = tableSettings.tableType === "collectionGroup"; + useEffect(() => { + if (!tableSchemaDocRef) return; + + setUpdateTableSchema( + () => (update: TableSchema, deleteFields?: string[]) => { + const updateToDb = cloneDeep(update); + + if (Array.isArray(deleteFields)) { + for (const field of deleteFields) { + // Use deterministic set firestore sentinel's on schema columns config + // Required for nested columns + // i.e field = "columns.base.nested.nested" + // key: columns, rest: base.nested.nested + // set columns["base.nested.nested"] instead columns.base.nested.nested + const [key, ...rest] = field.split("."); + if (key === "columns") { + (updateToDb as any).columns[rest.join(".")] = deleteField(); + } else { + set(updateToDb, field, deleteField()); + } + } + } + + // Update UI state to reflect changes immediately to prevent flickering effects + setTableSchema((tableSchema) => ({ ...tableSchema, ...update })); + + return setDoc(tableSchemaDocRef, updateToDb, { merge: true }).catch( + (e) => { + enqueueSnackbar((e as Error).message, { variant: "error" }); + } + ); + } + ); + + return () => { + setUpdateTableSchema(undefined); + }; + }, [tableSchemaDocRef, setTableSchema, setUpdateTableSchema, enqueueSnackbar]); + // Get tableSchema and store in tableSchemaAtom. // If it doesn’t exist, initialize columns useFirestoreDocWithAtom( @@ -47,7 +108,6 @@ export const TableSourceFirestore = memo(function TableSourceFirestore() { getTableSchemaPath(tableSettings), { createIfNonExistent: { columns: {} }, - updateDataAtom: updateTableSchemaAtom, disableSuspense: true, } ); @@ -58,7 +118,6 @@ export const TableSourceFirestore = memo(function TableSourceFirestore() { const [page] = useAtom(tablePageAtom, tableScope); // Get documents from collection and store in tableRowsDbAtom // and handle some errors with snackbars - const { enqueueSnackbar } = useSnackbar(); const elevateError = useErrorHandler(); const handleErrorCallback = useCallback( (error: FirestoreError) => From 5ce182a0ec52d59cb7dd7a55065d9be6a61f7c27 Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Sat, 14 Jan 2023 11:19:55 +0000 Subject: [PATCH 068/114] Fix: copy/paste is picking up all fields in a subtable as subtable type --- package.json | 1 - src/components/Table/Table.tsx | 36 +++++---- src/components/Table/useHotKeys.tsx | 99 +++++++++++++++++++++++++ src/contexts/TableKbShortcutContext.tsx | 69 +++++------------ yarn.lock | 5 -- 5 files changed, 140 insertions(+), 70 deletions(-) create mode 100644 src/components/Table/useHotKeys.tsx diff --git a/package.json b/package.json index 53fb464e5..4fac66726 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,6 @@ "dependencies": { "@emotion/react": "^11.10.5", "@emotion/styled": "^11.10.5", - "@mantine/hooks": "^5.10.0", "@mdi/js": "^6.6.96", "@monaco-editor/react": "^4.4.4", "@mui/icons-material": "^5.10.16", diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 0b772a2b5..76d504b5c 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -30,12 +30,14 @@ import { tableNextPageAtom, tablePageAtom, updateColumnAtom, + selectedCellAtom, } from "@src/atoms/tableScope"; +import { useMenuAction } from "@src/contexts/TableKbShortcutContext"; import { getFieldType, getFieldProp } from "@src/components/fields"; import { useKeyboardNavigation } from "./useKeyboardNavigation"; import { useSaveColumnSizing } from "./useSaveColumnSizing"; +import useHotKeys from "./useHotKeys"; import type { TableRow, ColumnConfig } from "@src/types/table"; -import { TableKbShortcutProvider } from "@src/contexts/TableKbShortcutContext"; export const DEFAULT_ROW_HEIGHT = 41; export const DEFAULT_COL_WIDTH = 150; @@ -182,6 +184,13 @@ export default function Table({ tableRows, leafColumns, }); + const [selectedCell] = useAtom(selectedCellAtom, tableScope); + const { handleCopy, handlePaste, handleCut } = useMenuAction(selectedCell); + const { handler } = useHotKeys([ + ["mod+C", handleCopy], + ["mod+X", handleCut], + ["mod+V", handlePaste], + ]); // Handle prompt to save local column sizes if user `canEditColumns` useSaveColumnSizing(columnSizing, canEditColumns); @@ -243,7 +252,10 @@ export default function Table({ "--row-height": `${tableSchema.rowHeight || DEFAULT_ROW_HEIGHT}px`, } as any } - onKeyDown={handleKeyDown} + onKeyDown={(e) => { + handleKeyDown(e); + handler(e); + }} >
    ) : ( - - - + )} diff --git a/src/components/Table/useHotKeys.tsx b/src/components/Table/useHotKeys.tsx new file mode 100644 index 000000000..2791dc8fc --- /dev/null +++ b/src/components/Table/useHotKeys.tsx @@ -0,0 +1,99 @@ +import { useCallback } from "react"; + +type HotKeysAction = [ + string, + (event: React.KeyboardEvent | KeyboardEvent) => void +]; + +export default function useHotKeys(actions: HotKeysAction[]) { + // master event handler + const handler = useCallback( + (event: React.KeyboardEvent) => { + const event_ = "nativeEvent" in event ? event.nativeEvent : event; + actions.forEach(([hotkey, handler_]) => { + if (getHotkeyMatcher(hotkey)(event_)) { + event.preventDefault(); + handler_(event_); + } + }); + }, + [actions] + ); + + return { handler }; +} + +type KeyboardModifiers = { + alt: boolean; + ctrl: boolean; + meta: boolean; + mod: boolean; + shift: boolean; +}; + +export type Hotkey = KeyboardModifiers & { + key?: string; +}; +function isExactHotkey(hotkey: Hotkey, event: KeyboardEvent): boolean { + const { alt, ctrl, meta, mod, shift, key } = hotkey; + const { altKey, ctrlKey, metaKey, shiftKey, key: pressedKey } = event; + + if (alt !== altKey) { + return false; + } + + if (mod) { + if (!ctrlKey && !metaKey) { + return false; + } + } else { + if (ctrl !== ctrlKey) { + return false; + } + if (meta !== metaKey) { + return false; + } + } + if (shift !== shiftKey) { + return false; + } + + if ( + key && + (pressedKey.toLowerCase() === key.toLowerCase() || + event.code.replace("Key", "").toLowerCase() === key.toLowerCase()) + ) { + return true; + } + + return false; +} + +type CheckHotkeyMatch = (event: KeyboardEvent) => boolean; +export function getHotkeyMatcher(hotkey: string): CheckHotkeyMatch { + return (event) => isExactHotkey(parseHotkey(hotkey), event); +} + +function parseHotkey(hotkey: string): Hotkey { + const keys = hotkey + .toLowerCase() + .split("+") + .map((part) => part.trim()); + + const modifiers: KeyboardModifiers = { + alt: keys.includes("alt"), + ctrl: keys.includes("ctrl"), + meta: keys.includes("meta"), + mod: keys.includes("mod"), + shift: keys.includes("shift"), + }; + + const reservedKeys = ["alt", "ctrl", "meta", "shift", "mod"]; + + const freeKey = keys.find((key) => !reservedKeys.includes(key)); + + return { + ...modifiers, + key: freeKey, + }; +} diff --git a/src/contexts/TableKbShortcutContext.tsx b/src/contexts/TableKbShortcutContext.tsx index 1576bd002..516124d3c 100644 --- a/src/contexts/TableKbShortcutContext.tsx +++ b/src/contexts/TableKbShortcutContext.tsx @@ -1,21 +1,13 @@ -import { - createContext, - useContext, - useCallback, - useState, - useEffect, -} from "react"; +import { useCallback, useState, useEffect } from "react"; import { useAtom, useSetAtom } from "jotai"; import { useSnackbar } from "notistack"; import { get, find } from "lodash-es"; -import { useHotkeys } from "@mantine/hooks"; import { tableScope, tableSchemaAtom, tableRowsAtom, updateFieldAtom, - selectedCellAtom, SelectedCell, } from "@src/atoms/tableScope"; import { getFieldProp, getFieldType } from "@src/components/fields"; @@ -45,7 +37,6 @@ export function useMenuAction( const updateField = useSetAtom(updateFieldAtom, tableScope); const [cellValue, setCellValue] = useState(); const [selectedCol, setSelectedCol] = useState(); - const [enableAction, setEnableAction] = useState(false); const handleCopy = useCallback(async () => { try { @@ -134,10 +125,6 @@ export function useMenuAction( if (handleClose) handleClose(); }, [selectedCell, selectedCol, updateField, enqueueSnackbar, handleClose]); - useEffect(() => { - setEnableAction(SUPPORTED_TYPES.has(selectedCol?.type)); - }, [selectedCol]); - useEffect(() => { if (!selectedCell) return setCellValue(""); const selectedCol = tableSchema.columns?.[selectedCell.columnKey]; @@ -147,20 +134,23 @@ export function useMenuAction( setCellValue(get(selectedRow, selectedCol.fieldName)); }, [selectedCell, tableSchema, tableRows]); - const checkEnabled = (func: Function) => { - return function () { - if (enableAction) { - return func(); - } else { - enqueueSnackbar( - `${selectedCol?.type} field cannot be copied using keyboard shortcut`, - { - variant: "info", - } - ); - } - }; - }; + const checkEnabled = useCallback( + (func: Function) => { + return function () { + if (SUPPORTED_TYPES.has(selectedCol?.type)) { + return func(); + } else { + enqueueSnackbar( + `${selectedCol?.type} field cannot be copied using keyboard shortcut`, + { + variant: "info", + } + ); + } + }; + }, + [selectedCol] + ); return { handleCopy: checkEnabled(handleCopy), @@ -169,26 +159,3 @@ export function useMenuAction( cellValue, }; } - -const TableKbShortcutContext = createContext(null); - -export function useTableKbShortcut() { - return useContext(TableKbShortcutContext); -} - -export function TableKbShortcutProvider(props: { children: any }) { - const [selectedCell] = useAtom(selectedCellAtom, tableScope); - const { handleCopy, handlePaste, handleCut } = useMenuAction(selectedCell); - - useHotkeys([ - ["mod+C", handleCopy], - ["mod+X", handleCut], - ["mod+V", handlePaste], - ]); - - return ( - - {props.children} - - ); -} diff --git a/yarn.lock b/yarn.lock index 61ea6da53..003c37e7e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2217,11 +2217,6 @@ resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A== -"@mantine/hooks@^5.10.0": - version "5.10.0" - resolved "https://registry.yarnpkg.com/@mantine/hooks/-/hooks-5.10.0.tgz#e7886025a11dfa25f99c8c7fb7186d6a065c9d5c" - integrity sha512-dAefxpvqjFtXNeKse+awkIa4U1XGnMMOqWg1+07Y2Ino2G6EiT8AEnYqQyTXgcPoNaWwG9533Q/DDadmyweqaQ== - "@mark.probst/unicode-properties@~1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@mark.probst/unicode-properties/-/unicode-properties-1.1.0.tgz#5caafeab4737df93163d6d288007df33f9939b80" From 1ee76fa37e0e280ed62058f775c7531de244a1a6 Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Sat, 14 Jan 2023 11:35:32 +0000 Subject: [PATCH 069/114] renaming/ cleaning --- .../Table/ContextMenu/BasicCellContextMenuActions.tsx | 2 +- src/components/Table/Table.tsx | 8 ++++---- src/components/Table/{useHotKeys.tsx => useHotKey.tsx} | 2 +- .../Table/useMenuAction.tsx} | 0 4 files changed, 6 insertions(+), 6 deletions(-) rename src/components/Table/{useHotKeys.tsx => useHotKey.tsx} (98%) rename src/{contexts/TableKbShortcutContext.tsx => components/Table/useMenuAction.tsx} (100%) diff --git a/src/components/Table/ContextMenu/BasicCellContextMenuActions.tsx b/src/components/Table/ContextMenu/BasicCellContextMenuActions.tsx index a82c6e783..25b6ce0fd 100644 --- a/src/components/Table/ContextMenu/BasicCellContextMenuActions.tsx +++ b/src/components/Table/ContextMenu/BasicCellContextMenuActions.tsx @@ -2,7 +2,7 @@ import { Copy as CopyCells } from "@src/assets/icons"; // import Cut from "@mui/icons-material/ContentCut"; import Paste from "@mui/icons-material/ContentPaste"; import { IFieldConfig } from "@src/components/fields/types"; -import { useMenuAction } from "@src/contexts/TableKbShortcutContext"; +import { useMenuAction } from "@src/components/Table/useMenuAction"; // TODO: Remove this and add `handlePaste` function to column config export const BasicContextMenuActions: IFieldConfig["contextMenuActions"] = ( diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 76d504b5c..b99c11295 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -32,11 +32,11 @@ import { updateColumnAtom, selectedCellAtom, } from "@src/atoms/tableScope"; -import { useMenuAction } from "@src/contexts/TableKbShortcutContext"; import { getFieldType, getFieldProp } from "@src/components/fields"; import { useKeyboardNavigation } from "./useKeyboardNavigation"; +import { useMenuAction } from "./useMenuAction"; import { useSaveColumnSizing } from "./useSaveColumnSizing"; -import useHotKeys from "./useHotKeys"; +import useHotKeys from "./useHotKey"; import type { TableRow, ColumnConfig } from "@src/types/table"; export const DEFAULT_ROW_HEIGHT = 41; @@ -186,7 +186,7 @@ export default function Table({ }); const [selectedCell] = useAtom(selectedCellAtom, tableScope); const { handleCopy, handlePaste, handleCut } = useMenuAction(selectedCell); - const { handler } = useHotKeys([ + const { handler: hotKeysHandler } = useHotKeys([ ["mod+C", handleCopy], ["mod+X", handleCut], ["mod+V", handlePaste], @@ -254,7 +254,7 @@ export default function Table({ } onKeyDown={(e) => { handleKeyDown(e); - handler(e); + hotKeysHandler(e); }} >
    Date: Sat, 7 Jan 2023 21:51:12 +0530 Subject: [PATCH 070/114] fix: reordering of Select and Multiselect field option --- .../fields/SingleSelect/Settings.tsx | 191 ++++++++---------- 1 file changed, 82 insertions(+), 109 deletions(-) diff --git a/src/components/fields/SingleSelect/Settings.tsx b/src/components/fields/SingleSelect/Settings.tsx index 822697758..c028f8748 100644 --- a/src/components/fields/SingleSelect/Settings.tsx +++ b/src/components/fields/SingleSelect/Settings.tsx @@ -15,77 +15,23 @@ import { import AddIcon from "@mui/icons-material/AddCircle"; import RemoveIcon from "@mui/icons-material/CancelRounded"; -import { HTML5Backend } from "react-dnd-html5-backend"; -import { DndProvider } from "react-dnd"; -import { useDrag, useDrop } from "react-dnd"; - -interface IsettingsDraggableProps { - options: string[]; - option: string; - index: number; - onChange: (key: string) => (value: any) => void; -} - -function DraggableSettingsCard({ - options, - option, - index, - onChange, -}: IsettingsDraggableProps) { - const [{ isDragging }, dragRef] = useDrag({ - type: "SETTING_DRAG", - item: { key: index }, - collect: (monitor) => ({ - isDragging: monitor.isDragging(), - }), - }); - - const [{ isOver }, dropRef] = useDrop({ - accept: "SETTING_DRAG", - drop: ({ key }: { key: number }) => { - const temp = options[key]; - options[key] = options[index]; - options[index] = temp; - onChange("options")([...options]); - }, - collect: (monitor) => ({ - isOver: monitor.isOver(), - canDrop: monitor.canDrop(), - }), - }); +import { + DragDropContext, + Draggable, + DraggingStyle, + Droppable, + NotDraggingStyle, +} from "react-beautiful-dnd"; +import DragIndicatorOutlinedIcon from "@mui/icons-material/DragIndicatorOutlined"; - return ( -
    - { - dragRef(ref); - dropRef(ref); - }} - style={{ cursor: "move" }} - container - direction="row" - key={`option-${option}`} - justifyContent="space-between" - alignItems="center" - > - - {option} - - - - onChange("options")(options.filter((o: string) => o !== option)) - } - > - {} - - - - -
    - ); -} +const getItemStyle = ( + isDragging: boolean, + draggableStyle: DraggingStyle | NotDraggingStyle | undefined +) => ({ + backgroundColor: isDragging ? "rgba(255, 255, 255, 0.08)" : "", + borderRadius: "4px", + ...draggableStyle, +}); export default function Settings({ onChange, config }: ISettingsProps) { const listEndRef: any = useRef(null); @@ -103,6 +49,13 @@ export default function Settings({ onChange, config }: ISettingsProps) { } }; + const handleOnDragEnd = (result: any) => { + if (!result.destination) return; + const [removed] = options.splice(result.source.index, 1); + options.splice(result.destination.index, 0, removed); + onChange("options")([...options]); + }; + return (
    Options @@ -114,45 +67,65 @@ export default function Settings({ onChange, config }: ISettingsProps) { marginBottom: 5, }} > - - {options?.map((option: string, index: number) => ( - - ))} - - - {/* {options?.map((option: string) => ( - <> - - - {option} - - - - onChange("options")( - options.filter((o: string) => o !== option) - ) - } - > - {} - - - - - - ))} */} + + + {(provided) => ( +
    + {options?.map((option: string, index: number) => ( + + {(provided, snapshot) => ( + <> + + + + {option} + + + + onChange("options")( + options.filter((o: string) => o !== option) + ) + } + > + {} + + + + + + )} + + ))} + {provided.placeholder} +
    + )} +
    +
    From 45ab7aba04a1c85975285afcfd1bb69a8a3b7bc5 Mon Sep 17 00:00:00 2001 From: shamsmosowi Date: Mon, 16 Jan 2023 14:58:46 +0100 Subject: [PATCH 071/114] add value formatter to reference field filter --- src/components/fields/Reference/filters.ts | 6 ++++++ src/components/fields/Reference/index.tsx | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 src/components/fields/Reference/filters.ts diff --git a/src/components/fields/Reference/filters.ts b/src/components/fields/Reference/filters.ts new file mode 100644 index 000000000..8f92284ed --- /dev/null +++ b/src/components/fields/Reference/filters.ts @@ -0,0 +1,6 @@ +import { DocumentReference } from "@google-cloud/firestore"; + +export const valueFormatter = (value: DocumentReference, operator: string) => { + if (value && value.path) return value.path; + return ""; +}; diff --git a/src/components/fields/Reference/index.tsx b/src/components/fields/Reference/index.tsx index c4fdce528..0dd1c6976 100644 --- a/src/components/fields/Reference/index.tsx +++ b/src/components/fields/Reference/index.tsx @@ -6,6 +6,7 @@ import { Reference } from "@src/assets/icons"; import DisplayCell from "./DisplayCell"; import EditorCell from "./EditorCell"; import { filterOperators } from "@src/components/fields/ShortText/Filter"; +import { valueFormatter } from "./filters"; const SideDrawerField = lazy( () => @@ -27,6 +28,6 @@ export const config: IFieldConfig = { disablePadding: true, }), SideDrawerField, - filter: { operators: filterOperators }, + filter: { operators: filterOperators, valueFormatter: valueFormatter }, }; export default config; From 9845477abc86297c2f7460c182fcfb4605c74dea Mon Sep 17 00:00:00 2001 From: shamsmosowi Date: Mon, 16 Jan 2023 15:10:43 +0100 Subject: [PATCH 072/114] allow derivatives to be filterable from context menu --- src/components/Table/ContextMenu/MenuContents.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/Table/ContextMenu/MenuContents.tsx b/src/components/Table/ContextMenu/MenuContents.tsx index 5a10aaef7..88a973c29 100644 --- a/src/components/Table/ContextMenu/MenuContents.tsx +++ b/src/components/Table/ContextMenu/MenuContents.tsx @@ -192,7 +192,12 @@ export default function MenuContents({ onClose }: IMenuContentsProps) { value: null, deleteField: true, }); - const columnFilters = getFieldProp("filter", selectedColumn?.type); + const columnFilters = getFieldProp( + "filter", + selectedColumn?.type === FieldType.derivative + ? selectedColumn.config?.renderFieldType + : selectedColumn?.type + ); const handleFilterValue = () => { openTableFiltersPopover({ defaultQuery: { From 35e4d5f6e47c4f4b8d4970683a17f09df60ee00e Mon Sep 17 00:00:00 2001 From: shamsmosowi Date: Mon, 16 Jan 2023 15:55:37 +0100 Subject: [PATCH 073/114] enable filter from column menu for derivatives --- src/components/ColumnMenu/ColumnMenu.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/ColumnMenu/ColumnMenu.tsx b/src/components/ColumnMenu/ColumnMenu.tsx index 7dff465e0..8253eeb4b 100644 --- a/src/components/ColumnMenu/ColumnMenu.tsx +++ b/src/components/ColumnMenu/ColumnMenu.tsx @@ -234,14 +234,24 @@ export default function ColumnMenu({ defaultQuery: { key: column.fieldName, operator: - getFieldProp("filter", column.type)!.operators[0]?.value || "==", + getFieldProp( + "filter", + column.type === FieldType.derivative + ? column.config?.renderFieldType + : column.type + )!.operators[0]?.value || "==", value: "", }, }); handleClose(); }, active: column.hidden, - disabled: !getFieldProp("filter", column.type), + disabled: !getFieldProp( + "filter", + column.type === FieldType.derivative + ? column.config?.renderFieldType + : column.type + ), }, ]; From d55846e117432e1d28365f13a9b67de993771be5 Mon Sep 17 00:00:00 2001 From: shamsmosowi Date: Mon, 16 Jan 2023 21:00:48 +0100 Subject: [PATCH 074/114] add auth and storage to webhook parser --- src/components/TableModals/WebhooksModal/utils.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/TableModals/WebhooksModal/utils.tsx b/src/components/TableModals/WebhooksModal/utils.tsx index e16ca1d9c..cefc6c2f7 100644 --- a/src/components/TableModals/WebhooksModal/utils.tsx +++ b/src/components/TableModals/WebhooksModal/utils.tsx @@ -36,6 +36,8 @@ export const parserExtraLibs = [ sendStatus: (status:number)=>void }; logging: RowyLogging; + auth:firebaseauth.BaseAuth; + storage:firebasestorage.Storage; } ) => Promise;`, ]; @@ -51,6 +53,8 @@ export const conditionExtraLibs = [ sendStatus: (status:number)=>void }; logging: RowyLogging; + auth:firebaseauth.BaseAuth; + storage:firebasestorage.Storage; } ) => Promise;`, ]; From 3b715d3ab8a5fe8f4f6c4bbe3d69a0499386f207 Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Wed, 18 Jan 2023 18:16:43 +0000 Subject: [PATCH 075/114] worked on making cards fully clickable --- src/components/Tables/TableGrid/TableCard.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/Tables/TableGrid/TableCard.tsx b/src/components/Tables/TableGrid/TableCard.tsx index dd5489a9a..88472b5c4 100644 --- a/src/components/Tables/TableGrid/TableCard.tsx +++ b/src/components/Tables/TableGrid/TableCard.tsx @@ -1,4 +1,4 @@ -import { Link } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import { Card, @@ -25,6 +25,7 @@ export default function TableCard({ link, actions, }: ITableCardProps) { + const navigate = useNavigate(); return ( @@ -46,7 +47,11 @@ export default function TableCard({ backgroundColor: "action.input", borderRadius: 1, overflow: "hidden", + "&:hover": { + cursor: "pointer", + }, }} + onClick={() => navigate(link)} > Date: Thu, 19 Jan 2023 14:52:53 +0100 Subject: [PATCH 076/114] link for action scripts --- src/components/fields/Action/ActionFab.tsx | 25 ++++++++++++++++++++-- src/components/fields/Action/action.d.ts | 1 + 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/components/fields/Action/ActionFab.tsx b/src/components/fields/Action/ActionFab.tsx index 6f87fb157..c09c29030 100644 --- a/src/components/fields/Action/ActionFab.tsx +++ b/src/components/fields/Action/ActionFab.tsx @@ -4,7 +4,7 @@ import { get } from "lodash-es"; import { useAtom, useSetAtom } from "jotai"; import { httpsCallable } from "firebase/functions"; -import { Fab, FabProps } from "@mui/material"; +import { Button, Fab, FabProps, Link } from "@mui/material"; import RunIcon from "@mui/icons-material/PlayArrow"; import RedoIcon from "@mui/icons-material/Refresh"; import UndoIcon from "@mui/icons-material/Undo"; @@ -118,11 +118,32 @@ export default function ActionFab({ } else { result = await handleCallableAction(data); } - const { message, success } = result ?? {}; + const { message, success, link } = result ?? {}; enqueueSnackbar( typeof message === "string" ? message : JSON.stringify(message), { variant: success ? "success" : "error", + action: link ? ( + typeof link === "string" ? ( + + ) : ( + + ) + ) : undefined, } ); } catch (e) { diff --git a/src/components/fields/Action/action.d.ts b/src/components/fields/Action/action.d.ts index 1daab613b..b41ff94cd 100644 --- a/src/components/fields/Action/action.d.ts +++ b/src/components/fields/Action/action.d.ts @@ -22,6 +22,7 @@ type ActionResult = { success: boolean; message?: any; status?: string | number | null | undefined; + link?: string | { url: string; label: string }; }; type Action = (context: ActionContext) => Promise | ActionResult; From 0e3fbaa1d9e52b5efd2bc3a151e573b99a65b5e2 Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Sat, 21 Jan 2023 14:34:11 +0700 Subject: [PATCH 077/114] add default logging statements to all code templates --- .../ColumnConfigModal/DefaultValueInput.tsx | 6 ++++-- .../TableModals/ExtensionsModal/utils.ts | 18 +++++++++++++++--- .../WebhooksModal/Schemas/basic.tsx | 4 +++- .../WebhooksModal/Schemas/sendgrid.tsx | 2 ++ .../WebhooksModal/Schemas/stripe.tsx | 4 +++- .../WebhooksModal/Schemas/typeform.tsx | 2 ++ .../WebhooksModal/Schemas/webform.tsx | 2 ++ src/components/fields/Action/templates.ts | 2 ++ src/components/fields/Connector/utils.ts | 1 + src/components/fields/Derivative/Settings.tsx | 6 ++++-- 10 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/components/ColumnModals/ColumnConfigModal/DefaultValueInput.tsx b/src/components/ColumnModals/ColumnConfigModal/DefaultValueInput.tsx index 659ca8315..5146c387b 100644 --- a/src/components/ColumnModals/ColumnConfigModal/DefaultValueInput.tsx +++ b/src/components/ColumnModals/ColumnConfigModal/DefaultValueInput.tsx @@ -53,15 +53,17 @@ function CodeEditor({ type, column, handleChange }: ICodeEditorProps) { } else if (column.config?.defaultValue?.dynamicValueFn) { dynamicValueFn = column.config?.defaultValue?.dynamicValueFn; } else if (column.config?.defaultValue?.script) { - dynamicValueFn = `const dynamicValueFn : DefaultValue = async ({row,ref,db,storage,auth,logging})=>{ + dynamicValueFn = `const dynamicValueFn: DefaultValue = async ({row,ref,db,storage,auth,logging})=>{ // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + logging.log("dynamicValueFn started") ${column.config?.defaultValue.script} // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY }`; } else { - dynamicValueFn = `const dynamicValueFn : DefaultValue = async ({row,ref,db,storage,auth,logging})=>{ + dynamicValueFn = `const dynamicValueFn: DefaultValue = async ({row,ref,db,storage,auth,logging})=>{ // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + logging.log("dynamicValueFn started") // Example: generate random hex color // const color = "#" + Math.floor(Math.random() * 16777215).toString(16); diff --git a/src/components/TableModals/ExtensionsModal/utils.ts b/src/components/TableModals/ExtensionsModal/utils.ts index 6fe636cf5..30974b440 100644 --- a/src/components/TableModals/ExtensionsModal/utils.ts +++ b/src/components/TableModals/ExtensionsModal/utils.ts @@ -63,6 +63,7 @@ export const triggerTypes: ExtensionTrigger[] = ["create", "update", "delete"]; const extensionBodyTemplate = { task: `const extensionBody: TaskBody = async({row, db, change, ref, logging}) => { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + logging.log("extensionBody started") // Task Extension is very flexible, you can do anything. // From updating other documents in your database, to making an api request to 3rd party service. @@ -90,7 +91,8 @@ const extensionBodyTemplate = { }`, docSync: `const extensionBody: DocSyncBody = async({row, db, change, ref, logging}) => { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY - + logging.log("extensionBody started") + return ({ fieldsToSync: [], // a list of string of column names row: row, // object of data to sync, usually the row itself @@ -100,6 +102,7 @@ const extensionBodyTemplate = { }`, historySnapshot: `const extensionBody: HistorySnapshotBody = async({row, db, change, ref, logging}) => { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + logging.log("extensionBody started") return ({ trackedFields: [], // a list of string of column names @@ -109,6 +112,7 @@ const extensionBodyTemplate = { }`, algoliaIndex: `const extensionBody: AlgoliaIndexBody = async({row, db, change, ref, logging}) => { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + logging.log("extensionBody started") return ({ fieldsToSync: [], // a list of string of column names @@ -120,7 +124,8 @@ const extensionBodyTemplate = { }`, meiliIndex: `const extensionBody: MeiliIndexBody = async({row, db, change, ref, logging}) => { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY - + logging.log("extensionBody started") + return({ fieldsToSync: [], // a list of string of column names row: row, // object of data to sync, usually the row itself @@ -131,7 +136,8 @@ const extensionBodyTemplate = { }`, bigqueryIndex: `const extensionBody: BigqueryIndexBody = async({row, db, change, ref, logging}) => { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY - + logging.log("extensionBody started") + return ({ fieldsToSync: [], // a list of string of column names row: row, // object of data to sync, usually the row itself @@ -142,6 +148,7 @@ const extensionBodyTemplate = { }`, slackMessage: `const extensionBody: SlackMessageBody = async({row, db, change, ref, logging}) => { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + logging.log("extensionBody started") return ({ channels: [], // a list of slack channel IDs in string @@ -153,6 +160,7 @@ const extensionBodyTemplate = { }`, sendgridEmail: `const extensionBody: SendgridEmailBody = async({row, db, change, ref, logging}) => { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + logging.log("extensionBody started") return ({ from: "Name", // send from field @@ -174,6 +182,7 @@ const extensionBodyTemplate = { }`, apiCall: `const extensionBody: ApiCallBody = async({row, db, change, ref, logging}) => { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + logging.log("extensionBody started") return ({ body: "", @@ -185,6 +194,7 @@ const extensionBodyTemplate = { }`, twilioMessage: `const extensionBody: TwilioMessageBody = async({row, db, change, ref, logging}) => { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + logging.log("extensionBody started") // Setup twilio secret key: https://docs.rowy.io/extensions/twilio-message#secret-manager-setup // Add any code here to customize your message or dynamically get the from/to numbers @@ -197,6 +207,7 @@ const extensionBodyTemplate = { }`, pushNotification: `const extensionBody: PushNotificationBody = async({row, db, change, ref, logging}) => { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + logging.log("extensionBody started") // You can use FCM token from the row or from the user document in the database // const FCMtoken = row.FCMtoken @@ -248,6 +259,7 @@ export function emptyExtensionObject( trackedFields: [], conditions: `const condition: Condition = async({row, change, logging}) => { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + logging.log("condition started") return true; // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY diff --git a/src/components/TableModals/WebhooksModal/Schemas/basic.tsx b/src/components/TableModals/WebhooksModal/Schemas/basic.tsx index 6c7ab3de6..dfd6f74da 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/basic.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/basic.tsx @@ -66,7 +66,8 @@ export const webhookBasic = { table: TableSettings ) => `const basicParser: Parser = async({req, db, ref, logging}) => { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY - + logging.log("basicParser started") + // Optionally return an object to be added as a new row to the table // Example: add the webhook body as row const {body} = req; @@ -91,6 +92,7 @@ export const webhookBasic = { table: TableSettings ) => `const condition: Condition = async({ref, req, db, logging}) => { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + logging.log("condition started") return true; // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY diff --git a/src/components/TableModals/WebhooksModal/Schemas/sendgrid.tsx b/src/components/TableModals/WebhooksModal/Schemas/sendgrid.tsx index bafa321ae..4330e54f0 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/sendgrid.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/sendgrid.tsx @@ -15,6 +15,7 @@ export const webhookSendgrid = { table: TableSettings ) => `const sendgridParser: Parser = async ({ req, db, ref, logging }) => { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + logging.log("sendgridParser started") const { body } = req const eventHandler = async (sgEvent) => { @@ -41,6 +42,7 @@ export const webhookSendgrid = { table: TableSettings ) => `const condition: Condition = async({ref, req, db, logging}) => { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + logging.log("condition started") return true; // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY diff --git a/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx b/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx index e77e1df7b..bd0887455 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx @@ -17,8 +17,9 @@ export const webhookStripe = { extraLibs: null, template: ( table: TableSettings - ) => `const sendgridParser: Parser = async ({ req, db, ref, logging }) => { + ) => `const stripeParser: Parser = async ({ req, db, ref, logging }) => { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + logging.log("stripeParser started") const event = req.body switch (event.type) { @@ -39,6 +40,7 @@ export const webhookStripe = { table: TableSettings ) => `const condition: Condition = async({ref, req, db, logging}) => { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + logging.log("condition started") return true; // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY diff --git a/src/components/TableModals/WebhooksModal/Schemas/typeform.tsx b/src/components/TableModals/WebhooksModal/Schemas/typeform.tsx index bf42b7813..922145f66 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/typeform.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/typeform.tsx @@ -15,6 +15,7 @@ export const webhookTypeform = { table: TableSettings ) => `const typeformParser: Parser = async({req, db, ref, logging}) =>{ // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + logging.log("typeformParser started") // This reduces the form submission into a single object of key value pairs // Example: {name: "John", age: 20} @@ -73,6 +74,7 @@ export const webhookTypeform = { table: TableSettings ) => `const condition: Condition = async({ref, req, db, logging}) => { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + logging.log("condition started") return true; // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY diff --git a/src/components/TableModals/WebhooksModal/Schemas/webform.tsx b/src/components/TableModals/WebhooksModal/Schemas/webform.tsx index abad9c292..e8d89884c 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/webform.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/webform.tsx @@ -16,6 +16,7 @@ export const webhook = { table: TableSettings ) => `const formParser: Parser = async({req, db, ref, logging}) => { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + logging.log("formParser started") // Optionally return an object to be added as a new row to the table // Example: add the webhook body as row @@ -41,6 +42,7 @@ export const webhook = { table: TableSettings ) => `const condition: Condition = async({ref, req, db, logging}) => { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + logging.log("condition started") return true; // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY diff --git a/src/components/fields/Action/templates.ts b/src/components/fields/Action/templates.ts index 9d8779c85..c16ff361a 100644 --- a/src/components/fields/Action/templates.ts +++ b/src/components/fields/Action/templates.ts @@ -1,5 +1,6 @@ export const RUN_ACTION_TEMPLATE = `const action:Action = async ({row,ref,db,storage,auth,actionParams,user,logging}) => { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + logging.log("action started") // Example: /* @@ -30,6 +31,7 @@ export const RUN_ACTION_TEMPLATE = `const action:Action = async ({row,ref,db,sto export const UNDO_ACTION_TEMPLATE = `const action : Action = async ({row,ref,db,storage,auth,actionParams,user,logging}) => { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + logging.log("action started") // Example: /* diff --git a/src/components/fields/Connector/utils.ts b/src/components/fields/Connector/utils.ts index a9668ccb8..a86eec0b5 100644 --- a/src/components/fields/Connector/utils.ts +++ b/src/components/fields/Connector/utils.ts @@ -13,6 +13,7 @@ export const replacer = (data: any) => (m: string, key: string) => { export const baseFunction = `const connectorFn: Connector = async ({query, row, user, logging}) => { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + logging.log("connectorFn started") return []; // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY diff --git a/src/components/fields/Derivative/Settings.tsx b/src/components/fields/Derivative/Settings.tsx index 14cbd3932..f5bb5176f 100644 --- a/src/components/fields/Derivative/Settings.tsx +++ b/src/components/fields/Derivative/Settings.tsx @@ -66,13 +66,15 @@ export default function Settings({ ? config.derivativeFn : config?.script ? `const derivative:Derivative = async ({row,ref,db,storage,auth,logging})=>{ - // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY - + // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + logging.log("derivative started") + ${config.script.replace(/utilFns.getSecret/g, "rowy.secrets.get")} // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY }` : `const derivative:Derivative = async ({row,ref,db,storage,auth,logging})=>{ // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY + logging.log("derivative started") // Example: // const sum = row.a + row.b; From 3e0b429292acc1603ee72b07c44906377d8a2eff Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Sun, 22 Jan 2023 11:49:41 +0000 Subject: [PATCH 078/114] Updated link in create table --- src/components/TableSettingsDialog/form.tsx | 31 +++++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/components/TableSettingsDialog/form.tsx b/src/components/TableSettingsDialog/form.tsx index 4bd8a7637..8cfcaa14f 100644 --- a/src/components/TableSettingsDialog/form.tsx +++ b/src/components/TableSettingsDialog/form.tsx @@ -1,6 +1,11 @@ import { find } from "lodash-es"; +import { useAtom } from "jotai"; import { Field, FieldType } from "@rowy/form-builder"; -import { TableSettingsDialogState } from "@src/atoms/projectScope"; +import { + projectIdAtom, + projectScope, + TableSettingsDialogState, +} from "@src/atoms/projectScope"; import { Link, ListItemText, Typography } from "@mui/material"; import OpenInNewIcon from "@src/components/InlineOpenInNewIcon"; @@ -9,6 +14,21 @@ import WarningIcon from "@mui/icons-material/WarningAmber"; import { WIKI_LINKS } from "@src/constants/externalLinks"; import { FieldType as TableFieldType } from "@src/constants/fields"; +function CollectionLink() { + const [projectId] = useAtom(projectIdAtom, projectScope); + + return ( + + Your collections + + + ); +} + export const tableSettings = ( mode: TableSettingsDialogState["mode"], roles: string[] | undefined, @@ -105,14 +125,7 @@ export const tableSettings = ( ) : ( "Choose which Firestore collection to display." )}{" "} - - Your collections - - + ), AddButtonProps: { From 3ba7b0d3b2bd516c33fb1ce751d848527ccd0d77 Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Sun, 22 Jan 2023 14:07:07 +0000 Subject: [PATCH 079/114] fixed numbers getting inverted --- src/components/SteppedAccordion.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/components/SteppedAccordion.tsx b/src/components/SteppedAccordion.tsx index 97a62f8d1..4d93d363c 100644 --- a/src/components/SteppedAccordion.tsx +++ b/src/components/SteppedAccordion.tsx @@ -92,7 +92,17 @@ export default function SteppedAccordion({ } {...labelButtonProps} > - + {title} {content && } From d4f2c707468f45f85fb19d2f5021b19cb34c7f01 Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Thu, 26 Jan 2023 12:56:49 +0000 Subject: [PATCH 080/114] editable user column --- src/components/fields/User/DisplayCell.tsx | 118 ++++++++++--- src/components/fields/User/EditorCell.tsx | 6 + src/components/fields/User/Settings.tsx | 25 +++ .../fields/User/SideDrawerField.tsx | 118 +++++++++---- src/components/fields/User/UserSelect.tsx | 159 ++++++++++++++++++ src/components/fields/User/index.tsx | 9 +- 6 files changed, 377 insertions(+), 58 deletions(-) create mode 100644 src/components/fields/User/EditorCell.tsx create mode 100644 src/components/fields/User/Settings.tsx create mode 100644 src/components/fields/User/UserSelect.tsx diff --git a/src/components/fields/User/DisplayCell.tsx b/src/components/fields/User/DisplayCell.tsx index d299b6c5a..a70f981ec 100644 --- a/src/components/fields/User/DisplayCell.tsx +++ b/src/components/fields/User/DisplayCell.tsx @@ -1,30 +1,108 @@ +import { useAtom } from "jotai"; +import { Avatar, AvatarGroup, ButtonBase, Stack, Tooltip } from "@mui/material"; +import { allUsersAtom, projectScope } from "@src/atoms/projectScope"; import { IDisplayCellProps } from "@src/components/fields/types"; +import { ChevronDown } from "@src/assets/icons/ChevronDown"; +import { UserDataType } from "./UserSelect"; -import { Tooltip, Stack, Avatar } from "@mui/material"; +export default function User({ + value, + showPopoverCell, + disabled, + tabIndex, +}: IDisplayCellProps) { + const [users] = useAtom(allUsersAtom, projectScope); -import { format } from "date-fns"; -import { DATE_TIME_FORMAT } from "@src/constants/dates"; + let userValue: UserDataType[] = []; + let emails = new Set(); -export default function User({ value, column }: IDisplayCellProps) { - if (!value || !value.displayName) return null; + if (value !== undefined) { + for (const user of users) { + if (user.user && user.user?.email && value.includes(user.user.email)) { + if (!emails.has(user.user.email)) { + emails.add(user.user.email); + userValue.push(user.user); + } + } + } + } - const chip = ( - - - {value.displayName} + if (userValue.length === 0) { + return ( + showPopoverCell(true)} + style={{ + width: "100%", + height: "100%", + font: "inherit", + color: "inherit !important", + letterSpacing: "inherit", + textAlign: "inherit", + justifyContent: "flex-end", + }} + tabIndex={tabIndex} + > + + + ); + } + + const rendered = ( + + {userValue.length > 1 ? ( + + {userValue.map((user: UserDataType) => ( + + + + ))} + + ) : ( + <> + + {userValue[0].displayName} + + )} ); - if (!value.timestamp) return chip; - - const dateLabel = format( - value.timestamp.toDate ? value.timestamp.toDate() : value.timestamp, - column.config?.format || DATE_TIME_FORMAT + if (disabled) { + return rendered; + } + return ( + showPopoverCell(true)} + style={{ + width: "100%", + height: "100%", + font: "inherit", + color: "inherit !important", + letterSpacing: "inherit", + textAlign: "inherit", + justifyContent: "flex-start", + }} + tabIndex={tabIndex} + > + {rendered} + + ); - - return {chip}; } diff --git a/src/components/fields/User/EditorCell.tsx b/src/components/fields/User/EditorCell.tsx new file mode 100644 index 000000000..a65101dd9 --- /dev/null +++ b/src/components/fields/User/EditorCell.tsx @@ -0,0 +1,6 @@ +import { IEditorCellProps } from "@src/components/fields/types"; +import UserSelect from "./UserSelect"; + +export default function EditorCell({ ...props }: IEditorCellProps) { + return ; +} diff --git a/src/components/fields/User/Settings.tsx b/src/components/fields/User/Settings.tsx new file mode 100644 index 000000000..d1e6d3c8c --- /dev/null +++ b/src/components/fields/User/Settings.tsx @@ -0,0 +1,25 @@ +import { Typography, FormControlLabel, Checkbox } from "@mui/material"; +import { ISettingsProps } from "@src/components/fields/types"; + +export default function Settings({ onChange, config }: ISettingsProps) { + return ( + + Accept multiple value + + Make this column to support multiple values. + + + } + control={ + onChange("multiple")(e.target.checked)} + name="multiple" + /> + } + /> + ); +} diff --git a/src/components/fields/User/SideDrawerField.tsx b/src/components/fields/User/SideDrawerField.tsx index 1c21792a6..2ec723fa7 100644 --- a/src/components/fields/User/SideDrawerField.tsx +++ b/src/components/fields/User/SideDrawerField.tsx @@ -1,50 +1,98 @@ -import { format } from "date-fns"; -import { ISideDrawerFieldProps } from "@src/components/fields/types"; - -import { Box, Stack, Typography, Avatar } from "@mui/material"; +import { useRef, useState } from "react"; +import { useAtom } from "jotai"; +import { Tooltip, Stack, AvatarGroup, Avatar } from "@mui/material"; -import { fieldSx, getFieldId } from "@src/components/SideDrawer/utils"; -import { DATE_TIME_FORMAT } from "@src/constants/dates"; +import { allUsersAtom, projectScope } from "@src/atoms/projectScope"; +import { fieldSx } from "@src/components/SideDrawer/utils"; +import { ChevronDown } from "@src/assets/icons/ChevronDown"; +import { ISideDrawerFieldProps } from "@src/components/fields/types"; +import UserSelect, { UserDataType } from "./UserSelect"; -export default function User({ +export default function SideDrawerSelect({ column, - _rowy_ref, value, - onDirty, onChange, onSubmit, disabled, }: ISideDrawerFieldProps) { - if (!value || !value.displayName || !value.timestamp) - return ; + const [open, setOpen] = useState(false); + const [users] = useAtom(allUsersAtom, projectScope); + const parentRef = useRef(null); - const dateLabel = value.timestamp - ? format( - value.timestamp.toDate ? value.timestamp.toDate() : value.timestamp, - column.config?.format || DATE_TIME_FORMAT - ) - : null; + let userValue: UserDataType[] = []; + let emails = new Set(); - return ( - - + if (value !== undefined) { + for (const user of users) { + if (user.user && user.user?.email && value.includes(user.user.email)) { + if (!emails.has(user.user.email)) { + emails.add(user.user.email); + userValue.push(user.user); + } + } + } + } - + setOpen(true)} + direction="row" + sx={[ + fieldSx, + { + alignItems: "center", + justifyContent: userValue.length > 0 ? "space-between" : "flex-end", + marginTop: "8px", + marginBottom: "8px", + }, + ]} > - {value.displayName} ({value.email}) - {dateLabel && ( - - {dateLabel} - + {userValue.length === 0 ? null : userValue.length > 1 ? ( + + {userValue.map( + (user: UserDataType) => + user && ( + + + + ) + )} + + ) : ( +
    + + {userValue[0].displayName} +
    )} -
    -
    + +
    + + ); } diff --git a/src/components/fields/User/UserSelect.tsx b/src/components/fields/User/UserSelect.tsx new file mode 100644 index 000000000..c2f0563d3 --- /dev/null +++ b/src/components/fields/User/UserSelect.tsx @@ -0,0 +1,159 @@ +import { useMemo } from "react"; +import { useAtom } from "jotai"; + +import MultiSelect from "@rowy/multiselect"; +import { + AutocompleteProps, + Avatar, + Box, + PopoverProps, + Stack, +} from "@mui/material"; +import { createFilterOptions } from "@mui/material/Autocomplete"; + +import { projectScope, allUsersAtom } from "@src/atoms/projectScope"; +import { ColumnConfig } from "@src/types/table"; + +export type UserDataType = { + email: string; + displayName?: string; + photoURL?: string; + phoneNumber?: string; +}; + +type UserOptionType = { + label: string; + value: string; + user: UserDataType; +}; + +interface IUserSelectProps { + open?: boolean; + value: T; + onChange: (value: T) => void; + onSubmit: () => void; + parentRef?: PopoverProps["anchorEl"]; + column: ColumnConfig; + disabled: boolean; + showPopoverCell: (value: boolean) => void; +} + +export default function UserSelect({ + open, + value, + onChange, + onSubmit, + parentRef, + column, + showPopoverCell, + disabled, +}: IUserSelectProps) { + const [users] = useAtom(allUsersAtom, projectScope); + + const options = useMemo(() => { + let options: UserOptionType[] = []; + let emails = new Set(); + for (const user of users) { + if (user.user && user.user?.email) { + if (!emails.has(user.user.email)) { + emails.add(user.user.email); + options.push({ + label: user.user.email, + value: user.user.email, + user: user.user, + }); + } + } + } + return options; + }, [users]); + + const filterOptions = createFilterOptions({ + trim: true, + ignoreCase: true, + matchFrom: "start", + stringify: (option: UserOptionType) => option.user.displayName || "", + }); + + const renderOption: AutocompleteProps< + UserOptionType, + false, + false, + false + >["renderOption"] = (props, option) => { + return ; + }; + + return ( + { + if (typeof v === "string") { + v = [v]; + } + onChange(v); + }} + disabled={disabled} + clearText="Clear" + doneText="Done" + {...{ + AutocompleteProps: { + renderOption, + filterOptions, + }, + }} + onClose={() => { + onSubmit(); + showPopoverCell(false); + }} + // itemRenderer={(option: UserOptionType) => } + TextFieldProps={{ + style: { display: "none" }, + SelectProps: { + open: open === undefined ? true : open, + MenuProps: { + anchorEl: parentRef || null, + anchorOrigin: { vertical: "bottom", horizontal: "center" }, + transformOrigin: { vertical: "top", horizontal: "center" }, + sx: { + "& .MuiPaper-root": { minWidth: `${column.width}px !important` }, + }, + }, + }, + }} + /> + ); +} + +const UserListItem = ({ user, ...props }: { user: UserDataType }) => { + return ( +
  • + + + + {user.displayName ? user.displayName[0] : ""} + + {user.displayName} + + +
  • + ); +}; diff --git a/src/components/fields/User/index.tsx b/src/components/fields/User/index.tsx index b06680df2..f7d42921f 100644 --- a/src/components/fields/User/index.tsx +++ b/src/components/fields/User/index.tsx @@ -4,14 +4,14 @@ import withRenderTableCell from "@src/components/Table/TableCell/withRenderTable import UserIcon from "@mui/icons-material/PersonOutlined"; import DisplayCell from "./DisplayCell"; +import EditorCell from "./EditorCell"; const SideDrawerField = lazy( () => import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-User" */) ); const Settings = lazy( - () => - import("../CreatedBy/Settings" /* webpackChunkName: "Settings-CreatedBy" */) + () => import("./Settings" /* webpackChunkName: "Settings-User" */) ); export const config: IFieldConfig = { @@ -23,7 +23,10 @@ export const config: IFieldConfig = { initialValue: null, icon: , description: "User information and optionally, timestamp. Read-only.", - TableCell: withRenderTableCell(DisplayCell, null), + TableCell: withRenderTableCell(DisplayCell, EditorCell, "popover", { + disablePadding: true, + transparentPopover: true, + }), SideDrawerField, settings: Settings, }; From 502553c83645473d86cda63695110470b67ad89c Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Fri, 27 Jan 2023 03:07:51 +0700 Subject: [PATCH 081/114] add npm usage examples to code templates --- .../TableModals/ExtensionsModal/utils.ts | 18 ++++++++++++++++++ .../WebhooksModal/Schemas/basic.tsx | 3 +++ .../WebhooksModal/Schemas/sendgrid.tsx | 3 +++ .../WebhooksModal/Schemas/stripe.tsx | 3 +++ .../WebhooksModal/Schemas/typeform.tsx | 3 +++ .../WebhooksModal/Schemas/webform.tsx | 3 +++ src/components/fields/Action/templates.ts | 6 ++++++ src/components/fields/Connector/utils.ts | 3 +++ src/components/fields/Derivative/Settings.tsx | 6 ++++++ 9 files changed, 48 insertions(+) diff --git a/src/components/TableModals/ExtensionsModal/utils.ts b/src/components/TableModals/ExtensionsModal/utils.ts index 30974b440..272816251 100644 --- a/src/components/TableModals/ExtensionsModal/utils.ts +++ b/src/components/TableModals/ExtensionsModal/utils.ts @@ -65,6 +65,9 @@ const extensionBodyTemplate = { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY logging.log("extensionBody started") + // Import any NPM package needed + // const vision = require('@google-cloud/vision'); + // Task Extension is very flexible, you can do anything. // From updating other documents in your database, to making an api request to 3rd party service. // Example: post notification to different discord channels based on row data @@ -150,6 +153,9 @@ const extensionBodyTemplate = { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY logging.log("extensionBody started") + // Import any NPM package needed + // const lodash = require('lodash'); + return ({ channels: [], // a list of slack channel IDs in string blocks: [], // the blocks parameter to pass in to slack api @@ -162,6 +168,9 @@ const extensionBodyTemplate = { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY logging.log("extensionBody started") + // Import any NPM package needed + // const lodash = require('lodash'); + return ({ from: "Name", // send from field personalizations: [ @@ -184,6 +193,9 @@ const extensionBodyTemplate = { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY logging.log("extensionBody started") + // Import any NPM package needed + // const lodash = require('lodash'); + return ({ body: "", url: "", @@ -196,6 +208,9 @@ const extensionBodyTemplate = { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY logging.log("extensionBody started") + // Import any NPM package needed + // const lodash = require('lodash'); + // Setup twilio secret key: https://docs.rowy.io/extensions/twilio-message#secret-manager-setup // Add any code here to customize your message or dynamically get the from/to numbers return ({ @@ -209,6 +224,9 @@ const extensionBodyTemplate = { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY logging.log("extensionBody started") + // Import any NPM package needed + // const lodash = require('lodash'); + // You can use FCM token from the row or from the user document in the database // const FCMtoken = row.FCMtoken // Or push through topic diff --git a/src/components/TableModals/WebhooksModal/Schemas/basic.tsx b/src/components/TableModals/WebhooksModal/Schemas/basic.tsx index dfd6f74da..dfb3e90f3 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/basic.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/basic.tsx @@ -68,6 +68,9 @@ export const webhookBasic = { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY logging.log("basicParser started") + // Import any NPM package needed + // const lodash = require('lodash'); + // Optionally return an object to be added as a new row to the table // Example: add the webhook body as row const {body} = req; diff --git a/src/components/TableModals/WebhooksModal/Schemas/sendgrid.tsx b/src/components/TableModals/WebhooksModal/Schemas/sendgrid.tsx index 4330e54f0..ea24dabe1 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/sendgrid.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/sendgrid.tsx @@ -17,6 +17,9 @@ export const webhookSendgrid = { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY logging.log("sendgridParser started") + // Import any NPM package needed + // const lodash = require('lodash'); + const { body } = req const eventHandler = async (sgEvent) => { // Event handlers can be modiefed to preform different actions based on the sendgrid event diff --git a/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx b/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx index bd0887455..bbf8c5a5f 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/stripe.tsx @@ -21,6 +21,9 @@ export const webhookStripe = { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY logging.log("stripeParser started") + // Import any NPM package needed + // const lodash = require('lodash'); + const event = req.body switch (event.type) { case "payment_intent.succeeded": diff --git a/src/components/TableModals/WebhooksModal/Schemas/typeform.tsx b/src/components/TableModals/WebhooksModal/Schemas/typeform.tsx index 922145f66..6fb1b6834 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/typeform.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/typeform.tsx @@ -17,6 +17,9 @@ export const webhookTypeform = { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY logging.log("typeformParser started") + // Import any NPM package needed + // const lodash = require('lodash'); + // This reduces the form submission into a single object of key value pairs // Example: {name: "John", age: 20} // ⚠️ Ensure that you have assigned ref values of the fields diff --git a/src/components/TableModals/WebhooksModal/Schemas/webform.tsx b/src/components/TableModals/WebhooksModal/Schemas/webform.tsx index e8d89884c..bcc1b0281 100644 --- a/src/components/TableModals/WebhooksModal/Schemas/webform.tsx +++ b/src/components/TableModals/WebhooksModal/Schemas/webform.tsx @@ -18,6 +18,9 @@ export const webhook = { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY logging.log("formParser started") + // Import any NPM package needed + // const lodash = require('lodash'); + // Optionally return an object to be added as a new row to the table // Example: add the webhook body as row const {body} = req; diff --git a/src/components/fields/Action/templates.ts b/src/components/fields/Action/templates.ts index c16ff361a..46b0a0b58 100644 --- a/src/components/fields/Action/templates.ts +++ b/src/components/fields/Action/templates.ts @@ -2,6 +2,9 @@ export const RUN_ACTION_TEMPLATE = `const action:Action = async ({row,ref,db,sto // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY logging.log("action started") + // Import any NPM package needed + // const lodash = require('lodash'); + // Example: /* const authToken = await rowy.secrets.get("service") @@ -33,6 +36,9 @@ export const UNDO_ACTION_TEMPLATE = `const action : Action = async ({row,ref,db, // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY logging.log("action started") + // Import any NPM package needed + // const lodash = require('lodash'); + // Example: /* const authToken = await rowy.secrets.get("service") diff --git a/src/components/fields/Connector/utils.ts b/src/components/fields/Connector/utils.ts index a86eec0b5..921997ad0 100644 --- a/src/components/fields/Connector/utils.ts +++ b/src/components/fields/Connector/utils.ts @@ -15,6 +15,9 @@ export const baseFunction = `const connectorFn: Connector = async ({query, row, // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY logging.log("connectorFn started") + // Import any NPM package needed + // const lodash = require('lodash'); + return []; // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY };`; diff --git a/src/components/fields/Derivative/Settings.tsx b/src/components/fields/Derivative/Settings.tsx index f5bb5176f..1c2486d29 100644 --- a/src/components/fields/Derivative/Settings.tsx +++ b/src/components/fields/Derivative/Settings.tsx @@ -69,6 +69,9 @@ export default function Settings({ // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY logging.log("derivative started") + // Import any NPM package needed + // const lodash = require('lodash'); + ${config.script.replace(/utilFns.getSecret/g, "rowy.secrets.get")} // WRITE YOUR CODE ONLY ABOVE THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY }` @@ -76,6 +79,9 @@ export default function Settings({ // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY logging.log("derivative started") + // Import any NPM package needed + // const lodash = require('lodash'); + // Example: // const sum = row.a + row.b; // return sum; From 19867b64ad3fb8086cb1b235e32dbb9e9383eef2 Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Sat, 28 Jan 2023 09:17:41 +0000 Subject: [PATCH 082/114] worked on backward compatibility --- src/components/fields/User/DisplayCell.tsx | 3 +++ src/components/fields/User/SideDrawerField.tsx | 3 +++ src/components/fields/User/UserSelect.tsx | 8 +++++++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/fields/User/DisplayCell.tsx b/src/components/fields/User/DisplayCell.tsx index a70f981ec..fe8d6c86b 100644 --- a/src/components/fields/User/DisplayCell.tsx +++ b/src/components/fields/User/DisplayCell.tsx @@ -17,6 +17,9 @@ export default function User({ let emails = new Set(); if (value !== undefined) { + if (!Array.isArray(value)) { + value = [value.email]; + } for (const user of users) { if (user.user && user.user?.email && value.includes(user.user.email)) { if (!emails.has(user.user.email)) { diff --git a/src/components/fields/User/SideDrawerField.tsx b/src/components/fields/User/SideDrawerField.tsx index 2ec723fa7..82ebe3092 100644 --- a/src/components/fields/User/SideDrawerField.tsx +++ b/src/components/fields/User/SideDrawerField.tsx @@ -23,6 +23,9 @@ export default function SideDrawerSelect({ let emails = new Set(); if (value !== undefined) { + if (!Array.isArray(value)) { + value = [value.email]; + } for (const user of users) { if (user.user && user.user?.email && value.includes(user.user.email)) { if (!emails.has(user.user.email)) { diff --git a/src/components/fields/User/UserSelect.tsx b/src/components/fields/User/UserSelect.tsx index c2f0563d3..a8640a4fa 100644 --- a/src/components/fields/User/UserSelect.tsx +++ b/src/components/fields/User/UserSelect.tsx @@ -84,9 +84,15 @@ export default function UserSelect({ return ; }; + if (value === undefined) { + value = []; + } else if (!Array.isArray(value)) { + value = [value.email]; + } + return ( Date: Wed, 1 Feb 2023 10:11:14 +0530 Subject: [PATCH 083/114] feat: add image reorder user interface --- src/components/Table/Styled/StyledCell.tsx | 11 ++ .../Table/TableCell/withRenderTableCell.tsx | 2 +- src/components/fields/Image/EditorCell.tsx | 153 +++++++++++------- 3 files changed, 109 insertions(+), 57 deletions(-) diff --git a/src/components/Table/Styled/StyledCell.tsx b/src/components/Table/Styled/StyledCell.tsx index 5423e8e34..2d93245fe 100644 --- a/src/components/Table/Styled/StyledCell.tsx +++ b/src/components/Table/Styled/StyledCell.tsx @@ -19,6 +19,17 @@ export const StyledCell = styled("div")(({ theme }) => ({ alignItems: "center", }, + "& > .cell-contents-contain-none": { + padding: "0 var(--cell-padding)", + width: "100%", + height: "100%", + contain: "none", + overflow: "hidden", + + display: "flex", + alignItems: "center", + }, + backgroundColor: "var(--cell-background-color)", border: `1px solid ${theme.palette.divider}`, diff --git a/src/components/Table/TableCell/withRenderTableCell.tsx b/src/components/Table/TableCell/withRenderTableCell.tsx index 60935e07a..352b2b675 100644 --- a/src/components/Table/TableCell/withRenderTableCell.tsx +++ b/src/components/Table/TableCell/withRenderTableCell.tsx @@ -191,7 +191,7 @@ export default function withRenderTableCell( if (editorMode === "inline") { return (
    diff --git a/src/components/fields/Image/EditorCell.tsx b/src/components/fields/Image/EditorCell.tsx index ceec43070..55e4cbf2b 100644 --- a/src/components/fields/Image/EditorCell.tsx +++ b/src/components/fields/Image/EditorCell.tsx @@ -1,4 +1,4 @@ -import { useMemo } from "react"; +import { Fragment, useMemo } from "react"; import { IEditorCellProps } from "@src/components/fields/types"; import { useAtom, useSetAtom } from "jotai"; import { assignIn } from "lodash-es"; @@ -18,6 +18,9 @@ import useFileUpload from "@src/components/fields/File/useFileUpload"; import { IMAGE_MIME_TYPES } from "./index"; import { imgSx, thumbnailSx, deleteImgHoverSx } from "./DisplayCell"; +import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; +import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"; + export default function Image_({ column, value, @@ -84,62 +87,100 @@ export default function Image_({ marginLeft: "0 !important", }} > - - {Array.isArray(value) && - value.map((file: FileValue, i) => ( - - { - confirm({ - title: "Delete image?", - body: "This image cannot be recovered after", - confirm: "Delete", - confirmColor: "error", - handleConfirm: () => handleDelete(file), - }); - }} - disabled={disabled} - tabIndex={tabIndex} - > - - - - - - - ))} - - {localImages && - localImages.map((image) => ( - - - `0 0 0 1px ${theme.palette.divider} inset`, - }, - ]} - style={{ - backgroundImage: `url("${image.localURL}")`, - }} - /> + console.log("Drag Ended")}> + + {(provided) => ( + + {Array.isArray(value) && + value.map((file: FileValue, i) => ( + + {(provided) => ( + +
    + +
    + { + confirm({ + title: "Delete image?", + body: "This image cannot be recovered after", + confirm: "Delete", + confirmColor: "error", + handleConfirm: () => handleDelete(file), + }); + }} + disabled={disabled} + tabIndex={tabIndex} + > + + + + + +
    + )} +
    + ))} + {localImages && + localImages.map((image) => ( + + + `0 0 0 1px ${theme.palette.divider} inset`, + }, + ]} + style={{ + backgroundImage: `url("${image.localURL}")`, + }} + /> + + ))} + {provided.placeholder}
    - ))} -
    + )} + +
    {!loading ? ( From cc54ae2f6a4b6fcaca931a67e5f4af97017e6395 Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Wed, 1 Feb 2023 07:37:20 +0000 Subject: [PATCH 084/114] worked on saving table sorts --- .../Table/ColumnHeader/ColumnHeader.tsx | 1 + .../Table/ColumnHeader/ColumnHeaderSort.tsx | 8 ++ .../Table/ColumnHeader/useSaveTableSorts.tsx | 85 +++++++++++++++++++ .../TableToolbar/Filters/Filters.tsx | 4 +- src/pages/Table/TablePage.tsx | 15 +++- src/types/table.d.ts | 1 + 6 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 src/components/Table/ColumnHeader/useSaveTableSorts.tsx diff --git a/src/components/Table/ColumnHeader/ColumnHeader.tsx b/src/components/Table/ColumnHeader/ColumnHeader.tsx index 40e5c5a15..9edec5449 100644 --- a/src/components/Table/ColumnHeader/ColumnHeader.tsx +++ b/src/components/Table/ColumnHeader/ColumnHeader.tsx @@ -233,6 +233,7 @@ export const ColumnHeader = memo(function ColumnHeader({ sortKey={sortKey} currentSort={currentSort} tabIndex={focusInsideCell ? 0 : -1} + canEditColumns={canEditColumns} /> )} diff --git a/src/components/Table/ColumnHeader/ColumnHeaderSort.tsx b/src/components/Table/ColumnHeader/ColumnHeaderSort.tsx index b9d1f6ac6..929b5680c 100644 --- a/src/components/Table/ColumnHeader/ColumnHeaderSort.tsx +++ b/src/components/Table/ColumnHeader/ColumnHeaderSort.tsx @@ -9,6 +9,7 @@ import IconSlash, { } from "@src/components/IconSlash"; import { tableScope, tableSortsAtom } from "@src/atoms/tableScope"; +import useSaveTableSortBy from "./useSaveTableSorts"; export const SORT_STATES = ["none", "desc", "asc"] as const; @@ -16,6 +17,7 @@ export interface IColumnHeaderSortProps { sortKey: string; currentSort: typeof SORT_STATES[number]; tabIndex?: number; + canEditColumns: boolean; } /** @@ -26,15 +28,21 @@ export const ColumnHeaderSort = memo(function ColumnHeaderSort({ sortKey, currentSort, tabIndex, + canEditColumns, }: IColumnHeaderSortProps) { const setTableSorts = useSetAtom(tableSortsAtom, tableScope); const nextSort = SORT_STATES[SORT_STATES.indexOf(currentSort) + 1] ?? SORT_STATES[0]; + const trigger = useSaveTableSortBy(canEditColumns); + const handleSortClick = () => { if (nextSort === "none") setTableSorts([]); else setTableSorts([{ key: sortKey, direction: nextSort }]); + trigger([ + { key: sortKey, direction: nextSort === "none" ? "asc" : nextSort }, + ]); }; return ( diff --git a/src/components/Table/ColumnHeader/useSaveTableSorts.tsx b/src/components/Table/ColumnHeader/useSaveTableSorts.tsx new file mode 100644 index 000000000..45f6d1427 --- /dev/null +++ b/src/components/Table/ColumnHeader/useSaveTableSorts.tsx @@ -0,0 +1,85 @@ +import { useCallback, useState } from "react"; +import { useAtom } from "jotai"; +import { SnackbarKey, useSnackbar } from "notistack"; + +import LoadingButton from "@mui/lab/LoadingButton"; +import CheckIcon from "@mui/icons-material/Check"; + +import CircularProgressOptical from "@src/components/CircularProgressOptical"; +import { tableScope, updateTableSchemaAtom } from "@src/atoms/tableScope"; +import { TableSort } from "@src/types/table"; + +function useSaveTableSortBy(canEditColumns: boolean) { + const [updateTableSchema] = useAtom(updateTableSchemaAtom, tableScope); + if (!updateTableSchema) throw new Error("Cannot update table schema"); + const { enqueueSnackbar, closeSnackbar } = useSnackbar(); + const [snackbarId, setSnackbarId] = useState(null); + + // Offer to save when table sort by changes + const trigger = useCallback( + (sorts: TableSort[]) => { + if (!canEditColumns) return; + console.log(snackbarId); + if (snackbarId) { + closeSnackbar(snackbarId); + } + setSnackbarId( + enqueueSnackbar("Apply this sorting for all users?", { + action: ( + + await updateTableSchema({ sorts: sorts }) + } + /> + ), + anchorOrigin: { horizontal: "center", vertical: "top" }, + }) + ); + + return () => (snackbarId ? closeSnackbar(snackbarId) : null); + }, + [ + snackbarId, + canEditColumns, + enqueueSnackbar, + closeSnackbar, + updateTableSchema, + ] + ); + + return trigger; +} + +function SaveTableSortButton({ updateTable }: { updateTable: Function }) { + const [state, setState] = useState<"" | "loading" | "success" | "error">(""); + + const handleSaveToSchema = async () => { + setState("loading"); + try { + await updateTable(); + setState("success"); + } catch (e) { + setState("error"); + } + }; + + return ( + + ) : ( + + ) + } + > + Save + + ); +} + +export default useSaveTableSortBy; diff --git a/src/components/TableToolbar/Filters/Filters.tsx b/src/components/TableToolbar/Filters/Filters.tsx index b1c6b77e5..8ebc32ff0 100644 --- a/src/components/TableToolbar/Filters/Filters.tsx +++ b/src/components/TableToolbar/Filters/Filters.tsx @@ -112,7 +112,9 @@ export default function Filters() { setLocalFilters(filtersToApply); // Reset order so we don’t have to make a new index - setTableSorts([]); + if (filtersToApply.length) { + setTableSorts([]); + } }, [ hasTableFilters, hasUserFilters, diff --git a/src/pages/Table/TablePage.tsx b/src/pages/Table/TablePage.tsx index 04fc0e618..f03f5cd80 100644 --- a/src/pages/Table/TablePage.tsx +++ b/src/pages/Table/TablePage.tsx @@ -1,5 +1,5 @@ -import { Suspense, lazy } from "react"; -import { useAtom } from "jotai"; +import { Suspense, lazy, useEffect, useState } from "react"; +import { useAtom, useSetAtom } from "jotai"; import { ErrorBoundary } from "react-error-boundary"; import { isEmpty, intersection } from "lodash-es"; @@ -33,6 +33,7 @@ import { tableSchemaAtom, columnModalAtom, tableModalAtom, + tableSortsAtom, } from "@src/atoms/tableScope"; import useBeforeUnload from "@src/hooks/useBeforeUnload"; import ActionParamsProvider from "@src/components/fields/Action/FormDialog/Provider"; @@ -77,6 +78,7 @@ export default function TablePage({ const [tableId] = useAtom(tableIdAtom, tableScope); const [tableSettings] = useAtom(tableSettingsAtom, tableScope); const [tableSchema] = useAtom(tableSchemaAtom, tableScope); + const setTableSorts = useSetAtom(tableSortsAtom, tableScope); const snackLogContext = useSnackLogContext(); // Set permissions here so we can pass them to the `Table` component, which @@ -96,6 +98,15 @@ export default function TablePage({ useBeforeUnload(columnModalAtom, tableScope); useBeforeUnload(tableModalAtom, tableScope); + // Initially set the TableSorts values from table schema + const [setSort, setSetSort] = useState(true); + useEffect(() => { + if (setSort && Object.keys(tableSchema).length) { + setTableSorts(tableSchema.sorts || []); + setSetSort(false); + } + }, [tableSchema, setSort, setTableSorts, setSetSort]); + if (!(tableSchema as any)._rowy_ref) return ( <> diff --git a/src/types/table.d.ts b/src/types/table.d.ts index 46a5a939e..a05705422 100644 --- a/src/types/table.d.ts +++ b/src/types/table.d.ts @@ -101,6 +101,7 @@ export type TableSchema = { rowHeight?: number; filters?: TableFilter[]; filtersOverridable?: boolean; + sorts?: TableSort[]; functionConfigPath?: string; functionBuilderRef?: any; From f82075cb228c258a03e91a60b6814544cdfae5db Mon Sep 17 00:00:00 2001 From: Anish Roy <62830866+iamanishroy@users.noreply.github.com> Date: Wed, 1 Feb 2023 13:39:50 +0000 Subject: [PATCH 085/114] applied on column menu --- src/components/ColumnMenu/ColumnMenu.tsx | 9 +++++++++ .../Table/ColumnHeader/ColumnHeaderSort.tsx | 11 +++++++---- .../Table/ColumnHeader/useSaveTableSorts.tsx | 6 +++--- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/components/ColumnMenu/ColumnMenu.tsx b/src/components/ColumnMenu/ColumnMenu.tsx index 8253eeb4b..dec3f6fe4 100644 --- a/src/components/ColumnMenu/ColumnMenu.tsx +++ b/src/components/ColumnMenu/ColumnMenu.tsx @@ -64,6 +64,7 @@ import { } from "@src/utils/table"; import { runRoutes } from "@src/constants/runRoutes"; import { useSnackLogContext } from "@src/contexts/SnackLogContext"; +import useSaveTableSorts from "@src/components/Table/ColumnHeader/useSaveTableSorts"; export interface IMenuModalProps { name: string; @@ -116,6 +117,8 @@ export default function ColumnMenu({ const [altPress] = useAtom(altPressAtom, projectScope); const { enqueueSnackbar, closeSnackbar } = useSnackbar(); + const triggerSaveTableSorts = useSaveTableSorts(canEditColumns); + if (!columnMenu) return null; const { column, anchorEl } = columnMenu; if (column.type === FieldType.last) return null; @@ -189,6 +192,9 @@ export default function ColumnMenu({ setTableSorts( isSorted && !isAsc ? [] : [{ key: sortKey, direction: "desc" }] ); + if (!isSorted || isAsc) { + triggerSaveTableSorts([{ key: sortKey, direction: "desc" }]); + } handleClose(); }, active: isSorted && !isAsc, @@ -203,6 +209,9 @@ export default function ColumnMenu({ setTableSorts( isSorted && isAsc ? [] : [{ key: sortKey, direction: "asc" }] ); + if (!isSorted || !isAsc) { + triggerSaveTableSorts([{ key: sortKey, direction: "asc" }]); + } handleClose(); }, active: isSorted && isAsc, diff --git a/src/components/Table/ColumnHeader/ColumnHeaderSort.tsx b/src/components/Table/ColumnHeader/ColumnHeaderSort.tsx index 929b5680c..45e924f0d 100644 --- a/src/components/Table/ColumnHeader/ColumnHeaderSort.tsx +++ b/src/components/Table/ColumnHeader/ColumnHeaderSort.tsx @@ -9,7 +9,7 @@ import IconSlash, { } from "@src/components/IconSlash"; import { tableScope, tableSortsAtom } from "@src/atoms/tableScope"; -import useSaveTableSortBy from "./useSaveTableSorts"; +import useSaveTableSorts from "./useSaveTableSorts"; export const SORT_STATES = ["none", "desc", "asc"] as const; @@ -35,13 +35,16 @@ export const ColumnHeaderSort = memo(function ColumnHeaderSort({ const nextSort = SORT_STATES[SORT_STATES.indexOf(currentSort) + 1] ?? SORT_STATES[0]; - const trigger = useSaveTableSortBy(canEditColumns); + const triggerSaveTableSorts = useSaveTableSorts(canEditColumns); const handleSortClick = () => { if (nextSort === "none") setTableSorts([]); else setTableSorts([{ key: sortKey, direction: nextSort }]); - trigger([ - { key: sortKey, direction: nextSort === "none" ? "asc" : nextSort }, + triggerSaveTableSorts([ + { + key: sortKey, + direction: nextSort === "none" ? "asc" : nextSort, + }, ]); }; diff --git a/src/components/Table/ColumnHeader/useSaveTableSorts.tsx b/src/components/Table/ColumnHeader/useSaveTableSorts.tsx index 45f6d1427..fb3046745 100644 --- a/src/components/Table/ColumnHeader/useSaveTableSorts.tsx +++ b/src/components/Table/ColumnHeader/useSaveTableSorts.tsx @@ -9,13 +9,13 @@ import CircularProgressOptical from "@src/components/CircularProgressOptical"; import { tableScope, updateTableSchemaAtom } from "@src/atoms/tableScope"; import { TableSort } from "@src/types/table"; -function useSaveTableSortBy(canEditColumns: boolean) { +function useSaveTableSorts(canEditColumns: boolean) { const [updateTableSchema] = useAtom(updateTableSchemaAtom, tableScope); if (!updateTableSchema) throw new Error("Cannot update table schema"); const { enqueueSnackbar, closeSnackbar } = useSnackbar(); const [snackbarId, setSnackbarId] = useState(null); - // Offer to save when table sort by changes + // Offer to save when table sorts changes const trigger = useCallback( (sorts: TableSort[]) => { if (!canEditColumns) return; @@ -82,4 +82,4 @@ function SaveTableSortButton({ updateTable }: { updateTable: Function }) { ); } -export default useSaveTableSortBy; +export default useSaveTableSorts; From e66bdf9c11d402b857811a5374b3dac0d09ec875 Mon Sep 17 00:00:00 2001 From: Miriam Shams-Rainey Date: Wed, 1 Feb 2023 15:44:49 -0500 Subject: [PATCH 086/114] #ROWY-851: Centered sign out screen --- src/layouts/AuthLayout.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/layouts/AuthLayout.tsx b/src/layouts/AuthLayout.tsx index 031056c16..061f121bb 100644 --- a/src/layouts/AuthLayout.tsx +++ b/src/layouts/AuthLayout.tsx @@ -105,18 +105,20 @@ export default function AuthLayout({ display: "flex", flexDirection: "column", + justifyContent: "center", + alignContent: "center", "& > :not(style) + :not(style)": { mt: 4 }, } as any } > {title && ( - + {title} )} {description && ( - + {description} )} @@ -126,6 +128,7 @@ export default function AuthLayout({ justifyContent="center" alignItems="flex-start" style={{ flexGrow: 1 }} + margin="auto" > {children} @@ -146,6 +149,7 @@ export default function AuthLayout({ Project: {projectId} From 0603c1f63f1a0c4850cc38507b6543b7716f935a Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Sat, 4 Feb 2023 04:18:40 +0800 Subject: [PATCH 087/114] fix javascript floating point error in percentage field --- .../fields/Percentage/DisplayCell.tsx | 3 +- .../fields/Percentage/EditorCell.tsx | 11 +- src/components/fields/Percentage/Settings.tsx | 3 +- .../fields/Percentage/SideDrawerField.tsx | 5 +- src/components/fields/Percentage/utils.ts | 126 ++++++++++++++++++ 5 files changed, 143 insertions(+), 5 deletions(-) create mode 100644 src/components/fields/Percentage/utils.ts diff --git a/src/components/fields/Percentage/DisplayCell.tsx b/src/components/fields/Percentage/DisplayCell.tsx index 532103e45..a6e5700a1 100644 --- a/src/components/fields/Percentage/DisplayCell.tsx +++ b/src/components/fields/Percentage/DisplayCell.tsx @@ -2,6 +2,7 @@ import { IDisplayCellProps } from "@src/components/fields/types"; import { useTheme } from "@mui/material"; import { resultColorsScale } from "@src/utils/color"; +import { multiply100WithPrecision } from "./utils"; export default function Percentage({ column, value }: IDisplayCellProps) { const theme = useTheme(); @@ -34,7 +35,7 @@ export default function Percentage({ column, value }: IDisplayCellProps) { zIndex: 1, }} > - {Math.round(percentage * 100)}% + {multiply100WithPrecision(percentage)}%
    ); diff --git a/src/components/fields/Percentage/EditorCell.tsx b/src/components/fields/Percentage/EditorCell.tsx index 1e3a7e96b..54247f558 100644 --- a/src/components/fields/Percentage/EditorCell.tsx +++ b/src/components/fields/Percentage/EditorCell.tsx @@ -1,13 +1,20 @@ import type { IEditorCellProps } from "@src/components/fields/types"; import EditorCellTextField from "@src/components/Table/TableCell/EditorCellTextField"; +import { multiply100WithPrecision, divide100WithPrecision } from "./utils"; export default function Percentage(props: IEditorCellProps) { return ( props.onChange(Number(v) / 100)} + value={ + typeof props.value === "number" + ? multiply100WithPrecision(props.value) + : props.value + } + onChange={(v) => { + props.onChange(divide100WithPrecision(Number(v))); + }} /> ); } diff --git a/src/components/fields/Percentage/Settings.tsx b/src/components/fields/Percentage/Settings.tsx index e28dde2cc..b2552fc27 100644 --- a/src/components/fields/Percentage/Settings.tsx +++ b/src/components/fields/Percentage/Settings.tsx @@ -16,6 +16,7 @@ import { ISettingsProps } from "@src/components/fields/types"; import { Color, toColor } from "react-color-palette"; import { fieldSx } from "@src/components/SideDrawer/utils"; import { resultColorsScale, defaultColors } from "@src/utils/color"; +import { multiply100WithPrecision } from "./utils"; const colorLabels: { [key: string]: string } = { 0: "Start", @@ -160,7 +161,7 @@ const Preview = ({ colors }: { colors: any }) => { }} /> - {Math.floor(value * 100)}% + {multiply100WithPrecision(value)}% ); diff --git a/src/components/fields/Percentage/SideDrawerField.tsx b/src/components/fields/Percentage/SideDrawerField.tsx index a6cc43662..feaf5673c 100644 --- a/src/components/fields/Percentage/SideDrawerField.tsx +++ b/src/components/fields/Percentage/SideDrawerField.tsx @@ -3,6 +3,7 @@ import { ISideDrawerFieldProps } from "@src/components/fields/types"; import { TextField, InputAdornment, Box, useTheme } from "@mui/material"; import { resultColorsScale } from "@src/utils/color"; import { getFieldId } from "@src/components/SideDrawer/utils"; +import { multiply100WithPrecision } from "./utils"; export default function Percentage({ column, @@ -20,7 +21,9 @@ export default function Percentage({ margin="none" onChange={(e) => onChange(Number(e.target.value) / 100)} onBlur={onSubmit} - value={typeof value === "number" ? value * 100 : value} + value={ + typeof value === "number" ? multiply100WithPrecision(value) : value + } id={getFieldId(column.key)} label="" hiddenLabel diff --git a/src/components/fields/Percentage/utils.ts b/src/components/fields/Percentage/utils.ts new file mode 100644 index 000000000..f9e4ee25d --- /dev/null +++ b/src/components/fields/Percentage/utils.ts @@ -0,0 +1,126 @@ +import { trim, trimEnd } from "lodash-es"; + +/** + * Multiply a number by 100 and return a string without floating point error + * by shifting the decimal point 2 places to the right as a string + * e.g. floating point error: 0.07 * 100 === 7.000000000000001 + * + * A few examples: + * + * let number = 0.07; + * console.log(number, multiply100WithPrecision(number)); + * --> 7 + * + * number = 0; + * console.log(number, multiply100WithPrecision(number)); + * --> 0 + * + * number = 0.1; + * console.log(number, multiply100WithPrecision(number)); + * --> 10 + * + * number = 0.001; + * console.log(number, multiply100WithPrecision(number)); + * --> 0.1 + * + * number = 0.00001; + * console.log(number, multiply100WithPrecision(number)); + * --> 0.001 + * + * number = 100; + * console.log(number, multiply100WithPrecision(number)); + * --> 10000 + * + * number = 1999.99; + * console.log(number, multiply100WithPrecision(number)); + * --> 199999 + * + * number = 1999.999; + * console.log(number, multiply100WithPrecision(number)); + * --> 199999.9 + * + * number = 0.25; + * console.log(number, multiply100WithPrecision(number)); + * --> 25 + * + * number = 0.15; + * console.log(number, multiply100WithPrecision(number)); + * --> 15 + * + * number = 1.23456789; + * console.log(number, multiply100WithPrecision(number)); + * --> 123.456789 + * + * number = 0.0000000001; + * console.log(number, multiply100WithPrecision(number)); + * --> 1e-8 + */ +export const multiply100WithPrecision = (value: number) => { + if (value === 0) { + return 0; + } + + let valueString = value.toString(); + + // e.g 1e-10 becomes 1e-8 + if (valueString.includes("e")) { + return value * 100; + } + + // if the number is integer, add .00 + if (!valueString.includes(".")) { + valueString = valueString.concat(".00"); + } + + let [before, after] = valueString.split("."); + + // if after decimal has only 1 digit, pad a 0 + if (after.length === 1) { + after = after.concat("0"); + } + + let newNumber = `${before}${after.slice(0, 2)}.${after.slice(2)}`; + newNumber = trimEnd(trim(newNumber, "0"), "."); + if (newNumber.startsWith(".")) { + newNumber = "0" + newNumber; + } + return Number(newNumber); +}; + +/** + * Divide a number by 100 and return a string without floating point error + * by shifting the decimal point 2 places to the left as a string + */ +export const divide100WithPrecision = (value: number) => { + if (value === 0) { + return 0; + } + + let valueString = value.toString(); + + // e.g 1e-10 becomes 1e-8 + if (valueString.includes("e")) { + return value / 100; + } + + // add decimal if integer + if (!valueString.includes(".")) { + valueString = valueString + "."; + } + + let [before, after] = valueString.split("."); + + // if before decimal has less than digit, pad 0 + if (before.length < 2) { + before = "00" + before; + } + + let newNumber = `${before.slice(0, before.length - 2)}.${before.slice( + before.length - 2 + )}${after}`; + newNumber = trimEnd(trimEnd(newNumber, "0"), "."); + if (newNumber.startsWith(".")) { + newNumber = "0" + newNumber; + } + return Number(newNumber); +}; From cd65a5cbcfedb370fb1ea7789cf907dd2761d14a Mon Sep 17 00:00:00 2001 From: iamanishroy <6275anishroy@gmail.com> Date: Sat, 4 Feb 2023 21:50:48 +0530 Subject: [PATCH 088/114] variable name changed --- src/pages/Table/TablePage.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/Table/TablePage.tsx b/src/pages/Table/TablePage.tsx index f03f5cd80..15b5ed59c 100644 --- a/src/pages/Table/TablePage.tsx +++ b/src/pages/Table/TablePage.tsx @@ -99,13 +99,13 @@ export default function TablePage({ useBeforeUnload(tableModalAtom, tableScope); // Initially set the TableSorts values from table schema - const [setSort, setSetSort] = useState(true); + const [applySort, setApplySort] = useState(true); useEffect(() => { - if (setSort && Object.keys(tableSchema).length) { + if (applySort && Object.keys(tableSchema).length) { setTableSorts(tableSchema.sorts || []); - setSetSort(false); + setApplySort(false); } - }, [tableSchema, setSort, setTableSorts, setSetSort]); + }, [tableSchema, applySort, setTableSorts, setApplySort]); if (!(tableSchema as any)._rowy_ref) return ( From d655c2699c0c8ed67a18d36f3c6a2b05cbf9a15d Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 6 Feb 2023 21:13:52 +0530 Subject: [PATCH 089/114] feat: add image reorder functionality --- src/components/fields/File/useFileUpload.ts | 10 +++++ src/components/fields/Image/EditorCell.tsx | 50 +++++++++++++++++---- 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/src/components/fields/File/useFileUpload.ts b/src/components/fields/File/useFileUpload.ts index d99ccf674..22795349e 100644 --- a/src/components/fields/File/useFileUpload.ts +++ b/src/components/fields/File/useFileUpload.ts @@ -75,6 +75,15 @@ export default function useFileUpload( [deleteUpload, docRef, fieldName, updateField] ); + // Drag and Drop + const handleUpdate = (files: any) => { + updateField({ + path: docRef.path, + fieldName, + value: files, + }); + }; + return { localFiles, progress, @@ -83,5 +92,6 @@ export default function useFileUpload( handleUpload, handleDelete, dropzoneState, + handleUpdate, }; } diff --git a/src/components/fields/Image/EditorCell.tsx b/src/components/fields/Image/EditorCell.tsx index 55e4cbf2b..181b19e9c 100644 --- a/src/components/fields/Image/EditorCell.tsx +++ b/src/components/fields/Image/EditorCell.tsx @@ -1,4 +1,4 @@ -import { Fragment, useMemo } from "react"; +import { useMemo } from "react"; import { IEditorCellProps } from "@src/components/fields/types"; import { useAtom, useSetAtom } from "jotai"; import { assignIn } from "lodash-es"; @@ -19,7 +19,13 @@ import { IMAGE_MIME_TYPES } from "./index"; import { imgSx, thumbnailSx, deleteImgHoverSx } from "./DisplayCell"; import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; -import { DragDropContext, Droppable, Draggable } from "react-beautiful-dnd"; +import { + DragDropContext, + Droppable, + Draggable, + DropResult, + ResponderProvided, +} from "react-beautiful-dnd"; export default function Image_({ column, @@ -31,11 +37,17 @@ export default function Image_({ }: IEditorCellProps) { const confirm = useSetAtom(confirmDialogAtom, projectScope); - const { loading, progress, handleDelete, localFiles, dropzoneState } = - useFileUpload(_rowy_ref, column.key, { - multiple: true, - accept: IMAGE_MIME_TYPES, - }); + const { + loading, + progress, + handleDelete, + localFiles, + dropzoneState, + handleUpdate, + } = useFileUpload(_rowy_ref, column.key, { + multiple: true, + accept: IMAGE_MIME_TYPES, + }); const localImages = useMemo( () => @@ -48,6 +60,28 @@ export default function Image_({ const { getRootProps, getInputProps, isDragActive } = dropzoneState; const dropzoneProps = getRootProps(); + const onDragEnd = (result: DropResult, provided: ResponderProvided) => { + const { destination, source, draggableId } = result; + + if (!destination) { + return; + } + + if ( + destination.droppableId === source.droppableId && + destination.index === source.index + ) { + return; + } + + const newValue = Array.from(value); + + newValue.splice(source.index, 1); + newValue.splice(destination.index, 0, value[source.index]); + + handleUpdate([...newValue]); + }; + let thumbnailSize = "100x100"; if (rowHeight > 50) thumbnailSize = "200x200"; if (rowHeight > 100) thumbnailSize = "400x400"; @@ -87,7 +121,7 @@ export default function Image_({ marginLeft: "0 !important", }} > - console.log("Drag Ended")}> + {(provided) => ( Date: Tue, 7 Feb 2023 10:53:23 +0530 Subject: [PATCH 090/114] removed info icon --- .../Breadcrumbs/BreadcrumbsTableRoot.tsx | 27 +------------------ 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/src/components/Table/Breadcrumbs/BreadcrumbsTableRoot.tsx b/src/components/Table/Breadcrumbs/BreadcrumbsTableRoot.tsx index 676408e30..9eea60b24 100644 --- a/src/components/Table/Breadcrumbs/BreadcrumbsTableRoot.tsx +++ b/src/components/Table/Breadcrumbs/BreadcrumbsTableRoot.tsx @@ -1,6 +1,6 @@ import { useAtom } from "jotai"; import { useParams, Link as RouterLink } from "react-router-dom"; -import { find, camelCase, uniq } from "lodash-es"; +import { find, camelCase } from "lodash-es"; import { Stack, @@ -12,9 +12,6 @@ import { } from "@mui/material"; import ReadOnlyIcon from "@mui/icons-material/EditOffOutlined"; -import InfoTooltip from "@src/components/InfoTooltip"; -import RenderedMarkdown from "@src/components/RenderedMarkdown"; - import { projectScope, userRolesAtom, @@ -83,28 +80,6 @@ export default function BreadcrumbsTableRoot(props: StackProps) { )} - - {tableSettings.description && ( - - -
    - } - buttonLabel="Table info" - tooltipProps={{ - componentsProps: { - popper: { sx: { zIndex: "appBar" } }, - tooltip: { sx: { maxWidth: "75vw" } }, - } as any, - }} - defaultOpen={!dismissed.includes(tableSettings.id)} - onClose={() => setDismissed((d) => uniq([...d, tableSettings.id]))} - /> - )} ); } From 2c77eeb8f86ca89d3f19c4653432f947c64ee2e9 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 8 Feb 2023 21:47:25 +0530 Subject: [PATCH 091/114] feat: add drag and drop to file field --- src/components/fields/File/EditorCell.tsx | 206 +++++++++++++++------- 1 file changed, 143 insertions(+), 63 deletions(-) diff --git a/src/components/fields/File/EditorCell.tsx b/src/components/fields/File/EditorCell.tsx index 247c99faa..722491e7f 100644 --- a/src/components/fields/File/EditorCell.tsx +++ b/src/components/fields/File/EditorCell.tsx @@ -15,6 +15,15 @@ import { DATE_TIME_FORMAT } from "@src/constants/dates"; import { FileValue } from "@src/types/table"; import useFileUpload from "./useFileUpload"; +import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; +import { + DragDropContext, + Droppable, + Draggable, + DropResult, + ResponderProvided, +} from "react-beautiful-dnd"; + export default function File_({ column, value, @@ -25,11 +34,40 @@ export default function File_({ }: IEditorCellProps) { const confirm = useSetAtom(confirmDialogAtom, projectScope); - const { loading, progress, handleDelete, localFiles, dropzoneState } = - useFileUpload(_rowy_ref, column.key, { multiple: true }); + const { + loading, + progress, + handleDelete, + localFiles, + dropzoneState, + handleUpdate, + } = useFileUpload(_rowy_ref, column.key, { multiple: true }); const { isDragActive, getRootProps, getInputProps } = dropzoneState; const dropzoneProps = getRootProps(); + + const onDragEnd = (result: DropResult, provided: ResponderProvided) => { + const { destination, source, draggableId } = result; + + if (!destination) { + return; + } + + if ( + destination.droppableId === source.droppableId && + destination.index === source.index + ) { + return; + } + + const newValue = Array.from(value); + + newValue.splice(source.index, 1); + newValue.splice(destination.index, 0, value[source.index]); + + handleUpdate([...newValue]); + }; + return ( - - {Array.isArray(value) && - value.map((file: FileValue) => ( - 1 ? { maxWidth: `calc(100% - 12px)` } : {} - } - > - + + {(provided) => ( + + - } - sx={{ - "& .MuiChip-label": { - lineHeight: 5 / 3, - }, - }} - onClick={(e: any) => e.stopPropagation()} - component="a" - href={file.downloadURL} - target="_blank" - rel="noopener noreferrer" - clickable - onDelete={ - disabled - ? undefined - : () => - confirm({ - handleConfirm: () => handleDelete(file), - title: "Delete file?", - body: "This file cannot be recovered after", - confirm: "Delete", - confirmColor: "error", - }) - } - tabIndex={tabIndex} - style={{ width: "100%", cursor: "pointer" }} - /> - - - ))} - {localFiles && - localFiles.map((file) => ( - - } - label={file.name} - deleteIcon={ - - } - /> - - ))} - + {Array.isArray(value) && + value.map((file: FileValue, i) => ( + + {(provided) => ( + 1 + ? { + maxWidth: `calc(100% - 12px)`, + display: "flex", + alignItems: "center", + ...provided.draggableProps.style, + } + : {} + } + > +
    + +
    + + } + sx={{ + "& .MuiChip-label": { + lineHeight: 5 / 3, + }, + }} + onClick={(e: any) => e.stopPropagation()} + component="a" + href={file.downloadURL} + target="_blank" + rel="noopener noreferrer" + clickable + onDelete={ + disabled + ? undefined + : () => + confirm({ + handleConfirm: () => handleDelete(file), + title: "Delete file?", + body: "This file cannot be recovered after", + confirm: "Delete", + confirmColor: "error", + }) + } + tabIndex={tabIndex} + style={{ width: "100%", cursor: "pointer" }} + /> + +
    + )} +
    + ))} + + + {localFiles && + localFiles.map((file) => ( + + } + label={file.name} + deleteIcon={ + + } + /> + + ))} + + )} + + {!loading ? ( !disabled && ( From d621afca6d784d7c8062df4bc188e06241b52cbe Mon Sep 17 00:00:00 2001 From: iamanishroy <6275anishroy@gmail.com> Date: Sat, 11 Feb 2023 19:02:11 +0530 Subject: [PATCH 092/114] Added new filed ARRAY --- src/components/fields/Array/DisplayCell.tsx | 24 ++ .../Array/SideDrawerField/AddButton.tsx | 92 ++++++++ .../Array/SideDrawerField/SupportedTypes.ts | 105 +++++++++ .../fields/Array/SideDrawerField/index.tsx | 205 ++++++++++++++++++ .../fields/Array/SideDrawerField/utils.ts | 59 +++++ src/components/fields/Array/index.tsx | 30 +++ src/components/fields/index.ts | 2 + src/constants/fields.ts | 1 + 8 files changed, 518 insertions(+) create mode 100644 src/components/fields/Array/DisplayCell.tsx create mode 100644 src/components/fields/Array/SideDrawerField/AddButton.tsx create mode 100644 src/components/fields/Array/SideDrawerField/SupportedTypes.ts create mode 100644 src/components/fields/Array/SideDrawerField/index.tsx create mode 100644 src/components/fields/Array/SideDrawerField/utils.ts create mode 100644 src/components/fields/Array/index.tsx diff --git a/src/components/fields/Array/DisplayCell.tsx b/src/components/fields/Array/DisplayCell.tsx new file mode 100644 index 000000000..e934ea5f9 --- /dev/null +++ b/src/components/fields/Array/DisplayCell.tsx @@ -0,0 +1,24 @@ +import { useTheme } from "@mui/material"; +import { IDisplayCellProps } from "@src/components/fields/types"; + +export default function Array({ value }: IDisplayCellProps) { + const theme = useTheme(); + + if (!value) { + return null; + } + + return ( +
    + {JSON.stringify(value, null, 4)} +
    + ); +} diff --git a/src/components/fields/Array/SideDrawerField/AddButton.tsx b/src/components/fields/Array/SideDrawerField/AddButton.tsx new file mode 100644 index 000000000..4719970ea --- /dev/null +++ b/src/components/fields/Array/SideDrawerField/AddButton.tsx @@ -0,0 +1,92 @@ +import { useRef, useState } from "react"; +import { + Button, + ButtonGroup, + ListItemText, + MenuItem, + Select, +} from "@mui/material"; +import AddIcon from "@mui/icons-material/Add"; + +import { ChevronDown as ArrowDropDownIcon } from "@src/assets/icons"; +import { FieldType } from "@src/components/fields/types"; +import { getFieldProp } from "@src/components/fields"; + +import { + ArraySupportedFields, + ArraySupportedFiledTypes, +} from "./SupportedTypes"; + +function AddButton({ handleAddNew }: { handleAddNew: Function }) { + const anchorEl = useRef(null); + const [open, setOpen] = useState(false); + const [fieldType, setFieldType] = useState( + FieldType.shortText + ); + + return ( + <> + + + + + + + + + ); +} + +export default AddButton; diff --git a/src/components/fields/Array/SideDrawerField/SupportedTypes.ts b/src/components/fields/Array/SideDrawerField/SupportedTypes.ts new file mode 100644 index 000000000..28acd30c5 --- /dev/null +++ b/src/components/fields/Array/SideDrawerField/SupportedTypes.ts @@ -0,0 +1,105 @@ +import { DocumentReference, GeoPoint, Timestamp } from "firebase/firestore"; + +import { FieldType } from "@src/components/fields/types"; + +import NumberValueSidebar from "@src/components/fields/Number/SideDrawerField"; +import ShortTextValueSidebar from "@src/components/fields/ShortText/SideDrawerField"; +import JsonValueSidebar from "@src/components/fields/Json/SideDrawerField"; +import CheckBoxValueSidebar from "@src/components/fields/Checkbox/SideDrawerField"; +import GeoPointValueSidebar from "@src/components/fields/GeoPoint/SideDrawerField"; +import DateTimeValueSidebar from "@src/components/fields/DateTime/SideDrawerField"; +import ReferenceValueSidebar from "@src/components/fields/Reference/SideDrawerField"; + +export const ArraySupportedFields = [ + FieldType.number, + FieldType.shortText, + FieldType.json, + FieldType.checkbox, + FieldType.geoPoint, + FieldType.dateTime, + FieldType.reference, +] as const; + +export type ArraySupportedFiledTypes = typeof ArraySupportedFields[number]; + +export const SupportedTypes = { + [FieldType.number]: { + Sidebar: NumberValueSidebar, + initialValue: 0, + dataType: "common", + instance: Object, + }, + [FieldType.shortText]: { + Sidebar: ShortTextValueSidebar, + initialValue: "", + dataType: "common", + instance: Object, + }, + [FieldType.checkbox]: { + Sidebar: CheckBoxValueSidebar, + initialValue: false, + dataType: "common", + instance: Object, + }, + [FieldType.json]: { + Sidebar: JsonValueSidebar, + initialValue: {}, + sx: [ + { + marginTop: "24px", + }, + ], + dataType: "common", + instance: Object, + }, + [FieldType.geoPoint]: { + Sidebar: GeoPointValueSidebar, + initialValue: new GeoPoint(0, 0), + dataType: "firestore-type", + instance: GeoPoint, + }, + [FieldType.dateTime]: { + Sidebar: DateTimeValueSidebar, + initialValue: Timestamp.now(), + dataType: "firestore-type", + instance: Timestamp, + }, + [FieldType.reference]: { + Sidebar: ReferenceValueSidebar, + initialValue: null, + dataType: "firestore-type", + instance: DocumentReference, + }, +}; + +export function detectType(value: any): ArraySupportedFiledTypes { + if (value === null) { + return FieldType.reference; + } + for (const supportedField of ArraySupportedFields) { + if (SupportedTypes[supportedField].dataType === "firestore-type") { + if (value instanceof SupportedTypes[supportedField].instance) { + return supportedField; + } + } + } + + switch (typeof value) { + case "bigint": + case "number": { + return FieldType.number; + } + case "string": { + return FieldType.shortText; + } + case "boolean": { + return FieldType.checkbox; + } + case "object": { + return FieldType.json; + } + default: { + return FieldType.shortText; + } + } +} diff --git a/src/components/fields/Array/SideDrawerField/index.tsx b/src/components/fields/Array/SideDrawerField/index.tsx new file mode 100644 index 000000000..b8415eb52 --- /dev/null +++ b/src/components/fields/Array/SideDrawerField/index.tsx @@ -0,0 +1,205 @@ +import { + DragDropContext, + Droppable, + Draggable, + DropResult, +} from "react-beautiful-dnd"; + +import { Stack, Box, Button, ListItem, List } from "@mui/material"; +import ClearIcon from "@mui/icons-material/Clear"; +import DragIndicatorOutlinedIcon from "@mui/icons-material/DragIndicatorOutlined"; +import DeleteIcon from "@mui/icons-material/DeleteOutline"; + +import { FieldType, ISideDrawerFieldProps } from "@src/components/fields/types"; +import { TableRowRef } from "@src/types/table"; + +import AddButton from "./AddButton"; +import { getPseudoColumn } from "./utils"; +import { + ArraySupportedFiledTypes, + detectType, + SupportedTypes, +} from "./SupportedTypes"; + +function ArrayFieldInput({ + onChange, + value, + _rowy_ref, + index, + onRemove, + onSubmit, + id, +}: { + index: number; + onRemove: (index: number) => void; + onChange: (value: any) => void; + value: any; + onSubmit: () => void; + _rowy_ref: TableRowRef; + id: string; +}) { + const typeDetected = detectType(value); + + const Sidebar = SupportedTypes[typeDetected].Sidebar; + return ( + + {(provided) => ( + + + + false ? theme.palette.action.disabledOpacity : 1, + }, + ]} + /> + + + + + onRemove(index)} + > + + + + )} + + ); +} + +export default function ArraySideDrawerField({ + column, + value, + onChange, + onSubmit, + disabled, + _rowy_ref, + onDirty, + ...props +}: ISideDrawerFieldProps) { + const handleAddNew = (fieldType: ArraySupportedFiledTypes) => { + onChange([...(value || []), SupportedTypes[fieldType].initialValue]); + onDirty(true); + }; + const handleChange = (newValue_: any, indexUpdated: number) => { + onChange( + [...(value || [])].map((v: any, i) => { + if (i === indexUpdated) { + return newValue_; + } + + return v; + }) + ); + }; + + const handleRemove = (index: number) => { + value.splice(index, 1); + onChange([...value]); + onDirty(true); + onSubmit(); + }; + + const handleClearField = () => { + onChange([]); + onSubmit(); + }; + + function handleOnDragEnd(result: DropResult) { + if ( + !result.destination || + result.destination.index === result.source.index + ) { + return; + } + const list = Array.from(value); + const [removed] = list.splice(result.source.index, 1); + list.splice(result.destination.index, 0, removed); + onChange(list); + onSubmit(); + } + + if (value === undefined || Array.isArray(value)) { + return ( + <> + + + {(provided) => ( + + {(value || []).map((v: any, index: number) => ( + handleChange(newValue, index)} + onRemove={handleRemove} + index={index} + onSubmit={onSubmit} + /> + ))} + {provided.placeholder} + + )} + + + + + ); + } + + return ( + + + {JSON.stringify(value, null, 4)} + + + + ); +} diff --git a/src/components/fields/Array/SideDrawerField/utils.ts b/src/components/fields/Array/SideDrawerField/utils.ts new file mode 100644 index 000000000..599c46949 --- /dev/null +++ b/src/components/fields/Array/SideDrawerField/utils.ts @@ -0,0 +1,59 @@ +import { ColumnConfig } from "@src/types/table"; +import { FieldType } from "@src/constants/fields"; +import { ArraySupportedFiledTypes } from "./SupportedTypes"; +import { GeoPoint, DocumentReference } from "firebase/firestore"; +export function getPseudoColumn( + fieldType: FieldType, + index: number, + value: any +): ColumnConfig { + return { + fieldName: (+new Date()).toString(), + index: index, + key: (+new Date()).toString(), + name: value + "", + type: fieldType, + }; +} + +// archive: detectType / TODO: remove +export function detectType(value: any): ArraySupportedFiledTypes { + if (value === null) { + return FieldType.reference; + } + console.log(typeof GeoPoint); + console.log(value instanceof DocumentReference, value); + + if (typeof value === "object") { + const keys = Object.keys(value); + // console.log({ keys, value }, typeof value); + if (keys.length === 2) { + if (keys.includes("_lat") && keys.includes("_long")) { + return FieldType.geoPoint; + } + if (keys.includes("nanoseconds") && keys.includes("seconds")) { + return FieldType.dateTime; + } + } + if (+new Date(value)) { + return FieldType.dateTime; + } + return FieldType.json; + } + + switch (typeof value) { + case "bigint": + case "number": { + return FieldType.number; + } + case "string": { + return FieldType.shortText; + } + case "boolean": { + return FieldType.checkbox; + } + default: { + return FieldType.shortText; + } + } +} diff --git a/src/components/fields/Array/index.tsx b/src/components/fields/Array/index.tsx new file mode 100644 index 000000000..9aad247c6 --- /dev/null +++ b/src/components/fields/Array/index.tsx @@ -0,0 +1,30 @@ +import { lazy } from "react"; +import DataArrayIcon from "@mui/icons-material/DataArray"; + +import { IFieldConfig, FieldType } from "@src/components/fields/types"; +import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; + +import DisplayCell from "./DisplayCell"; + +const SideDrawerField = lazy( + () => + import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-Array" */) +); + +export const config: IFieldConfig = { + type: FieldType.array, + name: "Array", + group: "Code", + dataType: "object", + initialValue: [], + initializable: true, + icon: , + description: + "Connects to a sub-table in the current row. Also displays number of rows inside the sub-table. Max sub-table depth: 100.", + TableCell: withRenderTableCell(DisplayCell, SideDrawerField, "popover", { + popoverProps: { PaperProps: { sx: { p: 1, minWidth: "200px" } } }, + }), + SideDrawerField, + requireConfiguration: true, +}; +export default config; diff --git a/src/components/fields/index.ts b/src/components/fields/index.ts index 0d54b0a57..4b16d1f2f 100644 --- a/src/components/fields/index.ts +++ b/src/components/fields/index.ts @@ -31,6 +31,7 @@ import ConnectTable from "./ConnectTable"; import ConnectService from "./ConnectService"; import Json from "./Json"; import Code from "./Code"; +import Array from "./Array"; import Action from "./Action"; import Derivative from "./Derivative"; import Formula from "./Formula"; @@ -82,6 +83,7 @@ export const FIELDS: IFieldConfig[] = [ Json, Code, Markdown, + Array, /** CLOUD FUNCTION */ Action, Derivative, diff --git a/src/constants/fields.ts b/src/constants/fields.ts index 900b88db6..b05c6b896 100644 --- a/src/constants/fields.ts +++ b/src/constants/fields.ts @@ -35,6 +35,7 @@ export enum FieldType { json = "JSON", code = "CODE", markdown = "MARKDOWN", + array = "ARRAY", // CLOUD FUNCTION action = "ACTION", derivative = "DERIVATIVE", From 1464b633382b1230a77c7643909b8de6d056d4d3 Mon Sep 17 00:00:00 2001 From: iamanishroy <6275anishroy@gmail.com> Date: Sat, 11 Feb 2023 19:09:03 +0530 Subject: [PATCH 093/114] date-time detect fix --- src/components/fields/Array/SideDrawerField/SupportedTypes.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/fields/Array/SideDrawerField/SupportedTypes.ts b/src/components/fields/Array/SideDrawerField/SupportedTypes.ts index 28acd30c5..6f3fc20b4 100644 --- a/src/components/fields/Array/SideDrawerField/SupportedTypes.ts +++ b/src/components/fields/Array/SideDrawerField/SupportedTypes.ts @@ -96,6 +96,9 @@ export function detectType(value: any): ArraySupportedFiledTypes { return FieldType.checkbox; } case "object": { + if (+new Date(value)) { + return FieldType.dateTime; + } return FieldType.json; } default: { From f9b54d70a24de3ed40f71a78ef39201f3cdcbb73 Mon Sep 17 00:00:00 2001 From: iamanishroy <6275anishroy@gmail.com> Date: Mon, 13 Feb 2023 10:41:10 +0530 Subject: [PATCH 094/114] set common format for import export of date/time --- src/components/fields/Date/index.tsx | 5 ++--- src/components/fields/DateTime/index.tsx | 6 +++--- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/fields/Date/index.tsx b/src/components/fields/Date/index.tsx index 7078e1f18..a8989d48d 100644 --- a/src/components/fields/Date/index.tsx +++ b/src/components/fields/Date/index.tsx @@ -34,10 +34,9 @@ export const config: IFieldConfig = { SideDrawerField, filter: { operators: filterOperators, valueFormatter }, settings: Settings, - csvImportParser: (value, config) => - parse(value, config?.format ?? DATE_FORMAT, new Date()), + csvImportParser: (value, config) => parse(value, DATE_FORMAT, new Date()), csvExportFormatter: (value: any, config?: any) => - format(value.toDate(), config?.format ?? DATE_FORMAT), + format(value.toDate(), DATE_FORMAT), }; export default config; diff --git a/src/components/fields/DateTime/index.tsx b/src/components/fields/DateTime/index.tsx index d414fb6e2..38e0e4129 100644 --- a/src/components/fields/DateTime/index.tsx +++ b/src/components/fields/DateTime/index.tsx @@ -1,7 +1,7 @@ import { lazy } from "react"; import { IFieldConfig, FieldType } from "@src/components/fields/types"; import withRenderTableCell from "@src/components/Table/TableCell/withRenderTableCell"; -import { parseJSON, format } from "date-fns"; +import { format } from "date-fns"; import { DATE_TIME_FORMAT } from "@src/constants/dates"; import DateTimeIcon from "@mui/icons-material/AccessTime"; @@ -46,9 +46,9 @@ export const config: IFieldConfig = { customInput: FilterCustomInput, }, settings: Settings, - csvImportParser: (value) => parseJSON(value).getTime(), + csvImportParser: (value) => new Date(value), csvExportFormatter: (value: any, config?: any) => - format(value.toDate(), config?.format ?? DATE_TIME_FORMAT), + format(value.toDate(), DATE_TIME_FORMAT), }; export default config; From 6a947c69ef5a011d8815edf775a43d10035aa10f Mon Sep 17 00:00:00 2001 From: iamanishroy <6275anishroy@gmail.com> Date: Mon, 13 Feb 2023 16:06:53 +0530 Subject: [PATCH 095/114] removed unwanted imports --- src/components/Table/Breadcrumbs/BreadcrumbsTableRoot.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/components/Table/Breadcrumbs/BreadcrumbsTableRoot.tsx b/src/components/Table/Breadcrumbs/BreadcrumbsTableRoot.tsx index 9eea60b24..1e238e3dc 100644 --- a/src/components/Table/Breadcrumbs/BreadcrumbsTableRoot.tsx +++ b/src/components/Table/Breadcrumbs/BreadcrumbsTableRoot.tsx @@ -15,7 +15,6 @@ import ReadOnlyIcon from "@mui/icons-material/EditOffOutlined"; import { projectScope, userRolesAtom, - tableDescriptionDismissedAtom, tablesAtom, } from "@src/atoms/projectScope"; import { ROUTES } from "@src/constants/routes"; @@ -28,10 +27,6 @@ export default function BreadcrumbsTableRoot(props: StackProps) { const { id } = useParams(); const [userRoles] = useAtom(userRolesAtom, projectScope); - const [dismissed, setDismissed] = useAtom( - tableDescriptionDismissedAtom, - projectScope - ); const [tables] = useAtom(tablesAtom, projectScope); const tableSettings = find(tables, ["id", id]); From 98b316eb3cb42324ecb7a5dcca721a8e87e469e5 Mon Sep 17 00:00:00 2001 From: iamanishroy <6275anishroy@gmail.com> Date: Tue, 14 Feb 2023 18:43:28 +0530 Subject: [PATCH 096/114] detect type fixes --- .../TableModals/ImportExistingWizard/utils.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/components/TableModals/ImportExistingWizard/utils.ts b/src/components/TableModals/ImportExistingWizard/utils.ts index 457504207..a0aed774a 100644 --- a/src/components/TableModals/ImportExistingWizard/utils.ts +++ b/src/components/TableModals/ImportExistingWizard/utils.ts @@ -37,12 +37,24 @@ export const REGEX_URL = export const REGEX_HTML = /<\/?[a-z][\s\S]*>/; const inferTypeFromValue = (value: any) => { + // by default the type of value is string, so trying to convert it to JSON/Object. + try { + value = JSON.parse(value); + } catch (e) {} if (!value || typeof value === "function") return; if (Array.isArray(value) && typeof value[0] === "string") return FieldType.multiSelect; if (typeof value === "boolean") return FieldType.checkbox; if (isDate(value)) return FieldType.dateTime; + // trying to convert the value to date + if (+new Date(value)) { + // date and time are separated by a blank space, checking if time present. + if (value.split(" ").length > 1) { + return FieldType.dateTime; + } + return FieldType.date; + } if (typeof value === "object") { if ("hex" in value && "rgb" in value) return FieldType.color; @@ -71,6 +83,7 @@ const inferTypeFromValue = (value: any) => { export const suggestType = (data: { [key: string]: any }[], field: string) => { const results: Record = {}; + // console.log(data) data.forEach((row) => { const result = inferTypeFromValue(row[field]); if (!result) return; From 0febee00720f3a12876564b60358453535356377 Mon Sep 17 00:00:00 2001 From: Han Tuerker Date: Wed, 15 Feb 2023 17:12:49 +0300 Subject: [PATCH 097/114] hide unneccessary fields --- .../fields/Formula/PreviewTable.tsx | 20 ++++++--- src/components/fields/Formula/Settings.tsx | 4 +- .../fields/Formula/TableSourcePreview.ts | 44 +++++++++++++++---- 3 files changed, 53 insertions(+), 15 deletions(-) diff --git a/src/components/fields/Formula/PreviewTable.tsx b/src/components/fields/Formula/PreviewTable.tsx index 66113b326..8b2107fcc 100644 --- a/src/components/fields/Formula/PreviewTable.tsx +++ b/src/components/fields/Formula/PreviewTable.tsx @@ -42,12 +42,12 @@ const PreviewTable = ({ tableSchema }: { tableSchema: TableSchema }) => { marginTop: 1, marginLeft: 0, - // tableToolbar - "& > div:first-child > *:not(:first-child)": { + // table toolbar + "& > div:first-child": { display: "none", }, // table grid - "& > div:nth-child(2)": { + "& > div:nth-of-type(2)": { height: "unset", }, // emtpy state @@ -56,12 +56,20 @@ const PreviewTable = ({ tableSchema }: { tableSchema: TableSchema }) => { }, // column actions - add column '& [data-col-id="_rowy_column_actions"]': { - padding: 0, - width: 0, + display: "none", }, - '& [data-col-id="_rowy_column_actions"] > button': { + // row headers - sort by, column settings + '& [data-row-id="_rowy_header"] > button': { display: "none", }, + // row headers - drag handler + '& [data-row-id="_rowy_header"] > .column-drag-handle': { + display: "none !important", + }, + // row headers - resize handler + '& [data-row-id="_rowy_header"] >:last-child': { + display: "none !important", + }, }} > diff --git a/src/components/fields/Formula/Settings.tsx b/src/components/fields/Formula/Settings.tsx index 5a0ac1012..2015c78b7 100644 --- a/src/components/fields/Formula/Settings.tsx +++ b/src/components/fields/Formula/Settings.tsx @@ -21,6 +21,7 @@ import { import FieldSkeleton from "@src/components/SideDrawer/FieldSkeleton"; import { ISettingsProps } from "@src/components/fields/types"; import FieldsDropdown from "@src/components/ColumnModals/FieldsDropdown"; +import { DEFAULT_COL_WIDTH, DEFAULT_ROW_HEIGHT } from "@src/components/Table"; import { ColumnConfig } from "@src/types/table"; import { defaultFn, listenerFieldTypes, outputFieldTypes } from "./util"; @@ -62,7 +63,7 @@ export default function Settings({ previewSchema[key] = { ...columns[key], fixed: false, - width: undefined, + width: DEFAULT_COL_WIDTH, }; } if (columns[key].fieldName === fieldName) { @@ -74,6 +75,7 @@ export default function Settings({ } return previewSchema; }, {} as { [key: string]: ColumnConfig }), + rowHeight: DEFAULT_ROW_HEIGHT, }; }, [config, fieldName, tableSchema]); diff --git a/src/components/fields/Formula/TableSourcePreview.ts b/src/components/fields/Formula/TableSourcePreview.ts index 57deb3d9b..81d72662e 100644 --- a/src/components/fields/Formula/TableSourcePreview.ts +++ b/src/components/fields/Formula/TableSourcePreview.ts @@ -1,7 +1,7 @@ -import { useCallback } from "react"; +import { useCallback, useEffect } from "react"; import { useSetAtom } from "jotai"; import { useAtomCallback } from "jotai/utils"; -import { cloneDeep, findIndex, sortBy } from "lodash-es"; +import { cloneDeep, findIndex, initial, sortBy } from "lodash-es"; import { _deleteRowDbAtom, @@ -15,16 +15,44 @@ import { import { TableRow, TableSchema } from "@src/types/table"; import { updateRowData } from "@src/utils/table"; +const initialRows = [ + { + _rowy_ref: { + id: "zzzzzzzzzzzzzzzzzzzw", + path: "preview-collection/zzzzzzzzzzzzzzzzzzzw", + }, + }, + { + _rowy_ref: { + id: "zzzzzzzzzzzzzzzzzzzx", + path: "preview-collection/zzzzzzzzzzzzzzzzzzzx", + }, + }, + { + _rowy_ref: { + id: "zzzzzzzzzzzzzzzzzzzy", + path: "preview-collection/zzzzzzzzzzzzzzzzzzzy", + }, + }, +]; + const TableSourcePreview = ({ tableSchema }: { tableSchema: TableSchema }) => { const setTableSchemaAtom = useSetAtom(tableSchemaAtom, tableScope); - setTableSchemaAtom(() => ({ - ...tableSchema, - _rowy_ref: "preview", - })); - const setRows = useSetAtom(tableRowsDbAtom, tableScope); + + useEffect(() => { + setRows(initialRows); + }, [setRows]); + + useEffect(() => { + setTableSchemaAtom(() => ({ + ...tableSchema, + _rowy_ref: "preview", + })); + }, [tableSchema, setTableSchemaAtom]); + const readRowsDb = useAtomCallback( - useCallback((get) => get(tableRowsDbAtom), []), + useCallback((get) => get(tableRowsDbAtom) || initialRows, []), tableScope ); From 00a76bbd7b2775dc8c031191146a4535bf59f000 Mon Sep 17 00:00:00 2001 From: Han Tuerker Date: Wed, 15 Feb 2023 17:32:00 +0300 Subject: [PATCH 098/114] fix clone object error --- src/components/fields/Formula/useFormula.tsx | 2 +- src/components/fields/Formula/worker.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/fields/Formula/useFormula.tsx b/src/components/fields/Formula/useFormula.tsx index cfac7ef39..b4cacccaa 100644 --- a/src/components/fields/Formula/useFormula.tsx +++ b/src/components/fields/Formula/useFormula.tsx @@ -60,7 +60,7 @@ export const useFormula = ({ worker.postMessage({ formulaFn, - row: availableFields, + row: JSON.stringify(availableFields), }); return () => { diff --git a/src/components/fields/Formula/worker.ts b/src/components/fields/Formula/worker.ts index 16295548e..4baf08234 100644 --- a/src/components/fields/Formula/worker.ts +++ b/src/components/fields/Formula/worker.ts @@ -8,7 +8,7 @@ onmessage = async ({ data }) => { "row", `const fn = async () => \n${fnBody}\n return fn();` ); - const result = await fn(row); + const result = await fn(JSON.parse(row)); postMessage({ result }); } catch (error: any) { console.error("Error: ", error); From ebb8b8537496c3c8a373051f91af7b994ec5de32 Mon Sep 17 00:00:00 2001 From: Shams Date: Fri, 17 Feb 2023 17:06:39 +1100 Subject: [PATCH 099/114] Update index.tsx --- src/components/fields/Array/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/fields/Array/index.tsx b/src/components/fields/Array/index.tsx index 9aad247c6..d00722edd 100644 --- a/src/components/fields/Array/index.tsx +++ b/src/components/fields/Array/index.tsx @@ -25,6 +25,6 @@ export const config: IFieldConfig = { popoverProps: { PaperProps: { sx: { p: 1, minWidth: "200px" } } }, }), SideDrawerField, - requireConfiguration: true, + requireConfiguration: false, }; export default config; From 845c141e8e7ef32a3fbe342330f0acf78a3dc294 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 18 Feb 2023 19:56:25 +0530 Subject: [PATCH 100/114] refactor: hide the drag and drop icon when there's only one image and file in the cell, to save on space --- src/components/fields/File/EditorCell.tsx | 23 ++++++++++--------- src/components/fields/Image/EditorCell.tsx | 26 +++++++++++----------- 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src/components/fields/File/EditorCell.tsx b/src/components/fields/File/EditorCell.tsx index 49c2040a1..dedb97f64 100644 --- a/src/components/fields/File/EditorCell.tsx +++ b/src/components/fields/File/EditorCell.tsx @@ -1,4 +1,3 @@ -import { useCallback } from "react"; import { IEditorCellProps } from "@src/components/fields/types"; import { useSetAtom } from "jotai"; @@ -47,7 +46,7 @@ export default function File_({ const dropzoneProps = getRootProps(); const onDragEnd = (result: DropResult, provided: ResponderProvided) => { - const { destination, source, draggableId } = result; + const { destination, source } = result; if (!destination) { return; @@ -127,15 +126,17 @@ export default function File_({ ...provided.draggableProps.style, }} > -
    - -
    + {value.length > 1 && ( +
    + +
    + )} { - const { destination, source, draggableId } = result; + const { destination, source } = result; if (!destination) { return; @@ -149,15 +147,17 @@ export default function Image_({ ...provided.draggableProps.style, }} > -
    - -
    + {value.length > 1 && ( +
    + +
    + )} Date: Sat, 18 Feb 2023 20:46:16 +0530 Subject: [PATCH 101/114] add support for reordering in the sidedrawer --- .../fields/File/SideDrawerField.tsx | 139 ++++++--- .../fields/Image/SideDrawerField.tsx | 280 +++++++++++------- 2 files changed, 274 insertions(+), 145 deletions(-) diff --git a/src/components/fields/File/SideDrawerField.tsx b/src/components/fields/File/SideDrawerField.tsx index 00287c23d..5d9ca52cd 100644 --- a/src/components/fields/File/SideDrawerField.tsx +++ b/src/components/fields/File/SideDrawerField.tsx @@ -20,6 +20,15 @@ import { FileValue } from "@src/types/table"; import useFileUpload from "./useFileUpload"; import { FileIcon } from "."; +import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; +import { + DragDropContext, + Droppable, + Draggable, + DropResult, + ResponderProvided, +} from "react-beautiful-dnd"; + export default function File_({ column, _rowy_ref, @@ -72,52 +81,94 @@ export default function File_({ )} - - {Array.isArray(value) && - value.map((file: FileValue) => ( - - -
    - } - label={file.name} - onClick={() => window.open(file.downloadURL)} - onDelete={ - !disabled - ? () => - confirm({ - title: "Delete file?", - body: "This file cannot be recovered after", - confirm: "Delete", - confirmColor: "error", - handleConfirm: () => handleDelete(file), - }) - : undefined - } - /> -
    -
    -
    - ))} + console.log("onDragEnd")}> + + {(provided) => ( + + {Array.isArray(value) && + value.map((file: FileValue, i) => ( + + {(provided) => ( + + {value.length > 1 && ( +
    + +
    + )} + +
    + } + label={file.name} + onClick={() => window.open(file.downloadURL)} + onDelete={ + !disabled + ? () => + confirm({ + title: "Delete file?", + body: "This file cannot be recovered after", + confirm: "Delete", + confirmColor: "error", + handleConfirm: () => handleDelete(file), + }) + : undefined + } + /> +
    +
    +
    + )} +
    + ))} - {localFiles && - localFiles.map((file) => ( - - } - label={file.name} - deleteIcon={ - - } - /> + {localFiles && + localFiles.map((file) => ( + + } + label={file.name} + deleteIcon={ + + } + /> + + ))} + {provided.placeholder} - ))} -
    + )} +
    +
    ); } diff --git a/src/components/fields/Image/SideDrawerField.tsx b/src/components/fields/Image/SideDrawerField.tsx index 70c58b885..9efb2dfda 100644 --- a/src/components/fields/Image/SideDrawerField.tsx +++ b/src/components/fields/Image/SideDrawerField.tsx @@ -26,6 +26,15 @@ import { fieldSx, getFieldId } from "@src/components/SideDrawer/utils"; import useFileUpload from "@src/components/fields/File/useFileUpload"; import { IMAGE_MIME_TYPES } from "."; +import DragIndicatorIcon from "@mui/icons-material/DragIndicator"; +import { + DragDropContext, + Droppable, + Draggable, + DropResult, + ResponderProvided, +} from "react-beautiful-dnd"; + const imgSx = { position: "relative", width: 80, @@ -94,6 +103,7 @@ export default function Image_({ uploaderState, localFiles, dropzoneState, + handleUpdate, } = useFileUpload(_rowy_ref, column.key, { multiple: true, accept: IMAGE_MIME_TYPES, @@ -109,6 +119,28 @@ export default function Image_({ const { getRootProps, getInputProps, isDragActive } = dropzoneState; + const onDragEnd = (result: DropResult, provided: ResponderProvided) => { + const { destination, source } = result; + + if (!destination) { + return; + } + + if ( + destination.droppableId === source.droppableId && + destination.index === source.index + ) { + return; + } + + const newValue = Array.from(value); + + newValue.splice(source.index, 1); + newValue.splice(destination.index, 0, value[source.index]); + + handleUpdate([...newValue]); + }; + return ( <> {!disabled && ( @@ -151,112 +183,158 @@ export default function Image_({ )} - - {Array.isArray(value) && - value.map((image: FileValue) => ( - - {disabled ? ( - - window.open(image.downloadURL, "_blank")} - className="img" + + + {(provided) => ( + + {Array.isArray(value) && + value.map((image: FileValue, i) => ( + - - - {disabled ? : } - - - - ) : ( -
    - - - ( + + {disabled ? ( + + + window.open(image.downloadURL, "_blank") + } + className="img" + > + + + {disabled ? ( + + ) : ( + + )} + + + + ) : ( +
    + {value.length > 1 && ( +
    + +
    + )} + + + + + + confirm({ + title: "Delete image?", + body: "This image cannot be recovered after", + confirm: "Delete", + confirmColor: "error", + handleConfirm: () => + handleDelete(image), + }) + } + > + + + + + + window.open(image.downloadURL, "_blank") + } + > + + + + + +
    + )} +
    + )} + + ))} + {localImages && + localImages.map((image) => ( + + - - - confirm({ - title: "Delete image?", - body: "This image cannot be recovered after", - confirm: "Delete", - confirmColor: "error", - handleConfirm: () => handleDelete(image), - }) - } + {uploaderState[image.name] && ( + - - - - - - window.open(image.downloadURL, "_blank") - } - > - - - - -
    -
    - )} -
    - ))} - - {localImages && - localImages.map((image) => ( - - - {uploaderState[image.name] && ( - - + + + )} + - )} - + ))} + {provided.placeholder}
    - ))} -
    + )} + + ); } From e26949ac6563f9cbf77704f8631cc456f01b5e49 Mon Sep 17 00:00:00 2001 From: iamanishroy <6275anishroy@gmail.com> Date: Sat, 18 Feb 2023 21:34:39 +0530 Subject: [PATCH 102/114] parse array values --- .../TableToolbar/ImportData/ImportFromCsv.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/components/TableToolbar/ImportData/ImportFromCsv.tsx b/src/components/TableToolbar/ImportData/ImportFromCsv.tsx index 9739efab7..f164f90af 100644 --- a/src/components/TableToolbar/ImportData/ImportFromCsv.tsx +++ b/src/components/TableToolbar/ImportData/ImportFromCsv.tsx @@ -62,7 +62,20 @@ function convertJSONToCSV(rawData: string): string | false { return false; } const fields = extractFields(rawDataJSONified); - const opts = { fields }; + const opts = { + fields, + transforms: [ + (value: any) => { + // if the value is an array, join it with a comma + for (let key in value) { + if (Array.isArray(value[key])) { + value[key] = value[key].join(","); + } + } + return value; + }, + ], + }; try { const csv = parseJSON(rawDataJSONified, opts); From c8d340e1d3ec27fb5d6f4f07b78abf93021c276e Mon Sep 17 00:00:00 2001 From: iamanishroy <6275anishroy@gmail.com> Date: Sat, 18 Feb 2023 21:45:57 +0530 Subject: [PATCH 103/114] number/date fix --- src/components/TableModals/ImportExistingWizard/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TableModals/ImportExistingWizard/utils.ts b/src/components/TableModals/ImportExistingWizard/utils.ts index a0aed774a..d855da032 100644 --- a/src/components/TableModals/ImportExistingWizard/utils.ts +++ b/src/components/TableModals/ImportExistingWizard/utils.ts @@ -48,7 +48,7 @@ const inferTypeFromValue = (value: any) => { if (typeof value === "boolean") return FieldType.checkbox; if (isDate(value)) return FieldType.dateTime; // trying to convert the value to date - if (+new Date(value)) { + if (typeof value !== "number" && +new Date(value)) { // date and time are separated by a blank space, checking if time present. if (value.split(" ").length > 1) { return FieldType.dateTime; From 818fc52adb71644ed569958f78c5b3508704df37 Mon Sep 17 00:00:00 2001 From: iamanishroy <6275anishroy@gmail.com> Date: Tue, 21 Feb 2023 23:58:15 +0530 Subject: [PATCH 104/114] save default sort for the user --- .../Table/ColumnHeader/useSaveTableSorts.tsx | 25 +++++++++--- src/pages/Table/TablePage.tsx | 14 +------ .../TableSourceFirestore.tsx | 2 + .../TableSourceFirestore/useApplySorts.ts | 40 +++++++++++++++++++ 4 files changed, 63 insertions(+), 18 deletions(-) create mode 100644 src/sources/TableSourceFirestore/useApplySorts.ts diff --git a/src/components/Table/ColumnHeader/useSaveTableSorts.tsx b/src/components/Table/ColumnHeader/useSaveTableSorts.tsx index fb3046745..8ad053d58 100644 --- a/src/components/Table/ColumnHeader/useSaveTableSorts.tsx +++ b/src/components/Table/ColumnHeader/useSaveTableSorts.tsx @@ -6,11 +6,18 @@ import LoadingButton from "@mui/lab/LoadingButton"; import CheckIcon from "@mui/icons-material/Check"; import CircularProgressOptical from "@src/components/CircularProgressOptical"; -import { tableScope, updateTableSchemaAtom } from "@src/atoms/tableScope"; +import { + tableIdAtom, + tableScope, + updateTableSchemaAtom, +} from "@src/atoms/tableScope"; +import { projectScope, updateUserSettingsAtom } from "@src/atoms/projectScope"; import { TableSort } from "@src/types/table"; function useSaveTableSorts(canEditColumns: boolean) { const [updateTableSchema] = useAtom(updateTableSchemaAtom, tableScope); + const [updateUserSettings] = useAtom(updateUserSettingsAtom, projectScope); + const [tableId] = useAtom(tableIdAtom, tableScope); if (!updateTableSchema) throw new Error("Cannot update table schema"); const { enqueueSnackbar, closeSnackbar } = useSnackbar(); const [snackbarId, setSnackbarId] = useState(null); @@ -18,8 +25,14 @@ function useSaveTableSorts(canEditColumns: boolean) { // Offer to save when table sorts changes const trigger = useCallback( (sorts: TableSort[]) => { + if (updateUserSettings) { + updateUserSettings({ + tables: { + [`${tableId}`]: { sorts }, + }, + }); + } if (!canEditColumns) return; - console.log(snackbarId); if (snackbarId) { closeSnackbar(snackbarId); } @@ -27,9 +40,7 @@ function useSaveTableSorts(canEditColumns: boolean) { enqueueSnackbar("Apply this sorting for all users?", { action: ( - await updateTableSchema({ sorts: sorts }) - } + updateTable={async () => await updateTableSchema({ sorts })} /> ), anchorOrigin: { horizontal: "center", vertical: "top" }, @@ -39,9 +50,11 @@ function useSaveTableSorts(canEditColumns: boolean) { return () => (snackbarId ? closeSnackbar(snackbarId) : null); }, [ - snackbarId, + updateUserSettings, canEditColumns, + snackbarId, enqueueSnackbar, + tableId, closeSnackbar, updateTableSchema, ] diff --git a/src/pages/Table/TablePage.tsx b/src/pages/Table/TablePage.tsx index 15b5ed59c..0014ab810 100644 --- a/src/pages/Table/TablePage.tsx +++ b/src/pages/Table/TablePage.tsx @@ -1,5 +1,5 @@ -import { Suspense, lazy, useEffect, useState } from "react"; -import { useAtom, useSetAtom } from "jotai"; +import { Suspense, lazy } from "react"; +import { useAtom } from "jotai"; import { ErrorBoundary } from "react-error-boundary"; import { isEmpty, intersection } from "lodash-es"; @@ -78,7 +78,6 @@ export default function TablePage({ const [tableId] = useAtom(tableIdAtom, tableScope); const [tableSettings] = useAtom(tableSettingsAtom, tableScope); const [tableSchema] = useAtom(tableSchemaAtom, tableScope); - const setTableSorts = useSetAtom(tableSortsAtom, tableScope); const snackLogContext = useSnackLogContext(); // Set permissions here so we can pass them to the `Table` component, which @@ -98,15 +97,6 @@ export default function TablePage({ useBeforeUnload(columnModalAtom, tableScope); useBeforeUnload(tableModalAtom, tableScope); - // Initially set the TableSorts values from table schema - const [applySort, setApplySort] = useState(true); - useEffect(() => { - if (applySort && Object.keys(tableSchema).length) { - setTableSorts(tableSchema.sorts || []); - setApplySort(false); - } - }, [tableSchema, applySort, setTableSorts, setApplySort]); - if (!(tableSchema as any)._rowy_ref) return ( <> diff --git a/src/sources/TableSourceFirestore/TableSourceFirestore.tsx b/src/sources/TableSourceFirestore/TableSourceFirestore.tsx index 9f3e2cedf..67833d533 100644 --- a/src/sources/TableSourceFirestore/TableSourceFirestore.tsx +++ b/src/sources/TableSourceFirestore/TableSourceFirestore.tsx @@ -39,6 +39,7 @@ import { getTableSchemaPath } from "@src/utils/table"; import { TableSchema } from "@src/types/table"; import { firebaseDbAtom } from "@src/sources/ProjectSourceFirebase"; import { projectScope } from "@src/atoms/projectScope"; +import useApplySorts from "./useApplySorts"; /** * When rendered, provides atom values for top-level tables and sub-tables @@ -141,6 +142,7 @@ export const TableSourceFirestore = memo(function TableSourceFirestore() { } ); + useApplySorts(); useAuditChange(); useBulkWriteDb(); diff --git a/src/sources/TableSourceFirestore/useApplySorts.ts b/src/sources/TableSourceFirestore/useApplySorts.ts new file mode 100644 index 000000000..1d0bc9888 --- /dev/null +++ b/src/sources/TableSourceFirestore/useApplySorts.ts @@ -0,0 +1,40 @@ +import { useEffect, useState } from "react"; +import { useAtom, useSetAtom } from "jotai"; + +import { projectScope, userSettingsAtom } from "@src/atoms/projectScope"; +import { + tableIdAtom, + tableSchemaAtom, + tableScope, + tableSortsAtom, +} from "@src/atoms/tableScope"; + +/** + * Sets the value of tableSortsAtom + */ +export default function useApplySorts() { + // Apply the sorts + + const setTableSorts = useSetAtom(tableSortsAtom, tableScope); + const [userSettings] = useAtom(userSettingsAtom, projectScope); + const [tableId] = useAtom(tableIdAtom, tableScope); + const [tableSchema] = useAtom(tableSchemaAtom, tableScope); + + // Apply only once + const [applySort, setApplySort] = useState(true); + + useEffect(() => { + if (applySort && Object.keys(tableSchema).length) { + console.log("useApplySorts"); + const userDefaultSort = userSettings.tables?.[tableId]?.sorts || []; + console.log({ + userDefaultSort, + tableSchemaSorts: tableSchema.sorts, + }); + setTableSorts( + userDefaultSort.length ? userDefaultSort : tableSchema.sorts || [] + ); + setApplySort(false); + } + }, [setTableSorts, userSettings, tableId, applySort, tableSchema]); +} From f756f1f0a0d44ba83a3c71d218ecd24eb6305f7d Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Thu, 23 Feb 2023 13:54:15 +0700 Subject: [PATCH 105/114] allow multiple params for logging --- src/components/CodeEditor/rowy.d.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/CodeEditor/rowy.d.ts b/src/components/CodeEditor/rowy.d.ts index 42582dd0b..dc04a080f 100644 --- a/src/components/CodeEditor/rowy.d.ts +++ b/src/components/CodeEditor/rowy.d.ts @@ -18,9 +18,9 @@ type uploadOptions = { fileName?: string; }; type RowyLogging = { - log: (payload: any) => void; - warn: (payload: any) => void; - error: (payload: any) => void; + log: (...payload: any[]) => void; + warn: (...payload: any[]) => void; + error: (...payload: any[]) => void; }; interface Rowy { metadata: { From cf569c1c1216a77d8b6c75a9ec0411b6d7fc5644 Mon Sep 17 00:00:00 2001 From: iamanishroy <6275anishroy@gmail.com> Date: Thu, 23 Feb 2023 20:55:24 +0530 Subject: [PATCH 106/114] fixed: bug on clearing value --- src/components/fields/User/DisplayCell.tsx | 2 +- src/components/fields/User/SideDrawerField.tsx | 2 +- src/components/fields/User/UserSelect.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/fields/User/DisplayCell.tsx b/src/components/fields/User/DisplayCell.tsx index fe8d6c86b..2b059ed9a 100644 --- a/src/components/fields/User/DisplayCell.tsx +++ b/src/components/fields/User/DisplayCell.tsx @@ -16,7 +16,7 @@ export default function User({ let userValue: UserDataType[] = []; let emails = new Set(); - if (value !== undefined) { + if (value !== undefined && value !== null) { if (!Array.isArray(value)) { value = [value.email]; } diff --git a/src/components/fields/User/SideDrawerField.tsx b/src/components/fields/User/SideDrawerField.tsx index 82ebe3092..8187bc83e 100644 --- a/src/components/fields/User/SideDrawerField.tsx +++ b/src/components/fields/User/SideDrawerField.tsx @@ -22,7 +22,7 @@ export default function SideDrawerSelect({ let userValue: UserDataType[] = []; let emails = new Set(); - if (value !== undefined) { + if (value !== undefined && value !== null) { if (!Array.isArray(value)) { value = [value.email]; } diff --git a/src/components/fields/User/UserSelect.tsx b/src/components/fields/User/UserSelect.tsx index a8640a4fa..df9eac974 100644 --- a/src/components/fields/User/UserSelect.tsx +++ b/src/components/fields/User/UserSelect.tsx @@ -84,7 +84,7 @@ export default function UserSelect({ return ; }; - if (value === undefined) { + if (value === undefined || value === null) { value = []; } else if (!Array.isArray(value)) { value = [value.email]; From e5f0cbd29ae8a3e254ec78c42bb9eefd272262e6 Mon Sep 17 00:00:00 2001 From: iamanishroy <6275anishroy@gmail.com> Date: Thu, 23 Feb 2023 21:08:18 +0530 Subject: [PATCH 107/114] remove not required console logs --- src/sources/TableSourceFirestore/useApplySorts.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/sources/TableSourceFirestore/useApplySorts.ts b/src/sources/TableSourceFirestore/useApplySorts.ts index 1d0bc9888..5edf6b0a3 100644 --- a/src/sources/TableSourceFirestore/useApplySorts.ts +++ b/src/sources/TableSourceFirestore/useApplySorts.ts @@ -25,12 +25,7 @@ export default function useApplySorts() { useEffect(() => { if (applySort && Object.keys(tableSchema).length) { - console.log("useApplySorts"); const userDefaultSort = userSettings.tables?.[tableId]?.sorts || []; - console.log({ - userDefaultSort, - tableSchemaSorts: tableSchema.sorts, - }); setTableSorts( userDefaultSort.length ? userDefaultSort : tableSchema.sorts || [] ); From 869763c5a9e87e9284f1b81f862f8cf73188db5e Mon Sep 17 00:00:00 2001 From: Han Tuerker Date: Sun, 26 Feb 2023 08:11:43 +0300 Subject: [PATCH 108/114] add ref object to worker context --- src/components/fields/Formula/DisplayCell.tsx | 1 + src/components/fields/Formula/Settings.tsx | 15 ++++++--------- .../fields/Formula/TableSourcePreview.ts | 2 +- src/components/fields/Formula/formula.d.ts | 4 +++- src/components/fields/Formula/useFormula.tsx | 5 ++++- src/components/fields/Formula/worker.ts | 5 +++-- 6 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/components/fields/Formula/DisplayCell.tsx b/src/components/fields/Formula/DisplayCell.tsx index 24c7e2c66..adf59b9df 100644 --- a/src/components/fields/Formula/DisplayCell.tsx +++ b/src/components/fields/Formula/DisplayCell.tsx @@ -7,6 +7,7 @@ import { defaultFn, getDisplayCell } from "./util"; export default function Formula(props: IDisplayCellProps) { const { result, error, loading } = useFormula({ row: props.row, + ref: props._rowy_ref, listenerFields: props.column.config?.listenerFields || [], formulaFn: props.column.config?.formulaFn || defaultFn, }); diff --git a/src/components/fields/Formula/Settings.tsx b/src/components/fields/Formula/Settings.tsx index 912ad6b51..666615fe7 100644 --- a/src/components/fields/Formula/Settings.tsx +++ b/src/components/fields/Formula/Settings.tsx @@ -3,14 +3,7 @@ import { useDebouncedCallback } from "use-debounce"; import { useAtom } from "jotai"; import MultiSelect from "@rowy/multiselect"; -import { - Grid, - InputLabel, - Typography, - Stack, - FormHelperText, - Tooltip, -} from "@mui/material"; +import { Grid, InputLabel, Stack, FormHelperText } from "@mui/material"; import { tableColumnsOrderedAtom, @@ -142,7 +135,11 @@ export default function Settings({ additionalVariables={[ { key: "row", - description: `Current row's data`, + description: `row has the value of doc.data() it has type definitions using this table's schema, but you can only access formula's listener fields.`, + }, + { + key: "ref", + description: `reference object that holds the readonly reference of the row document.(i.e ref.id)`, }, ]} /> diff --git a/src/components/fields/Formula/TableSourcePreview.ts b/src/components/fields/Formula/TableSourcePreview.ts index 81d72662e..c4536c01e 100644 --- a/src/components/fields/Formula/TableSourcePreview.ts +++ b/src/components/fields/Formula/TableSourcePreview.ts @@ -1,7 +1,7 @@ import { useCallback, useEffect } from "react"; import { useSetAtom } from "jotai"; import { useAtomCallback } from "jotai/utils"; -import { cloneDeep, findIndex, initial, sortBy } from "lodash-es"; +import { cloneDeep, findIndex, sortBy } from "lodash-es"; import { _deleteRowDbAtom, diff --git a/src/components/fields/Formula/formula.d.ts b/src/components/fields/Formula/formula.d.ts index 517b7e570..69fcde0d8 100644 --- a/src/components/fields/Formula/formula.d.ts +++ b/src/components/fields/Formula/formula.d.ts @@ -1,6 +1,8 @@ +type RowRef = Pick; + type FormulaContext = { row: Row; - // ref: FirebaseFirestore.DocumentReference; + ref: RowRef; // storage: firebasestorage.Storage; // db: FirebaseFirestore.Firestore; }; diff --git a/src/components/fields/Formula/useFormula.tsx b/src/components/fields/Formula/useFormula.tsx index b4cacccaa..64d0fa6a7 100644 --- a/src/components/fields/Formula/useFormula.tsx +++ b/src/components/fields/Formula/useFormula.tsx @@ -2,17 +2,19 @@ import { useEffect, useMemo, useState } from "react"; import { pick, zipObject } from "lodash-es"; import { useAtom } from "jotai"; -import { TableRow } from "@src/types/table"; +import { TableRow, TableRowRef } from "@src/types/table"; import { tableColumnsOrderedAtom, tableScope } from "@src/atoms/tableScope"; import { listenerFieldTypes, useDeepCompareMemoize } from "./util"; export const useFormula = ({ row, + ref, listenerFields, formulaFn, }: { row: TableRow; + ref: TableRowRef; listenerFields: string[]; formulaFn: string; }) => { @@ -61,6 +63,7 @@ export const useFormula = ({ worker.postMessage({ formulaFn, row: JSON.stringify(availableFields), + ref: { id: ref.id, path: ref.path }, }); return () => { diff --git a/src/components/fields/Formula/worker.ts b/src/components/fields/Formula/worker.ts index 4baf08234..a0d85fd36 100644 --- a/src/components/fields/Formula/worker.ts +++ b/src/components/fields/Formula/worker.ts @@ -1,14 +1,15 @@ onmessage = async ({ data }) => { try { - const { formulaFn, row } = data; + const { formulaFn, row, ref } = data; const AsyncFunction = async function () {}.constructor as any; const [_, fnBody] = formulaFn.match(/=>\s*({?[\s\S]*}?)$/); if (!fnBody) return; const fn = new AsyncFunction( "row", + "ref", `const fn = async () => \n${fnBody}\n return fn();` ); - const result = await fn(JSON.parse(row)); + const result = await fn(JSON.parse(row), ref); postMessage({ result }); } catch (error: any) { console.error("Error: ", error); From e48d06377f835dee757f32882299854d7aece9d6 Mon Sep 17 00:00:00 2001 From: iamanishroy <6275anishroy@gmail.com> Date: Sun, 26 Feb 2023 16:13:03 +0530 Subject: [PATCH 109/114] bug fix: infinite rendering --- src/components/Table/Table.tsx | 19 ++++++++++ .../TableSourceFirestore.tsx | 2 -- .../TableSourceFirestore/useApplySorts.ts | 35 ------------------- 3 files changed, 19 insertions(+), 37 deletions(-) delete mode 100644 src/sources/TableSourceFirestore/useApplySorts.ts diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index b99c11295..4296c68a8 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -31,6 +31,8 @@ import { tablePageAtom, updateColumnAtom, selectedCellAtom, + tableSortsAtom, + tableIdAtom, } from "@src/atoms/tableScope"; import { getFieldType, getFieldProp } from "@src/components/fields"; import { useKeyboardNavigation } from "./useKeyboardNavigation"; @@ -38,6 +40,7 @@ import { useMenuAction } from "./useMenuAction"; import { useSaveColumnSizing } from "./useSaveColumnSizing"; import useHotKeys from "./useHotKey"; import type { TableRow, ColumnConfig } from "@src/types/table"; +import { projectScope, userSettingsAtom } from "@src/atoms/projectScope"; export const DEFAULT_ROW_HEIGHT = 41; export const DEFAULT_COL_WIDTH = 150; @@ -98,6 +101,10 @@ export default function Table({ const updateColumn = useSetAtom(updateColumnAtom, tableScope); + const [userSettings] = useAtom(userSettingsAtom, projectScope); + const [tableId] = useAtom(tableIdAtom, tableScope); + const setTableSorts = useSetAtom(tableSortsAtom, tableScope); + // Store a **state** and reference to the container element // so the state can re-render `TableBody`, preventing virtualization // not detecting scroll if the container element was initially `null` @@ -233,6 +240,18 @@ export default function Table({ containerRef, ]); + // apply user default sort on first render + const [applySort, setApplySort] = useState(true); + useEffect(() => { + if (applySort && Object.keys(tableSchema).length) { + const userDefaultSort = userSettings.tables?.[tableId]?.sorts || []; + setTableSorts( + userDefaultSort.length ? userDefaultSort : tableSchema.sorts || [] + ); + setApplySort(false); + } + }, [tableSchema, userSettings, tableId, setTableSorts, applySort]); + return (
    setContainerEl(el)} diff --git a/src/sources/TableSourceFirestore/TableSourceFirestore.tsx b/src/sources/TableSourceFirestore/TableSourceFirestore.tsx index 67833d533..9f3e2cedf 100644 --- a/src/sources/TableSourceFirestore/TableSourceFirestore.tsx +++ b/src/sources/TableSourceFirestore/TableSourceFirestore.tsx @@ -39,7 +39,6 @@ import { getTableSchemaPath } from "@src/utils/table"; import { TableSchema } from "@src/types/table"; import { firebaseDbAtom } from "@src/sources/ProjectSourceFirebase"; import { projectScope } from "@src/atoms/projectScope"; -import useApplySorts from "./useApplySorts"; /** * When rendered, provides atom values for top-level tables and sub-tables @@ -142,7 +141,6 @@ export const TableSourceFirestore = memo(function TableSourceFirestore() { } ); - useApplySorts(); useAuditChange(); useBulkWriteDb(); diff --git a/src/sources/TableSourceFirestore/useApplySorts.ts b/src/sources/TableSourceFirestore/useApplySorts.ts deleted file mode 100644 index 5edf6b0a3..000000000 --- a/src/sources/TableSourceFirestore/useApplySorts.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { useEffect, useState } from "react"; -import { useAtom, useSetAtom } from "jotai"; - -import { projectScope, userSettingsAtom } from "@src/atoms/projectScope"; -import { - tableIdAtom, - tableSchemaAtom, - tableScope, - tableSortsAtom, -} from "@src/atoms/tableScope"; - -/** - * Sets the value of tableSortsAtom - */ -export default function useApplySorts() { - // Apply the sorts - - const setTableSorts = useSetAtom(tableSortsAtom, tableScope); - const [userSettings] = useAtom(userSettingsAtom, projectScope); - const [tableId] = useAtom(tableIdAtom, tableScope); - const [tableSchema] = useAtom(tableSchemaAtom, tableScope); - - // Apply only once - const [applySort, setApplySort] = useState(true); - - useEffect(() => { - if (applySort && Object.keys(tableSchema).length) { - const userDefaultSort = userSettings.tables?.[tableId]?.sorts || []; - setTableSorts( - userDefaultSort.length ? userDefaultSort : tableSchema.sorts || [] - ); - setApplySort(false); - } - }, [setTableSorts, userSettings, tableId, applySort, tableSchema]); -} From eafb209d3b10454ca5f19f3e74e8c5b0a081721e Mon Sep 17 00:00:00 2001 From: iamanishroy <6275anishroy@gmail.com> Date: Sun, 26 Feb 2023 16:17:46 +0530 Subject: [PATCH 110/114] added comments --- src/components/Table/Table.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 4296c68a8..03285ac47 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -34,13 +34,13 @@ import { tableSortsAtom, tableIdAtom, } from "@src/atoms/tableScope"; +import { projectScope, userSettingsAtom } from "@src/atoms/projectScope"; import { getFieldType, getFieldProp } from "@src/components/fields"; import { useKeyboardNavigation } from "./useKeyboardNavigation"; import { useMenuAction } from "./useMenuAction"; import { useSaveColumnSizing } from "./useSaveColumnSizing"; import useHotKeys from "./useHotKey"; import type { TableRow, ColumnConfig } from "@src/types/table"; -import { projectScope, userSettingsAtom } from "@src/atoms/projectScope"; export const DEFAULT_ROW_HEIGHT = 41; export const DEFAULT_COL_WIDTH = 150; @@ -101,6 +101,7 @@ export default function Table({ const updateColumn = useSetAtom(updateColumnAtom, tableScope); + // Get user settings and tableId for applying sort sorting const [userSettings] = useAtom(userSettingsAtom, projectScope); const [tableId] = useAtom(tableIdAtom, tableScope); const setTableSorts = useSetAtom(tableSortsAtom, tableScope); From 6b136b0d0eb0208142c4385ebd8e99ac57e92b4c Mon Sep 17 00:00:00 2001 From: Bobby Wang Date: Mon, 27 Feb 2023 07:59:23 +0700 Subject: [PATCH 111/114] add code action: replace console.log with logging.log --- src/components/CodeEditor/CodeEditor.tsx | 38 +++++++++++- .../CodeEditor/useMonacoCustomizations.ts | 59 +++++++++++++++---- 2 files changed, 85 insertions(+), 12 deletions(-) diff --git a/src/components/CodeEditor/CodeEditor.tsx b/src/components/CodeEditor/CodeEditor.tsx index 2abfd45bf..6c5030856 100644 --- a/src/components/CodeEditor/CodeEditor.tsx +++ b/src/components/CodeEditor/CodeEditor.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import Editor, { EditorProps } from "@monaco-editor/react"; +import Editor, { EditorProps, Monaco } from "@monaco-editor/react"; import type { editor } from "monaco-editor/esm/vs/editor/editor.api"; import { useTheme, Box, BoxProps, AppBar, Toolbar } from "@mui/material"; @@ -72,6 +72,36 @@ export default function CodeEditor({ onValidate?.(markers); }; + const validate = (monaco: Monaco, model: editor.ITextModel) => { + const markers = []; + for (let i = 1; i < model.getLineCount() + 1; i++) { + const range = { + startLineNumber: i, + startColumn: 1, + endLineNumber: i, + endColumn: model.getLineLength(i) + 1, + }; + const line = model.getValueInRange(range); + for (const keyword of ["console.log", "console.warn", "console.error"]) { + const consoleLogIndex = line.indexOf(keyword); + if (consoleLogIndex >= 0) { + markers.push({ + message: `Replace with ${keyword.replace( + "console", + "logging" + )}: Rowy Cloud Logging provides a better experience to view logs. Simply replace 'console' with 'logging'. \n\nhttps://docs.rowy.io/cloud-logs`, + severity: monaco.MarkerSeverity.Warning, + startLineNumber: range.startLineNumber, + endLineNumber: range.endLineNumber, + startColumn: consoleLogIndex + 1, + endColumn: consoleLogIndex + keyword.length + 1, + }); + } + } + } + monaco.editor.setModelMarkers(model, "owner", markers); + }; + return ( { monaco.editor.defineTheme("github-light", githubLightTheme as any); monaco.editor.defineTheme("github-dark", githubDarkTheme as any); + monaco.editor.onDidCreateModel((model) => { + validate(monaco, model); + model.onDidChangeContent(() => { + validate(monaco, model); + }); + }); }} onMount={(editor) => { if (onFocus) editor.onDidFocusEditorWidget(onFocus); diff --git a/src/components/CodeEditor/useMonacoCustomizations.ts b/src/components/CodeEditor/useMonacoCustomizations.ts index e6d555332..d145d72e6 100644 --- a/src/components/CodeEditor/useMonacoCustomizations.ts +++ b/src/components/CodeEditor/useMonacoCustomizations.ts @@ -1,9 +1,4 @@ import { useEffect } from "react"; -// import { -// quicktype, -// InputData, -// jsonInputForTargetLanguage, -// } from "quicktype-core"; import { useAtom } from "jotai"; import { @@ -13,15 +8,10 @@ import { } from "@src/atoms/tableScope"; import { useMonaco } from "@monaco-editor/react"; import type { languages } from "monaco-editor/esm/vs/editor/editor.api"; -import githubLightTheme from "./github-light-default.json"; -import githubDarkTheme from "./github-dark-default.json"; import { useTheme } from "@mui/material"; import type { SystemStyleObject, Theme } from "@mui/system"; -// TODO: -// import { getFieldType, getFieldProp } from "@src/components/fields"; - /* eslint-disable import/no-webpack-loader-syntax */ import firestoreDefs from "!!raw-loader!./firestore.d.ts"; import firebaseAuthDefs from "!!raw-loader!./firebaseAuth.d.ts"; @@ -72,7 +62,6 @@ export default function useMonacoCustomizations({ }; }, []); - // Initialize external libs & TypeScript compiler options useEffect(() => { if (!monaco) return; @@ -95,6 +84,8 @@ export default function useMonacoCustomizations({ "ts:filename/utils.d.ts" ); monaco.languages.typescript.javascriptDefaults.addExtraLib(rowyUtilsDefs); + + setLoggingReplacementActions(); } catch (error) { console.error( "An error occurred during initialization of Monaco: ", @@ -135,6 +126,52 @@ export default function useMonacoCustomizations({ } }, [monaco, stringifiedDiagnosticsOptions]); + const setLoggingReplacementActions = () => { + if (!monaco) return; + const { dispose } = monaco.languages.registerCodeActionProvider( + "javascript", + { + provideCodeActions: (model, range, context, token) => { + const actions = context.markers + .filter((error) => { + return error.message.includes("Rowy Cloud Logging"); + }) + .map((error) => { + // first sentence of the message is "Replace with logging.[log/warn/error]" + const firstSentence = error.message.split(":")[0]; + const replacement = firstSentence.split("with ")[1]; + return { + title: firstSentence, + diagnostics: [error], + kind: "quickfix", + edit: { + edits: [ + { + resource: model.uri, + edit: { + range: error, + text: replacement, + }, + }, + ], + }, + isPreferred: true, + }; + }); + return { + actions: actions, + dispose: () => {}, + }; + }, + } + ); + monaco.editor.onWillDisposeModel((model) => { + // dispose code action provider when model is disposed + // this makes sure code actions are not displayed multiple times + dispose(); + }); + }; + const addJsonFieldDefinition = async ( columnKey: string, interfaceName: string From f36adf296c36ba41c0e16d6fedc1a27397de84bc Mon Sep 17 00:00:00 2001 From: Han Tuerker Date: Wed, 1 Mar 2023 22:23:16 +0100 Subject: [PATCH 112/114] fix(preview-table): fix table sort bug --- src/components/Table/ColumnHeader/useSaveTableSorts.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Table/ColumnHeader/useSaveTableSorts.tsx b/src/components/Table/ColumnHeader/useSaveTableSorts.tsx index 8ad053d58..c0c239ed1 100644 --- a/src/components/Table/ColumnHeader/useSaveTableSorts.tsx +++ b/src/components/Table/ColumnHeader/useSaveTableSorts.tsx @@ -18,13 +18,13 @@ function useSaveTableSorts(canEditColumns: boolean) { const [updateTableSchema] = useAtom(updateTableSchemaAtom, tableScope); const [updateUserSettings] = useAtom(updateUserSettingsAtom, projectScope); const [tableId] = useAtom(tableIdAtom, tableScope); - if (!updateTableSchema) throw new Error("Cannot update table schema"); const { enqueueSnackbar, closeSnackbar } = useSnackbar(); const [snackbarId, setSnackbarId] = useState(null); // Offer to save when table sorts changes const trigger = useCallback( (sorts: TableSort[]) => { + if (!updateTableSchema) throw new Error("Cannot update table schema"); if (updateUserSettings) { updateUserSettings({ tables: { From d02d72f3ea8a4d56ffc7abd136c6edd46d29b158 Mon Sep 17 00:00:00 2001 From: Han Tuerker Date: Thu, 2 Mar 2023 10:10:37 +0100 Subject: [PATCH 113/114] feat(preview-table): add serialized ref including parent recursively --- .../fields/Formula/PreviewTable.tsx | 9 +---- .../fields/Formula/TableSourcePreview.ts | 37 ++++++------------- src/components/fields/Formula/formula.d.ts | 7 ++-- src/components/fields/Formula/useFormula.tsx | 18 ++++++--- src/components/fields/Formula/util.tsx | 24 +++++++++++- src/components/fields/Formula/worker.ts | 4 +- 6 files changed, 52 insertions(+), 47 deletions(-) diff --git a/src/components/fields/Formula/PreviewTable.tsx b/src/components/fields/Formula/PreviewTable.tsx index 8b2107fcc..6cf85c229 100644 --- a/src/components/fields/Formula/PreviewTable.tsx +++ b/src/components/fields/Formula/PreviewTable.tsx @@ -23,14 +23,7 @@ const PreviewTable = ({ tableSchema }: { tableSchema: TableSchema }) => { scope={tableScope} initialValues={[ [currentUserAtom, currentUser], - [ - tableSettingsAtom, - { - ...tableSettings, - id: "preview-table", - collection: "preview-collection", - }, - ], + [tableSettingsAtom, tableSettings], [tableRowsDbAtom, []], ]} > diff --git a/src/components/fields/Formula/TableSourcePreview.ts b/src/components/fields/Formula/TableSourcePreview.ts index c4536c01e..07712f87f 100644 --- a/src/components/fields/Formula/TableSourcePreview.ts +++ b/src/components/fields/Formula/TableSourcePreview.ts @@ -1,5 +1,5 @@ import { useCallback, useEffect } from "react"; -import { useSetAtom } from "jotai"; +import { useAtom, useSetAtom } from "jotai"; import { useAtomCallback } from "jotai/utils"; import { cloneDeep, findIndex, sortBy } from "lodash-es"; @@ -10,39 +10,24 @@ import { tableRowsDbAtom, tableSchemaAtom, tableScope, + tableSettingsAtom, } from "@src/atoms/tableScope"; import { TableRow, TableSchema } from "@src/types/table"; import { updateRowData } from "@src/utils/table"; - -const initialRows = [ - { - _rowy_ref: { - id: "zzzzzzzzzzzzzzzzzzzw", - path: "preview-collection/zzzzzzzzzzzzzzzzzzzw", - }, - }, - { - _rowy_ref: { - id: "zzzzzzzzzzzzzzzzzzzx", - path: "preview-collection/zzzzzzzzzzzzzzzzzzzx", - }, - }, - { - _rowy_ref: { - id: "zzzzzzzzzzzzzzzzzzzy", - path: "preview-collection/zzzzzzzzzzzzzzzzzzzy", - }, - }, -]; +import { serializeRef } from "./util"; const TableSourcePreview = ({ tableSchema }: { tableSchema: TableSchema }) => { + const [tableSettings] = useAtom(tableSettingsAtom, tableScope); const setTableSchemaAtom = useSetAtom(tableSchemaAtom, tableScope); const setRows = useSetAtom(tableRowsDbAtom, tableScope); - useEffect(() => { - setRows(initialRows); - }, [setRows]); + setRows( + ["preview-doc-1", "preview-doc-2", "preview-doc-3"].map((docId) => ({ + _rowy_ref: serializeRef(`${tableSettings.collection}/${docId}`), + })) + ); + }, [setRows, tableSettings.collection]); useEffect(() => { setTableSchemaAtom(() => ({ @@ -52,7 +37,7 @@ const TableSourcePreview = ({ tableSchema }: { tableSchema: TableSchema }) => { }, [tableSchema, setTableSchemaAtom]); const readRowsDb = useAtomCallback( - useCallback((get) => get(tableRowsDbAtom) || initialRows, []), + useCallback((get) => get(tableRowsDbAtom) || [], []), tableScope ); diff --git a/src/components/fields/Formula/formula.d.ts b/src/components/fields/Formula/formula.d.ts index 69fcde0d8..8c6210c80 100644 --- a/src/components/fields/Formula/formula.d.ts +++ b/src/components/fields/Formula/formula.d.ts @@ -1,10 +1,9 @@ -type RowRef = Pick; +type RowRef = { id: string; path: string; parent: T }; +interface Ref extends RowRef {} type FormulaContext = { row: Row; - ref: RowRef; - // storage: firebasestorage.Storage; - // db: FirebaseFirestore.Firestore; + ref: Ref; }; type Formula = (context: FormulaContext) => "PLACEHOLDER_OUTPUT_TYPE"; diff --git a/src/components/fields/Formula/useFormula.tsx b/src/components/fields/Formula/useFormula.tsx index 64d0fa6a7..f6c44b758 100644 --- a/src/components/fields/Formula/useFormula.tsx +++ b/src/components/fields/Formula/useFormula.tsx @@ -5,7 +5,11 @@ import { useAtom } from "jotai"; import { TableRow, TableRowRef } from "@src/types/table"; import { tableColumnsOrderedAtom, tableScope } from "@src/atoms/tableScope"; -import { listenerFieldTypes, useDeepCompareMemoize } from "./util"; +import { + listenerFieldTypes, + serializeRef, + useDeepCompareMemoize, +} from "./util"; export const useFormula = ({ row, @@ -60,11 +64,13 @@ export const useFormula = ({ setLoading(false); }; - worker.postMessage({ - formulaFn, - row: JSON.stringify(availableFields), - ref: { id: ref.id, path: ref.path }, - }); + worker.postMessage( + JSON.stringify({ + formulaFn, + row: availableFields, + ref: serializeRef(ref.path), + }) + ); return () => { worker.terminate(); diff --git a/src/components/fields/Formula/util.tsx b/src/components/fields/Formula/util.tsx index 46d8cbee5..71973a2ec 100644 --- a/src/components/fields/Formula/util.tsx +++ b/src/components/fields/Formula/util.tsx @@ -24,6 +24,8 @@ import JsonDisplayCell from "@src/components/fields/Json/DisplayCell"; import CodeDisplayCell from "@src/components/fields/Code/DisplayCell"; import MarkdownDisplayCell from "@src/components/fields/Markdown/DisplayCell"; import CreatedByDisplayCell from "@src/components/fields/CreatedBy/DisplayCell"; +import { TableRowRef } from "@src/types/table"; +import { DocumentData, DocumentReference } from "firebase/firestore"; export function useDeepCompareMemoize(value: T) { const ref = useRef(value); @@ -65,7 +67,7 @@ export const outputFieldTypes = Object.values(FieldType).filter( ].includes(type) ); -export const defaultFn = `const formula:Formula = async ({ row })=> { +export const defaultFn = `const formula:Formula = async ({ row, ref })=> { // WRITE YOUR CODE ONLY BELOW THIS LINE. DO NOT WRITE CODE/COMMENTS OUTSIDE THE FUNCTION BODY // Example: @@ -120,3 +122,23 @@ export const getDisplayCell = (type: FieldType) => { return ShortTextDisplayCell; } }; + +export const serializeRef = (path: string, maxDepth = 20) => { + const pathArr = path.split("/"); + const serializedRef = { + path: pathArr.join("/"), + id: pathArr.pop(), + } as any; + let curr: TableRowRef | Partial> = + serializedRef; + let depth = 0; + while (pathArr.length > 0 && curr && depth < maxDepth) { + (curr.parent as any) = { + path: pathArr.join("/"), + id: pathArr.pop(), + } as Partial>; + curr = curr.parent as any; + maxDepth++; + } + return serializedRef; +}; diff --git a/src/components/fields/Formula/worker.ts b/src/components/fields/Formula/worker.ts index a0d85fd36..2b0af366d 100644 --- a/src/components/fields/Formula/worker.ts +++ b/src/components/fields/Formula/worker.ts @@ -1,6 +1,6 @@ onmessage = async ({ data }) => { try { - const { formulaFn, row, ref } = data; + const { formulaFn, row, ref } = JSON.parse(data); const AsyncFunction = async function () {}.constructor as any; const [_, fnBody] = formulaFn.match(/=>\s*({?[\s\S]*}?)$/); if (!fnBody) return; @@ -9,7 +9,7 @@ onmessage = async ({ data }) => { "ref", `const fn = async () => \n${fnBody}\n return fn();` ); - const result = await fn(JSON.parse(row), ref); + const result = await fn(row, ref); postMessage({ result }); } catch (error: any) { console.error("Error: ", error); From efce39a11798abb6baf439d68c8e85b44e9ee5d4 Mon Sep 17 00:00:00 2001 From: iamanishroy <6275anishroy@gmail.com> Date: Tue, 7 Mar 2023 12:52:13 +0530 Subject: [PATCH 114/114] added custom hook [to be tested] --- src/components/Table/Table.tsx | 12 +++++++--- src/components/Table/useStateWithRef.ts | 29 +++++++++++++++++++++++++ src/components/Table/useTraceUpdates.ts | 22 +++++++++++++++++++ 3 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 src/components/Table/useStateWithRef.ts create mode 100644 src/components/Table/useTraceUpdates.ts diff --git a/src/components/Table/Table.tsx b/src/components/Table/Table.tsx index 03285ac47..b81385832 100644 --- a/src/components/Table/Table.tsx +++ b/src/components/Table/Table.tsx @@ -1,5 +1,5 @@ import { useMemo, useRef, useState, useEffect, useCallback } from "react"; -import useStateRef from "react-usestateref"; +// import useStateRef from "react-usestateref"; // testing with useStateWithRef import { useAtom, useSetAtom } from "jotai"; import { useThrottledCallback } from "use-debounce"; import { @@ -41,6 +41,7 @@ import { useMenuAction } from "./useMenuAction"; import { useSaveColumnSizing } from "./useSaveColumnSizing"; import useHotKeys from "./useHotKey"; import type { TableRow, ColumnConfig } from "@src/types/table"; +import useStateWithRef from "./useStateWithRef"; // testing with useStateWithRef export const DEFAULT_ROW_HEIGHT = 41; export const DEFAULT_COL_WIDTH = 150; @@ -110,7 +111,9 @@ export default function Table({ // so the state can re-render `TableBody`, preventing virtualization // not detecting scroll if the container element was initially `null` const [containerEl, setContainerEl, containerRef] = - useStateRef(null); + // useStateRef(null); // <-- older approach with useStateRef + useStateWithRef(null); // <-- newer approach with custom hook + const gridRef = useRef(null); // Get column defs from table schema @@ -255,7 +258,10 @@ export default function Table({ return (
    setContainerEl(el)} + ref={(el) => { + if (!el) return; + setContainerEl(el); + }} onScroll={(e) => fetchMoreOnBottomReached(e.target as HTMLDivElement)} style={{ overflow: "auto", width: "100%", height: "100%" }} > diff --git a/src/components/Table/useStateWithRef.ts b/src/components/Table/useStateWithRef.ts new file mode 100644 index 000000000..be6309779 --- /dev/null +++ b/src/components/Table/useStateWithRef.ts @@ -0,0 +1,29 @@ +import { + MutableRefObject, + useCallback, + useRef, + useSyncExternalStore, +} from "react"; + +// NOTE: This is not the final solution. But is a potential solution for this problem. +export default function useStateWithRef( + initialState: T +): [T, (newValue: T) => void, MutableRefObject] { + const value = useRef(initialState); + const get = useCallback(() => value.current, []); + const subscribers = useRef(new Set<() => void>()); + + const set = useCallback((newValue: T) => { + value.current = newValue; + subscribers.current.forEach((callback) => callback()); + }, []); + + const subscribe = useCallback((callback: () => void) => { + subscribers.current.add(callback); + return () => subscribers.current.delete(callback); + }, []); + + const state = useSyncExternalStore(subscribe, get); + + return [state, set, value]; +} diff --git a/src/components/Table/useTraceUpdates.ts b/src/components/Table/useTraceUpdates.ts new file mode 100644 index 000000000..9485c28ae --- /dev/null +++ b/src/components/Table/useTraceUpdates.ts @@ -0,0 +1,22 @@ +import { useEffect, useRef } from "react"; + +// This hook is used to log changes to props in a component. +export default function useTraceUpdates( + props: { [key: string]: any }, + printMessage: string = "Changed props:" +) { + const prev = useRef(props); + useEffect(() => { + const changedProps = Object.entries(props).reduce((ps, [k, v]) => { + if (prev.current[k] !== v) { + // @ts-ignore + ps[k] = [prev.current[k], v]; + } + return ps; + }, {}); + if (Object.keys(changedProps).length > 0) { + console.log(printMessage, changedProps); + } + prev.current = props; + }); +}