mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-29 00:16:39 +01:00
Merge branch 'develop' into feat/webhooks
* develop: ConnectServiceSelect: fix list edge styles on dark theme remove duplicate ConnectServiceSelect code setup: remove insecure rule insecure rule that allows anyone to access any part of your database Test page: fix small switch spacing SideDrawer: add option to show hidden fields fix Switch with label spacing fix hidden fields & filters not working with multiple table views column settings: add basic validation on blur & change ConfirmationDialog: add hideCancel prop remove monaco diff editor validation for firestore rules connect service config fix action SchemaPath ref NavDrawer: use material icon for closing drawer add DiffEditor to setup rules step add DiffEditor
This commit is contained in:
@@ -47,8 +47,8 @@ export default function CodeEditorHelper({
|
||||
justifyContent="space-between"
|
||||
sx={{ my: 1 }}
|
||||
>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
You can access:
|
||||
<Typography variant="body2" color="textSecondary" sx={{ mr: 0.5 }}>
|
||||
Available:
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={1}>
|
||||
|
||||
93
src/components/CodeEditor/DiffEditor.tsx
Normal file
93
src/components/CodeEditor/DiffEditor.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
DiffEditor as MonacoDiffEditor,
|
||||
DiffEditorProps,
|
||||
EditorProps,
|
||||
} from "@monaco-editor/react";
|
||||
|
||||
import { useTheme, Box, BoxProps } from "@mui/material";
|
||||
import CircularProgressOptical from "@src/components/CircularProgressOptical";
|
||||
import ResizeBottomRightIcon from "@src/assets/icons/ResizeBottomRight";
|
||||
|
||||
import useMonacoCustomizations, {
|
||||
IUseMonacoCustomizationsProps,
|
||||
} from "./useMonacoCustomizations";
|
||||
|
||||
export interface IDiffEditorProps
|
||||
extends Partial<DiffEditorProps>,
|
||||
IUseMonacoCustomizationsProps {
|
||||
onChange?: EditorProps["onChange"];
|
||||
containerProps?: Partial<BoxProps>;
|
||||
}
|
||||
|
||||
export default function DiffEditor({
|
||||
onChange,
|
||||
minHeight = 100,
|
||||
disabled,
|
||||
error,
|
||||
containerProps,
|
||||
|
||||
extraLibs,
|
||||
diagnosticsOptions,
|
||||
onUnmount,
|
||||
|
||||
...props
|
||||
}: IDiffEditorProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
const { boxSx } = useMonacoCustomizations({
|
||||
minHeight,
|
||||
disabled,
|
||||
error,
|
||||
extraLibs,
|
||||
diagnosticsOptions,
|
||||
onUnmount,
|
||||
});
|
||||
|
||||
// Needs manual patch since `onMount` prop is not available in `DiffEditor`
|
||||
// https://github.com/suren-atoyan/monaco-react/issues/281
|
||||
const handleEditorMount: DiffEditorProps["onMount"] = (editor, monaco) => {
|
||||
const modifiedEditor = editor.getModifiedEditor();
|
||||
modifiedEditor.onDidChangeModelContent((ev) => {
|
||||
onChange?.(modifiedEditor.getValue(), ev);
|
||||
});
|
||||
|
||||
props.onMount?.(editor, monaco);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ ...boxSx, ...containerProps?.sx }}>
|
||||
<MonacoDiffEditor
|
||||
language="javascript"
|
||||
loading={<CircularProgressOptical size={20} sx={{ m: 2 }} />}
|
||||
className="editor"
|
||||
{...props}
|
||||
onMount={handleEditorMount}
|
||||
options={
|
||||
{
|
||||
readOnly: disabled,
|
||||
fontFamily: theme.typography.fontFamilyMono,
|
||||
rulers: [80],
|
||||
minimap: { enabled: false },
|
||||
lineNumbersMinChars: 4,
|
||||
lineDecorationsWidth: "18",
|
||||
automaticLayout: true,
|
||||
fixedOverflowWidgets: true,
|
||||
tabSize: 2,
|
||||
...props.options,
|
||||
} as any
|
||||
}
|
||||
/>
|
||||
|
||||
<ResizeBottomRightIcon
|
||||
aria-label="Resize code editor"
|
||||
color="action"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 1,
|
||||
right: 1,
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -126,8 +126,8 @@
|
||||
"editorGutter.modifiedBackground": "#d4a72c66",
|
||||
"editorGutter.addedBackground": "#4ac26b66",
|
||||
"editorGutter.deletedBackground": "#ff818266",
|
||||
"diffEditor.insertedTextBackground": "#dafbe1",
|
||||
"diffEditor.removedTextBackground": "#ffebe9",
|
||||
"diffEditor.insertedTextBackground": "#85e89d33",
|
||||
"diffEditor.removedTextBackground": "#f9758326",
|
||||
"scrollbar.shadow": "#6a737d33",
|
||||
"scrollbarSlider.background": "#959da533",
|
||||
"scrollbarSlider.hoverBackground": "#959da544",
|
||||
|
||||
@@ -1,39 +1,26 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import Editor, { EditorProps, useMonaco } from "@monaco-editor/react";
|
||||
import type { editor, languages } from "monaco-editor/esm/vs/editor/editor.api";
|
||||
import githubLightTheme from "./github-light-default.json";
|
||||
import githubDarkTheme from "./github-dark-default.json";
|
||||
import { useState } from "react";
|
||||
import Editor, { EditorProps } from "@monaco-editor/react";
|
||||
import type { editor } from "monaco-editor/esm/vs/editor/editor.api";
|
||||
|
||||
import { useTheme, Box, BoxProps } from "@mui/material";
|
||||
import CircularProgressOptical from "@src/components/CircularProgressOptical";
|
||||
import ResizeBottomRightIcon from "@src/assets/icons/ResizeBottomRight";
|
||||
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
import { getFieldProp } from "@src/components/fields";
|
||||
import useMonacoCustomizations, {
|
||||
IUseMonacoCustomizationsProps,
|
||||
} from "./useMonacoCustomizations";
|
||||
|
||||
/* eslint-disable import/no-webpack-loader-syntax */
|
||||
import firestoreDefs from "!!raw-loader!./firestore.d.ts";
|
||||
import firebaseAuthDefs from "!!raw-loader!./firebaseAuth.d.ts";
|
||||
import firebaseStorageDefs from "!!raw-loader!./firebaseStorage.d.ts";
|
||||
import utilsDefs from "!!raw-loader!./utils.d.ts";
|
||||
import extensionsDefs from "!!raw-loader!./extensions.d.ts";
|
||||
|
||||
export interface ICodeEditorProps extends Partial<EditorProps> {
|
||||
export interface ICodeEditorProps
|
||||
extends Partial<EditorProps>,
|
||||
IUseMonacoCustomizationsProps {
|
||||
value: string;
|
||||
minHeight?: number;
|
||||
disabled?: boolean;
|
||||
error?: boolean;
|
||||
containerProps?: Partial<BoxProps>;
|
||||
|
||||
extraLibs?: string[];
|
||||
onValidate?: EditorProps["onValidate"];
|
||||
onValidStatusUpdate?: (result: {
|
||||
isValid: boolean;
|
||||
markers: editor.IMarker[];
|
||||
}) => void;
|
||||
diagnosticsOptions?: languages.typescript.DiagnosticsOptions;
|
||||
onUnmount?: () => void;
|
||||
}
|
||||
|
||||
export default function CodeEditor({
|
||||
@@ -43,210 +30,44 @@ export default function CodeEditor({
|
||||
error,
|
||||
containerProps,
|
||||
|
||||
extraLibs,
|
||||
onValidate,
|
||||
onValidStatusUpdate,
|
||||
|
||||
extraLibs,
|
||||
diagnosticsOptions,
|
||||
onUnmount,
|
||||
|
||||
...props
|
||||
}: ICodeEditorProps) {
|
||||
const theme = useTheme();
|
||||
const { tableState } = useProjectContext();
|
||||
|
||||
// Store editor value to prevent code editor values not being saved when
|
||||
// Side Drawer is in the middle of a refresh
|
||||
const [initialEditorValue] = useState(value ?? "");
|
||||
const monaco = useMonaco();
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
onUnmount?.();
|
||||
};
|
||||
}, []);
|
||||
const { boxSx } = useMonacoCustomizations({
|
||||
minHeight,
|
||||
disabled,
|
||||
error,
|
||||
extraLibs,
|
||||
diagnosticsOptions,
|
||||
onUnmount,
|
||||
});
|
||||
|
||||
const onValidate_: EditorProps["onValidate"] = (markers) => {
|
||||
if (onValidStatusUpdate)
|
||||
onValidStatusUpdate({ isValid: markers.length <= 0, markers });
|
||||
else if (onValidate) onValidate(markers);
|
||||
onValidStatusUpdate?.({ isValid: markers.length <= 0, markers });
|
||||
onValidate?.(markers);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!monaco) {
|
||||
// useMonaco returns a monaco instance but initialisation is done asynchronously
|
||||
// dont execute the logic until the instance is initialised
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
try {
|
||||
monaco.editor.defineTheme("github-light", githubLightTheme as any);
|
||||
monaco.editor.defineTheme("github-dark", githubDarkTheme as any);
|
||||
monaco.editor.setTheme("github-" + theme.palette.mode);
|
||||
} catch (error) {
|
||||
console.error("Could not set Monaco theme: ", error);
|
||||
}
|
||||
});
|
||||
}, [monaco, theme.palette.mode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!monaco) {
|
||||
// useMonaco returns a monaco instance but initialisation is done asynchronously
|
||||
// dont execute the logic until the instance is initialised
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
monaco.editor.defineTheme("github-light", githubLightTheme as any);
|
||||
monaco.editor.defineTheme("github-dark", githubDarkTheme as any);
|
||||
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(firestoreDefs);
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
firebaseAuthDefs
|
||||
);
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
firebaseStorageDefs
|
||||
);
|
||||
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions(
|
||||
diagnosticsOptions ?? {
|
||||
noSemanticValidation: true,
|
||||
noSyntaxValidation: false,
|
||||
}
|
||||
);
|
||||
// compiler options
|
||||
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
|
||||
target: monaco.languages.typescript.ScriptTarget.ES2020,
|
||||
allowNonTsExtensions: true,
|
||||
});
|
||||
if (extraLibs) {
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
extraLibs.join("\n"),
|
||||
"ts:filename/extraLibs.d.ts"
|
||||
);
|
||||
}
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
utilsDefs,
|
||||
"ts:filename/utils.d.ts"
|
||||
);
|
||||
|
||||
const rowDefinition =
|
||||
Object.keys(tableState?.columns!)
|
||||
.map((columnKey: string) => {
|
||||
const column = tableState?.columns[columnKey];
|
||||
return `static ${columnKey}: ${getFieldProp("type", column.type)}`;
|
||||
})
|
||||
.join(";\n") + ";";
|
||||
|
||||
const availableFields = Object.keys(tableState?.columns!)
|
||||
.map((columnKey: string) => `"${columnKey}"`)
|
||||
.join("|\n");
|
||||
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
[
|
||||
"/**",
|
||||
" * extensions type configuration",
|
||||
" */",
|
||||
"// basic types that are used in all places",
|
||||
`type Row = {${rowDefinition}};`,
|
||||
`type Field = ${availableFields} | string | object;`,
|
||||
`type Fields = Field[];`,
|
||||
extensionsDefs,
|
||||
].join("\n"),
|
||||
"ts:filename/extensions.d.ts"
|
||||
);
|
||||
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
[
|
||||
"declare var require: any;",
|
||||
"declare var Buffer: any;",
|
||||
"const ref: FirebaseFirestore.DocumentReference;",
|
||||
"const storage: firebasestorage.Storage;",
|
||||
"const db: FirebaseFirestore.Firestore;",
|
||||
"const auth: adminauth.BaseAuth;",
|
||||
"declare class row {",
|
||||
" /**",
|
||||
" * Returns the row fields",
|
||||
" */",
|
||||
rowDefinition,
|
||||
"}",
|
||||
].join("\n"),
|
||||
"ts:filename/rowFields.d.ts"
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"An error occurred during initialization of Monaco: ",
|
||||
error
|
||||
);
|
||||
}
|
||||
}, [tableState?.columns, monaco, diagnosticsOptions, extraLibs]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
minWidth: 400,
|
||||
minHeight,
|
||||
height: minHeight,
|
||||
borderRadius: 1,
|
||||
resize: "vertical",
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
backgroundColor: disabled ? "transparent" : theme.palette.action.input,
|
||||
|
||||
"&::after": {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
pointerEvents: "none",
|
||||
borderRadius: "inherit",
|
||||
|
||||
boxShadow: `0 -1px 0 0 ${theme.palette.text.disabled} inset,
|
||||
0 0 0 1px ${theme.palette.action.inputOutline} inset`,
|
||||
transition: theme.transitions.create("box-shadow", {
|
||||
duration: theme.transitions.duration.short,
|
||||
}),
|
||||
},
|
||||
|
||||
"&:hover::after": {
|
||||
boxShadow: `0 -1px 0 0 ${theme.palette.text.primary} inset,
|
||||
0 0 0 1px ${theme.palette.action.inputOutline} inset`,
|
||||
},
|
||||
"&:focus-within::after": {
|
||||
boxShadow: `0 -2px 0 0 ${theme.palette.primary.main} inset,
|
||||
0 0 0 1px ${theme.palette.action.inputOutline} inset`,
|
||||
},
|
||||
|
||||
...(error
|
||||
? {
|
||||
"&::after, &:hover::after, &:focus-within::after": {
|
||||
boxShadow: `0 -2px 0 0 ${theme.palette.error.main} inset,
|
||||
0 0 0 1px ${theme.palette.action.inputOutline} inset`,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
|
||||
"& .editor": {
|
||||
// Overwrite user-select: none that causes editor
|
||||
// to not be focusable in Safari
|
||||
userSelect: "auto",
|
||||
height: "100%",
|
||||
},
|
||||
|
||||
"& .monaco-editor, & .monaco-editor .margin, & .monaco-editor-background":
|
||||
{
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
|
||||
...containerProps?.sx,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ ...boxSx, ...containerProps?.sx }}>
|
||||
<Editor
|
||||
defaultLanguage="javascript"
|
||||
value={initialEditorValue}
|
||||
onValidate={onValidate_}
|
||||
loading={<CircularProgressOptical size={20} sx={{ m: 2 }} />}
|
||||
className="editor"
|
||||
{...props}
|
||||
onValidate={onValidate_}
|
||||
options={{
|
||||
readOnly: disabled,
|
||||
fontFamily: theme.typography.fontFamilyMono,
|
||||
@@ -262,7 +83,7 @@ export default function CodeEditor({
|
||||
/>
|
||||
|
||||
<ResizeBottomRightIcon
|
||||
aria-label="This code editor is resizable"
|
||||
aria-label="Resize code editor"
|
||||
color="action"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
|
||||
218
src/components/CodeEditor/useMonacoCustomizations.ts
Normal file
218
src/components/CodeEditor/useMonacoCustomizations.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import { useEffect } from "react";
|
||||
|
||||
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 { SxProps, Theme } from "@mui/system";
|
||||
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
import { 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";
|
||||
import firebaseStorageDefs from "!!raw-loader!./firebaseStorage.d.ts";
|
||||
import utilsDefs from "!!raw-loader!./utils.d.ts";
|
||||
import extensionsDefs from "!!raw-loader!./extensions.d.ts";
|
||||
|
||||
export interface IUseMonacoCustomizationsProps {
|
||||
minHeight?: number;
|
||||
disabled?: boolean;
|
||||
error?: boolean;
|
||||
|
||||
extraLibs?: string[];
|
||||
diagnosticsOptions?: languages.typescript.DiagnosticsOptions;
|
||||
onUnmount?: () => void;
|
||||
}
|
||||
|
||||
export default function useMonacoCustomizations({
|
||||
minHeight,
|
||||
disabled,
|
||||
error,
|
||||
|
||||
extraLibs,
|
||||
diagnosticsOptions,
|
||||
onUnmount,
|
||||
}: IUseMonacoCustomizationsProps) {
|
||||
const theme = useTheme();
|
||||
const { tableState } = useProjectContext();
|
||||
|
||||
const monaco = useMonaco();
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
onUnmount?.();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!monaco) {
|
||||
// useMonaco returns a monaco instance but initialisation is done asynchronously
|
||||
// dont execute the logic until the instance is initialised
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
try {
|
||||
monaco.editor.defineTheme("github-light", githubLightTheme as any);
|
||||
monaco.editor.defineTheme("github-dark", githubDarkTheme as any);
|
||||
monaco.editor.setTheme("github-" + theme.palette.mode);
|
||||
} catch (error) {
|
||||
console.error("Could not set Monaco theme: ", error);
|
||||
}
|
||||
});
|
||||
}, [monaco, theme.palette.mode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!monaco) {
|
||||
// useMonaco returns a monaco instance but initialisation is done asynchronously
|
||||
// dont execute the logic until the instance is initialised
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
monaco.editor.defineTheme("github-light", githubLightTheme as any);
|
||||
monaco.editor.defineTheme("github-dark", githubDarkTheme as any);
|
||||
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(firestoreDefs);
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
firebaseAuthDefs
|
||||
);
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
firebaseStorageDefs
|
||||
);
|
||||
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions(
|
||||
diagnosticsOptions ?? {
|
||||
noSemanticValidation: true,
|
||||
noSyntaxValidation: false,
|
||||
}
|
||||
);
|
||||
// compiler options
|
||||
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
|
||||
target: monaco.languages.typescript.ScriptTarget.ES2020,
|
||||
allowNonTsExtensions: true,
|
||||
});
|
||||
if (extraLibs) {
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
extraLibs.join("\n"),
|
||||
"ts:filename/extraLibs.d.ts"
|
||||
);
|
||||
}
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
utilsDefs,
|
||||
"ts:filename/utils.d.ts"
|
||||
);
|
||||
|
||||
const rowDefinition =
|
||||
Object.keys(tableState?.columns!)
|
||||
.map((columnKey: string) => {
|
||||
const column = tableState?.columns[columnKey];
|
||||
return `static ${columnKey}: ${getFieldProp("type", column.type)}`;
|
||||
})
|
||||
.join(";\n") + ";";
|
||||
|
||||
const availableFields = Object.keys(tableState?.columns!)
|
||||
.map((columnKey: string) => `"${columnKey}"`)
|
||||
.join("|\n");
|
||||
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
[
|
||||
"/**",
|
||||
" * extensions type configuration",
|
||||
" */",
|
||||
"// basic types that are used in all places",
|
||||
`type Row = {${rowDefinition}};`,
|
||||
`type Field = ${availableFields} | string | object;`,
|
||||
`type Fields = Field[];`,
|
||||
extensionsDefs,
|
||||
].join("\n"),
|
||||
"ts:filename/extensions.d.ts"
|
||||
);
|
||||
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
[
|
||||
"declare var require: any;",
|
||||
"declare var Buffer: any;",
|
||||
"const ref: FirebaseFirestore.DocumentReference;",
|
||||
"const storage: firebasestorage.Storage;",
|
||||
"const db: FirebaseFirestore.Firestore;",
|
||||
"const auth: adminauth.BaseAuth;",
|
||||
"declare class row {",
|
||||
" /**",
|
||||
" * Returns the row fields",
|
||||
" */",
|
||||
rowDefinition,
|
||||
"}",
|
||||
].join("\n"),
|
||||
"ts:filename/rowFields.d.ts"
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"An error occurred during initialization of Monaco: ",
|
||||
error
|
||||
);
|
||||
}
|
||||
}, [tableState?.columns, monaco, diagnosticsOptions, extraLibs]);
|
||||
|
||||
const boxSx: SxProps<Theme> = {
|
||||
minWidth: 400,
|
||||
minHeight,
|
||||
height: minHeight,
|
||||
borderRadius: 1,
|
||||
resize: "vertical",
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
backgroundColor: disabled ? "transparent" : theme.palette.action.input,
|
||||
|
||||
"&::after": {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
pointerEvents: "none",
|
||||
borderRadius: "inherit",
|
||||
|
||||
boxShadow: `0 -1px 0 0 ${theme.palette.text.disabled} inset,
|
||||
0 0 0 1px ${theme.palette.action.inputOutline} inset`,
|
||||
transition: theme.transitions.create("box-shadow", {
|
||||
duration: theme.transitions.duration.short,
|
||||
}),
|
||||
},
|
||||
|
||||
"&:hover::after": {
|
||||
boxShadow: `0 -1px 0 0 ${theme.palette.text.primary} inset,
|
||||
0 0 0 1px ${theme.palette.action.inputOutline} inset`,
|
||||
},
|
||||
"&:focus-within::after": {
|
||||
boxShadow: `0 -2px 0 0 ${theme.palette.primary.main} inset,
|
||||
0 0 0 1px ${theme.palette.action.inputOutline} inset`,
|
||||
},
|
||||
|
||||
...(error
|
||||
? {
|
||||
"&::after, &:hover::after, &:focus-within::after": {
|
||||
boxShadow: `0 -2px 0 0 ${theme.palette.error.main} inset,
|
||||
0 0 0 1px ${theme.palette.action.inputOutline} inset`,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
|
||||
"& .editor": {
|
||||
// Overwrite user-select: none that causes editor
|
||||
// to not be focusable in Safari
|
||||
userSelect: "auto",
|
||||
height: "100%",
|
||||
},
|
||||
|
||||
"& .monaco-editor, & .monaco-editor .margin, & .monaco-editor-background": {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
};
|
||||
|
||||
return { boxSx };
|
||||
}
|
||||
@@ -17,6 +17,7 @@ export default function Confirmation({
|
||||
customBody,
|
||||
body,
|
||||
cancel,
|
||||
hideCancel,
|
||||
confirm,
|
||||
confirmationCommand,
|
||||
handleConfirm,
|
||||
@@ -55,7 +56,9 @@ export default function Confirmation({
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose}>{cancel ?? "Cancel"}</Button>
|
||||
{!hideCancel && (
|
||||
<Button onClick={handleClose}>{cancel ?? "Cancel"}</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => {
|
||||
handleConfirm();
|
||||
|
||||
@@ -4,6 +4,7 @@ export type confirmationProps =
|
||||
customBody?: React.ReactNode;
|
||||
body?: string;
|
||||
cancel?: string;
|
||||
hideCancel?: boolean;
|
||||
confirm?: string | JSX.Element;
|
||||
confirmationCommand?: string;
|
||||
handleConfirm: () => void;
|
||||
|
||||
@@ -1,204 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import clsx from "clsx";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import _get from "lodash/get";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Divider,
|
||||
Grid,
|
||||
InputAdornment,
|
||||
List,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
MenuItem,
|
||||
TextField,
|
||||
Typography,
|
||||
Radio,
|
||||
} from "@mui/material";
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
|
||||
import { IConnectServiceSelectProps } from ".";
|
||||
import useStyles from "./styles";
|
||||
import Loading from "@src/components/Loading";
|
||||
|
||||
export interface IPopupContentsProps
|
||||
extends Omit<IConnectServiceSelectProps, "className" | "TextFieldProps"> {}
|
||||
|
||||
// TODO: Implement infinite scroll here
|
||||
export default function PopupContents({
|
||||
value = [],
|
||||
onChange,
|
||||
config,
|
||||
|
||||
docRef,
|
||||
}: IPopupContentsProps) {
|
||||
const url = config.url;
|
||||
const titleKey = config.titleKey ?? config.primaryKey;
|
||||
const subtitleKey = config.subtitleKey;
|
||||
const resultsKey = config.resultsKey;
|
||||
const primaryKey = config.primaryKey;
|
||||
const multiple = Boolean(config.multiple);
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
// Webservice search query
|
||||
const [query, setQuery] = useState("");
|
||||
// Webservice response
|
||||
const [response, setResponse] = useState<any | null>(null);
|
||||
|
||||
const [docData, setDocData] = useState<any | null>(null);
|
||||
useEffect(() => {
|
||||
docRef.get().then((d) => setDocData(d.data()));
|
||||
}, []);
|
||||
|
||||
const hits: any["hits"] = _get(response, resultsKey) ?? [];
|
||||
const [search] = useDebouncedCallback(
|
||||
async (query: string) => {
|
||||
if (!docData) return;
|
||||
if (!url) return;
|
||||
const uri = new URL(url),
|
||||
params = { q: query };
|
||||
Object.keys(params).forEach((key) =>
|
||||
uri.searchParams.append(key, params[key])
|
||||
);
|
||||
|
||||
const resp = await fetch(uri.toString(), {
|
||||
method: "POST",
|
||||
body: JSON.stringify(docData),
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
|
||||
const jsonBody = await resp.json();
|
||||
setResponse(jsonBody);
|
||||
},
|
||||
1000,
|
||||
{ leading: true }
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
search(query);
|
||||
}, [query, docData]);
|
||||
|
||||
if (!response) return <Loading />;
|
||||
|
||||
const select = (hit: any) => () => {
|
||||
if (multiple) onChange([...value, hit]);
|
||||
else onChange([hit]);
|
||||
};
|
||||
const deselect = (hit: any) => () => {
|
||||
if (multiple)
|
||||
onChange(value.filter((v) => v[primaryKey] !== hit[primaryKey]));
|
||||
else onChange([]);
|
||||
};
|
||||
|
||||
const selectedValues = value?.map((item) => _get(item, primaryKey));
|
||||
|
||||
const clearSelection = () => onChange([]);
|
||||
|
||||
return (
|
||||
<Grid container direction="column" className={classes.grid}>
|
||||
<Grid item className={classes.searchRow}>
|
||||
<TextField
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
fullWidth
|
||||
variant="filled"
|
||||
margin="dense"
|
||||
label="Search items"
|
||||
className={classes.noMargins}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">
|
||||
<SearchIcon />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onKeyDown={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs className={classes.listRow}>
|
||||
<List className={classes.list}>
|
||||
{hits.map((hit) => {
|
||||
const isSelected =
|
||||
selectedValues.indexOf(_get(hit, primaryKey)) !== -1;
|
||||
console.log(`Selected Values: ${selectedValues}`);
|
||||
return (
|
||||
<React.Fragment key={_get(hit, primaryKey)}>
|
||||
<MenuItem
|
||||
dense
|
||||
onClick={isSelected ? deselect(hit) : select(hit)}
|
||||
>
|
||||
<ListItemIcon className={classes.checkboxContainer}>
|
||||
{multiple ? (
|
||||
<Checkbox
|
||||
edge="start"
|
||||
checked={isSelected}
|
||||
tabIndex={-1}
|
||||
color="secondary"
|
||||
className={classes.checkbox}
|
||||
disableRipple
|
||||
inputProps={{
|
||||
"aria-labelledby": `label-${_get(hit, primaryKey)}`,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Radio
|
||||
edge="start"
|
||||
checked={isSelected}
|
||||
tabIndex={-1}
|
||||
color="secondary"
|
||||
className={classes.checkbox}
|
||||
disableRipple
|
||||
inputProps={{
|
||||
"aria-labelledby": `label-${_get(hit, primaryKey)}`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
id={`label-${_get(hit, primaryKey)}`}
|
||||
primary={_get(hit, titleKey)}
|
||||
secondary={!subtitleKey ? "" : _get(hit, subtitleKey)}
|
||||
/>
|
||||
</MenuItem>
|
||||
<Divider className={classes.divider} />
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</Grid>
|
||||
|
||||
{multiple && (
|
||||
<Grid item className={clsx(classes.footerRow, classes.selectedRow)}>
|
||||
<Grid
|
||||
container
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
>
|
||||
<Typography
|
||||
variant="button"
|
||||
color="textSecondary"
|
||||
className={classes.selectedNum}
|
||||
>
|
||||
{value?.length} of {hits?.length}
|
||||
</Typography>
|
||||
|
||||
<Button
|
||||
disabled={!value || value.length === 0}
|
||||
onClick={clearSelection}
|
||||
color="primary"
|
||||
className={classes.selectAllButton}
|
||||
>
|
||||
Clear selection
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import { lazy, Suspense } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
import { TextField, TextFieldProps } from "@mui/material";
|
||||
import useStyles from "./styles";
|
||||
import Loading from "@src/components/Loading";
|
||||
import ErrorBoundary from "@src/components/ErrorBoundary";
|
||||
|
||||
const PopupContents = lazy(
|
||||
() => import("./PopupContents" /* webpackChunkName: "PopupContents" */)
|
||||
);
|
||||
|
||||
export type ServiceValue = { value: string; [prop: string]: any };
|
||||
|
||||
export interface IConnectServiceSelectProps {
|
||||
value: ServiceValue[];
|
||||
onChange: (value: ServiceValue[]) => void;
|
||||
row: any;
|
||||
config: {
|
||||
displayKey: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
editable?: boolean;
|
||||
/** Optional style overrides for root MUI `TextField` component */
|
||||
className?: string;
|
||||
/** Override any props of the root MUI `TextField` component */
|
||||
TextFieldProps?: Partial<TextFieldProps>;
|
||||
docRef: firebase.default.firestore.DocumentReference;
|
||||
}
|
||||
|
||||
export default function ConnectServiceSelect({
|
||||
value = [],
|
||||
className,
|
||||
TextFieldProps = {},
|
||||
...props
|
||||
}: IConnectServiceSelectProps) {
|
||||
const classes = useStyles();
|
||||
|
||||
const sanitisedValue = Array.isArray(value) ? value : [];
|
||||
|
||||
return (
|
||||
<TextField
|
||||
label=""
|
||||
hiddenLabel
|
||||
variant={"filled" as any}
|
||||
select
|
||||
value={sanitisedValue}
|
||||
className={clsx(classes.root, className)}
|
||||
{...TextFieldProps}
|
||||
SelectProps={{
|
||||
renderValue: (value) => `${(value as any[]).length} selected`,
|
||||
displayEmpty: true,
|
||||
classes: { root: classes.selectRoot },
|
||||
...TextFieldProps.SelectProps,
|
||||
// Must have this set to prevent MUI transforming `value`
|
||||
// prop for this component to a comma-separated string
|
||||
MenuProps: {
|
||||
classes: { paper: classes.paper, list: classes.menuChild },
|
||||
MenuListProps: { disablePadding: true },
|
||||
anchorOrigin: { vertical: "bottom", horizontal: "center" },
|
||||
transformOrigin: { vertical: "top", horizontal: "center" },
|
||||
...TextFieldProps.SelectProps?.MenuProps,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ErrorBoundary>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<PopupContents value={sanitisedValue} {...props} />
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
</TextField>
|
||||
);
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
import { makeStyles, createStyles } from "@mui/styles";
|
||||
|
||||
export const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
root: { minWidth: 200 },
|
||||
selectRoot: { paddingRight: theme.spacing(4) },
|
||||
|
||||
paper: { overflow: "hidden", maxHeight: "calc(100% - 48px)" },
|
||||
menuChild: {
|
||||
padding: `0 ${theme.spacing(2)}`,
|
||||
minWidth: 340,
|
||||
// Need to set fixed height here so popup is positioned correctly
|
||||
height: 340,
|
||||
},
|
||||
|
||||
grid: { outline: 0 },
|
||||
|
||||
noMargins: { margin: 0 },
|
||||
|
||||
searchRow: { marginTop: theme.spacing(2) },
|
||||
|
||||
listRow: {
|
||||
background: `${theme.palette.background.paper} no-repeat`,
|
||||
position: "relative",
|
||||
margin: theme.spacing(0, -2),
|
||||
maxWidth: `calc(100% + ${theme.spacing(4)})`,
|
||||
|
||||
"&::before, &::after": {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 9,
|
||||
|
||||
display: "block",
|
||||
height: 16,
|
||||
|
||||
background: `linear-gradient(to bottom, #fff, rgba(255, 255, 255, 0))`,
|
||||
},
|
||||
|
||||
"&::after": {
|
||||
top: "auto",
|
||||
bottom: 0,
|
||||
background: `linear-gradient(to top, #fff, rgba(255, 255, 255, 0))`,
|
||||
},
|
||||
},
|
||||
list: () => {
|
||||
let maxHeightDeductions = 0;
|
||||
maxHeightDeductions -= 64; // search box
|
||||
maxHeightDeductions -= 48; // multiple
|
||||
maxHeightDeductions += 8; // footer padding
|
||||
|
||||
return {
|
||||
padding: theme.spacing(2, 0),
|
||||
overflowY: "auto" as "auto",
|
||||
// height: `calc(340px - ${-maxHeightDeductions}px)`,
|
||||
height: 340 + maxHeightDeductions,
|
||||
};
|
||||
},
|
||||
|
||||
checkboxContainer: { minWidth: theme.spacing(36 / 8) },
|
||||
checkbox: {
|
||||
padding: theme.spacing(6 / 8, 9 / 8),
|
||||
"&:hover": { background: "transparent" },
|
||||
},
|
||||
|
||||
divider: { margin: theme.spacing(0, 2, 0, 6.5) },
|
||||
|
||||
footerRow: { marginBottom: theme.spacing(2) },
|
||||
selectedRow: {
|
||||
"$listRow + &": { marginTop: -theme.spacing(1) },
|
||||
"$footerRow + &": { marginTop: -theme.spacing(2) },
|
||||
|
||||
marginBottom: 0,
|
||||
"& > div": { height: 48 },
|
||||
},
|
||||
selectAllButton: { marginRight: -theme.spacing(1) },
|
||||
selectedNum: { fontFeatureSettings: '"tnum"' },
|
||||
})
|
||||
);
|
||||
|
||||
export default useStyles;
|
||||
@@ -15,7 +15,7 @@ import HomeIcon from "@mui/icons-material/HomeOutlined";
|
||||
import SettingsIcon from "@mui/icons-material/SettingsOutlined";
|
||||
import ProjectSettingsIcon from "@mui/icons-material/BuildCircleOutlined";
|
||||
import UserManagementIcon from "@mui/icons-material/AccountCircleOutlined";
|
||||
import CloseIcon from "@src/assets/icons/Backburger";
|
||||
import CloseIcon from "@mui/icons-material/MenuOpen";
|
||||
import PinIcon from "@mui/icons-material/PushPinOutlined";
|
||||
import UnpinIcon from "@mui/icons-material/PushPin";
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function SetupItem({
|
||||
<ArrowIcon aria-label="Item" color="primary" />
|
||||
)}
|
||||
|
||||
<Stack spacing={2} alignItems="flex-start">
|
||||
<Stack spacing={2} alignItems="flex-start" style={{ flexGrow: 1 }}>
|
||||
<Typography variant="inherit">{title}</Typography>
|
||||
|
||||
{children}
|
||||
|
||||
@@ -7,21 +7,28 @@ import {
|
||||
Checkbox,
|
||||
Button,
|
||||
Link,
|
||||
TextField,
|
||||
Grid,
|
||||
} from "@mui/material";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import InfoIcon from "@mui/icons-material/InfoOutlined";
|
||||
import CopyIcon from "@src/assets/icons/Copy";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
|
||||
import SetupItem from "./SetupItem";
|
||||
import DiffEditor from "@src/components/CodeEditor/DiffEditor";
|
||||
|
||||
import { name } from "@root/package.json";
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
import { CONFIG } from "@src/config/dbPaths";
|
||||
import { requiredRules, adminRules, utilFns } from "@src/config/firestoreRules";
|
||||
import {
|
||||
requiredRules,
|
||||
adminRules,
|
||||
utilFns,
|
||||
insecureRule,
|
||||
} from "@src/config/firestoreRules";
|
||||
import { rowyRun } from "@src/utils/rowyRun";
|
||||
import { runRoutes } from "@src/constants/runRoutes";
|
||||
import { useConfirmation } from "@src/components/ConfirmationDialog";
|
||||
// import { useConfirmation } from "@src/components/ConfirmationDialog";
|
||||
|
||||
export default function Step4Rules({
|
||||
rowyRunUrl,
|
||||
@@ -29,7 +36,7 @@ export default function Step4Rules({
|
||||
setCompletion,
|
||||
}: ISetupStepBodyProps) {
|
||||
const { projectId, getAuthToken } = useAppContext();
|
||||
const { requestConfirmation } = useConfirmation();
|
||||
// const { requestConfirmation } = useConfirmation();
|
||||
|
||||
const [hasRules, setHasRules] = useState(completion.rules);
|
||||
const [adminRule, setAdminRule] = useState(true);
|
||||
@@ -52,6 +59,17 @@ export default function Step4Rules({
|
||||
.then((data) => setCurrentRules(data?.source?.[0]?.content ?? ""));
|
||||
}, [rowyRunUrl, hasRules, currentRules, getAuthToken]);
|
||||
|
||||
const insecureRuleRegExp = new RegExp(
|
||||
insecureRule
|
||||
.replace(/\//g, "\\/")
|
||||
.replace(/\*/g, "\\*")
|
||||
.replace(/\s{2,}/g, "\\s+")
|
||||
.replace(/\s/g, "\\s*")
|
||||
.replace(/\n/g, "\\s+")
|
||||
.replace(/;/g, ";?")
|
||||
);
|
||||
const hasInsecureRule = insecureRuleRegExp.test(currentRules);
|
||||
|
||||
const [newRules, setNewRules] = useState("");
|
||||
useEffect(() => {
|
||||
let rulesToInsert = rules;
|
||||
@@ -63,13 +81,15 @@ export default function Step4Rules({
|
||||
rulesToInsert = rulesToInsert.replace(/function hasAnyRole[^}]*}/s, "");
|
||||
}
|
||||
|
||||
const inserted = currentRules.replace(
|
||||
let inserted = currentRules.replace(
|
||||
/match\s*\/databases\/\{database\}\/documents\s*\{/,
|
||||
`match /databases/{database}/documents {\n` + rulesToInsert
|
||||
);
|
||||
|
||||
if (hasInsecureRule) inserted = inserted.replace(insecureRuleRegExp, "");
|
||||
|
||||
setNewRules(inserted);
|
||||
}, [currentRules, rules]);
|
||||
}, [currentRules, rules, hasInsecureRule, insecureRuleRegExp]);
|
||||
|
||||
const [rulesStatus, setRulesStatus] = useState<"LOADING" | string>("");
|
||||
const setRules = async () => {
|
||||
@@ -97,18 +117,20 @@ export default function Step4Rules({
|
||||
}
|
||||
};
|
||||
|
||||
const handleSkip = () => {
|
||||
requestConfirmation({
|
||||
title: "Skip rules",
|
||||
body: "This might prevent you or other users in your project from accessing firestore data on Rowy",
|
||||
confirm: "Skip",
|
||||
cancel: "cancel",
|
||||
handleConfirm: async () => {
|
||||
setCompletion((c) => ({ ...c, rules: true }));
|
||||
setHasRules(true);
|
||||
},
|
||||
});
|
||||
};
|
||||
const [showManualMode, setShowManualMode] = useState(false);
|
||||
|
||||
// const handleSkip = () => {
|
||||
// requestConfirmation({
|
||||
// title: "Skip rules",
|
||||
// body: "This might prevent you or other users in your project from accessing firestore data on Rowy",
|
||||
// confirm: "Skip",
|
||||
// cancel: "cancel",
|
||||
// handleConfirm: async () => {
|
||||
// setCompletion((c) => ({ ...c, rules: true }));
|
||||
// setHasRules(true);
|
||||
// },
|
||||
// });
|
||||
// };
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -139,88 +161,30 @@ export default function Step4Rules({
|
||||
sx={{ "&&": { ml: -11 / 8, mb: -11 / 8 }, width: "100%" }}
|
||||
/>
|
||||
|
||||
<Typography
|
||||
variant="body2"
|
||||
component="pre"
|
||||
sx={{
|
||||
width: { sm: "100%", md: 840 - 72 - 32 },
|
||||
height: 136,
|
||||
resize: "both",
|
||||
overflow: "auto",
|
||||
<Typography>
|
||||
<InfoIcon
|
||||
aria-label="Info"
|
||||
sx={{ fontSize: 18, mr: 11 / 8, verticalAlign: "sub" }}
|
||||
/>
|
||||
We removed an insecure rule that allows anyone to access any part
|
||||
of your database
|
||||
</Typography>
|
||||
|
||||
"& .comment": { color: "info.dark" },
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: rules.replace(
|
||||
/(\/\/.*$)/gm,
|
||||
`<span class="comment">$1</span>`
|
||||
),
|
||||
}}
|
||||
<DiffEditor
|
||||
original={currentRules}
|
||||
modified={newRules}
|
||||
containerProps={{ sx: { width: "100%" } }}
|
||||
minHeight={400}
|
||||
options={{ renderValidationDecorations: "off" }}
|
||||
/>
|
||||
|
||||
<Button
|
||||
startIcon={<CopyIcon />}
|
||||
onClick={() => navigator.clipboard.writeText(rules)}
|
||||
<Typography
|
||||
variant="inherit"
|
||||
color={
|
||||
rulesStatus !== "LOADING" && rulesStatus ? "error" : undefined
|
||||
}
|
||||
>
|
||||
Copy to clipboard
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</SetupItem>
|
||||
|
||||
{!hasRules && (
|
||||
<SetupItem
|
||||
status="incomplete"
|
||||
title={
|
||||
<>
|
||||
You can add these rules{" "}
|
||||
<Link
|
||||
href={`https://console.firebase.google.com/project/${
|
||||
projectId || "_"
|
||||
}/firestore/rules`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
in the Firebase Console
|
||||
<InlineOpenInNewIcon />
|
||||
</Link>{" "}
|
||||
or directly below:
|
||||
</>
|
||||
}
|
||||
>
|
||||
<TextField
|
||||
id="new-rules"
|
||||
label="New rules"
|
||||
value={newRules}
|
||||
onChange={(e) => setNewRules(e.target.value)}
|
||||
multiline
|
||||
rows={5}
|
||||
fullWidth
|
||||
sx={{
|
||||
"& .MuiInputBase-input": {
|
||||
fontFamily: "mono",
|
||||
letterSpacing: 0,
|
||||
resize: "vertical",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Typography
|
||||
variant="inherit"
|
||||
color={
|
||||
rulesStatus !== "LOADING" && rulesStatus ? "error" : undefined
|
||||
}
|
||||
>
|
||||
Please check the generated rules first.
|
||||
</Typography>
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
{" "}
|
||||
Please verify the new rules first.
|
||||
</Typography>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
@@ -229,13 +193,73 @@ export default function Step4Rules({
|
||||
>
|
||||
Set Firestore Rules
|
||||
</LoadingButton>
|
||||
<Button onClick={handleSkip}>Skip</Button>
|
||||
{rulesStatus !== "LOADING" && typeof rulesStatus === "string" && (
|
||||
<Typography variant="caption" color="error">
|
||||
{rulesStatus}
|
||||
</Typography>
|
||||
)}
|
||||
{!showManualMode && (
|
||||
<Link
|
||||
component="button"
|
||||
variant="body2"
|
||||
onClick={() => setShowManualMode(true)}
|
||||
>
|
||||
Alternatively, add these rules in the Firebase Console
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</SetupItem>
|
||||
|
||||
{!hasRules && showManualMode && (
|
||||
<SetupItem
|
||||
status="incomplete"
|
||||
title="Alternatively, you can add these rules in the Firebase Console."
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="pre"
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: 400,
|
||||
resize: "both",
|
||||
overflow: "auto",
|
||||
|
||||
"& .comment": { color: "info.dark" },
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: rules.replace(
|
||||
/(\/\/.*$)/gm,
|
||||
`<span class="comment">$1</span>`
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item>
|
||||
<Button
|
||||
startIcon={<CopyIcon />}
|
||||
onClick={() => navigator.clipboard.writeText(rules)}
|
||||
>
|
||||
Copy to clipboard
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<Button
|
||||
href={`https://console.firebase.google.com/project/${
|
||||
projectId || "_"
|
||||
}/firestore/rules`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Firebase Console
|
||||
<InlineOpenInNewIcon />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</div>
|
||||
{rulesStatus !== "LOADING" && typeof rulesStatus === "string" && (
|
||||
<Typography variant="caption" color="error">
|
||||
{rulesStatus}
|
||||
</Typography>
|
||||
)}
|
||||
</SetupItem>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -2,8 +2,9 @@ import { createElement, useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import _sortBy from "lodash/sortBy";
|
||||
import _isEmpty from "lodash/isEmpty";
|
||||
import createPersistedState from "use-persisted-state";
|
||||
|
||||
import { Stack } from "@mui/material";
|
||||
import { Stack, FormControlLabel, Switch } from "@mui/material";
|
||||
|
||||
import { Values } from "./utils";
|
||||
import { getFieldProp } from "@src/components/fields";
|
||||
@@ -16,6 +17,10 @@ import { useAppContext } from "@src/contexts/AppContext";
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
import { sanitizeFirestoreRefs } from "@src/utils/fns";
|
||||
|
||||
const useSideDrawerShowHiddenFieldsState = createPersistedState(
|
||||
"__ROWY__SIDE_DRAWER_SHOW_HIDDEN_FIELDS"
|
||||
);
|
||||
|
||||
export interface IFormProps {
|
||||
values: Values;
|
||||
}
|
||||
@@ -23,12 +28,18 @@ export interface IFormProps {
|
||||
export default function Form({ values }: IFormProps) {
|
||||
const { tableState, sideDrawerRef } = useProjectContext();
|
||||
const { userDoc } = useAppContext();
|
||||
const userDocHiddenFields =
|
||||
userDoc.state.doc?.tables?.[`${tableState!.tablePath}`]?.hiddenFields ?? [];
|
||||
|
||||
const fields = _sortBy(Object.values(tableState!.columns), "index").filter(
|
||||
(f) => !userDocHiddenFields.includes(f.name)
|
||||
);
|
||||
const userDocHiddenFields =
|
||||
userDoc.state.doc?.tables?.[`${tableState!.config.id}`]?.hiddenFields ?? [];
|
||||
|
||||
const [showHiddenFields, setShowHiddenFields] =
|
||||
useSideDrawerShowHiddenFieldsState(false);
|
||||
|
||||
const fields = showHiddenFields
|
||||
? _sortBy(Object.values(tableState!.columns), "index")
|
||||
: _sortBy(Object.values(tableState!.columns), "index").filter(
|
||||
(f) => !userDocHiddenFields.includes(f.key)
|
||||
);
|
||||
|
||||
// Get initial values from fields config. This won’t be written to the db
|
||||
// when the SideDrawer is opened. Only dirty fields will be written
|
||||
@@ -121,6 +132,24 @@ export default function Form({ values }: IFormProps) {
|
||||
label="Document path"
|
||||
debugText={values.ref?.path ?? values.id ?? "No ref"}
|
||||
/>
|
||||
|
||||
{userDocHiddenFields.length > 0 && (
|
||||
<FormControlLabel
|
||||
label="Show hidden fields"
|
||||
control={
|
||||
<Switch
|
||||
checked={showHiddenFields}
|
||||
onChange={(e) => setShowHiddenFields(e.target.checked)}
|
||||
/>
|
||||
}
|
||||
sx={{
|
||||
borderTop: 1,
|
||||
borderColor: "divider",
|
||||
pt: 3,
|
||||
"& .MuiSwitch-root": { ml: -0.5 },
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -70,7 +70,7 @@ export default function SideDrawer() {
|
||||
if (cell && tableState?.rows[cell.row]) {
|
||||
window.history.pushState(
|
||||
"",
|
||||
`${tableState?.tablePath}`,
|
||||
`${tableState?.config.id}`,
|
||||
`${window.location.pathname}?rowRef=${encodeURIComponent(
|
||||
tableState?.rows[cell.row].ref.path
|
||||
)}`
|
||||
|
||||
@@ -147,7 +147,7 @@ export default function BulkActions({ selectedRows, columns, clearSelection }) {
|
||||
},
|
||||
column: actionColumn,
|
||||
action: actionType,
|
||||
schemaDocPath: formatPath(tableState?.tablePath ?? ""),
|
||||
schemaDocPath: formatPath(tableState?.config.id ?? ""),
|
||||
actionParams: {},
|
||||
};
|
||||
return true;
|
||||
|
||||
@@ -22,11 +22,35 @@ export default function FieldSettings(props: IMenuModalProps) {
|
||||
const [showRebuildPrompt, setShowRebuildPrompt] = useState(false);
|
||||
const [newConfig, setNewConfig] = useState(config ?? {});
|
||||
const customFieldSettings = getFieldProp("settings", type);
|
||||
const settingsValidator = getFieldProp("settingsValidator", type);
|
||||
const initializable = getFieldProp("initializable", type);
|
||||
|
||||
const { requestConfirmation } = useConfirmation();
|
||||
const { tableState, rowyRun } = useProjectContext();
|
||||
|
||||
const rendedFieldSettings = useMemo(
|
||||
() =>
|
||||
[FieldType.derivative, FieldType.aggregate].includes(type) &&
|
||||
newConfig.renderFieldType
|
||||
? getFieldProp("settings", newConfig.renderFieldType)
|
||||
: null,
|
||||
[newConfig.renderFieldType, type]
|
||||
);
|
||||
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const validateSettings = () => {
|
||||
if (settingsValidator) {
|
||||
const errors = settingsValidator(newConfig);
|
||||
setErrors(errors);
|
||||
return errors;
|
||||
}
|
||||
setErrors({});
|
||||
return {};
|
||||
};
|
||||
|
||||
const handleChange = (key: string) => (update: any) => {
|
||||
if (
|
||||
showRebuildPrompt === false &&
|
||||
@@ -37,16 +61,8 @@ export default function FieldSettings(props: IMenuModalProps) {
|
||||
}
|
||||
const updatedConfig = _set({ ...newConfig }, key, update);
|
||||
setNewConfig(updatedConfig);
|
||||
validateSettings();
|
||||
};
|
||||
const rendedFieldSettings = useMemo(
|
||||
() =>
|
||||
[FieldType.derivative, FieldType.aggregate].includes(type) &&
|
||||
newConfig.renderFieldType
|
||||
? getFieldProp("settings", newConfig.renderFieldType)
|
||||
: null,
|
||||
[newConfig.renderFieldType, type]
|
||||
);
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -78,8 +94,10 @@ export default function FieldSettings(props: IMenuModalProps) {
|
||||
>
|
||||
{createElement(customFieldSettings, {
|
||||
config: newConfig,
|
||||
handleChange,
|
||||
onChange: handleChange,
|
||||
fieldName,
|
||||
onBlur: validateSettings,
|
||||
errors,
|
||||
})}
|
||||
</Stack>
|
||||
)}
|
||||
@@ -94,7 +112,9 @@ export default function FieldSettings(props: IMenuModalProps) {
|
||||
</Typography>
|
||||
{createElement(rendedFieldSettings, {
|
||||
config: newConfig,
|
||||
handleChange,
|
||||
onChange: handleChange,
|
||||
onBlur: validateSettings,
|
||||
errors,
|
||||
})}
|
||||
</Stack>
|
||||
)}
|
||||
@@ -111,6 +131,29 @@ export default function FieldSettings(props: IMenuModalProps) {
|
||||
actions={{
|
||||
primary: {
|
||||
onClick: () => {
|
||||
const errors = validateSettings();
|
||||
if (Object.keys(errors).length > 0) {
|
||||
requestConfirmation({
|
||||
title: "Invalid settings",
|
||||
customBody: (
|
||||
<>
|
||||
<Typography>Please fix the following settings:</Typography>
|
||||
<ul style={{ paddingLeft: "1.5em" }}>
|
||||
{Object.entries(errors).map(([key, message]) => (
|
||||
<li key={key}>
|
||||
<code>{key}</code>: {message}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
),
|
||||
confirm: "Fix",
|
||||
hideCancel: true,
|
||||
handleConfirm: () => {},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (showRebuildPrompt) {
|
||||
requestConfirmation({
|
||||
title: "Deploy changes",
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface IFieldsDropdownProps {
|
||||
hideLabel?: boolean;
|
||||
label?: string;
|
||||
options?: FieldType[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -22,6 +23,7 @@ export default function FieldsDropdown({
|
||||
hideLabel = false,
|
||||
label,
|
||||
options: optionsProp,
|
||||
...props
|
||||
}: IFieldsDropdownProps) {
|
||||
const options = optionsProp
|
||||
? FIELDS.filter((fieldConfig) => optionsProp.indexOf(fieldConfig.type) > -1)
|
||||
@@ -30,6 +32,7 @@ export default function FieldsDropdown({
|
||||
return (
|
||||
<MultiSelect
|
||||
multiple={false}
|
||||
{...props}
|
||||
value={value ? value : ""}
|
||||
onChange={onChange}
|
||||
options={options.map((fieldConfig) => ({
|
||||
@@ -54,6 +57,7 @@ export default function FieldsDropdown({
|
||||
TextFieldProps={{
|
||||
hiddenLabel: hideLabel,
|
||||
helperText: value && getFieldProp("description", value),
|
||||
...props.TextFieldProps,
|
||||
SelectProps: {
|
||||
displayEmpty: true,
|
||||
renderValue: () => (
|
||||
@@ -70,6 +74,7 @@ export default function FieldsDropdown({
|
||||
{getFieldProp("name", value as FieldType)}
|
||||
</>
|
||||
),
|
||||
...props.TextFieldProps?.SelectProps,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -39,15 +39,15 @@ export default function Filters() {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (userDoc.state.doc && tableState?.tablePath) {
|
||||
if (userDoc.state.doc.tables?.[tableState?.tablePath]?.filters) {
|
||||
if (userDoc.state.doc && tableState?.config.id) {
|
||||
if (userDoc.state.doc.tables?.[tableState?.config.id]?.filters) {
|
||||
tableActions?.table.filter(
|
||||
userDoc.state.doc.tables[tableState?.tablePath].filters
|
||||
userDoc.state.doc.tables[tableState?.config.id].filters
|
||||
);
|
||||
tableActions?.table.orderBy();
|
||||
}
|
||||
}
|
||||
}, [userDoc.state, tableState?.tablePath]);
|
||||
}, [userDoc.state, tableState?.config.id]);
|
||||
|
||||
const filterColumns = _sortBy(Object.values(tableState!.columns), "index")
|
||||
.filter((c) => getFieldProp("filter", c.type))
|
||||
@@ -100,7 +100,7 @@ export default function Filters() {
|
||||
userDoc.dispatch({
|
||||
action: DocActions.update,
|
||||
data: {
|
||||
tables: { [`${tableState?.tablePath}`]: { filters } },
|
||||
tables: { [`${tableState?.config.id}`]: { filters } },
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -257,7 +257,7 @@ export default function Filters() {
|
||||
control,
|
||||
docRef: {},
|
||||
disabled: false,
|
||||
handleChange: () => {},
|
||||
onChange: () => {},
|
||||
})}
|
||||
</Suspense>
|
||||
</form>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
import { DocActions } from "@src/hooks/useDoc";
|
||||
import { formatSubTableName } from "../../utils/fns";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
listbox: {},
|
||||
@@ -69,7 +70,7 @@ export default function HiddenFields() {
|
||||
|
||||
// Initialise hiddenFields from user doc
|
||||
const userDocHiddenFields =
|
||||
userDoc.state.doc?.tables?.[formatSubTableName(tableState?.tablePath!)]
|
||||
userDoc.state.doc?.tables?.[formatSubTableName(tableState?.config.id!)]
|
||||
?.hiddenFields;
|
||||
useEffect(() => {
|
||||
if (userDocHiddenFields) setHiddenFields(userDocHiddenFields);
|
||||
@@ -94,7 +95,7 @@ export default function HiddenFields() {
|
||||
action: DocActions.update,
|
||||
data: {
|
||||
tables: {
|
||||
[formatSubTableName(tableState?.tablePath)]: { hiddenFields },
|
||||
[formatSubTableName(tableState?.config.id)]: { hiddenFields },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -64,7 +64,7 @@ export default function Export({ query, closeModal }) {
|
||||
const [columns, setColumns] = useState<any[]>([]);
|
||||
const [labelColumnsEnabled, setLabelColumnsEnabled] = useState(false);
|
||||
const [labelColumns, setLabelColumns] = useState<any[]>([]);
|
||||
const [packageName, setPackageName] = useState(tableState?.tablePath);
|
||||
const [packageName, setPackageName] = useState(tableState?.config.id);
|
||||
|
||||
const handleClose = () => {
|
||||
closeModal();
|
||||
|
||||
@@ -126,7 +126,8 @@ export default function Export({ query, closeModal }) {
|
||||
...doc.data(),
|
||||
}));
|
||||
|
||||
const fileName = `${tableState?.tablePath!}-${new Date().toISOString()}.${exportType}`;
|
||||
const fileName = `${tableState?.config
|
||||
.id!}-${new Date().toISOString()}.${exportType}`;
|
||||
switch (exportType) {
|
||||
case "csv":
|
||||
const csvData = docs.map((doc: any) =>
|
||||
|
||||
@@ -38,7 +38,7 @@ export type TableColumn = Column<any> & {
|
||||
};
|
||||
|
||||
const rowKeyGetter = (row: any) => row.id;
|
||||
const SelectColumn = { ..._SelectColumn, width: 42, maxWidth: 42 };
|
||||
// const SelectColumn = { ..._SelectColumn, width: 42, maxWidth: 42 };
|
||||
|
||||
export default function Table() {
|
||||
const classes = useStyles();
|
||||
@@ -48,7 +48,7 @@ export default function Table() {
|
||||
const { userDoc } = useAppContext();
|
||||
|
||||
const userDocHiddenFields =
|
||||
userDoc.state.doc?.tables?.[formatSubTableName(tableState?.tablePath)]
|
||||
userDoc.state.doc?.tables?.[formatSubTableName(tableState?.config.id)]
|
||||
?.hiddenFields ?? [];
|
||||
|
||||
const [columns, setColumns] = useState<TableColumn[]>([]);
|
||||
|
||||
@@ -34,7 +34,7 @@ export const tableSettings = (
|
||||
label: "Table ID",
|
||||
required: true,
|
||||
watchedField: "name",
|
||||
assistiveText: `Unique ID for this table used to store configuration. Cannot be edited ${
|
||||
assistiveText: `Unique ID for this table used to store configuration. Cannot be edited${
|
||||
mode === TableSettingsDialogModes.create ? " later" : ""
|
||||
}.`,
|
||||
disabled: mode === TableSettingsDialogModes.update,
|
||||
@@ -55,6 +55,7 @@ export const tableSettings = (
|
||||
<>
|
||||
<WarningIcon
|
||||
color="warning"
|
||||
aria-label="Warning"
|
||||
sx={{ fontSize: 16, mr: 0.5, verticalAlign: "middle" }}
|
||||
/>
|
||||
You change which Firestore collection to display. Data in the new
|
||||
|
||||
@@ -54,7 +54,6 @@ export default function ActionFab({
|
||||
const { tableState, rowyRun } = useProjectContext();
|
||||
const { ref } = row;
|
||||
const { config } = column as any;
|
||||
|
||||
const action = !value
|
||||
? "run"
|
||||
: value.undo
|
||||
@@ -71,7 +70,7 @@ export default function ActionFab({
|
||||
ref: { path: ref.path },
|
||||
column: { ...column, editor: undefined },
|
||||
action,
|
||||
schemaDocPath: formatPath(tableState?.tablePath ?? ""),
|
||||
schemaDocPath: tableState?.config.tableConfig.path,
|
||||
actionParams,
|
||||
});
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ const CodeEditor = lazy(
|
||||
import("@src/components/CodeEditor" /* webpackChunkName: "CodeEditor" */)
|
||||
);
|
||||
|
||||
const Settings = ({ config, handleChange }) => {
|
||||
const Settings = ({ config, onChange }) => {
|
||||
const { tableState, roles } = useProjectContext();
|
||||
const columnOptions = Object.values(tableState?.columns ?? {}).map((c) => ({
|
||||
label: c.name,
|
||||
@@ -34,7 +34,7 @@ const Settings = ({ config, handleChange }) => {
|
||||
label="Allowed roles"
|
||||
options={roles ?? []}
|
||||
value={config.requiredRoles ?? []}
|
||||
onChange={handleChange("requiredRoles")}
|
||||
onChange={onChange("requiredRoles")}
|
||||
/>
|
||||
|
||||
<Typography variant="overline">Required fields</Typography>
|
||||
@@ -46,7 +46,7 @@ const Settings = ({ config, handleChange }) => {
|
||||
label="Required fields"
|
||||
options={columnOptions}
|
||||
value={config.requiredFields ?? []}
|
||||
onChange={handleChange("requiredFields")}
|
||||
onChange={onChange("requiredFields")}
|
||||
/>
|
||||
<Divider />
|
||||
<Typography variant="overline">Confirmation template</Typography>
|
||||
@@ -59,7 +59,7 @@ const Settings = ({ config, handleChange }) => {
|
||||
placeholder="Are sure you want to invest {{stockName}}?"
|
||||
value={config.confirmation}
|
||||
onChange={(e) => {
|
||||
handleChange("confirmation")(e.target.value);
|
||||
onChange("confirmation")(e.target.value);
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
@@ -68,16 +68,12 @@ const Settings = ({ config, handleChange }) => {
|
||||
<Switch
|
||||
checked={config.isActionScript}
|
||||
onChange={() =>
|
||||
handleChange("isActionScript")(!Boolean(config.isActionScript))
|
||||
onChange("isActionScript")(!Boolean(config.isActionScript))
|
||||
}
|
||||
name="actionScript"
|
||||
/>
|
||||
}
|
||||
label="Set as an action script"
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
"& .MuiFormControlLabel-label": { mt: 0 },
|
||||
}}
|
||||
/>
|
||||
{!Boolean(config.isActionScript) ? (
|
||||
<TextField
|
||||
@@ -86,7 +82,7 @@ const Settings = ({ config, handleChange }) => {
|
||||
value={config.callableName}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
handleChange("callableName")(e.target.value);
|
||||
onChange("callableName")(e.target.value);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
@@ -129,7 +125,7 @@ const Settings = ({ config, handleChange }) => {
|
||||
"}",
|
||||
].join("\n"),
|
||||
]}
|
||||
onChange={handleChange("script")}
|
||||
onChange={onChange("script")}
|
||||
/>
|
||||
</Suspense>
|
||||
<FormControlLabel
|
||||
@@ -137,32 +133,24 @@ const Settings = ({ config, handleChange }) => {
|
||||
<Switch
|
||||
checked={config.redo?.enabled}
|
||||
onChange={() =>
|
||||
handleChange("redo.enabled")(!Boolean(config.redo?.enabled))
|
||||
onChange("redo.enabled")(!Boolean(config.redo?.enabled))
|
||||
}
|
||||
name="redo toggle"
|
||||
/>
|
||||
}
|
||||
label="User can redo (re-runs the same script)"
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
"& .MuiFormControlLabel-label": { mt: 0 },
|
||||
}}
|
||||
/>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={config.undo?.enabled}
|
||||
onChange={() =>
|
||||
handleChange("undo.enabled")(!Boolean(config.undo?.enabled))
|
||||
onChange("undo.enabled")(!Boolean(config.undo?.enabled))
|
||||
}
|
||||
name="undo toggle"
|
||||
/>
|
||||
}
|
||||
label="User can undo"
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
"& .MuiFormControlLabel-label": { mt: 0 },
|
||||
}}
|
||||
/>
|
||||
{config["undo.enabled"] && (
|
||||
<>
|
||||
@@ -174,7 +162,7 @@ const Settings = ({ config, handleChange }) => {
|
||||
placeholder="are you sure you want to sell your stocks in {{stockName}}"
|
||||
value={config["undo.confirmation"]}
|
||||
onChange={(e) => {
|
||||
handleChange("undo.confirmation")(e.target.value);
|
||||
onChange("undo.confirmation")(e.target.value);
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
@@ -183,7 +171,7 @@ const Settings = ({ config, handleChange }) => {
|
||||
<CodeEditor
|
||||
minHeight={300}
|
||||
value={config["undo.script"]}
|
||||
onChange={handleChange("undo.script")}
|
||||
onChange={onChange("undo.script")}
|
||||
/>
|
||||
</Suspense>
|
||||
</>
|
||||
|
||||
@@ -10,7 +10,7 @@ const CodeEditor = lazy(
|
||||
import("@src/components/CodeEditor" /* webpackChunkName: "CodeEditor" */)
|
||||
);
|
||||
|
||||
const Settings = ({ config, handleChange }) => {
|
||||
const Settings = ({ config, onChange }) => {
|
||||
const { tableState } = useProjectContext();
|
||||
|
||||
const columnOptions = Object.values(tableState?.columns ?? {})
|
||||
@@ -22,7 +22,7 @@ const Settings = ({ config, handleChange }) => {
|
||||
label="Sub-tables"
|
||||
options={columnOptions}
|
||||
value={config.requiredFields ?? []}
|
||||
onChange={handleChange("subtables")}
|
||||
onChange={onChange("subtables")}
|
||||
/>
|
||||
<Typography variant="overline">Aggergate script</Typography>
|
||||
<Suspense fallback={<FieldSkeleton height={200} />}>
|
||||
@@ -49,7 +49,7 @@ switch (triggerType){
|
||||
|
||||
}`,
|
||||
]}
|
||||
onChange={handleChange("script")}
|
||||
onChange={onChange("script")}
|
||||
/>
|
||||
</Suspense>
|
||||
|
||||
@@ -66,7 +66,7 @@ switch (triggerType){
|
||||
].includes(f)
|
||||
)}
|
||||
onChange={(value) => {
|
||||
handleChange("renderFieldType")(value);
|
||||
onChange("renderFieldType")(value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -36,13 +36,13 @@ export const useStyles = makeStyles((theme) =>
|
||||
display: "block",
|
||||
height: 16,
|
||||
|
||||
background: `linear-gradient(to bottom, #fff, rgba(255, 255, 255, 0))`,
|
||||
background: `linear-gradient(to bottom, ${theme.palette.background.paper}, rgba(255, 255, 255, 0))`,
|
||||
},
|
||||
|
||||
"&::after": {
|
||||
top: "auto",
|
||||
bottom: 0,
|
||||
background: `linear-gradient(to top, #fff, rgba(255, 255, 255, 0))`,
|
||||
background: `linear-gradient(to top, ${theme.palette.background.paper}, rgba(255, 255, 255, 0))`,
|
||||
},
|
||||
},
|
||||
list: () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TextField, FormControlLabel, Switch } from "@mui/material";
|
||||
import { TextField, FormControlLabel, Switch, Grid } from "@mui/material";
|
||||
|
||||
export default function Settings({ config, handleChange }) {
|
||||
export default function Settings({ config, onChange }) {
|
||||
return (
|
||||
<>
|
||||
<TextField
|
||||
@@ -9,7 +9,7 @@ export default function Settings({ config, handleChange }) {
|
||||
value={config.url}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
handleChange("url")(e.target.value);
|
||||
onChange("url")(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
@@ -20,7 +20,7 @@ export default function Settings({ config, handleChange }) {
|
||||
value={config.resultsKey}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
handleChange("resultsKey")(e.target.value);
|
||||
onChange("resultsKey")(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
@@ -29,32 +29,38 @@ export default function Settings({ config, handleChange }) {
|
||||
value={config.primaryKey}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
handleChange("primaryKey")(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
label="Title key (optional)"
|
||||
name="titleKey"
|
||||
value={config.titleKey}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
handleChange("titleKey")(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
label="Subtitle key (optional)"
|
||||
name="subtitleKey"
|
||||
value={config.subtitleKey}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
handleChange("subtitleKey")(e.target.value);
|
||||
onChange("primaryKey")(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<Grid container direction="row" spacing={1}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField
|
||||
label="Title key (optional)"
|
||||
name="titleKey"
|
||||
value={config.titleKey}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
onChange("titleKey")(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField
|
||||
label="Subtitle key (optional)"
|
||||
name="subtitleKey"
|
||||
value={config.subtitleKey}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
onChange("subtitleKey")(e.target.value);
|
||||
}}
|
||||
/>{" "}
|
||||
</Grid>{" "}
|
||||
</Grid>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={config.multiple}
|
||||
onChange={() => handleChange("multiple")(!Boolean(config.multiple))}
|
||||
onChange={() => onChange("multiple")(!Boolean(config.multiple))}
|
||||
name="select-multiple"
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ export const config: IFieldConfig = {
|
||||
}),
|
||||
TableEditor: NullEditor as any,
|
||||
SideDrawerField,
|
||||
requireConfiguration: true,
|
||||
settings: Settings,
|
||||
};
|
||||
export default config;
|
||||
|
||||
@@ -20,7 +20,7 @@ import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
import { TABLE_SCHEMAS } from "@src/config/dbPaths";
|
||||
import { WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
|
||||
export default function Settings({ handleChange, config }: ISettingsProps) {
|
||||
export default function Settings({ onChange, config }: ISettingsProps) {
|
||||
const { tables } = useProjectContext();
|
||||
const tableOptions = _sortBy(
|
||||
tables?.map((table) => ({
|
||||
@@ -71,7 +71,7 @@ export default function Settings({ handleChange, config }: ISettingsProps) {
|
||||
options={tableOptions}
|
||||
freeText={false}
|
||||
value={config.index}
|
||||
onChange={handleChange("index")}
|
||||
onChange={onChange("index")}
|
||||
multiple={false}
|
||||
label="Table"
|
||||
labelPlural="tables"
|
||||
@@ -91,7 +91,7 @@ export default function Settings({ handleChange, config }: ISettingsProps) {
|
||||
control={
|
||||
<Checkbox
|
||||
checked={config.multiple !== false}
|
||||
onChange={(e) => handleChange("multiple")(e.target.checked)}
|
||||
onChange={(e) => onChange("multiple")(e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
@@ -137,7 +137,7 @@ export default function Settings({ handleChange, config }: ISettingsProps) {
|
||||
name="filters"
|
||||
fullWidth
|
||||
value={config.filters}
|
||||
onChange={(e) => handleChange("filters")(e.target.value)}
|
||||
onChange={(e) => onChange("filters")(e.target.value)}
|
||||
placeholder="attribute:value AND | OR | NOT attribute:value"
|
||||
id="connectTable-filters"
|
||||
helperText={
|
||||
@@ -161,7 +161,7 @@ export default function Settings({ handleChange, config }: ISettingsProps) {
|
||||
options={columns.filter((c) =>
|
||||
[FieldType.shortText, FieldType.singleSelect].includes(c.type)
|
||||
)}
|
||||
onChange={handleChange("primaryKeys")}
|
||||
onChange={onChange("primaryKeys")}
|
||||
TextFieldProps={{ helperText: "Field values displayed" }}
|
||||
/>
|
||||
|
||||
@@ -169,7 +169,7 @@ export default function Settings({ handleChange, config }: ISettingsProps) {
|
||||
label="Snapshot fields"
|
||||
value={config.snapshotFields ?? []}
|
||||
options={columns.filter((c) => ![FieldType.subTable].includes(c.type))}
|
||||
onChange={handleChange("snapshotFields")}
|
||||
onChange={onChange("snapshotFields")}
|
||||
TextFieldProps={{ helperText: "Fields stored in the snapshots" }}
|
||||
/>
|
||||
|
||||
@@ -177,7 +177,7 @@ export default function Settings({ handleChange, config }: ISettingsProps) {
|
||||
label="Tracked fields"
|
||||
value={config.trackedFields ?? []}
|
||||
options={columns.filter((c) => ![FieldType.subTable].includes(c.type))}
|
||||
onChange={handleChange("trackedFields")}
|
||||
onChange={onChange("trackedFields")}
|
||||
TextFieldProps={{
|
||||
helperText:
|
||||
"Fields to be tracked for changes and synced to the snapshot",
|
||||
|
||||
@@ -7,7 +7,7 @@ import MultiSelect from "@rowy/multiselect";
|
||||
import { DATE_TIME_FORMAT } from "@src/constants/dates";
|
||||
import { EXTERNAL_LINKS } from "@src/constants/externalLinks";
|
||||
|
||||
export default function Settings({ handleChange, config }: ISettingsProps) {
|
||||
export default function Settings({ onChange, config }: ISettingsProps) {
|
||||
return (
|
||||
<>
|
||||
<MultiSelect
|
||||
@@ -27,7 +27,7 @@ export default function Settings({ handleChange, config }: ISettingsProps) {
|
||||
clearable={false}
|
||||
searchable={false}
|
||||
value={config.format ?? DATE_TIME_FORMAT}
|
||||
onChange={handleChange("format")}
|
||||
onChange={onChange("format")}
|
||||
TextFieldProps={{
|
||||
helperText: (
|
||||
<Link
|
||||
|
||||
@@ -7,7 +7,7 @@ import MultiSelect from "@rowy/multiselect";
|
||||
import { DATE_FORMAT } from "@src/constants/dates";
|
||||
import { EXTERNAL_LINKS } from "@src/constants/externalLinks";
|
||||
|
||||
export default function Settings({ handleChange, config }: ISettingsProps) {
|
||||
export default function Settings({ onChange, config }: ISettingsProps) {
|
||||
return (
|
||||
<>
|
||||
<MultiSelect
|
||||
@@ -21,7 +21,7 @@ export default function Settings({ handleChange, config }: ISettingsProps) {
|
||||
clearable={false}
|
||||
searchable={false}
|
||||
value={config.format ?? DATE_FORMAT}
|
||||
onChange={handleChange("format")}
|
||||
onChange={onChange("format")}
|
||||
TextFieldProps={{
|
||||
helperText: (
|
||||
<Link
|
||||
|
||||
@@ -7,7 +7,7 @@ import MultiSelect from "@rowy/multiselect";
|
||||
import { DATE_TIME_FORMAT } from "@src/constants/dates";
|
||||
import { EXTERNAL_LINKS } from "@src/constants/externalLinks";
|
||||
|
||||
export default function Settings({ handleChange, config }: ISettingsProps) {
|
||||
export default function Settings({ onChange, config }: ISettingsProps) {
|
||||
return (
|
||||
<>
|
||||
<MultiSelect
|
||||
@@ -27,7 +27,7 @@ export default function Settings({ handleChange, config }: ISettingsProps) {
|
||||
clearable={false}
|
||||
searchable={false}
|
||||
value={config.format ?? DATE_TIME_FORMAT}
|
||||
onChange={handleChange("format")}
|
||||
onChange={onChange("format")}
|
||||
TextFieldProps={{
|
||||
helperText: (
|
||||
<Link
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { lazy, Suspense } from "react";
|
||||
import { ISettingsProps } from "../types";
|
||||
|
||||
import { Grid, InputLabel } from "@mui/material";
|
||||
import { Grid, InputLabel, FormHelperText } from "@mui/material";
|
||||
import MultiSelect from "@rowy/multiselect";
|
||||
import FieldSkeleton from "@src/components/SideDrawer/Form/FieldSkeleton";
|
||||
import FieldsDropdown from "@src/components/Table/ColumnMenu/FieldsDropdown";
|
||||
@@ -18,11 +18,14 @@ const CodeEditor = lazy(
|
||||
|
||||
export default function Settings({
|
||||
config,
|
||||
handleChange,
|
||||
onChange,
|
||||
fieldName,
|
||||
onBlur,
|
||||
errors,
|
||||
}: ISettingsProps) {
|
||||
const { tableState } = useProjectContext();
|
||||
if (!tableState?.columns) return <></>;
|
||||
|
||||
const columnOptions = Object.values(tableState.columns)
|
||||
.filter((column) => column.fieldName !== fieldName)
|
||||
.filter((column) => column.type !== FieldType.subTable)
|
||||
@@ -36,10 +39,25 @@ export default function Settings({
|
||||
label="Listener fields"
|
||||
options={columnOptions}
|
||||
value={config.listenerFields ?? []}
|
||||
onChange={handleChange("listenerFields")}
|
||||
onChange={onChange("listenerFields")}
|
||||
TextFieldProps={{
|
||||
helperText:
|
||||
"Changes to these fields will trigger the evaluation of the column.",
|
||||
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>
|
||||
@@ -58,7 +76,13 @@ export default function Settings({
|
||||
].includes(f)
|
||||
)}
|
||||
onChange={(value) => {
|
||||
handleChange("renderFieldType")(value);
|
||||
onChange("renderFieldType")(value);
|
||||
}}
|
||||
TextFieldProps={{
|
||||
required: true,
|
||||
error: errors.renderFieldType,
|
||||
helperText: errors.renderFieldType,
|
||||
onBlur,
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
@@ -68,9 +92,16 @@ export default function Settings({
|
||||
<InputLabel>Derivative script</InputLabel>
|
||||
<CodeEditorHelper docLink={WIKI_LINKS.fieldTypesDerivative} />
|
||||
<Suspense fallback={<FieldSkeleton height={200} />}>
|
||||
<CodeEditor value={config.script} onChange={handleChange("script")} />
|
||||
<CodeEditor value={config.script} onChange={onChange("script")} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const settingsValidator = (config) => {
|
||||
const errors: Record<string, any> = {};
|
||||
if (!config.listenerFields) errors.listenerFields = "Required";
|
||||
if (!config.renderFieldType) errors.renderFieldType = "Required";
|
||||
return errors;
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import withBasicCell from "../_withTableCell/withBasicCell";
|
||||
import DerivativeIcon from "@src/assets/icons/Derivative";
|
||||
import BasicCell from "../_BasicCell/BasicCellNull";
|
||||
import NullEditor from "@src/components/Table/editors/NullEditor";
|
||||
import Settings from "./Settings";
|
||||
import Settings, { settingsValidator } from "./Settings";
|
||||
|
||||
export const config: IFieldConfig = {
|
||||
type: FieldType.derivative,
|
||||
@@ -21,5 +21,6 @@ export const config: IFieldConfig = {
|
||||
TableEditor: NullEditor as any,
|
||||
SideDrawerField: BasicCell as any,
|
||||
settings: Settings,
|
||||
settingsValidator,
|
||||
};
|
||||
export default config;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Checkbox, FormControlLabel, FormHelperText } from "@mui/material";
|
||||
|
||||
const Settings = ({ config, handleChange }) => {
|
||||
const Settings = ({ config, onChange }) => {
|
||||
return (
|
||||
<>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={config.isArray}
|
||||
onChange={() => handleChange("isArray")(!Boolean(config.isArray))}
|
||||
onChange={() => onChange("isArray")(!Boolean(config.isArray))}
|
||||
name="isArray"
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ISettingsProps } from "../types";
|
||||
import { Slider } from "@mui/material";
|
||||
import Subheading from "@src/components/Table/ColumnMenu/Subheading";
|
||||
|
||||
export default function Settings({ handleChange, config }: ISettingsProps) {
|
||||
export default function Settings({ onChange, config }: ISettingsProps) {
|
||||
return (
|
||||
<>
|
||||
<Subheading>Maximum number of stars</Subheading>
|
||||
@@ -14,7 +14,7 @@ export default function Settings({ handleChange, config }: ISettingsProps) {
|
||||
aria-labelledby="max-slider"
|
||||
valueLabelDisplay="auto"
|
||||
onChange={(_, v) => {
|
||||
handleChange("max")(v);
|
||||
onChange("max")(v);
|
||||
}}
|
||||
step={1}
|
||||
marks
|
||||
@@ -29,7 +29,7 @@ export default function Settings({ handleChange, config }: ISettingsProps) {
|
||||
aria-labelledby="precision-slider"
|
||||
valueLabelDisplay="auto"
|
||||
onChange={(_, v) => {
|
||||
handleChange("precision")(v);
|
||||
onChange("precision")(v);
|
||||
}}
|
||||
step={0.25}
|
||||
marks
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TextField } from "@mui/material";
|
||||
|
||||
export default function Settings({ handleChange, config }) {
|
||||
export default function Settings({ onChange, config }) {
|
||||
return (
|
||||
<>
|
||||
<TextField
|
||||
@@ -10,8 +10,8 @@ export default function Settings({ handleChange, config }) {
|
||||
value={config.maxLength}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
if (e.target.value === "0") handleChange("maxLength")(null);
|
||||
else handleChange("maxLength")(e.target.value);
|
||||
if (e.target.value === "0") onChange("maxLength")(null);
|
||||
else onChange("maxLength")(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
@@ -21,8 +21,8 @@ export default function Settings({ handleChange, config }) {
|
||||
value={config.validationRegex}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
if (e.target.value === "") handleChange("validationRegex")(null);
|
||||
else handleChange("validationRegex")(e.target.value);
|
||||
if (e.target.value === "") onChange("validationRegex")(null);
|
||||
else onChange("validationRegex")(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -29,14 +29,14 @@ const useStyles = makeStyles(() =>
|
||||
})
|
||||
);
|
||||
|
||||
export default function Settings({ handleChange, config }) {
|
||||
export default function Settings({ onChange, config }) {
|
||||
const listEndRef: any = useRef(null);
|
||||
const options = config.options ?? [];
|
||||
const classes = useStyles();
|
||||
const [newOption, setNewOption] = useState("");
|
||||
const handleAdd = () => {
|
||||
if (newOption.trim() !== "") {
|
||||
handleChange("options")([...options, newOption.trim()]);
|
||||
onChange("options")([...options, newOption.trim()]);
|
||||
setNewOption("");
|
||||
listEndRef.current.scrollIntoView({ behavior: "smooth", block: "end" });
|
||||
}
|
||||
@@ -62,7 +62,7 @@ export default function Settings({ handleChange, config }) {
|
||||
<IconButton
|
||||
aria-label="Remove"
|
||||
onClick={() =>
|
||||
handleChange("options")(
|
||||
onChange("options")(
|
||||
options.filter((o: string) => o !== option)
|
||||
)
|
||||
}
|
||||
@@ -112,7 +112,7 @@ export default function Settings({ handleChange, config }) {
|
||||
control={
|
||||
<Checkbox
|
||||
checked={config.freeText}
|
||||
onChange={(e) => handleChange("freeText")(e.target.checked)}
|
||||
onChange={(e) => onChange("freeText")(e.target.checked)}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { TextField, FormControlLabel, Switch } from "@mui/material";
|
||||
import Subheading from "@src/components/Table/ColumnMenu/Subheading";
|
||||
|
||||
export default function Settings({ handleChange, config }) {
|
||||
export default function Settings({ onChange, config }) {
|
||||
return (
|
||||
<>
|
||||
<Subheading>Slider config</Subheading>
|
||||
@@ -10,7 +10,7 @@ export default function Settings({ handleChange, config }) {
|
||||
variant="filled"
|
||||
fullWidth
|
||||
margin="none"
|
||||
onChange={(e) => handleChange("min")(parseFloat(e.target.value))}
|
||||
onChange={(e) => onChange("min")(parseFloat(e.target.value))}
|
||||
value={config["min"]}
|
||||
id={`settings-field-min`}
|
||||
label="Minimum value"
|
||||
@@ -21,7 +21,7 @@ export default function Settings({ handleChange, config }) {
|
||||
variant="filled"
|
||||
fullWidth
|
||||
margin="none"
|
||||
onChange={(e) => handleChange("max")(parseFloat(e.target.value))}
|
||||
onChange={(e) => onChange("max")(parseFloat(e.target.value))}
|
||||
value={config["max"]}
|
||||
id={`settings-field-max`}
|
||||
label="Maximum value"
|
||||
@@ -32,7 +32,7 @@ export default function Settings({ handleChange, config }) {
|
||||
variant="filled"
|
||||
fullWidth
|
||||
margin="none"
|
||||
onChange={(e) => handleChange("step")(parseFloat(e.target.value))}
|
||||
onChange={(e) => onChange("step")(parseFloat(e.target.value))}
|
||||
value={config["step"]}
|
||||
id={`settings-field-step`}
|
||||
label="Step value"
|
||||
@@ -43,15 +43,11 @@ export default function Settings({ handleChange, config }) {
|
||||
control={
|
||||
<Switch
|
||||
checked={config.marks}
|
||||
onChange={() => handleChange("marks")(!Boolean(config.marks))}
|
||||
onChange={() => onChange("marks")(!Boolean(config.marks))}
|
||||
name="marks"
|
||||
/>
|
||||
}
|
||||
label="Show slider steps"
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
"& .MuiFormControlLabel-label": { mt: 0 },
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -172,7 +172,7 @@ const ConditionModal = ({ modal, setModal, conditions, setConditions }) => {
|
||||
);
|
||||
};
|
||||
|
||||
export default function Settings({ handleChange, config }: ISettingsProps) {
|
||||
export default function Settings({ onChange, config }: ISettingsProps) {
|
||||
const [modal, setModal] = useState(EMPTY_STATE);
|
||||
const { conditions } = config;
|
||||
return (
|
||||
@@ -228,7 +228,7 @@ export default function Settings({ handleChange, config }: ISettingsProps) {
|
||||
modal={modal}
|
||||
setModal={setModal}
|
||||
conditions={config.conditions}
|
||||
setConditions={handleChange("conditions")}
|
||||
setConditions={onChange("conditions")}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import MultiSelect from "@rowy/multiselect";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
|
||||
const Settings = ({ config, handleChange }) => {
|
||||
const Settings = ({ config, onChange }) => {
|
||||
const { tableState } = useProjectContext();
|
||||
if (!tableState?.columns) return <></>;
|
||||
const columnOptions = Object.values(tableState.columns)
|
||||
@@ -21,7 +21,7 @@ const Settings = ({ config, handleChange }) => {
|
||||
label="Parent label"
|
||||
options={columnOptions}
|
||||
value={config.parentLabel ?? []}
|
||||
onChange={handleChange("parentLabel")}
|
||||
onChange={onChange("parentLabel")}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -21,6 +21,7 @@ export interface IFieldConfig {
|
||||
TableEditor: React.ComponentType<EditorProps<any, any>>;
|
||||
SideDrawerField: React.ComponentType<ISideDrawerFieldProps>;
|
||||
settings?: React.ComponentType<ISettingsProps>;
|
||||
settingsValidator?: (config: Record<string, any>) => Record<string, string>;
|
||||
filter?: {
|
||||
operators: IFilterOperator[];
|
||||
customInput?: React.ComponentType<IFiltersProps>;
|
||||
@@ -59,14 +60,16 @@ export interface ISideDrawerFieldProps {
|
||||
}
|
||||
|
||||
export interface ISettingsProps {
|
||||
handleChange: (key: string) => (value: any) => void;
|
||||
onChange: (key: string) => (value: any) => void;
|
||||
config: Record<string, any>;
|
||||
fieldName: string;
|
||||
onBlur: React.FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>;
|
||||
errors: Record<string, any>;
|
||||
}
|
||||
|
||||
// TODO: WRITE TYPES
|
||||
export interface IFiltersProps {
|
||||
handleChange: (key: string) => (value: any) => void;
|
||||
onChange: (key: string) => (value: any) => void;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,3 +37,9 @@ export const utilFns = `
|
||||
return request.auth != null && request.auth.token.roles.hasAny(roles);
|
||||
}
|
||||
` as const;
|
||||
|
||||
export const insecureRule = `
|
||||
match /{document=**} {
|
||||
allow read, write: if true;
|
||||
}
|
||||
` as const;
|
||||
|
||||
@@ -60,6 +60,8 @@ export interface ISetupStepBodyProps {
|
||||
rowyRunUrl: string;
|
||||
}
|
||||
|
||||
const BASE_WIDTH = 1024;
|
||||
|
||||
const checkAllSteps = async (
|
||||
rowyRunUrl: string,
|
||||
currentUser: firebase.default.User | null | undefined,
|
||||
@@ -256,7 +258,7 @@ export default function SetupPage() {
|
||||
alpha(theme.palette.background.paper, 0.5),
|
||||
backdropFilter: "blur(20px) saturate(150%)",
|
||||
|
||||
maxWidth: 840,
|
||||
maxWidth: BASE_WIDTH,
|
||||
width: (theme) => `calc(100vw - ${theme.spacing(2)})`,
|
||||
maxHeight: (theme) =>
|
||||
`calc(${
|
||||
@@ -264,7 +266,7 @@ export default function SetupPage() {
|
||||
} - ${theme.spacing(
|
||||
2
|
||||
)} - env(safe-area-inset-top) - env(safe-area-inset-bottom))`,
|
||||
height: 840 * 0.75,
|
||||
height: BASE_WIDTH * 0.75,
|
||||
resize: "both",
|
||||
|
||||
p: 0,
|
||||
|
||||
@@ -768,14 +768,7 @@ export default function TestView() {
|
||||
</Stack>
|
||||
|
||||
<div>
|
||||
<FormControlLabel
|
||||
control={<Switch />}
|
||||
label="Label"
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
"& .MuiFormControlLabel-label": { mt: 0 },
|
||||
}}
|
||||
/>
|
||||
<FormControlLabel control={<Switch />} label="Label" />
|
||||
<FormControlLabel
|
||||
control={<Switch size="medium" />}
|
||||
label="Label"
|
||||
|
||||
@@ -792,7 +792,13 @@ export const components = (theme: Theme): ThemeOptions => {
|
||||
root: {
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
"& .MuiSwitch-root": { marginRight: theme.spacing(1) },
|
||||
"& .MuiSwitch-root": {
|
||||
marginRight: theme.spacing(1),
|
||||
|
||||
"&.MuiSwitch-sizeSmall + .MuiFormControlLabel-label": {
|
||||
marginTop: 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
label: {
|
||||
marginTop: 10,
|
||||
|
||||
Reference in New Issue
Block a user