Merge branch 'develop' into feature/functions-logging

This commit is contained in:
Bobby Wang
2022-12-30 15:30:56 +09:30
23 changed files with 588 additions and 90 deletions

View File

@@ -22,8 +22,8 @@ to start.
## Working on existing issues
If you are working on an [issue](https://github.com/rowyio/rowy/issues), share that you are working on it by commenting on the issue and posting a message on #contributions channel in Rowy's [Discord](https://discord.com/invite/fjBugmvzZP).
This allows others in the community and the maintainers a chance to provide feedback and guidance before you spend time working on it.
Before you get started working on an [issue](https://github.com/rowyio/rowy/issues), please make sure to share that you are working on it by commenting on the issue and posting a message on #contributions channel in Rowy's [Discord](https://discord.com/invite/fjBugmvzZP). The maintainers will then assign the issue to you after making sure any relevant information or context in addition is provided before you can start on the task.
Once you are assigned a task, please provide periodic updates or share any questions or roadblocks on either discord or the Github issue, so that the commmunity or the project maintainers can provide you any feedback or guidance as needed. If you are inactive for more than 1-2 week on a issue that was assigned to you, then we will assume you have stopped working on it and we will unassign it from you - so that we can give a chance to others in the community to work on it.

View File

@@ -30,7 +30,8 @@ Connect to your database, manage data in table-UI with role based access control
<img width="100%" src="https://user-images.githubusercontent.com/307298/157184506-f94f3f5b-e6d3-49df-9a2c-f665511883f2.png" />
## Live Demo
💥 Check out the [live demo](https://demo.rowy.io/) of Rowy 💥
💥 Check out the [live demo](https://demo.rowy.io/) of Rowy 💥
## Quick Deploy
@@ -42,7 +43,7 @@ https://rowy.app
## Documentation
You can find the full documentation with how-to guides and templates
You can find the full documentation with how-to guides and templates
[here](http://docs.rowy.io/).
## Features
@@ -51,6 +52,7 @@ https://user-images.githubusercontent.com/307298/157185793-f67511cd-7b7b-4229-95
<!-- <img width="85%" src="https://firebasestorage.googleapis.com/v0/b/rowyio.appspot.com/o/publicDemo%2FRowy%20Website%20Video%20GIF%20Small.gif?alt=media&token=3f699a8f-c1f2-4046-8ed5-e4ff66947cd8" />
-->
### Powerful spreadsheet interface for Firestore
- CMS for Firestore
@@ -62,22 +64,22 @@ https://user-images.githubusercontent.com/307298/157185793-f67511cd-7b7b-4229-95
### Automate with cloud functions and ready made extensions
- Build cloud functions workflows on field level data changes
- Use any NPM modules or APIs
- Build cloud functions workflows on field level data changes
- Use any NPM modules or APIs
- Connect to your favourite tool with pre-built code blocks or create your own
- SendGrid, Algolia, Twilio, Bigquery and more
- SendGrid, Algolia, Twilio, Bigquery and more
### Rich and flexible data fields
- [30+ fields supported](https://docs.rowy.io/field-types/supported-fields)
- Basic types: Short Text, Long Text, Email, Phone, URL…
- Custom UI pickers: Date, Checkbox, Single Select, Multi Select…
- Uploaders: Image, File
- Rich Editors: JSON, Code, Rich Text (HTML), Markdown
- Basic types: Short Text, Long Text, Email, Phone, URL…
- Custom UI pickers: Date, Checkbox, Single Select, Multi Select…
- Uploaders: Image, File
- Rich Editors: JSON, Code, Rich Text (HTML), Markdown
- Data validation, default values, required fields
- Action field: Clickable trigger for any Cloud Function
- Aggregate field: Populate cell with value aggregated from the rows sub-table
- Connector field: Connect data from multiple table collections
- Connector field: Connect data from multiple table collections
- Connect Service: Get data from any HTTP endpoint
### Collaborate with your team
@@ -89,7 +91,8 @@ https://user-images.githubusercontent.com/307298/157185793-f67511cd-7b7b-4229-95
## Install
Set up Rowy on your Google Cloud project with this one-click deploy button. Your data and cloud functions stay on your own Firestore/GCP.
Set up Rowy on your Google Cloud project with this one-click deploy button. Your
data and cloud functions stay on your own Firestore/GCP.
[![Run on Google Cloud](https://deploy.cloud.run/button.svg)](https://rowy.app/)
@@ -105,16 +108,24 @@ Alternatively, you can manually install by
## Roadmap
[View our roadmap](https://demo.rowy.io/table/roadmap) on Rowy - Upvote, downvote, share your thoughts!
[View our roadmap](https://demo.rowy.io/table/roadmap) on Rowy - Upvote,
downvote, share your thoughts!
If you'd like to propose a feature, submit an issue [here](https://github.com/rowyio/rowy/issues/new?assignees=&labels=&template=feature_request.md&title=).
If you'd like to propose a feature, submit an issue
[here](https://github.com/rowyio/rowy/issues/new?assignees=&labels=&template=feature_request.md&title=).
## Support the project
- Join a community of developers on [Discord](https://discord.gg/fjBugmvzZP) and share your ideas/feedback 💬
- Follow us on [Twitter](https://twitter.com/rowyio) and help [spread the word](https://twitter.com/intent/tweet?text=Check%20out%20@rowyio%20-%20It%27s%20like%20an%20open-source%20Airtable%20for%20your%20database,%20but%20with%20a%20built-in%20code%20editor%20for%20cloud%20functions%20to%20run%20on%20data%20CRUD!%0a%0aEsp%20if%20building%20on%20@googlecloud%20and%20@Firebase%20stack,%20it%20is%20the%20fastest%20way%20to%20build%20your%20product.%20Live%20demo:%20https://demo.rowy.io) 🙏
- Join a community of developers on [Discord](https://discord.gg/fjBugmvzZP) and
share your ideas/feedback 💬
- Follow us on [Twitter](https://twitter.com/rowyio) and help
[spread the word](https://twitter.com/intent/tweet?text=Check%20out%20@rowyio%20-%20It%27s%20like%20an%20open-source%20Airtable%20for%20your%20database,%20but%20with%20a%20built-in%20code%20editor%20for%20cloud%20functions%20to%20run%20on%20data%20CRUD!%0a%0aEsp%20if%20building%20on%20@googlecloud%20and%20@Firebase%20stack,%20it%20is%20the%20fastest%20way%20to%20build%20your%20product.%20Live%20demo:%20https://demo.rowy.io)
🙏
- Give us a star to this Github repo ⭐️
- Submit a PR. Take a look at our [contribution guide](https://github.com/rowyio/rowy/blob/main/CONTRIBUTING.md) and get started with [good first issues](https://github.com/rowyio/rowy/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).
- Submit a PR. Take a look at our
[contribution guide](https://github.com/rowyio/rowy/blob/main/CONTRIBUTING.md)
and get started with
[good first issues](https://github.com/rowyio/rowy/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).
## Help

View File

@@ -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]

View File

@@ -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 (
<Box
role="columnheader"
{...props}
sx={[
{
backgroundColor: "background.default",
border: (theme) => `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"
>
<Button
onClick={() => openColumnModal({ type: "new" })}
variant="contained"
color="primary"
startIcon={<AddColumnIcon />}
style={{ zIndex: 1, flexShrink: 0 }}
tabIndex={focusInsideCell ? 0 : -1}
>
Add column
</Button>
</Box>
);
else
return (
<Box
role="columnheader"
@@ -47,40 +79,4 @@ export default function FinalColumnHeader({
Actions
</Box>
);
return (
<Box
role="columnheader"
{...props}
sx={[
{
backgroundColor: "background.default",
border: (theme) => `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"
>
<Button
onClick={() => openColumnModal({ type: "new" })}
variant="contained"
color="primary"
startIcon={<AddColumnIcon />}
style={{ zIndex: 1, flexShrink: 0 }}
tabIndex={focusInsideCell ? 0 : -1}
>
Add column
</Button>
</Box>
);
}

View File

@@ -164,6 +164,7 @@ export default function withRenderTableCell(
<DisplayCellComponent {...basicCellProps} />
</div>
);
if (disabled || (editorMode !== "inline" && !focusInsideCell))
return displayCell;

View File

@@ -232,7 +232,7 @@ export function emptyExtensionObject(
): IExtension {
return {
name: `${type} extension`,
active: false,
active: true,
triggers: [],
type,
extensionBody: extensionBodyTemplate[type] ?? extensionBodyTemplate["task"],

View File

@@ -115,7 +115,7 @@ export function emptyWebhookObject(
): IWebhook {
return {
name: `${type} webhook`,
active: false,
active: true,
endpoint: generateId(),
type,
parser: webhookSchemas[type].parser?.template(table),

View File

@@ -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
{

View File

@@ -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) => (
<Button
href={`https://console.cloud.google.com/run/deploy/${
projectSettings.rowyRunRegion ?? "us-central1"
}/rowy-backend?project=${projectId}`}
target="_blank"
rel="noopener noreferrer"
onClick={() => closeSnackbar(snackbarId)}
variant="contained"
color="secondary"
>
Open GCP Console <InlineOpenInNewIcon />
</Button>
),
}
);
} else {
enqueueSnackbar(`Failed: ${error}`, { variant: "error" });
}
}
};
const isEmpty =

View File

@@ -2,27 +2,31 @@ import { ISettingsProps } from "@src/components/fields/types";
import { TextField, Button } from "@mui/material";
export default function Settings({ onChange, config }: ISettingsProps) {
const copyStandardRegex = () => {
onChange("validationRegex")("^[a-zA-Z0-9+_.-]+@[a-zA-Z0-9.-]+.[a-zA-z]{2,3}$");
}
return (
<>
<TextField
type="text"
label="Validation regex"
id="validation-regex"
value={config.validationRegex}
fullWidth
onChange={(e) => {
if (e.target.value === "") onChange("validationRegex")(null);
else onChange("validationRegex")(e.target.value);
}}
/>
<Button style={{ width: "200px", margin: "20px auto auto" }} onClick={copyStandardRegex}>
Use standard regex
</Button>
</>
const copyStandardRegex = () => {
onChange("validationRegex")(
"^[a-zA-Z0-9+_.-]+@[a-zA-Z0-9.-]+.[a-zA-z]{2,3}$"
);
};
return (
<>
<TextField
type="text"
label="Validation regex"
id="validation-regex"
value={config.validationRegex}
fullWidth
onChange={(e) => {
if (e.target.value === "") onChange("validationRegex")(null);
else onChange("validationRegex")(e.target.value);
}}
/>
<Button
style={{ width: "200px", margin: "20px auto auto" }}
onClick={copyStandardRegex}
>
Use standard regex
</Button>
</>
);
}

View File

@@ -0,0 +1,26 @@
import CircularProgressOptical from "@src/components/CircularProgressOptical";
import { IDisplayCellProps } from "@src/components/fields/types";
import { useFormula } from "./useFormula";
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 || defaultFn,
});
const type = props.column.config?.renderFieldType;
const DisplayCell = getDisplayCell(type);
if (error) {
return <>Error: {error.message}</>;
}
if (loading) {
return <CircularProgressOptical id="progress" size={20} sx={{ m: 0.25 }} />;
}
return <DisplayCell {...props} value={result} disabled={true} />;
}

View File

@@ -0,0 +1,144 @@
import { lazy, Suspense } from "react";
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 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 { defaultFn, listenerFieldTypes, outputFieldTypes } from "./util";
import { getFieldProp } from "..";
/* eslint-disable import/no-webpack-loader-syntax */
import formulaDefs from "!!raw-loader!./formula.d.ts";
const CodeEditor = lazy(
() =>
import("@src/components/CodeEditor" /* webpackChunkName: "CodeEditor" */)
);
const diagnosticsOptions = {
noSemanticValidation: false,
noSyntaxValidation: false,
noSuggestionDiagnostics: true,
};
export default function Settings({
config,
onChange,
onBlur,
errors,
}: ISettingsProps) {
const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope);
const returnType = getFieldProp("dataType", config.renderFieldType) ?? "any";
const formulaFn = config?.formulaFn ? config.formulaFn : defaultFn;
return (
<Stack spacing={1}>
<Grid container direction="row" spacing={2} flexWrap="nowrap">
<Grid item xs={12} md={6}>
<MultiSelect
label="Listener fields"
options={tableColumnsOrdered
.filter((c) => listenerFieldTypes.includes(c.type))
.map((c) => ({ label: c.name, value: c.key }))}
value={config.listenerFields ?? []}
onChange={onChange("listenerFields")}
TextFieldProps={{
helperText: (
<>
{errors.listenerFields && (
<FormHelperText error style={{ margin: 0 }}>
{errors.listenerFields}
</FormHelperText>
)}
<FormHelperText error={false} style={{ margin: 0 }}>
Changes to these fields will trigger the evaluation of the
column.
</FormHelperText>
</>
),
FormHelperTextProps: { component: "div" } as any,
required: true,
error: errors.listenerFields,
onBlur,
}}
/>
</Grid>
<Grid item xs={12} md={6}>
<FieldsDropdown
label="Output field type"
value={config.renderFieldType}
options={outputFieldTypes}
onChange={(value) => {
onChange("renderFieldType")(value);
}}
TextFieldProps={{
required: true,
error: errors.renderFieldType,
helperText: errors.renderFieldType,
onBlur,
}}
/>
</Grid>
</Grid>
<InputLabel>Formula script</InputLabel>
<div>
<Stack
direction="row"
alignItems="flex-start"
justifyItems="space-between"
justifyContent="space-between"
marginBottom={1}
>
<Typography variant="body2" color="textSecondary">
Available:
</Typography>
<Grid
container
spacing={1}
style={{ flexGrow: 1, marginTop: -8, marginLeft: 0 }}
>
<Grid item>
<Tooltip title="Current row's data">
<code>row</code>
</Tooltip>
</Grid>
</Grid>
</Stack>
<Suspense fallback={<FieldSkeleton height={200} />}>
<CodeEditor
diagnosticsOptions={diagnosticsOptions}
value={formulaFn}
extraLibs={[
formulaDefs.replace(
`"PLACEHOLDER_OUTPUT_TYPE"`,
`${returnType} | Promise<${returnType}>`
),
]}
onChange={useDebouncedCallback(onChange("formulaFn"), 300)}
/>
</Suspense>
</div>
</Stack>
);
}
export const settingsValidator = (config: any) => {
const errors: Record<string, any> = {};
if (config.error) errors.error = config.error;
return errors;
};

View File

@@ -0,0 +1,8 @@
type FormulaContext = {
row: Row;
// ref: FirebaseFirestore.DocumentReference;
// storage: firebasestorage.Storage;
// db: FirebaseFirestore.Firestore;
};
type Formula = (context: FormulaContext) => "PLACEHOLDER_OUTPUT_TYPE";

View File

@@ -0,0 +1,24 @@
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";
import Settings, { settingsValidator } from "./Settings";
export const config: IFieldConfig = {
type: FieldType.formula,
name: "Formula",
group: "Client Function",
dataType: "any",
initialValue: "",
icon: <FormulaIcon />,
description: "Client Function (Alpha)",
TableCell: withRenderTableCell(DisplayCell as any, null, undefined, {
usesRowData: true,
}),
SideDrawerField: () => null as any,
settings: Settings,
settingsValidator: settingsValidator,
requireConfiguration: true,
};
export default config;

View File

@@ -0,0 +1,73 @@
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 { listenerFieldTypes, useDeepCompareMemoize } from "./util";
export const useFormula = ({
row,
listenerFields,
formulaFn,
}: {
row: TableRow;
listenerFields: string[];
formulaFn: string;
}) => {
const [result, setResult] = useState(null);
const [error, setError] = useState<any>(null);
const [loading, setLoading] = useState<boolean>(false);
const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope);
const availableColumns = tableColumnsOrdered
.filter((c) => listenerFieldTypes.includes(c.type))
.map((c) => c.key);
const availableFields = useMemo(
() => ({
...zipObject(
availableColumns,
Array(availableColumns.length).fill(undefined)
),
...pick(row, availableColumns),
}),
[row, availableColumns]
);
const listeners = useMemo(
() => pick(availableFields, listenerFields),
[availableFields, listenerFields]
);
useEffect(() => {
setError(null);
setLoading(true);
const worker = new Worker(new URL("./worker.ts", import.meta.url), {
type: "module",
});
worker.onmessage = ({ data: { result, error } }: any) => {
worker.terminate();
if (error) {
setError(error);
} else {
setResult(result);
}
setLoading(false);
};
worker.postMessage({
formulaFn,
row: availableFields,
});
return () => {
worker.terminate();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [useDeepCompareMemoize(listeners), formulaFn]);
return { result, error, loading };
};

View File

@@ -0,0 +1,121 @@
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 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";
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<T>(value: T) {
const ref = useRef<T>(value);
const signalRef = useRef<number>(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 listenerFieldTypes = Object.values(FieldType).filter(
(type) =>
![FieldType.formula, FieldType.subTable, 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 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) {
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.singleSelect:
return SingleSelectDisplayCell;
case FieldType.multiSelect:
return MultiSelectDisplayCell;
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:
return ShortTextDisplayCell;
}
};

View File

@@ -0,0 +1,24 @@
onmessage = async ({ data }) => {
try {
const { formulaFn, row } = data;
const AsyncFunction = async function () {}.constructor as any;
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);
postMessage({
error,
});
} finally {
// eslint-disable-next-line no-restricted-globals
self.close();
}
};
export {};

View File

@@ -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,

View File

@@ -52,6 +52,8 @@ const WIKI_PATHS = {
fieldTypesAction: "/field-types/action",
fieldTypesAdd: "/field-types/add",
fieldTypesFormula: "/field-types/formula",
rowyRun: "/rowy-run",
extensions: "/extensions",

View File

@@ -40,13 +40,14 @@ export enum FieldType {
derivative = "DERIVATIVE",
aggregate = "AGGREGATE",
status = "STATUS",
// CLIENT FUNCTION
formula = "FORMULA",
// AUDIT
createdBy = "CREATED_BY",
updatedBy = "UPDATED_BY",
createdAt = "CREATED_AT",
updatedAt = "UPDATED_AT",
// METADATA
user = "USER",
id = "ID",
last = "LAST",

View File

@@ -81,8 +81,10 @@ export default function TablePage({
// Set permissions here so we can pass them to the `Table` component, which
// shouldnt 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 =

View File

@@ -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<string, Record<string, TableSettings>>
);
// Updates settings doc with new tables array
const promiseUpdateSettings = setDoc(
doc(firebaseDb, SETTINGS),
{ tables },
{ tables, tablesSettings },
{ merge: true }
);

View File

@@ -75,6 +75,7 @@ export type TableSettings = {
description?: string;
details?: string;
thumbnailURL?: string;
modifiableBy?: string[];
_createdBy?: {
displayName?: string;