mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-28 16:06:41 +01:00
25
package.json
25
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Rowy",
|
||||
"version": "2.1.0",
|
||||
"version": "2.2.0",
|
||||
"homepage": "https://rowy.io",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -13,18 +13,19 @@
|
||||
"@emotion/react": "^11.4.0",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@hookform/resolvers": "^2.8.1",
|
||||
"@mdi/js": "^6.2.95",
|
||||
"@monaco-editor/react": "^4.1.0",
|
||||
"@mui/icons-material": "^5.0.0",
|
||||
"@mui/lab": "^5.0.0-alpha.50",
|
||||
"@mui/material": "^5.0.0",
|
||||
"@mui/styles": "^5.0.0",
|
||||
"@rowy/form-builder": "^0.3.1",
|
||||
"@mdi/js": "^6.5.95",
|
||||
"@monaco-editor/react": "^4.3.1",
|
||||
"@mui/icons-material": "^5.2.0",
|
||||
"@mui/lab": "^5.0.0-alpha.58",
|
||||
"@mui/material": "^5.2.2",
|
||||
"@mui/styles": "^5.2.2",
|
||||
"@rowy/form-builder": "^0.4.2",
|
||||
"@rowy/multiselect": "^0.2.3",
|
||||
"@tinymce/tinymce-react": "^3.12.6",
|
||||
"algoliasearch": "^4.8.6",
|
||||
"ansi-to-react": "^6.1.5",
|
||||
"colord": "^2.7.0",
|
||||
"compare-versions": "^4.1.1",
|
||||
"craco-swc": "^0.1.3",
|
||||
"csv-parse": "^4.15.3",
|
||||
"date-fns": "^2.19.0",
|
||||
@@ -32,13 +33,14 @@
|
||||
"file-saver": "^2.0.5",
|
||||
"firebase": "8.6.8",
|
||||
"hotkeys-js": "^3.7.2",
|
||||
"json-format": "^1.0.1",
|
||||
"jotai": "^1.4.2",
|
||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||
"json2csv": "^5.0.6",
|
||||
"jszip": "^3.6.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"lodash": "^4.17.21",
|
||||
"moment": "^2.29.1",
|
||||
"notistack": "^2.0.2",
|
||||
"pb-util": "^1.0.1",
|
||||
"query-string": "^6.8.3",
|
||||
"react": "^17.0.2",
|
||||
"react-beautiful-dnd": "^13.0.0",
|
||||
@@ -67,8 +69,7 @@
|
||||
"use-algolia": "^1.4.1",
|
||||
"use-debounce": "^3.3.0",
|
||||
"use-persisted-state": "^0.3.3",
|
||||
"yarn": "^1.22.10",
|
||||
"yup": "^0.32.9"
|
||||
"yarn": "^1.22.10"
|
||||
},
|
||||
"scripts": {
|
||||
"upstream": "git fetch upstream;git merge upstream/main;git commit -m'merge upstream';git push",
|
||||
|
||||
48
src/App.tsx
48
src/App.tsx
@@ -6,34 +6,34 @@ import AdapterDateFns from "@mui/lab/AdapterDateFns";
|
||||
import { StyledEngineProvider } from "@mui/material/styles";
|
||||
import "./space-grotesk.css";
|
||||
|
||||
import CustomBrowserRouter from "utils/CustomBrowserRouter";
|
||||
import PrivateRoute from "utils/PrivateRoute";
|
||||
import ErrorBoundary from "components/ErrorBoundary";
|
||||
import Loading from "components/Loading";
|
||||
import Navigation from "components/Navigation";
|
||||
import Logo from "assets/Logo";
|
||||
import CustomBrowserRouter from "@src/utils/CustomBrowserRouter";
|
||||
import PrivateRoute from "@src/utils/PrivateRoute";
|
||||
import ErrorBoundary from "@src/components/ErrorBoundary";
|
||||
import Loading from "@src/components/Loading";
|
||||
import Navigation from "@src/components/Navigation";
|
||||
import Logo from "@src/assets/Logo";
|
||||
|
||||
import SwrProvider from "contexts/SwrContext";
|
||||
import ConfirmationProvider from "components/ConfirmationDialog/Provider";
|
||||
import { AppProvider } from "contexts/AppContext";
|
||||
import { ProjectContextProvider } from "contexts/ProjectContext";
|
||||
import { SnackbarProvider } from "contexts/SnackbarContext";
|
||||
import { SnackLogProvider } from "contexts/SnackLogContext";
|
||||
import routes from "constants/routes";
|
||||
import SwrProvider from "@src/contexts/SwrContext";
|
||||
import ConfirmationProvider from "@src/components/ConfirmationDialog/Provider";
|
||||
import { AppProvider } from "@src/contexts/AppContext";
|
||||
import { ProjectContextProvider } from "@src/contexts/ProjectContext";
|
||||
import { SnackbarProvider } from "@src/contexts/SnackbarContext";
|
||||
import { SnackLogProvider } from "@src/contexts/SnackLogContext";
|
||||
import routes from "@src/constants/routes";
|
||||
|
||||
import AuthPage from "pages/Auth";
|
||||
import SignOutPage from "pages/Auth/SignOut";
|
||||
import SignUpPage from "pages/Auth/SignUp";
|
||||
import DeployPage from "pages/Deploy";
|
||||
import TestPage from "pages/Test";
|
||||
import RowyRunTestPage from "pages/RowyRunTest";
|
||||
import PageNotFound from "pages/PageNotFound";
|
||||
import AuthPage from "@src/pages/Auth";
|
||||
import SignOutPage from "@src/pages/Auth/SignOut";
|
||||
import SignUpPage from "@src/pages/Auth/SignUp";
|
||||
import DeployPage from "@src/pages/Deploy";
|
||||
import TestPage from "@src/pages/Test";
|
||||
import RowyRunTestPage from "@src/pages/RowyRunTest";
|
||||
import PageNotFound from "@src/pages/PageNotFound";
|
||||
|
||||
import Favicon from "assets/Favicon";
|
||||
import "analytics";
|
||||
import Favicon from "@src/assets/Favicon";
|
||||
import "@src/analytics";
|
||||
|
||||
// prettier-ignore
|
||||
const AuthSetupGuidePage = lazy(() => import("pages/Auth/SetupGuide" /* webpackChunkName: "AuthSetupGuide" */));
|
||||
const AuthSetupGuidePage = lazy(() => import("@src/pages/Auth/SetupGuide" /* webpackChunkName: "AuthSetupGuide" */));
|
||||
// prettier-ignore
|
||||
const ImpersonatorAuthPage = lazy(() => import("./pages/Auth/ImpersonatorAuth" /* webpackChunkName: "ImpersonatorAuthPage" */));
|
||||
// prettier-ignore
|
||||
@@ -51,7 +51,7 @@ const UserSettingsPage = lazy(() => import("./pages/Settings/UserSettings" /* we
|
||||
// prettier-ignore
|
||||
const UserManagementPage = lazy(() => import("./pages/Settings/UserManagement" /* webpackChunkName: "UserManagementPage" */));
|
||||
// prettier-ignore
|
||||
const SetupPage = lazy(() => import("pages/Setup" /* webpackChunkName: "SetupPage" */));
|
||||
const SetupPage = lazy(() => import("@src/pages/Setup" /* webpackChunkName: "SetupPage" */));
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
|
||||
@@ -4,8 +4,8 @@ import { use100vh } from "react-div-100vh";
|
||||
import { useTheme, alpha } from "@mui/material/styles";
|
||||
import { Box, BoxProps } from "@mui/material";
|
||||
|
||||
import bgPattern from "assets/bg-pattern.svg";
|
||||
import bgPatternDark from "assets/bg-pattern-dark.svg";
|
||||
import bgPattern from "@src/assets/bg-pattern.svg";
|
||||
import bgPatternDark from "@src/assets/bg-pattern-dark.svg";
|
||||
|
||||
export default function BrandedBackground() {
|
||||
const theme = useTheme();
|
||||
|
||||
10
src/assets/icons/ResizeBottomRight.tsx
Normal file
10
src/assets/icons/ResizeBottomRight.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
|
||||
import { mdiResizeBottomRight } from "@mdi/js";
|
||||
|
||||
export default function ResizeBottomRight(props: SvgIconProps) {
|
||||
return (
|
||||
<SvgIcon {...props}>
|
||||
<path d={mdiResizeBottomRight} />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
|
||||
import { mdiClockEditOutline } from "@mdi/js";
|
||||
|
||||
export default function UpdatedAt(props: SvgIconProps) {
|
||||
return (
|
||||
<SvgIcon {...props}>
|
||||
<path d="m19.06 14.88 2.05 2-6 6.07H13v-2.01l6.06-6.06ZM12 2a10 10 0 0 1 9.98 9.373 2.561 2.561 0 0 0-2.001.047A8 8 0 0 0 4 12a8.001 8.001 0 0 0 7 7.938v2.013C5.941 21.447 2 17.164 2 12 2 6.477 6.477 2 12 2Zm9.42 11.35 1.28 1.28c.21.21.21.56 0 .77l-1 .95-2.05-2 1-1a.55.55 0 0 1 .77 0ZM12.5 7v5.25l4.018 2.384-1.051 1.045L11 13V7h1.5Z" />
|
||||
<path d={mdiClockEditOutline} />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
|
||||
10
src/assets/icons/Webhook.tsx
Normal file
10
src/assets/icons/Webhook.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon";
|
||||
import { mdiWebhook } from "@mdi/js";
|
||||
|
||||
export default function Webhook(props: SvgIconProps) {
|
||||
return (
|
||||
<SvgIcon {...props}>
|
||||
<path d={mdiWebhook} />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
Binary file not shown.
@@ -7,11 +7,11 @@ import {
|
||||
LinkProps,
|
||||
} from "@mui/material";
|
||||
import { alpha } from "@mui/material/styles";
|
||||
import BrandedBackground, { Wrapper } from "assets/BrandedBackground";
|
||||
import Logo from "assets/Logo";
|
||||
import BrandedBackground, { Wrapper } from "@src/assets/BrandedBackground";
|
||||
import Logo from "@src/assets/Logo";
|
||||
|
||||
import { useAppContext } from "contexts/AppContext";
|
||||
import { EXTERNAL_LINKS } from "constants/externalLinks";
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
import { EXTERNAL_LINKS } from "@src/constants/externalLinks";
|
||||
|
||||
export interface IAuthLayoutProps {
|
||||
hideLogo?: boolean;
|
||||
|
||||
@@ -11,7 +11,7 @@ import Skeleton from "@mui/material/Skeleton";
|
||||
|
||||
import { auth, db } from "@src/firebase";
|
||||
import { defaultUiConfig, getSignInOptions } from "@src/firebase/firebaseui";
|
||||
import { PUBLIC_SETTINGS } from "config/dbPaths";
|
||||
import { PUBLIC_SETTINGS } from "@src/config/dbPaths";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Stack, Paper, Typography, Button } from "@mui/material";
|
||||
import { alpha } from "@mui/material/styles";
|
||||
import DiscordIcon from "assets/icons/Discord";
|
||||
import DiscordIcon from "@src/assets/icons/Discord";
|
||||
import TwitterIcon from "@mui/icons-material/Twitter";
|
||||
|
||||
import Logo from "assets/Logo";
|
||||
import { EXTERNAL_LINKS } from "constants/externalLinks";
|
||||
import Logo from "@src/assets/Logo";
|
||||
import { EXTERNAL_LINKS } from "@src/constants/externalLinks";
|
||||
|
||||
export default function MarketingBanner() {
|
||||
return (
|
||||
|
||||
19
src/components/CircularProgressOptical.tsx
Normal file
19
src/components/CircularProgressOptical.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { CircularProgress, CircularProgressProps } from "@mui/material";
|
||||
|
||||
export default function CircularProgressOptical({
|
||||
size = 40,
|
||||
...props
|
||||
}: CircularProgressProps & { size?: number }) {
|
||||
const DEFAULT_SIZE = 40;
|
||||
const DEFAULT_THICKNESS = 3.6;
|
||||
const linearThickness = (DEFAULT_SIZE / size) * DEFAULT_THICKNESS;
|
||||
const opticalRatio = 1 - (1 - size / DEFAULT_SIZE) / 2;
|
||||
|
||||
return (
|
||||
<CircularProgress
|
||||
{...props}
|
||||
size={size}
|
||||
thickness={linearThickness * opticalRatio}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
import React, { useRef, useMemo, useState } from "react";
|
||||
import clsx from "clsx";
|
||||
import Editor, { useMonaco } from "@monaco-editor/react";
|
||||
|
||||
import { makeStyles, createStyles } from "@mui/styles";
|
||||
import { useTheme } from "@mui/material";
|
||||
|
||||
import { useProjectContext } from "contexts/ProjectContext";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
editorWrapper: { position: "relative" },
|
||||
resizeIcon: {
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
color: theme.palette.text.disabled,
|
||||
},
|
||||
saveButton: {
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export interface ICodeEditorProps {
|
||||
onChange: (value: string) => void;
|
||||
value: string;
|
||||
height?: number;
|
||||
wrapperProps?: Partial<React.HTMLAttributes<HTMLDivElement>>;
|
||||
disabled?: boolean;
|
||||
editorOptions?: any;
|
||||
}
|
||||
|
||||
export default function CodeEditor({
|
||||
onChange,
|
||||
value,
|
||||
height = 400,
|
||||
wrapperProps,
|
||||
disabled,
|
||||
editorOptions,
|
||||
}: ICodeEditorProps) {
|
||||
const theme = useTheme();
|
||||
const [initialEditorValue] = useState(value ?? "");
|
||||
const { tableState } = useProjectContext();
|
||||
const classes = useStyles();
|
||||
const monacoInstance = useMonaco();
|
||||
|
||||
const editorRef = useRef<any>();
|
||||
|
||||
function handleEditorDidMount(_, editor) {
|
||||
editorRef.current = editor;
|
||||
}
|
||||
|
||||
const themeTransformer = (theme: string) => {
|
||||
switch (theme) {
|
||||
case "dark":
|
||||
return "vs-dark";
|
||||
default:
|
||||
return theme;
|
||||
}
|
||||
};
|
||||
|
||||
useMemo(async () => {
|
||||
if (!monacoInstance) {
|
||||
// useMonaco returns a monaco instance but initialisation is done asynchronously
|
||||
// dont execute the logic until the instance is initialised
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
monacoInstance.languages.typescript.javascriptDefaults.setDiagnosticsOptions(
|
||||
{
|
||||
noSemanticValidation: true,
|
||||
noSyntaxValidation: false,
|
||||
}
|
||||
);
|
||||
// compiler options
|
||||
monacoInstance.languages.typescript.javascriptDefaults.setCompilerOptions(
|
||||
{
|
||||
target: monacoInstance.languages.typescript.ScriptTarget.ES5,
|
||||
allowNonTsExtensions: true,
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"An error occurred during initialization of Monaco: ",
|
||||
error
|
||||
);
|
||||
}
|
||||
}, [tableState?.columns]);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...wrapperProps}
|
||||
className={clsx(classes.editorWrapper, wrapperProps?.className)}
|
||||
>
|
||||
<Editor
|
||||
theme={themeTransformer(theme.palette.mode)}
|
||||
height={height}
|
||||
onMount={handleEditorDidMount}
|
||||
language="javascript"
|
||||
value={initialEditorValue}
|
||||
options={{
|
||||
readOnly: disabled,
|
||||
fontFamily: theme.typography.fontFamilyMono,
|
||||
rulers: [80],
|
||||
minimap: { enabled: false },
|
||||
...editorOptions,
|
||||
}}
|
||||
onChange={onChange as any}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Stack, Typography, Grid, Tooltip, Button } from "@mui/material";
|
||||
import InlineOpenInNewIcon from "components/InlineOpenInNewIcon";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
|
||||
export interface ICodeEditorHelperProps {
|
||||
docLink: string;
|
||||
@@ -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}>
|
||||
@@ -63,6 +63,7 @@ export default function CodeEditorHelper({
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
color="primary"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={docLink}
|
||||
117
src/components/CodeEditor/DiffEditor.tsx
Normal file
117
src/components/CodeEditor/DiffEditor.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
DiffEditor as MonacoDiffEditor,
|
||||
DiffEditorProps,
|
||||
EditorProps,
|
||||
} from "@monaco-editor/react";
|
||||
|
||||
import { useTheme, Box, BoxProps } from "@mui/material";
|
||||
import TrapFocus from "@mui/material/Unstable_TrapFocus";
|
||||
import CircularProgressOptical from "@src/components/CircularProgressOptical";
|
||||
import ResizeBottomRightIcon from "@src/assets/icons/ResizeBottomRight";
|
||||
|
||||
import useMonacoCustomizations, {
|
||||
IUseMonacoCustomizationsProps,
|
||||
} from "./useMonacoCustomizations";
|
||||
import FullScreenButton from "./FullScreenButton";
|
||||
|
||||
export interface IDiffEditorProps
|
||||
extends Partial<DiffEditorProps>,
|
||||
Omit<IUseMonacoCustomizationsProps, "fullScreen"> {
|
||||
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 [fullScreen, setFullScreen] = useState(false);
|
||||
|
||||
const { boxSx } = useMonacoCustomizations({
|
||||
minHeight,
|
||||
disabled,
|
||||
error,
|
||||
extraLibs,
|
||||
diagnosticsOptions,
|
||||
onUnmount,
|
||||
fullScreen,
|
||||
});
|
||||
|
||||
// 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 (
|
||||
<TrapFocus open={fullScreen}>
|
||||
<Box
|
||||
sx={[
|
||||
boxSx,
|
||||
...(Array.isArray(containerProps?.sx)
|
||||
? containerProps!.sx
|
||||
: containerProps?.sx
|
||||
? [containerProps.sx]
|
||||
: []),
|
||||
]}
|
||||
style={fullScreen ? { height: "100%" } : {}}
|
||||
>
|
||||
<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
|
||||
}
|
||||
/>
|
||||
|
||||
<FullScreenButton
|
||||
onClick={() => setFullScreen((f) => !f)}
|
||||
active={fullScreen}
|
||||
style={{ right: 32 }}
|
||||
/>
|
||||
|
||||
<ResizeBottomRightIcon
|
||||
aria-label="Resize code editor"
|
||||
color="action"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 1,
|
||||
right: 1,
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</TrapFocus>
|
||||
);
|
||||
}
|
||||
32
src/components/CodeEditor/FullScreenButton.tsx
Normal file
32
src/components/CodeEditor/FullScreenButton.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Button, ButtonProps } from "@mui/material";
|
||||
import FullscreenIcon from "@mui/icons-material/Fullscreen";
|
||||
import FullscreenExitIcon from "@mui/icons-material/FullscreenExit";
|
||||
|
||||
export interface IFullScreenButtonProps extends ButtonProps {
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export default function FullScreenButton({
|
||||
active,
|
||||
...props
|
||||
}: IFullScreenButtonProps) {
|
||||
return (
|
||||
<Button
|
||||
aria-label={`${active ? "Exit" : "Enter"} full screen`}
|
||||
variant={active ? "contained" : "outlined"}
|
||||
color={active ? "secondary" : undefined}
|
||||
{...props}
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 4,
|
||||
right: 16,
|
||||
zIndex: 2,
|
||||
minWidth: 32,
|
||||
padding: 0,
|
||||
...props.style,
|
||||
}}
|
||||
>
|
||||
{active ? <FullscreenExitIcon /> : <FullscreenIcon />}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
97
src/components/CodeEditor/extensions.d.ts
vendored
Normal file
97
src/components/CodeEditor/extensions.d.ts
vendored
Normal file
@@ -0,0 +1,97 @@
|
||||
type Trigger = "create" | "update" | "delete";
|
||||
type Triggers = Trigger[];
|
||||
|
||||
// function types that defines extension body and should run
|
||||
type Condition =
|
||||
| boolean
|
||||
| ((data: ExtensionContext) => boolean | Promise<boolean>);
|
||||
|
||||
// the argument that the extension body takes in
|
||||
type ExtensionContext = {
|
||||
row: Row;
|
||||
ref: FirebaseFirestore.DocumentReference;
|
||||
storage: firebasestorage.Storage;
|
||||
db: FirebaseFirestore.Firestore;
|
||||
auth: adminauth.BaseAuth;
|
||||
change: any;
|
||||
triggerType: Triggers;
|
||||
fieldTypes: any;
|
||||
extensionConfig: {
|
||||
label: string;
|
||||
type: string;
|
||||
triggers: Trigger[];
|
||||
conditions: Condition;
|
||||
requiredFields: string[];
|
||||
extensionBody: any;
|
||||
};
|
||||
utilFns: any;
|
||||
};
|
||||
|
||||
// extension body definition
|
||||
type slackEmailBody = {
|
||||
channels?: string[];
|
||||
text?: string;
|
||||
emails: string[];
|
||||
blocks?: object[];
|
||||
attachments?: any;
|
||||
};
|
||||
|
||||
type slackChannelBody = {
|
||||
channels: string[];
|
||||
text?: string;
|
||||
emails?: string[];
|
||||
blocks?: object[];
|
||||
attachments?: any;
|
||||
};
|
||||
|
||||
type DocSyncBody = (context: ExtensionContext) => Promise<{
|
||||
fieldsToSync: Fields;
|
||||
row: Row;
|
||||
targetPath: string;
|
||||
}>;
|
||||
|
||||
type HistorySnapshotBody = (context: ExtensionContext) => Promise<{
|
||||
trackedFields: Fields;
|
||||
}>;
|
||||
|
||||
type AlgoliaIndexBody = (context: ExtensionContext) => Promise<{
|
||||
fieldsToSync: Fields;
|
||||
index: string;
|
||||
row: Row;
|
||||
objectID: string;
|
||||
}>;
|
||||
|
||||
type MeiliIndexBody = (context: ExtensionContext) => Promise<{
|
||||
fieldsToSync: Fields;
|
||||
index: string;
|
||||
row: Row;
|
||||
objectID: string;
|
||||
}>;
|
||||
|
||||
type BigqueryIndexBody = (context: ExtensionContext) => Promise<{
|
||||
fieldsToSync: Fields;
|
||||
index: string;
|
||||
row: Row;
|
||||
objectID: string;
|
||||
}>;
|
||||
|
||||
type SlackMessageBody = (
|
||||
context: ExtensionContext
|
||||
) => Promise<slackEmailBody | slackChannelBody>;
|
||||
|
||||
type SendgridEmailBody = (context: ExtensionContext) => Promise<any>;
|
||||
|
||||
type ApiCallBody = (context: ExtensionContext) => Promise<{
|
||||
body: string;
|
||||
url: string;
|
||||
method: string;
|
||||
callback: any;
|
||||
}>;
|
||||
|
||||
type TwilioMessageBody = (context: ExtensionContext) => Promise<{
|
||||
body: string;
|
||||
from: string;
|
||||
to: string;
|
||||
}>;
|
||||
|
||||
type TaskBody = (context: ExtensionContext) => Promise<any>;
|
||||
@@ -844,9 +844,9 @@ declare namespace FirebaseFirestore {
|
||||
* `exists` property will always be true and `data()` will never return
|
||||
* 'undefined'.
|
||||
*/
|
||||
export class QueryDocumentSnapshot<T = DocumentData> extends DocumentSnapshot<
|
||||
T
|
||||
> {
|
||||
export class QueryDocumentSnapshot<
|
||||
T = DocumentData
|
||||
> extends DocumentSnapshot<T> {
|
||||
private constructor();
|
||||
|
||||
/**
|
||||
535
src/components/CodeEditor/github-dark-default.json
Normal file
535
src/components/CodeEditor/github-dark-default.json
Normal file
@@ -0,0 +1,535 @@
|
||||
{
|
||||
"inherit": true,
|
||||
"base": "vs-dark",
|
||||
"colors": {
|
||||
"focusBorder": "#1f6feb",
|
||||
"foreground": "#c9d1d9",
|
||||
"descriptionForeground": "#8b949e",
|
||||
"errorForeground": "#f85149",
|
||||
"textLink.foreground": "#58a6ff",
|
||||
"textLink.activeForeground": "#58a6ff",
|
||||
"textBlockQuote.background": "#010409",
|
||||
"textBlockQuote.border": "#30363d",
|
||||
"textCodeBlock.background": "#6e768166",
|
||||
"textPreformat.foreground": "#8b949e",
|
||||
"textSeparator.foreground": "#21262d",
|
||||
"button.background": "#238636",
|
||||
"button.foreground": "#ffffff",
|
||||
"button.hoverBackground": "#2ea043",
|
||||
"button.secondaryBackground": "#282e33",
|
||||
"button.secondaryForeground": "#c9d1d9",
|
||||
"button.secondaryHoverBackground": "#30363d",
|
||||
"checkbox.background": "#161b22",
|
||||
"checkbox.border": "#30363d",
|
||||
"dropdown.background": "#161b22",
|
||||
"dropdown.border": "#30363d",
|
||||
"dropdown.foreground": "#c9d1d9",
|
||||
"dropdown.listBackground": "#161b22",
|
||||
"input.background": "#0d1117",
|
||||
"input.border": "#30363d",
|
||||
"input.foreground": "#c9d1d9",
|
||||
"input.placeholderForeground": "#484f58",
|
||||
"badge.foreground": "#f0f6fc",
|
||||
"badge.background": "#1f6feb",
|
||||
"progressBar.background": "#1f6feb",
|
||||
"titleBar.activeForeground": "#8b949e",
|
||||
"titleBar.activeBackground": "#0d1117",
|
||||
"titleBar.inactiveForeground": "#8b949e",
|
||||
"titleBar.inactiveBackground": "#010409",
|
||||
"titleBar.border": "#30363d",
|
||||
"activityBar.foreground": "#c9d1d9",
|
||||
"activityBar.inactiveForeground": "#8b949e",
|
||||
"activityBar.background": "#0d1117",
|
||||
"activityBarBadge.foreground": "#f0f6fc",
|
||||
"activityBarBadge.background": "#1f6feb",
|
||||
"activityBar.activeBorder": "#f78166",
|
||||
"activityBar.border": "#30363d",
|
||||
"sideBar.foreground": "#c9d1d9",
|
||||
"sideBar.background": "#010409",
|
||||
"sideBar.border": "#30363d",
|
||||
"sideBarTitle.foreground": "#c9d1d9",
|
||||
"sideBarSectionHeader.foreground": "#c9d1d9",
|
||||
"sideBarSectionHeader.background": "#010409",
|
||||
"sideBarSectionHeader.border": "#30363d",
|
||||
"list.hoverForeground": "#c9d1d9",
|
||||
"list.inactiveSelectionForeground": "#c9d1d9",
|
||||
"list.activeSelectionForeground": "#c9d1d9",
|
||||
"list.hoverBackground": "#6e76811a",
|
||||
"list.inactiveSelectionBackground": "#6e768166",
|
||||
"list.activeSelectionBackground": "#6e768166",
|
||||
"list.focusForeground": "#c9d1d9",
|
||||
"list.focusBackground": "#388bfd26",
|
||||
"list.inactiveFocusBackground": "#388bfd26",
|
||||
"list.highlightForeground": "#58a6ff",
|
||||
"tree.indentGuidesStroke": "#21262d",
|
||||
"notificationCenterHeader.foreground": "#8b949e",
|
||||
"notificationCenterHeader.background": "#161b22",
|
||||
"notifications.foreground": "#c9d1d9",
|
||||
"notifications.background": "#161b22",
|
||||
"notifications.border": "#30363d",
|
||||
"notificationsErrorIcon.foreground": "#f85149",
|
||||
"notificationsWarningIcon.foreground": "#d29922",
|
||||
"notificationsInfoIcon.foreground": "#58a6ff",
|
||||
"pickerGroup.border": "#30363d",
|
||||
"pickerGroup.foreground": "#8b949e",
|
||||
"quickInput.background": "#161b22",
|
||||
"quickInput.foreground": "#c9d1d9",
|
||||
"statusBar.foreground": "#8b949e",
|
||||
"statusBar.background": "#0d1117",
|
||||
"statusBar.border": "#30363d",
|
||||
"statusBar.noFolderBackground": "#0d1117",
|
||||
"statusBar.debuggingBackground": "#da3633",
|
||||
"statusBar.debuggingForeground": "#f0f6fc",
|
||||
"statusBarItem.prominentBackground": "#161b22",
|
||||
"editorGroupHeader.tabsBackground": "#010409",
|
||||
"editorGroupHeader.tabsBorder": "#30363d",
|
||||
"editorGroup.border": "#30363d",
|
||||
"tab.activeForeground": "#c9d1d9",
|
||||
"tab.inactiveForeground": "#8b949e",
|
||||
"tab.inactiveBackground": "#010409",
|
||||
"tab.activeBackground": "#0d1117",
|
||||
"tab.hoverBackground": "#0d1117",
|
||||
"tab.unfocusedHoverBackground": "#6e76811a",
|
||||
"tab.border": "#30363d",
|
||||
"tab.unfocusedActiveBorderTop": "#30363d",
|
||||
"tab.activeBorder": "#0d1117",
|
||||
"tab.unfocusedActiveBorder": "#0d1117",
|
||||
"tab.activeBorderTop": "#f78166",
|
||||
"breadcrumb.foreground": "#8b949e",
|
||||
"breadcrumb.focusForeground": "#c9d1d9",
|
||||
"breadcrumb.activeSelectionForeground": "#8b949e",
|
||||
"breadcrumbPicker.background": "#161b22",
|
||||
"editor.foreground": "#c9d1d9",
|
||||
"editor.background": "#0d1117",
|
||||
"editorWidget.background": "#161b22",
|
||||
"editor.foldBackground": "#6e76811a",
|
||||
"editor.lineHighlightBackground": "#6e76811a",
|
||||
"editorLineNumber.foreground": "#8b949e",
|
||||
"editorLineNumber.activeForeground": "#c9d1d9",
|
||||
"editorIndentGuide.background": "#21262d",
|
||||
"editorIndentGuide.activeBackground": "#30363d",
|
||||
"editorWhitespace.foreground": "#484f58",
|
||||
"editorCursor.foreground": "#58a6ff",
|
||||
"editor.findMatchBackground": "#ffd33d44",
|
||||
"editor.findMatchHighlightBackground": "#ffd33d22",
|
||||
"editor.linkedEditingBackground": "#3392FF22",
|
||||
"editor.inactiveSelectionBackground": "#3392FF22",
|
||||
"editor.selectionBackground": "#3392FF44",
|
||||
"editor.selectionHighlightBackground": "#17E5E633",
|
||||
"editor.selectionHighlightBorder": "#17E5E600",
|
||||
"editor.wordHighlightBackground": "#17E5E600",
|
||||
"editor.wordHighlightStrongBackground": "#17E5E600",
|
||||
"editor.wordHighlightBorder": "#17E5E699",
|
||||
"editor.wordHighlightStrongBorder": "#17E5E666",
|
||||
"editorBracketMatch.background": "#17E5E650",
|
||||
"editorBracketMatch.border": "#17E5E600",
|
||||
"editorGutter.modifiedBackground": "#bb800966",
|
||||
"editorGutter.addedBackground": "#2ea04366",
|
||||
"editorGutter.deletedBackground": "#f8514966",
|
||||
"diffEditor.insertedTextBackground": "#2ea04326",
|
||||
"diffEditor.removedTextBackground": "#f8514926",
|
||||
"scrollbar.shadow": "#0008",
|
||||
"scrollbarSlider.background": "#484F5833",
|
||||
"scrollbarSlider.hoverBackground": "#484F5844",
|
||||
"scrollbarSlider.activeBackground": "#484F5888",
|
||||
"editorOverviewRuler.border": "#010409",
|
||||
"panel.background": "#010409",
|
||||
"panel.border": "#30363d",
|
||||
"panelTitle.activeBorder": "#f78166",
|
||||
"panelTitle.activeForeground": "#c9d1d9",
|
||||
"panelTitle.inactiveForeground": "#8b949e",
|
||||
"panelInput.border": "#30363d",
|
||||
"terminal.foreground": "#8b949e",
|
||||
"terminal.ansiBlack": "#484f58",
|
||||
"terminal.ansiRed": "#ff7b72",
|
||||
"terminal.ansiGreen": "#3fb950",
|
||||
"terminal.ansiYellow": "#d29922",
|
||||
"terminal.ansiBlue": "#58a6ff",
|
||||
"terminal.ansiMagenta": "#bc8cff",
|
||||
"terminal.ansiCyan": "#39c5cf",
|
||||
"terminal.ansiWhite": "#b1bac4",
|
||||
"terminal.ansiBrightBlack": "#6e7681",
|
||||
"terminal.ansiBrightRed": "#ffa198",
|
||||
"terminal.ansiBrightGreen": "#56d364",
|
||||
"terminal.ansiBrightYellow": "#e3b341",
|
||||
"terminal.ansiBrightBlue": "#79c0ff",
|
||||
"terminal.ansiBrightMagenta": "#d2a8ff",
|
||||
"terminal.ansiBrightCyan": "#56d4dd",
|
||||
"terminal.ansiBrightWhite": "#f0f6fc",
|
||||
"gitDecoration.addedResourceForeground": "#3fb950",
|
||||
"gitDecoration.modifiedResourceForeground": "#d29922",
|
||||
"gitDecoration.deletedResourceForeground": "#f85149",
|
||||
"gitDecoration.untrackedResourceForeground": "#3fb950",
|
||||
"gitDecoration.ignoredResourceForeground": "#484f58",
|
||||
"gitDecoration.conflictingResourceForeground": "#db6d28",
|
||||
"gitDecoration.submoduleResourceForeground": "#8b949e",
|
||||
"debugToolBar.background": "#161b22",
|
||||
"editor.stackFrameHighlightBackground": "#D2992225",
|
||||
"editor.focusedStackFrameHighlightBackground": "#3FB95025",
|
||||
"peekViewEditor.matchHighlightBackground": "#ffd33d33",
|
||||
"peekViewResult.matchHighlightBackground": "#ffd33d33",
|
||||
"peekViewEditor.background": "#0d111788",
|
||||
"peekViewResult.background": "#0d1117",
|
||||
"settings.headerForeground": "#8b949e",
|
||||
"settings.modifiedItemIndicator": "#bb800966",
|
||||
"welcomePage.buttonBackground": "#21262d",
|
||||
"welcomePage.buttonHoverBackground": "#30363d"
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"foreground": "#8b949e",
|
||||
"token": "comment"
|
||||
},
|
||||
{
|
||||
"foreground": "#8b949e",
|
||||
"token": "punctuation.definition.comment"
|
||||
},
|
||||
{
|
||||
"foreground": "#8b949e",
|
||||
"token": "string.comment"
|
||||
},
|
||||
{
|
||||
"foreground": "#79c0ff",
|
||||
"token": "constant"
|
||||
},
|
||||
{
|
||||
"foreground": "#79c0ff",
|
||||
"token": "entity.name.constant"
|
||||
},
|
||||
{
|
||||
"foreground": "#79c0ff",
|
||||
"token": "variable.other.constant"
|
||||
},
|
||||
{
|
||||
"foreground": "#79c0ff",
|
||||
"token": "variable.language"
|
||||
},
|
||||
{
|
||||
"foreground": "#79c0ff",
|
||||
"token": "entity"
|
||||
},
|
||||
{
|
||||
"foreground": "#ffa657",
|
||||
"token": "entity.name"
|
||||
},
|
||||
{
|
||||
"foreground": "#ffa657",
|
||||
"token": "meta.export.default"
|
||||
},
|
||||
{
|
||||
"foreground": "#ffa657",
|
||||
"token": "meta.definition.variable"
|
||||
},
|
||||
{
|
||||
"foreground": "#c9d1d9",
|
||||
"token": "variable.parameter.function"
|
||||
},
|
||||
{
|
||||
"foreground": "#c9d1d9",
|
||||
"token": "meta.jsx.children"
|
||||
},
|
||||
{
|
||||
"foreground": "#c9d1d9",
|
||||
"token": "meta.block"
|
||||
},
|
||||
{
|
||||
"foreground": "#c9d1d9",
|
||||
"token": "meta.tag.attributes"
|
||||
},
|
||||
{
|
||||
"foreground": "#c9d1d9",
|
||||
"token": "entity.name.constant"
|
||||
},
|
||||
{
|
||||
"foreground": "#c9d1d9",
|
||||
"token": "meta.object.member"
|
||||
},
|
||||
{
|
||||
"foreground": "#c9d1d9",
|
||||
"token": "meta.embedded.expression"
|
||||
},
|
||||
{
|
||||
"foreground": "#d2a8ff",
|
||||
"token": "entity.name.function"
|
||||
},
|
||||
{
|
||||
"foreground": "#7ee787",
|
||||
"token": "entity.name.tag"
|
||||
},
|
||||
{
|
||||
"foreground": "#7ee787",
|
||||
"token": "support.class.component"
|
||||
},
|
||||
{
|
||||
"foreground": "#ff7b72",
|
||||
"token": "keyword"
|
||||
},
|
||||
{
|
||||
"foreground": "#ff7b72",
|
||||
"token": "storage"
|
||||
},
|
||||
{
|
||||
"foreground": "#ff7b72",
|
||||
"token": "storage.type"
|
||||
},
|
||||
{
|
||||
"foreground": "#c9d1d9",
|
||||
"token": "storage.modifier.package"
|
||||
},
|
||||
{
|
||||
"foreground": "#c9d1d9",
|
||||
"token": "storage.modifier.import"
|
||||
},
|
||||
{
|
||||
"foreground": "#c9d1d9",
|
||||
"token": "storage.type.java"
|
||||
},
|
||||
{
|
||||
"foreground": "#a5d6ff",
|
||||
"token": "string"
|
||||
},
|
||||
{
|
||||
"foreground": "#a5d6ff",
|
||||
"token": "punctuation.definition.string"
|
||||
},
|
||||
{
|
||||
"foreground": "#a5d6ff",
|
||||
"token": "string punctuation.section.embedded source"
|
||||
},
|
||||
{
|
||||
"foreground": "#79c0ff",
|
||||
"token": "support"
|
||||
},
|
||||
{
|
||||
"foreground": "#79c0ff",
|
||||
"token": "meta.property-name"
|
||||
},
|
||||
{
|
||||
"foreground": "#ffa657",
|
||||
"token": "variable"
|
||||
},
|
||||
{
|
||||
"foreground": "#c9d1d9",
|
||||
"token": "variable.other"
|
||||
},
|
||||
{
|
||||
"fontStyle": "italic",
|
||||
"foreground": "#ffa198",
|
||||
"token": "invalid.broken"
|
||||
},
|
||||
{
|
||||
"fontStyle": "italic",
|
||||
"foreground": "#ffa198",
|
||||
"token": "invalid.deprecated"
|
||||
},
|
||||
{
|
||||
"fontStyle": "italic",
|
||||
"foreground": "#ffa198",
|
||||
"token": "invalid.illegal"
|
||||
},
|
||||
{
|
||||
"fontStyle": "italic",
|
||||
"foreground": "#ffa198",
|
||||
"token": "invalid.unimplemented"
|
||||
},
|
||||
{
|
||||
"fontStyle": "italic underline",
|
||||
"background": "#ff7b72",
|
||||
"foreground": "#0d1117",
|
||||
"content": "^M",
|
||||
"token": "carriage-return"
|
||||
},
|
||||
{
|
||||
"foreground": "#ffa198",
|
||||
"token": "message.error"
|
||||
},
|
||||
{
|
||||
"foreground": "#c9d1d9",
|
||||
"token": "string source"
|
||||
},
|
||||
{
|
||||
"foreground": "#79c0ff",
|
||||
"token": "string variable"
|
||||
},
|
||||
{
|
||||
"foreground": "#a5d6ff",
|
||||
"token": "source.regexp"
|
||||
},
|
||||
{
|
||||
"foreground": "#a5d6ff",
|
||||
"token": "string.regexp"
|
||||
},
|
||||
{
|
||||
"foreground": "#a5d6ff",
|
||||
"token": "string.regexp.character-class"
|
||||
},
|
||||
{
|
||||
"foreground": "#a5d6ff",
|
||||
"token": "string.regexp constant.character.escape"
|
||||
},
|
||||
{
|
||||
"foreground": "#a5d6ff",
|
||||
"token": "string.regexp source.ruby.embedded"
|
||||
},
|
||||
{
|
||||
"foreground": "#a5d6ff",
|
||||
"token": "string.regexp string.regexp.arbitrary-repitition"
|
||||
},
|
||||
{
|
||||
"fontStyle": "bold",
|
||||
"foreground": "#7ee787",
|
||||
"token": "string.regexp constant.character.escape"
|
||||
},
|
||||
{
|
||||
"foreground": "#79c0ff",
|
||||
"token": "support.constant"
|
||||
},
|
||||
{
|
||||
"foreground": "#79c0ff",
|
||||
"token": "support.variable"
|
||||
},
|
||||
{
|
||||
"foreground": "#79c0ff",
|
||||
"token": "meta.module-reference"
|
||||
},
|
||||
{
|
||||
"foreground": "#ffa657",
|
||||
"token": "punctuation.definition.list.begin.markdown"
|
||||
},
|
||||
{
|
||||
"fontStyle": "bold",
|
||||
"foreground": "#79c0ff",
|
||||
"token": "markup.heading"
|
||||
},
|
||||
{
|
||||
"fontStyle": "bold",
|
||||
"foreground": "#79c0ff",
|
||||
"token": "markup.heading entity.name"
|
||||
},
|
||||
{
|
||||
"foreground": "#7ee787",
|
||||
"token": "markup.quote"
|
||||
},
|
||||
{
|
||||
"fontStyle": "italic",
|
||||
"foreground": "#c9d1d9",
|
||||
"token": "markup.italic"
|
||||
},
|
||||
{
|
||||
"fontStyle": "bold",
|
||||
"foreground": "#c9d1d9",
|
||||
"token": "markup.bold"
|
||||
},
|
||||
{
|
||||
"foreground": "#79c0ff",
|
||||
"token": "markup.raw"
|
||||
},
|
||||
{
|
||||
"background": "#490202",
|
||||
"foreground": "#ffa198",
|
||||
"token": "markup.deleted"
|
||||
},
|
||||
{
|
||||
"background": "#490202",
|
||||
"foreground": "#ffa198",
|
||||
"token": "meta.diff.header.from-file"
|
||||
},
|
||||
{
|
||||
"background": "#490202",
|
||||
"foreground": "#ffa198",
|
||||
"token": "punctuation.definition.deleted"
|
||||
},
|
||||
{
|
||||
"background": "#04260f",
|
||||
"foreground": "#7ee787",
|
||||
"token": "markup.inserted"
|
||||
},
|
||||
{
|
||||
"background": "#04260f",
|
||||
"foreground": "#7ee787",
|
||||
"token": "meta.diff.header.to-file"
|
||||
},
|
||||
{
|
||||
"background": "#04260f",
|
||||
"foreground": "#7ee787",
|
||||
"token": "punctuation.definition.inserted"
|
||||
},
|
||||
{
|
||||
"background": "#5a1e02",
|
||||
"foreground": "#ffa657",
|
||||
"token": "markup.changed"
|
||||
},
|
||||
{
|
||||
"background": "#5a1e02",
|
||||
"foreground": "#ffa657",
|
||||
"token": "punctuation.definition.changed"
|
||||
},
|
||||
{
|
||||
"foreground": "#161b22",
|
||||
"background": "#79c0ff",
|
||||
"token": "markup.ignored"
|
||||
},
|
||||
{
|
||||
"foreground": "#161b22",
|
||||
"background": "#79c0ff",
|
||||
"token": "markup.untracked"
|
||||
},
|
||||
{
|
||||
"foreground": "#d2a8ff",
|
||||
"fontStyle": "bold",
|
||||
"token": "meta.diff.range"
|
||||
},
|
||||
{
|
||||
"foreground": "#79c0ff",
|
||||
"token": "meta.diff.header"
|
||||
},
|
||||
{
|
||||
"fontStyle": "bold",
|
||||
"foreground": "#79c0ff",
|
||||
"token": "meta.separator"
|
||||
},
|
||||
{
|
||||
"foreground": "#79c0ff",
|
||||
"token": "meta.output"
|
||||
},
|
||||
{
|
||||
"foreground": "#8b949e",
|
||||
"token": "brackethighlighter.tag"
|
||||
},
|
||||
{
|
||||
"foreground": "#8b949e",
|
||||
"token": "brackethighlighter.curly"
|
||||
},
|
||||
{
|
||||
"foreground": "#8b949e",
|
||||
"token": "brackethighlighter.round"
|
||||
},
|
||||
{
|
||||
"foreground": "#8b949e",
|
||||
"token": "brackethighlighter.square"
|
||||
},
|
||||
{
|
||||
"foreground": "#8b949e",
|
||||
"token": "brackethighlighter.angle"
|
||||
},
|
||||
{
|
||||
"foreground": "#8b949e",
|
||||
"token": "brackethighlighter.quote"
|
||||
},
|
||||
{
|
||||
"foreground": "#ffa198",
|
||||
"token": "brackethighlighter.unmatched"
|
||||
},
|
||||
{
|
||||
"foreground": "#a5d6ff",
|
||||
"fontStyle": "underline",
|
||||
"token": "constant.other.reference.link"
|
||||
},
|
||||
{
|
||||
"foreground": "#a5d6ff",
|
||||
"fontStyle": "underline",
|
||||
"token": "string.other.link"
|
||||
}
|
||||
],
|
||||
"encodedTokensColors": []
|
||||
}
|
||||
531
src/components/CodeEditor/github-light-default.json
Normal file
531
src/components/CodeEditor/github-light-default.json
Normal file
@@ -0,0 +1,531 @@
|
||||
{
|
||||
"inherit": true,
|
||||
"base": "vs",
|
||||
"colors": {
|
||||
"focusBorder": "#0969da",
|
||||
"foreground": "#24292f",
|
||||
"descriptionForeground": "#57606a",
|
||||
"errorForeground": "#cf222e",
|
||||
"textLink.foreground": "#0969da",
|
||||
"textLink.activeForeground": "#0969da",
|
||||
"textBlockQuote.background": "#f6f8fa",
|
||||
"textBlockQuote.border": "#d0d7de",
|
||||
"textCodeBlock.background": "#afb8c133",
|
||||
"textPreformat.foreground": "#57606a",
|
||||
"textSeparator.foreground": "#d8dee4",
|
||||
"button.background": "#2da44e",
|
||||
"button.foreground": "#ffffff",
|
||||
"button.hoverBackground": "#2c974b",
|
||||
"button.secondaryBackground": "#ebecf0",
|
||||
"button.secondaryForeground": "#24292f",
|
||||
"button.secondaryHoverBackground": "#f3f4f6",
|
||||
"checkbox.background": "#f6f8fa",
|
||||
"checkbox.border": "#d0d7de",
|
||||
"dropdown.background": "#ffffff",
|
||||
"dropdown.border": "#d0d7de",
|
||||
"dropdown.foreground": "#24292f",
|
||||
"dropdown.listBackground": "#ffffff",
|
||||
"input.background": "#ffffff",
|
||||
"input.border": "#d0d7de",
|
||||
"input.foreground": "#24292f",
|
||||
"input.placeholderForeground": "#6e7781",
|
||||
"badge.foreground": "#ffffff",
|
||||
"badge.background": "#0969da",
|
||||
"progressBar.background": "#0969da",
|
||||
"titleBar.activeForeground": "#57606a",
|
||||
"titleBar.activeBackground": "#ffffff",
|
||||
"titleBar.inactiveForeground": "#57606a",
|
||||
"titleBar.inactiveBackground": "#f6f8fa",
|
||||
"titleBar.border": "#d0d7de",
|
||||
"activityBar.foreground": "#24292f",
|
||||
"activityBar.inactiveForeground": "#57606a",
|
||||
"activityBar.background": "#ffffff",
|
||||
"activityBarBadge.foreground": "#ffffff",
|
||||
"activityBarBadge.background": "#0969da",
|
||||
"activityBar.activeBorder": "#fd8c73",
|
||||
"activityBar.border": "#d0d7de",
|
||||
"sideBar.foreground": "#24292f",
|
||||
"sideBar.background": "#f6f8fa",
|
||||
"sideBar.border": "#d0d7de",
|
||||
"sideBarTitle.foreground": "#24292f",
|
||||
"sideBarSectionHeader.foreground": "#24292f",
|
||||
"sideBarSectionHeader.background": "#f6f8fa",
|
||||
"sideBarSectionHeader.border": "#d0d7de",
|
||||
"list.hoverForeground": "#24292f",
|
||||
"list.inactiveSelectionForeground": "#24292f",
|
||||
"list.activeSelectionForeground": "#24292f",
|
||||
"list.hoverBackground": "#eaeef280",
|
||||
"list.inactiveSelectionBackground": "#afb8c133",
|
||||
"list.activeSelectionBackground": "#afb8c133",
|
||||
"list.focusForeground": "#24292f",
|
||||
"list.focusBackground": "#ddf4ff",
|
||||
"list.inactiveFocusBackground": "#ddf4ff",
|
||||
"list.highlightForeground": "#0969da",
|
||||
"tree.indentGuidesStroke": "#d8dee4",
|
||||
"notificationCenterHeader.foreground": "#57606a",
|
||||
"notificationCenterHeader.background": "#f6f8fa",
|
||||
"notifications.foreground": "#24292f",
|
||||
"notifications.background": "#ffffff",
|
||||
"notifications.border": "#d0d7de",
|
||||
"notificationsErrorIcon.foreground": "#cf222e",
|
||||
"notificationsWarningIcon.foreground": "#9a6700",
|
||||
"notificationsInfoIcon.foreground": "#0969da",
|
||||
"pickerGroup.border": "#d0d7de",
|
||||
"pickerGroup.foreground": "#57606a",
|
||||
"quickInput.background": "#ffffff",
|
||||
"quickInput.foreground": "#24292f",
|
||||
"statusBar.foreground": "#57606a",
|
||||
"statusBar.background": "#ffffff",
|
||||
"statusBar.border": "#d0d7de",
|
||||
"statusBar.noFolderBackground": "#ffffff",
|
||||
"statusBar.debuggingBackground": "#cf222e",
|
||||
"statusBar.debuggingForeground": "#ffffff",
|
||||
"statusBarItem.prominentBackground": "#f6f8fa",
|
||||
"editorGroupHeader.tabsBackground": "#f6f8fa",
|
||||
"editorGroupHeader.tabsBorder": "#d0d7de",
|
||||
"editorGroup.border": "#d0d7de",
|
||||
"tab.activeForeground": "#24292f",
|
||||
"tab.inactiveForeground": "#57606a",
|
||||
"tab.inactiveBackground": "#f6f8fa",
|
||||
"tab.activeBackground": "#ffffff",
|
||||
"tab.hoverBackground": "#ffffff",
|
||||
"tab.unfocusedHoverBackground": "#eaeef280",
|
||||
"tab.border": "#d0d7de",
|
||||
"tab.unfocusedActiveBorderTop": "#d0d7de",
|
||||
"tab.activeBorder": "#ffffff",
|
||||
"tab.unfocusedActiveBorder": "#ffffff",
|
||||
"tab.activeBorderTop": "#fd8c73",
|
||||
"breadcrumb.foreground": "#57606a",
|
||||
"breadcrumb.focusForeground": "#24292f",
|
||||
"breadcrumb.activeSelectionForeground": "#57606a",
|
||||
"breadcrumbPicker.background": "#ffffff",
|
||||
"editor.foreground": "#24292f",
|
||||
"editor.background": "#ffffff",
|
||||
"editorWidget.background": "#ffffff",
|
||||
"editor.foldBackground": "#6e77811a",
|
||||
"editor.lineHighlightBackground": "#eaeef280",
|
||||
"editorLineNumber.foreground": "#57606a",
|
||||
"editorLineNumber.activeForeground": "#24292f",
|
||||
"editorIndentGuide.background": "#d8dee4",
|
||||
"editorIndentGuide.activeBackground": "#d0d7de",
|
||||
"editorWhitespace.foreground": "#6e7781",
|
||||
"editorCursor.foreground": "#0969da",
|
||||
"editor.findMatchBackground": "#bf8700",
|
||||
"editor.findMatchHighlightBackground": "#ffdf5d66",
|
||||
"editor.linkedEditingBackground": "#0366d611",
|
||||
"editor.inactiveSelectionBackground": "#0366d611",
|
||||
"editor.selectionBackground": "#0366d625",
|
||||
"editor.selectionHighlightBackground": "#34d05840",
|
||||
"editor.selectionHighlightBorder": "#34d05800",
|
||||
"editor.wordHighlightBackground": "#34d05800",
|
||||
"editor.wordHighlightStrongBackground": "#34d05800",
|
||||
"editor.wordHighlightBorder": "#24943e99",
|
||||
"editor.wordHighlightStrongBorder": "#24943e50",
|
||||
"editorBracketMatch.background": "#34d05840",
|
||||
"editorBracketMatch.border": "#34d05800",
|
||||
"editorGutter.modifiedBackground": "#d4a72c66",
|
||||
"editorGutter.addedBackground": "#4ac26b66",
|
||||
"editorGutter.deletedBackground": "#ff818266",
|
||||
"diffEditor.insertedTextBackground": "#85e89d33",
|
||||
"diffEditor.removedTextBackground": "#f9758326",
|
||||
"scrollbar.shadow": "#6a737d33",
|
||||
"scrollbarSlider.background": "#959da533",
|
||||
"scrollbarSlider.hoverBackground": "#959da544",
|
||||
"scrollbarSlider.activeBackground": "#959da588",
|
||||
"editorOverviewRuler.border": "#ffffff",
|
||||
"panel.background": "#f6f8fa",
|
||||
"panel.border": "#d0d7de",
|
||||
"panelTitle.activeBorder": "#fd8c73",
|
||||
"panelTitle.activeForeground": "#24292f",
|
||||
"panelTitle.inactiveForeground": "#57606a",
|
||||
"panelInput.border": "#d0d7de",
|
||||
"terminal.foreground": "#57606a",
|
||||
"terminal.ansiBlack": "#24292f",
|
||||
"terminal.ansiRed": "#cf222e",
|
||||
"terminal.ansiGreen": "#116329",
|
||||
"terminal.ansiYellow": "#4d2d00",
|
||||
"terminal.ansiBlue": "#0969da",
|
||||
"terminal.ansiMagenta": "#8250df",
|
||||
"terminal.ansiCyan": "#1b7c83",
|
||||
"terminal.ansiWhite": "#6e7781",
|
||||
"terminal.ansiBrightBlack": "#57606a",
|
||||
"terminal.ansiBrightRed": "#a40e26",
|
||||
"terminal.ansiBrightGreen": "#1a7f37",
|
||||
"terminal.ansiBrightYellow": "#633c01",
|
||||
"terminal.ansiBrightBlue": "#218bff",
|
||||
"terminal.ansiBrightMagenta": "#a475f9",
|
||||
"terminal.ansiBrightCyan": "#3192aa",
|
||||
"terminal.ansiBrightWhite": "#8c959f",
|
||||
"gitDecoration.addedResourceForeground": "#1a7f37",
|
||||
"gitDecoration.modifiedResourceForeground": "#9a6700",
|
||||
"gitDecoration.deletedResourceForeground": "#cf222e",
|
||||
"gitDecoration.untrackedResourceForeground": "#1a7f37",
|
||||
"gitDecoration.ignoredResourceForeground": "#6e7781",
|
||||
"gitDecoration.conflictingResourceForeground": "#bc4c00",
|
||||
"gitDecoration.submoduleResourceForeground": "#57606a",
|
||||
"debugToolBar.background": "#ffffff",
|
||||
"editor.stackFrameHighlightBackground": "#ffd33d33",
|
||||
"editor.focusedStackFrameHighlightBackground": "#28a74525",
|
||||
"settings.headerForeground": "#57606a",
|
||||
"settings.modifiedItemIndicator": "#d4a72c66",
|
||||
"welcomePage.buttonBackground": "#f6f8fa",
|
||||
"welcomePage.buttonHoverBackground": "#f3f4f6"
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"foreground": "#6e7781",
|
||||
"token": "comment"
|
||||
},
|
||||
{
|
||||
"foreground": "#6e7781",
|
||||
"token": "punctuation.definition.comment"
|
||||
},
|
||||
{
|
||||
"foreground": "#6e7781",
|
||||
"token": "string.comment"
|
||||
},
|
||||
{
|
||||
"foreground": "#0550ae",
|
||||
"token": "constant"
|
||||
},
|
||||
{
|
||||
"foreground": "#0550ae",
|
||||
"token": "entity.name.constant"
|
||||
},
|
||||
{
|
||||
"foreground": "#0550ae",
|
||||
"token": "variable.other.constant"
|
||||
},
|
||||
{
|
||||
"foreground": "#0550ae",
|
||||
"token": "variable.language"
|
||||
},
|
||||
{
|
||||
"foreground": "#0550ae",
|
||||
"token": "entity"
|
||||
},
|
||||
{
|
||||
"foreground": "#953800",
|
||||
"token": "entity.name"
|
||||
},
|
||||
{
|
||||
"foreground": "#953800",
|
||||
"token": "meta.export.default"
|
||||
},
|
||||
{
|
||||
"foreground": "#953800",
|
||||
"token": "meta.definition.variable"
|
||||
},
|
||||
{
|
||||
"foreground": "#24292f",
|
||||
"token": "variable.parameter.function"
|
||||
},
|
||||
{
|
||||
"foreground": "#24292f",
|
||||
"token": "meta.jsx.children"
|
||||
},
|
||||
{
|
||||
"foreground": "#24292f",
|
||||
"token": "meta.block"
|
||||
},
|
||||
{
|
||||
"foreground": "#24292f",
|
||||
"token": "meta.tag.attributes"
|
||||
},
|
||||
{
|
||||
"foreground": "#24292f",
|
||||
"token": "entity.name.constant"
|
||||
},
|
||||
{
|
||||
"foreground": "#24292f",
|
||||
"token": "meta.object.member"
|
||||
},
|
||||
{
|
||||
"foreground": "#24292f",
|
||||
"token": "meta.embedded.expression"
|
||||
},
|
||||
{
|
||||
"foreground": "#8250df",
|
||||
"token": "entity.name.function"
|
||||
},
|
||||
{
|
||||
"foreground": "#116329",
|
||||
"token": "entity.name.tag"
|
||||
},
|
||||
{
|
||||
"foreground": "#116329",
|
||||
"token": "support.class.component"
|
||||
},
|
||||
{
|
||||
"foreground": "#cf222e",
|
||||
"token": "keyword"
|
||||
},
|
||||
{
|
||||
"foreground": "#cf222e",
|
||||
"token": "storage"
|
||||
},
|
||||
{
|
||||
"foreground": "#cf222e",
|
||||
"token": "storage.type"
|
||||
},
|
||||
{
|
||||
"foreground": "#24292f",
|
||||
"token": "storage.modifier.package"
|
||||
},
|
||||
{
|
||||
"foreground": "#24292f",
|
||||
"token": "storage.modifier.import"
|
||||
},
|
||||
{
|
||||
"foreground": "#24292f",
|
||||
"token": "storage.type.java"
|
||||
},
|
||||
{
|
||||
"foreground": "#0a3069",
|
||||
"token": "string"
|
||||
},
|
||||
{
|
||||
"foreground": "#0a3069",
|
||||
"token": "punctuation.definition.string"
|
||||
},
|
||||
{
|
||||
"foreground": "#0a3069",
|
||||
"token": "string punctuation.section.embedded source"
|
||||
},
|
||||
{
|
||||
"foreground": "#0550ae",
|
||||
"token": "support"
|
||||
},
|
||||
{
|
||||
"foreground": "#0550ae",
|
||||
"token": "meta.property-name"
|
||||
},
|
||||
{
|
||||
"foreground": "#953800",
|
||||
"token": "variable"
|
||||
},
|
||||
{
|
||||
"foreground": "#24292f",
|
||||
"token": "variable.other"
|
||||
},
|
||||
{
|
||||
"fontStyle": "italic",
|
||||
"foreground": "#82071e",
|
||||
"token": "invalid.broken"
|
||||
},
|
||||
{
|
||||
"fontStyle": "italic",
|
||||
"foreground": "#82071e",
|
||||
"token": "invalid.deprecated"
|
||||
},
|
||||
{
|
||||
"fontStyle": "italic",
|
||||
"foreground": "#82071e",
|
||||
"token": "invalid.illegal"
|
||||
},
|
||||
{
|
||||
"fontStyle": "italic",
|
||||
"foreground": "#82071e",
|
||||
"token": "invalid.unimplemented"
|
||||
},
|
||||
{
|
||||
"fontStyle": "italic underline",
|
||||
"background": "#cf222e",
|
||||
"foreground": "#f6f8fa",
|
||||
"content": "^M",
|
||||
"token": "carriage-return"
|
||||
},
|
||||
{
|
||||
"foreground": "#82071e",
|
||||
"token": "message.error"
|
||||
},
|
||||
{
|
||||
"foreground": "#24292f",
|
||||
"token": "string source"
|
||||
},
|
||||
{
|
||||
"foreground": "#0550ae",
|
||||
"token": "string variable"
|
||||
},
|
||||
{
|
||||
"foreground": "#0a3069",
|
||||
"token": "source.regexp"
|
||||
},
|
||||
{
|
||||
"foreground": "#0a3069",
|
||||
"token": "string.regexp"
|
||||
},
|
||||
{
|
||||
"foreground": "#0a3069",
|
||||
"token": "string.regexp.character-class"
|
||||
},
|
||||
{
|
||||
"foreground": "#0a3069",
|
||||
"token": "string.regexp constant.character.escape"
|
||||
},
|
||||
{
|
||||
"foreground": "#0a3069",
|
||||
"token": "string.regexp source.ruby.embedded"
|
||||
},
|
||||
{
|
||||
"foreground": "#0a3069",
|
||||
"token": "string.regexp string.regexp.arbitrary-repitition"
|
||||
},
|
||||
{
|
||||
"fontStyle": "bold",
|
||||
"foreground": "#116329",
|
||||
"token": "string.regexp constant.character.escape"
|
||||
},
|
||||
{
|
||||
"foreground": "#0550ae",
|
||||
"token": "support.constant"
|
||||
},
|
||||
{
|
||||
"foreground": "#0550ae",
|
||||
"token": "support.variable"
|
||||
},
|
||||
{
|
||||
"foreground": "#0550ae",
|
||||
"token": "meta.module-reference"
|
||||
},
|
||||
{
|
||||
"foreground": "#953800",
|
||||
"token": "punctuation.definition.list.begin.markdown"
|
||||
},
|
||||
{
|
||||
"fontStyle": "bold",
|
||||
"foreground": "#0550ae",
|
||||
"token": "markup.heading"
|
||||
},
|
||||
{
|
||||
"fontStyle": "bold",
|
||||
"foreground": "#0550ae",
|
||||
"token": "markup.heading entity.name"
|
||||
},
|
||||
{
|
||||
"foreground": "#116329",
|
||||
"token": "markup.quote"
|
||||
},
|
||||
{
|
||||
"fontStyle": "italic",
|
||||
"foreground": "#24292f",
|
||||
"token": "markup.italic"
|
||||
},
|
||||
{
|
||||
"fontStyle": "bold",
|
||||
"foreground": "#24292f",
|
||||
"token": "markup.bold"
|
||||
},
|
||||
{
|
||||
"foreground": "#0550ae",
|
||||
"token": "markup.raw"
|
||||
},
|
||||
{
|
||||
"background": "#FFEBE9",
|
||||
"foreground": "#82071e",
|
||||
"token": "markup.deleted"
|
||||
},
|
||||
{
|
||||
"background": "#FFEBE9",
|
||||
"foreground": "#82071e",
|
||||
"token": "meta.diff.header.from-file"
|
||||
},
|
||||
{
|
||||
"background": "#FFEBE9",
|
||||
"foreground": "#82071e",
|
||||
"token": "punctuation.definition.deleted"
|
||||
},
|
||||
{
|
||||
"background": "#dafbe1",
|
||||
"foreground": "#116329",
|
||||
"token": "markup.inserted"
|
||||
},
|
||||
{
|
||||
"background": "#dafbe1",
|
||||
"foreground": "#116329",
|
||||
"token": "meta.diff.header.to-file"
|
||||
},
|
||||
{
|
||||
"background": "#dafbe1",
|
||||
"foreground": "#116329",
|
||||
"token": "punctuation.definition.inserted"
|
||||
},
|
||||
{
|
||||
"background": "#ffd8b5",
|
||||
"foreground": "#953800",
|
||||
"token": "markup.changed"
|
||||
},
|
||||
{
|
||||
"background": "#ffd8b5",
|
||||
"foreground": "#953800",
|
||||
"token": "punctuation.definition.changed"
|
||||
},
|
||||
{
|
||||
"foreground": "#eaeef2",
|
||||
"background": "#0550ae",
|
||||
"token": "markup.ignored"
|
||||
},
|
||||
{
|
||||
"foreground": "#eaeef2",
|
||||
"background": "#0550ae",
|
||||
"token": "markup.untracked"
|
||||
},
|
||||
{
|
||||
"foreground": "#8250df",
|
||||
"fontStyle": "bold",
|
||||
"token": "meta.diff.range"
|
||||
},
|
||||
{
|
||||
"foreground": "#0550ae",
|
||||
"token": "meta.diff.header"
|
||||
},
|
||||
{
|
||||
"fontStyle": "bold",
|
||||
"foreground": "#0550ae",
|
||||
"token": "meta.separator"
|
||||
},
|
||||
{
|
||||
"foreground": "#0550ae",
|
||||
"token": "meta.output"
|
||||
},
|
||||
{
|
||||
"foreground": "#57606a",
|
||||
"token": "brackethighlighter.tag"
|
||||
},
|
||||
{
|
||||
"foreground": "#57606a",
|
||||
"token": "brackethighlighter.curly"
|
||||
},
|
||||
{
|
||||
"foreground": "#57606a",
|
||||
"token": "brackethighlighter.round"
|
||||
},
|
||||
{
|
||||
"foreground": "#57606a",
|
||||
"token": "brackethighlighter.square"
|
||||
},
|
||||
{
|
||||
"foreground": "#57606a",
|
||||
"token": "brackethighlighter.angle"
|
||||
},
|
||||
{
|
||||
"foreground": "#57606a",
|
||||
"token": "brackethighlighter.quote"
|
||||
},
|
||||
{
|
||||
"foreground": "#82071e",
|
||||
"token": "brackethighlighter.unmatched"
|
||||
},
|
||||
{
|
||||
"foreground": "#0a3069",
|
||||
"fontStyle": "underline",
|
||||
"token": "constant.other.reference.link"
|
||||
},
|
||||
{
|
||||
"foreground": "#0a3069",
|
||||
"fontStyle": "underline",
|
||||
"token": "string.other.link"
|
||||
}
|
||||
],
|
||||
"encodedTokensColors": []
|
||||
}
|
||||
120
src/components/CodeEditor/index.tsx
Normal file
120
src/components/CodeEditor/index.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
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 TrapFocus from "@mui/material/Unstable_TrapFocus";
|
||||
import CircularProgressOptical from "@src/components/CircularProgressOptical";
|
||||
import ResizeBottomRightIcon from "@src/assets/icons/ResizeBottomRight";
|
||||
|
||||
import useMonacoCustomizations, {
|
||||
IUseMonacoCustomizationsProps,
|
||||
} from "./useMonacoCustomizations";
|
||||
import FullScreenButton from "./FullScreenButton";
|
||||
|
||||
export interface ICodeEditorProps
|
||||
extends Partial<EditorProps>,
|
||||
Omit<IUseMonacoCustomizationsProps, "fullScreen"> {
|
||||
value: string;
|
||||
containerProps?: Partial<BoxProps>;
|
||||
|
||||
onValidate?: EditorProps["onValidate"];
|
||||
onValidStatusUpdate?: (result: {
|
||||
isValid: boolean;
|
||||
markers: editor.IMarker[];
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export default function CodeEditor({
|
||||
value,
|
||||
minHeight = 100,
|
||||
disabled,
|
||||
error,
|
||||
containerProps,
|
||||
|
||||
onValidate,
|
||||
onValidStatusUpdate,
|
||||
|
||||
extraLibs,
|
||||
diagnosticsOptions,
|
||||
onUnmount,
|
||||
|
||||
...props
|
||||
}: ICodeEditorProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
// 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 [fullScreen, setFullScreen] = useState(false);
|
||||
|
||||
const { boxSx } = useMonacoCustomizations({
|
||||
minHeight,
|
||||
disabled,
|
||||
error,
|
||||
extraLibs,
|
||||
diagnosticsOptions,
|
||||
onUnmount,
|
||||
fullScreen,
|
||||
});
|
||||
|
||||
const onValidate_: EditorProps["onValidate"] = (markers) => {
|
||||
onValidStatusUpdate?.({ isValid: markers.length <= 0, markers });
|
||||
onValidate?.(markers);
|
||||
};
|
||||
|
||||
return (
|
||||
<TrapFocus open={fullScreen}>
|
||||
<Box
|
||||
sx={[
|
||||
boxSx,
|
||||
...(Array.isArray(containerProps?.sx)
|
||||
? containerProps!.sx
|
||||
: containerProps?.sx
|
||||
? [containerProps.sx]
|
||||
: []),
|
||||
]}
|
||||
style={fullScreen ? { height: "100%" } : {}}
|
||||
>
|
||||
<Editor
|
||||
defaultLanguage="javascript"
|
||||
value={initialEditorValue}
|
||||
loading={<CircularProgressOptical size={20} sx={{ m: 2 }} />}
|
||||
className="editor"
|
||||
{...props}
|
||||
onValidate={onValidate_}
|
||||
options={{
|
||||
readOnly: disabled,
|
||||
fontFamily: theme.typography.fontFamilyMono,
|
||||
rulers: [80],
|
||||
minimap: { enabled: false },
|
||||
lineNumbersMinChars: 4,
|
||||
lineDecorationsWidth: 0,
|
||||
automaticLayout: true,
|
||||
fixedOverflowWidgets: true,
|
||||
tabSize: 2,
|
||||
...props.options,
|
||||
}}
|
||||
/>
|
||||
|
||||
<FullScreenButton
|
||||
onClick={() => setFullScreen((f) => !f)}
|
||||
active={fullScreen}
|
||||
/>
|
||||
|
||||
{!fullScreen && (
|
||||
<ResizeBottomRightIcon
|
||||
aria-label="Resize code editor"
|
||||
color="action"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 1,
|
||||
right: 1,
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</TrapFocus>
|
||||
);
|
||||
}
|
||||
267
src/components/CodeEditor/useMonacoCustomizations.ts
Normal file
267
src/components/CodeEditor/useMonacoCustomizations.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
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 { SystemStyleObject, 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;
|
||||
|
||||
// Internal only
|
||||
fullScreen?: boolean;
|
||||
}
|
||||
|
||||
export default function useMonacoCustomizations({
|
||||
minHeight,
|
||||
disabled,
|
||||
error,
|
||||
|
||||
extraLibs,
|
||||
diagnosticsOptions = {
|
||||
noSemanticValidation: true,
|
||||
noSyntaxValidation: false,
|
||||
},
|
||||
onUnmount,
|
||||
|
||||
fullScreen,
|
||||
}: IUseMonacoCustomizationsProps) {
|
||||
const theme = useTheme();
|
||||
const { tableState } = useProjectContext();
|
||||
|
||||
const monaco = useMonaco();
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
onUnmount?.();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Initialize theme
|
||||
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]);
|
||||
|
||||
// Initialize external libs & TypeScript compiler options
|
||||
useEffect(() => {
|
||||
if (!monaco) return;
|
||||
|
||||
try {
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(firestoreDefs);
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
firebaseAuthDefs
|
||||
);
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
firebaseStorageDefs
|
||||
);
|
||||
// Compiler options
|
||||
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
|
||||
target: monaco.languages.typescript.ScriptTarget.ES2020,
|
||||
allowNonTsExtensions: true,
|
||||
});
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
utilsDefs,
|
||||
"ts:filename/utils.d.ts"
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"An error occurred during initialization of Monaco: ",
|
||||
error
|
||||
);
|
||||
}
|
||||
}, [monaco]);
|
||||
|
||||
// Initialize extraLibs from props
|
||||
useEffect(() => {
|
||||
if (!monaco) return;
|
||||
if (!extraLibs) return;
|
||||
|
||||
try {
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
extraLibs.join("\n"),
|
||||
"ts:filename/extraLibs.d.ts"
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Could not add extraLibs from props: ", error);
|
||||
}
|
||||
}, [monaco, extraLibs]);
|
||||
|
||||
// Set diagnostics options
|
||||
const stringifiedDiagnosticsOptions = JSON.stringify(diagnosticsOptions);
|
||||
useEffect(() => {
|
||||
if (!monaco) return;
|
||||
|
||||
try {
|
||||
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions(
|
||||
JSON.parse(stringifiedDiagnosticsOptions)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Could not set diagnostics options: ", error);
|
||||
}
|
||||
}, [monaco, stringifiedDiagnosticsOptions]);
|
||||
|
||||
// Set row definitions
|
||||
useEffect(() => {
|
||||
if (!monaco) return;
|
||||
|
||||
try {
|
||||
const rowDefinition =
|
||||
Object.keys(tableState?.columns!)
|
||||
.map((columnKey: string) => {
|
||||
const column = tableState?.columns[columnKey];
|
||||
return `static "${columnKey}": ${getFieldProp(
|
||||
"dataType",
|
||||
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("Could not set row definitions: ", error);
|
||||
}
|
||||
}, [monaco, tableState?.columns]);
|
||||
|
||||
let boxSx: SystemStyleObject<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",
|
||||
},
|
||||
};
|
||||
|
||||
if (fullScreen)
|
||||
boxSx = {
|
||||
...boxSx,
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: theme.zIndex.tooltip + 1,
|
||||
m: "0 !important",
|
||||
resize: "none",
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
|
||||
borderRadius: 0,
|
||||
"&::after": { display: "none" },
|
||||
};
|
||||
|
||||
return { boxSx };
|
||||
}
|
||||
50
src/components/CodeEditor/utils.d.ts
vendored
Normal file
50
src/components/CodeEditor/utils.d.ts
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* utility functions
|
||||
*/
|
||||
declare namespace utilFns {
|
||||
/**
|
||||
* Sends out an email through sendGrid
|
||||
*/
|
||||
function sendEmail(msg: {
|
||||
from: string;
|
||||
templateId: string;
|
||||
personalizations: { to: string; dynamic_template_data: any }[];
|
||||
}): void {}
|
||||
|
||||
/**
|
||||
* Gets the secret defined in Google Cloud Secret
|
||||
*/
|
||||
async function getSecret(name: string, v?: string): any {}
|
||||
|
||||
/**
|
||||
* Async version of forEach
|
||||
*/
|
||||
async function asyncForEach(array: any[], callback: Function): void {}
|
||||
|
||||
/**
|
||||
* Generate random ID from numbers and English characters including lowercase and uppercase
|
||||
*/
|
||||
function generateId(): string {}
|
||||
|
||||
/**
|
||||
* Add an item to an array field
|
||||
*/
|
||||
function arrayUnion(val: string): void {}
|
||||
|
||||
/**
|
||||
* Remove an item to an array field
|
||||
*/
|
||||
function arrayRemove(val: string): void {}
|
||||
|
||||
/**
|
||||
* Increment a number field
|
||||
*/
|
||||
function increment(val: number): void {}
|
||||
|
||||
function hasRequiredFields(requiredFields: string[], data: any): boolean {}
|
||||
|
||||
function hasAnyRole(
|
||||
authorizedRoles: string[],
|
||||
context: functions.https.CallableContext
|
||||
): boolean {}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogContentText from "@mui/material/DialogContentText";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import { SlideTransitionMui } from "components/Modal/SlideTransition";
|
||||
import { SlideTransitionMui } from "@src/components/Modal/SlideTransition";
|
||||
|
||||
const useStyles = makeStyles(() =>
|
||||
createStyles({
|
||||
|
||||
@@ -10,13 +10,14 @@ import {
|
||||
Button,
|
||||
} from "@mui/material";
|
||||
|
||||
import { SlideTransitionMui } from "components/Modal/SlideTransition";
|
||||
import { SlideTransitionMui } from "@src/components/Modal/SlideTransition";
|
||||
|
||||
export default function Confirmation({
|
||||
title,
|
||||
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 "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 "components/Loading";
|
||||
import ErrorBoundary from "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;
|
||||
@@ -1,11 +1,13 @@
|
||||
import React from "react";
|
||||
import { Component } from "react";
|
||||
import EmptyState, { IEmptyStateProps } from "./EmptyState";
|
||||
|
||||
import { Button } from "@mui/material";
|
||||
import ReloadIcon from "@mui/icons-material/Refresh";
|
||||
import InlineOpenInNewIcon from "components/InlineOpenInNewIcon";
|
||||
import meta from "../../package.json";
|
||||
class ErrorBoundary extends React.Component<
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
|
||||
import meta from "@root/package.json";
|
||||
|
||||
class ErrorBoundary extends Component<
|
||||
IEmptyStateProps & { render?: (errorMessage: string) => React.ReactNode }
|
||||
> {
|
||||
state = { hasError: false, errorMessage: "" };
|
||||
@@ -25,13 +27,14 @@ class ErrorBoundary extends React.Component<
|
||||
if (this.state.hasError) {
|
||||
if (this.props.render) return this.props.render(this.state.errorMessage);
|
||||
|
||||
return (
|
||||
<EmptyState
|
||||
message="Something went wrong"
|
||||
description={
|
||||
<>
|
||||
<span>{this.state.errorMessage}</span>
|
||||
{this.state.errorMessage.startsWith("Loading chunk") ? (
|
||||
if (this.state.errorMessage.startsWith("Loading chunk"))
|
||||
return (
|
||||
<EmptyState
|
||||
Icon={ReloadIcon}
|
||||
message="New update available"
|
||||
description={
|
||||
<>
|
||||
<span>Reload this page to get the latest update</span>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
@@ -40,19 +43,28 @@ class ErrorBoundary extends React.Component<
|
||||
>
|
||||
Reload
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
href={
|
||||
meta.repository.url.replace(".git", "") +
|
||||
"/issues/new/choose"
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Report issue
|
||||
<InlineOpenInNewIcon />
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
fullScreen
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<EmptyState
|
||||
message="Something went wrong"
|
||||
description={
|
||||
<>
|
||||
<span>{this.state.errorMessage}</span>
|
||||
<Button
|
||||
href={
|
||||
meta.repository.url.replace(".git", "") + "/issues/new/choose"
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Report issue
|
||||
<InlineOpenInNewIcon />
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
fullScreen
|
||||
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
} from "@mui/material";
|
||||
import SearchIcon from "@mui/icons-material/Search";
|
||||
|
||||
import SlideTransition from "components/Modal/SlideTransition";
|
||||
import { APP_BAR_HEIGHT } from "components/Navigation";
|
||||
import SlideTransition from "@src/components/Modal/SlideTransition";
|
||||
import { APP_BAR_HEIGHT } from "@src/components/Navigation";
|
||||
|
||||
export interface IFloatingSearchProps extends Partial<FilledTextFieldProps> {
|
||||
label: string;
|
||||
|
||||
@@ -13,7 +13,7 @@ export default function HelperText(props: IHelperTextProps) {
|
||||
style={{
|
||||
marginTop: theme.spacing(-3),
|
||||
padding: theme.spacing(0, 1.5),
|
||||
...theme.typography.body2,
|
||||
...(theme.typography.body2 as any),
|
||||
color: theme.palette.text.secondary,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -3,10 +3,10 @@ import { Link } from "react-router-dom";
|
||||
import { Typography, Link as MuiLink, Button } from "@mui/material";
|
||||
import SecurityIcon from "@mui/icons-material/SecurityOutlined";
|
||||
|
||||
import EmptyState from "components/EmptyState";
|
||||
import EmptyState from "@src/components/EmptyState";
|
||||
|
||||
import { WIKI_LINKS } from "constants/externalLinks";
|
||||
import routes from "constants/routes";
|
||||
import { WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
import routes from "@src/constants/routes";
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
|
||||
export default function AccessDenied() {
|
||||
|
||||
@@ -12,8 +12,7 @@ export default function HomeWelcomePrompt() {
|
||||
|
||||
width: 320,
|
||||
height: 320,
|
||||
p: 8,
|
||||
pl: 9,
|
||||
p: 5,
|
||||
borderRadius: "50% 50% 0 50%",
|
||||
|
||||
position: "fixed",
|
||||
@@ -21,14 +20,12 @@ export default function HomeWelcomePrompt() {
|
||||
right: 0,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h5" component="h1" gutterBottom>
|
||||
Welcome!
|
||||
<br />
|
||||
Create a table to get started.
|
||||
<Typography variant="overline" component="h1" gutterBottom>
|
||||
Get started
|
||||
</Typography>
|
||||
|
||||
<Typography>
|
||||
Tables connect to your Firestore collections and display their data.
|
||||
<Typography variant="h5" component="p">
|
||||
Create a table from a new or existing Firestore collection
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Zoom>
|
||||
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
CardActions,
|
||||
Button,
|
||||
} from "@mui/material";
|
||||
import GoIcon from "assets/icons/Go";
|
||||
import GoIcon from "@src/assets/icons/Go";
|
||||
|
||||
import { Table } from "contexts/ProjectContext";
|
||||
import { Table } from "@src/contexts/ProjectContext";
|
||||
|
||||
export interface ITableCardProps extends Table {
|
||||
link: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Container, Paper, Box, Grid } from "@mui/material";
|
||||
|
||||
import SectionHeadingSkeleton from "components/SectionHeadingSkeleton";
|
||||
import SectionHeadingSkeleton from "@src/components/SectionHeadingSkeleton";
|
||||
import TableCardSkeleton from "./TableCardSkeleton";
|
||||
|
||||
export default function TableGridSkeleton() {
|
||||
|
||||
@@ -2,11 +2,11 @@ import { TransitionGroup } from "react-transition-group";
|
||||
|
||||
import { Box, Grid, Collapse } from "@mui/material";
|
||||
|
||||
import SectionHeading from "components/SectionHeading";
|
||||
import SectionHeading from "@src/components/SectionHeading";
|
||||
import TableCard from "./TableCard";
|
||||
import SlideTransition from "components/Modal/SlideTransition";
|
||||
import SlideTransition from "@src/components/Modal/SlideTransition";
|
||||
|
||||
import { Table } from "contexts/ProjectContext";
|
||||
import { Table } from "@src/contexts/ProjectContext";
|
||||
|
||||
export interface ITableGridProps {
|
||||
sections: Record<string, Table[]>;
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "@mui/material";
|
||||
import GoIcon from "@mui/icons-material/ArrowForward";
|
||||
|
||||
import { Table } from "contexts/ProjectContext";
|
||||
import { Table } from "@src/contexts/ProjectContext";
|
||||
|
||||
export interface ITableListItemProps extends Table {
|
||||
link: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Container, Box, Paper } from "@mui/material";
|
||||
|
||||
import SectionHeadingSkeleton from "components/SectionHeadingSkeleton";
|
||||
import SectionHeadingSkeleton from "@src/components/SectionHeadingSkeleton";
|
||||
import TableListItemSkeleton from "./TableListItemSkeleton";
|
||||
|
||||
export default function TableGridSkeleton() {
|
||||
|
||||
@@ -2,11 +2,11 @@ import { TransitionGroup } from "react-transition-group";
|
||||
|
||||
import { Box, Paper, Collapse, List } from "@mui/material";
|
||||
|
||||
import SectionHeading from "components/SectionHeading";
|
||||
import SectionHeading from "@src/components/SectionHeading";
|
||||
import TableListItem from "./TableListItem";
|
||||
import SlideTransition from "components/Modal/SlideTransition";
|
||||
import SlideTransition from "@src/components/Modal/SlideTransition";
|
||||
|
||||
import { Table } from "contexts/ProjectContext";
|
||||
import { Table } from "@src/contexts/ProjectContext";
|
||||
|
||||
export interface ITableListProps {
|
||||
sections: Record<string, Table[]>;
|
||||
|
||||
@@ -4,7 +4,7 @@ export const InlineOpenInNewIcon = styled("span")(() => ({
|
||||
position: "relative",
|
||||
width: "1em",
|
||||
height: "1em",
|
||||
marginLeft: "0.25em",
|
||||
marginLeft: "0.25ch",
|
||||
display: "inline-block",
|
||||
verticalAlign: "baseline",
|
||||
|
||||
|
||||
138
src/components/KeyValueInput.tsx
Normal file
138
src/components/KeyValueInput.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import {
|
||||
FormControl,
|
||||
FormLabel,
|
||||
FormGroup,
|
||||
Stack,
|
||||
TextField,
|
||||
Button,
|
||||
} from "@mui/material";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import RemoveIcon from "@mui/icons-material/DeleteOutline";
|
||||
|
||||
export interface IKeyValueInputProps {
|
||||
value: Record<string, string>;
|
||||
onChange: (value: Record<string, string>) => void;
|
||||
label?: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function KeyValueInput({
|
||||
value: valueProp,
|
||||
onChange,
|
||||
label,
|
||||
}: IKeyValueInputProps) {
|
||||
const [value, setValue] = useState(
|
||||
Object.keys(valueProp).length > 0
|
||||
? Object.keys(valueProp)
|
||||
.sort()
|
||||
.map((key) => [key, valueProp[key]])
|
||||
: [["", ""]]
|
||||
);
|
||||
|
||||
const saveValue = (v: typeof value) => {
|
||||
onChange(
|
||||
v.reduce((acc, [key, value]) => {
|
||||
if (key.length > 0) acc[key] = value;
|
||||
return acc;
|
||||
}, {} as Record<string, string>)
|
||||
);
|
||||
};
|
||||
|
||||
const handleAdd = (i: number) => () =>
|
||||
setValue((v) => {
|
||||
const newValue = [...v];
|
||||
newValue.splice(i + 1, 0, ["", ""]);
|
||||
setTimeout(() =>
|
||||
document.getElementById(`keyValue-${i + 1}-key`)?.focus()
|
||||
);
|
||||
return newValue;
|
||||
});
|
||||
const handleRemove = (i: number) => () =>
|
||||
setValue((v) => {
|
||||
const newValue = [...v];
|
||||
newValue.splice(i, 1);
|
||||
saveValue(newValue);
|
||||
return newValue;
|
||||
});
|
||||
|
||||
const handleChange =
|
||||
(i: number, j: number) => (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setValue((v) => {
|
||||
const newValue = [...v];
|
||||
newValue[i][j] = e.target.value;
|
||||
saveValue(newValue);
|
||||
return newValue;
|
||||
});
|
||||
|
||||
return (
|
||||
<FormControl variant="filled" style={{ alignItems: "flex-start" }}>
|
||||
<FormLabel
|
||||
component="legend"
|
||||
sx={{ typography: "button", color: "text.primary", mb: 0.25, ml: 0.25 }}
|
||||
>
|
||||
{label}
|
||||
</FormLabel>
|
||||
|
||||
<FormGroup>
|
||||
{value.map(([propKey, propValue], i) => (
|
||||
<Stack
|
||||
key={i}
|
||||
direction="row"
|
||||
alignItems="flex-start"
|
||||
sx={{ "& + &": { mt: 1 } }}
|
||||
>
|
||||
<TextField
|
||||
id={`keyValue-${i}-key`}
|
||||
aria-label="Key"
|
||||
placeholder="Key"
|
||||
value={propKey}
|
||||
sx={{
|
||||
"& .MuiInputBase-root": {
|
||||
borderTopRightRadius: 0,
|
||||
borderBottomRightRadius: 0,
|
||||
},
|
||||
}}
|
||||
onChange={handleChange(i, 0)}
|
||||
error={propKey.length === 0}
|
||||
helperText={propKey.length === 0 ? "Required" : ""}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
id={`keyValue-${i}-value`}
|
||||
aria-label="Value"
|
||||
placeholder="Value"
|
||||
value={propValue}
|
||||
sx={{
|
||||
ml: "-1px",
|
||||
"& .MuiInputBase-root": {
|
||||
borderTopLeftRadius: 0,
|
||||
borderBottomLeftRadius: 0,
|
||||
},
|
||||
}}
|
||||
onChange={handleChange(i, 1)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={handleRemove(i)}
|
||||
aria-label="Remove row"
|
||||
sx={{ ml: 1, px: "0 !important", minWidth: 32 }}
|
||||
color="error"
|
||||
>
|
||||
<RemoveIcon />
|
||||
</Button>
|
||||
</Stack>
|
||||
))}
|
||||
</FormGroup>
|
||||
|
||||
<Button
|
||||
onClick={handleAdd(value.length - 1)}
|
||||
color="primary"
|
||||
startIcon={<AddIcon />}
|
||||
sx={{ mt: 1 }}
|
||||
>
|
||||
Add row
|
||||
</Button>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,8 @@
|
||||
import { use100vh } from "react-div-100vh";
|
||||
|
||||
import {
|
||||
Fade,
|
||||
Stack,
|
||||
StackProps,
|
||||
CircularProgress,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { Fade, Stack, StackProps, Typography } from "@mui/material";
|
||||
import CircularProgressOptical from "@src/components/CircularProgressOptical";
|
||||
|
||||
interface ILoadingProps extends Partial<StackProps> {
|
||||
message?: string;
|
||||
fullScreen?: boolean;
|
||||
@@ -33,7 +29,7 @@ export default function Loading({
|
||||
...props.style,
|
||||
}}
|
||||
>
|
||||
<CircularProgress />
|
||||
<CircularProgressOptical />
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
component="div"
|
||||
|
||||
@@ -26,9 +26,9 @@ export interface IScrollableDialogContentProps extends DialogContentProps {
|
||||
export default function ScrollableDialogContent({
|
||||
disableTopDivider = false,
|
||||
disableBottomDivider = false,
|
||||
dividerSx,
|
||||
topDividerSx,
|
||||
bottomDividerSx,
|
||||
dividerSx = [],
|
||||
topDividerSx = [],
|
||||
bottomDividerSx = [],
|
||||
...props
|
||||
}: IScrollableDialogContentProps) {
|
||||
const [scrollInfo, setRef] = useScrollInfo();
|
||||
@@ -40,7 +40,10 @@ export default function ScrollableDialogContent({
|
||||
style={{
|
||||
visibility: scrollInfo.y.percentage > 0 ? "visible" : "hidden",
|
||||
}}
|
||||
sx={{ ...dividerSx, ...topDividerSx }}
|
||||
sx={[
|
||||
...(Array.isArray(dividerSx) ? dividerSx : [dividerSx]),
|
||||
...(Array.isArray(topDividerSx) ? topDividerSx : [topDividerSx]),
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -51,7 +54,12 @@ export default function ScrollableDialogContent({
|
||||
style={{
|
||||
visibility: scrollInfo.y.percentage < 1 ? "visible" : "hidden",
|
||||
}}
|
||||
sx={{ ...dividerSx, ...bottomDividerSx }}
|
||||
sx={[
|
||||
...(Array.isArray(dividerSx) ? dividerSx : [dividerSx]),
|
||||
...(Array.isArray(bottomDividerSx)
|
||||
? bottomDividerSx
|
||||
: [bottomDividerSx]),
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -22,8 +22,9 @@ import ScrollableDialogContent, {
|
||||
} from "./ScrollableDialogContent";
|
||||
|
||||
export interface IModalProps extends Partial<Omit<DialogProps, "title">> {
|
||||
onClose: () => void;
|
||||
onClose: (setOpen: React.Dispatch<React.SetStateAction<boolean>>) => void;
|
||||
disableBackdropClick?: boolean;
|
||||
disableEscapeKeyDown?: boolean;
|
||||
|
||||
title: ReactNode;
|
||||
header?: ReactNode;
|
||||
@@ -45,6 +46,7 @@ export interface IModalProps extends Partial<Omit<DialogProps, "title">> {
|
||||
export default function Modal({
|
||||
onClose,
|
||||
disableBackdropClick,
|
||||
disableEscapeKeyDown,
|
||||
title,
|
||||
header,
|
||||
footer,
|
||||
@@ -60,13 +62,22 @@ export default function Modal({
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
|
||||
const [open, setOpen] = useState(true);
|
||||
const handleClose = (_, reason?: string) => {
|
||||
if (disableBackdropClick && reason === "backdropClick") return;
|
||||
const handleClose: NonNullable<DialogProps["onClose"]> = (_, reason) => {
|
||||
if (
|
||||
(disableBackdropClick && reason === "backdropClick") ||
|
||||
(disableEscapeKeyDown && reason === "escapeKeyDown")
|
||||
) {
|
||||
setEmphasizeCloseButton(true);
|
||||
return;
|
||||
}
|
||||
|
||||
setOpen(false);
|
||||
setTimeout(() => onClose(), 300);
|
||||
setEmphasizeCloseButton(false);
|
||||
setTimeout(() => onClose(setOpen), 300);
|
||||
};
|
||||
|
||||
const [emphasizeCloseButton, setEmphasizeCloseButton] = useState(false);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
@@ -99,11 +110,17 @@ export default function Modal({
|
||||
|
||||
{!hideCloseButton && (
|
||||
<IconButton
|
||||
onClick={handleClose}
|
||||
onClick={handleClose as any}
|
||||
aria-label="Close"
|
||||
sx={{
|
||||
m: { xs: 1, sm: 1.5 },
|
||||
ml: { xs: -1, sm: -1 },
|
||||
|
||||
bgcolor: emphasizeCloseButton ? "error.main" : undefined,
|
||||
color: emphasizeCloseButton ? "error.contrastText" : undefined,
|
||||
"&:hover": emphasizeCloseButton
|
||||
? { bgcolor: "error.dark" }
|
||||
: undefined,
|
||||
}}
|
||||
className="dialog-close"
|
||||
>
|
||||
|
||||
@@ -11,9 +11,9 @@ import {
|
||||
} from "@mui/material";
|
||||
import ArrowRightIcon from "@mui/icons-material/ChevronRight";
|
||||
|
||||
import { useProjectContext } from "contexts/ProjectContext";
|
||||
import useRouter from "hooks/useRouter";
|
||||
import routes from "constants/routes";
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
import useRouter from "@src/hooks/useRouter";
|
||||
import routes from "@src/constants/routes";
|
||||
|
||||
export default function Breadcrumbs(props: BreadcrumbsProps) {
|
||||
const { tables, tableState } = useProjectContext();
|
||||
|
||||
@@ -15,18 +15,19 @@ 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 "assets/icons/Backburger";
|
||||
import CloseIcon from "@mui/icons-material/MenuOpen";
|
||||
import PinIcon from "@mui/icons-material/PushPinOutlined";
|
||||
import UnpinIcon from "@mui/icons-material/PushPin";
|
||||
|
||||
import { APP_BAR_HEIGHT } from ".";
|
||||
import Logo from "assets/Logo";
|
||||
import Logo from "@src/assets/Logo";
|
||||
import NavItem from "./NavItem";
|
||||
import NavTableSection from "./NavTableSection";
|
||||
import UpdateCheckBadge from "./UpdateCheckBadge";
|
||||
|
||||
import { useAppContext } from "contexts/AppContext";
|
||||
import { useProjectContext } from "contexts/ProjectContext";
|
||||
import { routes } from "constants/routes";
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
import { routes } from "@src/constants/routes";
|
||||
|
||||
export const NAV_DRAWER_WIDTH = 256;
|
||||
|
||||
@@ -157,6 +158,7 @@ export default function NavDrawer({
|
||||
<ProjectSettingsIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Project Settings" />
|
||||
<UpdateCheckBadge sx={{ mr: 1.5 }} />
|
||||
</NavItem>
|
||||
</li>
|
||||
)}
|
||||
|
||||
@@ -5,8 +5,8 @@ import { List, ListItemText, Collapse } from "@mui/material";
|
||||
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
|
||||
import NavItem from "./NavItem";
|
||||
|
||||
import { Table } from "contexts/ProjectContext";
|
||||
import { routes } from "constants/routes";
|
||||
import { Table } from "@src/contexts/ProjectContext";
|
||||
import { routes } from "@src/constants/routes";
|
||||
|
||||
export interface INavDrawerItemProps {
|
||||
open?: boolean;
|
||||
|
||||
22
src/components/Navigation/UpdateCheckBadge.tsx
Normal file
22
src/components/Navigation/UpdateCheckBadge.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Badge, BadgeProps } from "@mui/material";
|
||||
import useUpdateCheck from "@src/hooks/useUpdateCheck";
|
||||
|
||||
export default function UpdateCheckBadge(props: Partial<BadgeProps>) {
|
||||
const [latestUpdate] = useUpdateCheck();
|
||||
|
||||
if (!latestUpdate.rowy && !latestUpdate.rowyRun) return <>{props.children}</>;
|
||||
|
||||
return (
|
||||
<Badge
|
||||
badgeContent=" "
|
||||
color="error"
|
||||
variant="dot"
|
||||
aria-label="Update available"
|
||||
{...props}
|
||||
sx={{
|
||||
"& .MuiBadge-badge": { bgcolor: "#f00" },
|
||||
...props.sx,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
Typography,
|
||||
ListItemSecondaryAction,
|
||||
Divider,
|
||||
Grow,
|
||||
@@ -17,16 +18,22 @@ import {
|
||||
import AccountCircleIcon from "@mui/icons-material/AccountCircleOutlined";
|
||||
import ArrowRightIcon from "@mui/icons-material/ArrowRight";
|
||||
|
||||
import { useAppContext } from "contexts/AppContext";
|
||||
import routes from "constants/routes";
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
import routes from "@src/constants/routes";
|
||||
|
||||
export default function UserMenu(props: IconButtonProps) {
|
||||
const anchorEl = useRef<HTMLButtonElement>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [themeSubMenu, setThemeSubMenu] = useState<EventTarget | null>(null);
|
||||
|
||||
const { userDoc, theme, themeOverridden, setTheme, setThemeOverridden } =
|
||||
useAppContext();
|
||||
const {
|
||||
userDoc,
|
||||
theme,
|
||||
themeOverridden,
|
||||
setTheme,
|
||||
setThemeOverridden,
|
||||
projectId,
|
||||
} = useAppContext();
|
||||
|
||||
const displayName = userDoc?.state?.doc?.user?.displayName;
|
||||
const avatarUrl = userDoc?.state?.doc?.user?.photoURL;
|
||||
@@ -95,7 +102,13 @@ export default function UserMenu(props: IconButtonProps) {
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={displayName}
|
||||
secondary={email}
|
||||
secondary={
|
||||
<>
|
||||
{email}
|
||||
<br />
|
||||
<Typography variant="caption">Project: {projectId}</Typography>
|
||||
</>
|
||||
}
|
||||
primaryTypographyProps={{ variant: "subtitle1" }}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
@@ -17,11 +17,12 @@ import MenuIcon from "@mui/icons-material/Menu";
|
||||
|
||||
import NavDrawer, { NAV_DRAWER_WIDTH } from "./NavDrawer";
|
||||
import UserMenu from "./UserMenu";
|
||||
import ErrorBoundary from "components/ErrorBoundary";
|
||||
import Loading from "components/Loading";
|
||||
import ErrorBoundary from "@src/components/ErrorBoundary";
|
||||
import Loading from "@src/components/Loading";
|
||||
import UpdateCheckBadge from "./UpdateCheckBadge";
|
||||
|
||||
import { useAppContext } from "contexts/AppContext";
|
||||
import useDocumentTitle from "hooks/useDocumentTitle";
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
import useDocumentTitle from "@src/hooks/useDocumentTitle";
|
||||
|
||||
export const APP_BAR_HEIGHT = 56;
|
||||
|
||||
@@ -43,7 +44,7 @@ export default function Navigation({
|
||||
currentSection,
|
||||
titleTransitionProps,
|
||||
}: INavigationProps) {
|
||||
const { projectId } = useAppContext();
|
||||
const { projectId, userRoles } = useAppContext();
|
||||
useDocumentTitle(projectId, title);
|
||||
|
||||
const [open, setOpen] = useOpenState(false);
|
||||
@@ -114,7 +115,13 @@ export default function Navigation({
|
||||
size="large"
|
||||
edge="start"
|
||||
>
|
||||
<MenuIcon />
|
||||
{userRoles.includes("ADMIN") ? (
|
||||
<UpdateCheckBadge>
|
||||
<MenuIcon />
|
||||
</UpdateCheckBadge>
|
||||
) : (
|
||||
<MenuIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
</Grow>
|
||||
)}
|
||||
|
||||
@@ -44,15 +44,15 @@ const useStyles = makeStyles((theme) =>
|
||||
border: "none",
|
||||
|
||||
backgroundColor: theme.palette.action.input,
|
||||
boxShadow: `0 0 0 1px ${theme.palette.action.inputOutline} inset,
|
||||
0 -1px 0 0 ${theme.palette.text.disabled} inset`,
|
||||
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": {
|
||||
boxShadow: `0 0 0 1px ${theme.palette.action.inputOutline} inset,
|
||||
0 -1px 0 0 ${theme.palette.text.primary} inset`,
|
||||
boxShadow: `0 -1px 0 0 ${theme.palette.text.primary} inset,
|
||||
0 0 0 1px ${theme.palette.action.inputOutline} inset`,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -92,8 +92,8 @@ const useStyles = makeStyles((theme) =>
|
||||
|
||||
focus: {
|
||||
"& .tox.tox-tinymce, & .tox.tox-tinymce:hover": {
|
||||
boxShadow: `0 0 0 1px ${theme.palette.action.inputOutline} inset,
|
||||
0 -2px 0 0 ${theme.palette.primary.main} inset`,
|
||||
boxShadow: `0 -2px 0 0 ${theme.palette.primary.main} inset,
|
||||
0 0 0 1px ${theme.palette.action.inputOutline} inset`,
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -81,6 +81,10 @@ export interface IRichTooltipProps
|
||||
message?: React.ReactNode;
|
||||
dismissButtonText?: React.ReactNode;
|
||||
dismissButtonProps?: Partial<ButtonProps>;
|
||||
defaultOpen?: boolean;
|
||||
onOpen?: () => void;
|
||||
onClose?: () => void;
|
||||
onToggle?: (state: boolean) => void;
|
||||
}
|
||||
|
||||
export default function RichTooltip({
|
||||
@@ -90,14 +94,28 @@ export default function RichTooltip({
|
||||
message,
|
||||
dismissButtonText,
|
||||
dismissButtonProps,
|
||||
defaultOpen,
|
||||
onOpen,
|
||||
onClose,
|
||||
onToggle,
|
||||
...props
|
||||
}: IRichTooltipProps) {
|
||||
const classes = useStyles();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [open, setOpen] = useState(defaultOpen || false);
|
||||
|
||||
const openTooltip = () => setOpen(true);
|
||||
const closeTooltip = () => setOpen(false);
|
||||
const toggleTooltip = () => setOpen((state) => !state);
|
||||
const openTooltip = () => {
|
||||
setOpen(true);
|
||||
if (onOpen) onOpen();
|
||||
};
|
||||
const closeTooltip = () => {
|
||||
setOpen(false);
|
||||
if (onClose) onClose();
|
||||
};
|
||||
const toggleTooltip = () =>
|
||||
setOpen((state) => {
|
||||
if (onToggle) onToggle(!state);
|
||||
return !state;
|
||||
});
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
@@ -145,6 +163,21 @@ export default function RichTooltip({
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
PopperProps={{
|
||||
modifiers: [
|
||||
{
|
||||
name: "preventOverflow",
|
||||
enabled: true,
|
||||
options: {
|
||||
altAxis: true,
|
||||
altBoundary: true,
|
||||
tether: false,
|
||||
rootBoundary: "document",
|
||||
padding: 8,
|
||||
},
|
||||
},
|
||||
],
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{render({ openTooltip, closeTooltip, toggleTooltip })}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { HashLink } from "react-router-hash-link";
|
||||
import { Stack, StackProps, Typography, IconButton } from "@mui/material";
|
||||
import LinkIcon from "@mui/icons-material/Link";
|
||||
|
||||
import { APP_BAR_HEIGHT } from "components/Navigation";
|
||||
import { APP_BAR_HEIGHT } from "@src/components/Navigation";
|
||||
|
||||
export interface ISectionHeadingProps extends Omit<StackProps, "children"> {
|
||||
children: string;
|
||||
|
||||
@@ -1,82 +1,21 @@
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import createPersistedState from "use-persisted-state";
|
||||
import { differenceInDays } from "date-fns";
|
||||
|
||||
import { Grid, Typography, Button, Link, Divider } from "@mui/material";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import GitHubIcon from "@mui/icons-material/GitHub";
|
||||
import DiscordIcon from "assets/icons/Discord";
|
||||
import DiscordIcon from "@src/assets/icons/Discord";
|
||||
import TwitterIcon from "@mui/icons-material/Twitter";
|
||||
|
||||
import Logo from "assets/Logo";
|
||||
import InlineOpenInNewIcon from "components/InlineOpenInNewIcon";
|
||||
import Logo from "@src/assets/Logo";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
|
||||
import { name, version, repository } from "@root/package.json";
|
||||
import { useAppContext } from "contexts/AppContext";
|
||||
import { EXTERNAL_LINKS, WIKI_LINKS } from "constants/externalLinks";
|
||||
|
||||
const useLastCheckedUpdateState = createPersistedState(
|
||||
"__ROWY__LAST_CHECKED_UPDATE"
|
||||
);
|
||||
export const useLatestUpdateState = createPersistedState(
|
||||
"__ROWY__LATEST_UPDATE"
|
||||
);
|
||||
import { name, version } from "@root/package.json";
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
import useUpdateCheck from "@src/hooks/useUpdateCheck";
|
||||
import { EXTERNAL_LINKS, WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
|
||||
export default function About() {
|
||||
const { projectId } = useAppContext();
|
||||
|
||||
const [lastCheckedUpdate, setLastCheckedUpdate] =
|
||||
useLastCheckedUpdateState<string>();
|
||||
const [latestUpdate, setLatestUpdate] = useLatestUpdateState<null | Record<
|
||||
string,
|
||||
any
|
||||
>>(null);
|
||||
|
||||
const [checkState, setCheckState] = useState<null | "LOADING" | "NO_UPDATE">(
|
||||
null
|
||||
);
|
||||
|
||||
const checkForUpdate = useCallback(async () => {
|
||||
setCheckState("LOADING");
|
||||
|
||||
// https://docs.github.com/en/rest/reference/repos#get-the-latest-release
|
||||
const endpoint = repository.url
|
||||
.replace("github.com", "api.github.com/repos")
|
||||
.replace(/.git$/, "/releases/latest");
|
||||
try {
|
||||
const req = await fetch(endpoint, {
|
||||
headers: {
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
},
|
||||
});
|
||||
const res = await req.json();
|
||||
|
||||
if (res.tag_name > "v" + version) {
|
||||
setLatestUpdate(res);
|
||||
setCheckState(null);
|
||||
} else {
|
||||
setCheckState("NO_UPDATE");
|
||||
}
|
||||
|
||||
setLastCheckedUpdate(new Date().toISOString());
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setLatestUpdate(null);
|
||||
setCheckState("NO_UPDATE");
|
||||
}
|
||||
}, [setLastCheckedUpdate, setLatestUpdate]);
|
||||
|
||||
// Check for new updates on page load, if last check was more than 7 days ago
|
||||
useEffect(() => {
|
||||
if (!lastCheckedUpdate) checkForUpdate();
|
||||
else if (differenceInDays(new Date(), new Date(lastCheckedUpdate)) > 7)
|
||||
checkForUpdate();
|
||||
}, [lastCheckedUpdate, checkForUpdate]);
|
||||
|
||||
// Verify latest update is not installed yet
|
||||
useEffect(() => {
|
||||
if (latestUpdate?.tag_name <= "v" + version) setLatestUpdate(null);
|
||||
}, [latestUpdate, setLatestUpdate]);
|
||||
const [latestUpdate, checkForUpdates, loading] = useUpdateCheck();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -132,19 +71,29 @@ export default function About() {
|
||||
<div>
|
||||
<Grid container spacing={1} alignItems="center" direction="row">
|
||||
<Grid item xs>
|
||||
{checkState === "LOADING" ? (
|
||||
{loading ? (
|
||||
<Typography display="block">Checking for updates…</Typography>
|
||||
) : latestUpdate === null ? (
|
||||
) : latestUpdate.rowy === null ? (
|
||||
<Typography display="block">Up to date</Typography>
|
||||
) : (
|
||||
<Typography display="block">
|
||||
<span
|
||||
style={{
|
||||
display: "inline-block",
|
||||
backgroundColor: "#f00",
|
||||
borderRadius: "50%",
|
||||
width: 10,
|
||||
height: 10,
|
||||
marginRight: 4,
|
||||
}}
|
||||
/>
|
||||
Update available:{" "}
|
||||
<Link
|
||||
href={latestUpdate.html_url}
|
||||
href={latestUpdate.rowy.html_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{latestUpdate.tag_name}
|
||||
{latestUpdate.rowy.tag_name}
|
||||
<InlineOpenInNewIcon />
|
||||
</Link>
|
||||
</Typography>
|
||||
@@ -156,11 +105,8 @@ export default function About() {
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
{latestUpdate === null ? (
|
||||
<LoadingButton
|
||||
onClick={checkForUpdate}
|
||||
loading={checkState === "LOADING"}
|
||||
>
|
||||
{latestUpdate.rowy === null ? (
|
||||
<LoadingButton onClick={checkForUpdates} loading={loading}>
|
||||
Check for updates
|
||||
</LoadingButton>
|
||||
) : (
|
||||
|
||||
@@ -4,9 +4,9 @@ import _startCase from "lodash/startCase";
|
||||
|
||||
import MultiSelect from "@rowy/multiselect";
|
||||
import { Typography, Link } from "@mui/material";
|
||||
import InlineOpenInNewIcon from "components/InlineOpenInNewIcon";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
|
||||
import { IProjectSettingsChildProps } from "pages/Settings/ProjectSettings";
|
||||
import { IProjectSettingsChildProps } from "@src/pages/Settings/ProjectSettings";
|
||||
|
||||
export default function Authentication({
|
||||
publicSettings,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { lazy, Suspense, useState } from "react";
|
||||
import { IProjectSettingsChildProps } from "pages/Settings/ProjectSettings";
|
||||
import { IProjectSettingsChildProps } from "@src/pages/Settings/ProjectSettings";
|
||||
import _merge from "lodash/merge";
|
||||
import _unset from "lodash/unset";
|
||||
|
||||
import { FormControlLabel, Checkbox, Collapse } from "@mui/material";
|
||||
import Loading from "components/Loading";
|
||||
import Loading from "@src/components/Loading";
|
||||
|
||||
// prettier-ignore
|
||||
const ThemeColorPicker = lazy(() => import("components/Settings/ThemeColorPicker") /* webpackChunkName: "Settings/ThemeColorPicker" */);
|
||||
const ThemeColorPicker = lazy(() => import("@src/components/Settings/ThemeColorPicker") /* webpackChunkName: "Settings/ThemeColorPicker" */);
|
||||
|
||||
export default function Customization({
|
||||
publicSettings,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import createPersistedState from "use-persisted-state";
|
||||
import { differenceInDays } from "date-fns";
|
||||
import { useState } from "react";
|
||||
|
||||
import {
|
||||
Typography,
|
||||
@@ -11,19 +9,14 @@ import {
|
||||
TextField,
|
||||
} from "@mui/material";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import InlineOpenInNewIcon from "components/InlineOpenInNewIcon";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
|
||||
|
||||
import { IProjectSettingsChildProps } from "pages/Settings/ProjectSettings";
|
||||
import { EXTERNAL_LINKS } from "constants/externalLinks";
|
||||
import { IProjectSettingsChildProps } from "@src/pages/Settings/ProjectSettings";
|
||||
import { EXTERNAL_LINKS, WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
import useUpdateCheck from "@src/hooks/useUpdateCheck";
|
||||
import { name } from "@root/package.json";
|
||||
import { runRoutes } from "constants/runRoutes";
|
||||
|
||||
const useLastCheckedUpdateState = createPersistedState(
|
||||
"__ROWY__RUN_LAST_CHECKED_UPDATE"
|
||||
);
|
||||
export const useLatestUpdateState = createPersistedState(
|
||||
"__ROWY__RUN_LATEST_UPDATE"
|
||||
);
|
||||
import { runRoutes } from "@src/constants/runRoutes";
|
||||
|
||||
export default function RowyRun({
|
||||
settings,
|
||||
@@ -41,7 +34,12 @@ export default function RowyRun({
|
||||
if (!versionReq.version) throw new Error("No version found");
|
||||
else {
|
||||
setVerified(true);
|
||||
setVersion(versionReq.version);
|
||||
|
||||
// If the deployed version is different from the last update check,
|
||||
// check for updates again to clear update
|
||||
if (versionReq.version !== latestUpdate.deployedRowyRun)
|
||||
checkForUpdates();
|
||||
|
||||
updateSettings({ rowyRunUrl: inputRowyRunUrl });
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -50,76 +48,7 @@ export default function RowyRun({
|
||||
}
|
||||
};
|
||||
|
||||
const [lastCheckedUpdate, setLastCheckedUpdate] =
|
||||
useLastCheckedUpdateState<string>();
|
||||
const [latestUpdate, setLatestUpdate] = useLatestUpdateState<null | Record<
|
||||
string,
|
||||
any
|
||||
>>(null);
|
||||
|
||||
const [checkState, setCheckState] = useState<null | "LOADING" | "NO_UPDATE">(
|
||||
null
|
||||
);
|
||||
const [version, setVersion] = useState("");
|
||||
useEffect(() => {
|
||||
fetch(settings.rowyRunUrl + runRoutes.version.path, {
|
||||
method: runRoutes.version.method,
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => setVersion(data.version));
|
||||
}, [settings.rowyRunUrl]);
|
||||
|
||||
const checkForUpdate = useCallback(async () => {
|
||||
setCheckState("LOADING");
|
||||
|
||||
// https://docs.github.com/en/rest/reference/repos#get-the-latest-release
|
||||
const endpoint =
|
||||
EXTERNAL_LINKS.rowyRunGitHub.replace(
|
||||
"github.com",
|
||||
"api.github.com/repos"
|
||||
) + "/releases/latest";
|
||||
try {
|
||||
const versionReq = await fetch(
|
||||
settings.rowyRunUrl + runRoutes.version.path,
|
||||
{ method: runRoutes.version.method }
|
||||
).then((res) => res.json());
|
||||
const version = versionReq.version;
|
||||
setVersion(version);
|
||||
|
||||
const req = await fetch(endpoint, {
|
||||
headers: {
|
||||
Accept: "application/vnd.github.v3+json",
|
||||
},
|
||||
});
|
||||
const res = await req.json();
|
||||
|
||||
if (res.tag_name > "v" + version) {
|
||||
setLatestUpdate(res);
|
||||
setCheckState(null);
|
||||
} else {
|
||||
setCheckState("NO_UPDATE");
|
||||
}
|
||||
|
||||
setLastCheckedUpdate(new Date().toISOString());
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setLatestUpdate(null);
|
||||
setCheckState("NO_UPDATE");
|
||||
}
|
||||
}, [setLastCheckedUpdate, setLatestUpdate, settings.rowyRunUrl]);
|
||||
|
||||
// Check for new updates on page load, if last check was more than 7 days ago
|
||||
useEffect(() => {
|
||||
if (!lastCheckedUpdate) checkForUpdate();
|
||||
else if (differenceInDays(new Date(), new Date(lastCheckedUpdate)) > 7)
|
||||
checkForUpdate();
|
||||
}, [lastCheckedUpdate, checkForUpdate]);
|
||||
|
||||
// Verify latest update is not installed yet
|
||||
useEffect(() => {
|
||||
if (version && latestUpdate?.tag_name <= "v" + version)
|
||||
setLatestUpdate(null);
|
||||
}, [latestUpdate, setLatestUpdate, version]);
|
||||
const [latestUpdate, checkForUpdates, loading] = useUpdateCheck();
|
||||
|
||||
const deployButton = window.location.hostname.includes(
|
||||
EXTERNAL_LINKS.rowyAppHostName
|
||||
@@ -152,11 +81,7 @@ export default function RowyRun({
|
||||
</a>
|
||||
)
|
||||
) : (
|
||||
<Button
|
||||
href={EXTERNAL_LINKS.rowyRunDocs}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button href={WIKI_LINKS.rowyRun} target="_blank" rel="noopener noreferrer">
|
||||
Deploy instructions
|
||||
</Button>
|
||||
);
|
||||
@@ -168,7 +93,7 @@ export default function RowyRun({
|
||||
such as table action scripts, user management, and easy Cloud Function
|
||||
deployment.{" "}
|
||||
<Link
|
||||
href={EXTERNAL_LINKS.rowyRun}
|
||||
href={WIKI_LINKS.rowyRun}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
@@ -183,35 +108,42 @@ export default function RowyRun({
|
||||
<div>
|
||||
<Grid container spacing={1} alignItems="center" direction="row">
|
||||
<Grid item xs>
|
||||
{checkState === "LOADING" ? (
|
||||
{loading ? (
|
||||
<Typography display="block">Checking for updates…</Typography>
|
||||
) : latestUpdate === null ? (
|
||||
) : latestUpdate.rowyRun === null ? (
|
||||
<Typography display="block">Up to date</Typography>
|
||||
) : (
|
||||
<Typography display="block">
|
||||
<span
|
||||
style={{
|
||||
display: "inline-block",
|
||||
backgroundColor: "#f00",
|
||||
borderRadius: "50%",
|
||||
width: 10,
|
||||
height: 10,
|
||||
marginRight: 4,
|
||||
}}
|
||||
/>
|
||||
Update available:{" "}
|
||||
<Link
|
||||
href={latestUpdate.html_url}
|
||||
href={latestUpdate.rowyRun.html_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{latestUpdate.tag_name}
|
||||
{latestUpdate.rowyRun.tag_name}
|
||||
<InlineOpenInNewIcon />
|
||||
</Link>
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Typography display="block" color="textSecondary">
|
||||
{name} Run v{version}
|
||||
{name} Run v{latestUpdate.deployedRowyRun}
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
{latestUpdate === null ? (
|
||||
<LoadingButton
|
||||
onClick={checkForUpdate}
|
||||
loading={checkState === "LOADING"}
|
||||
>
|
||||
{latestUpdate.rowyRun === null ? (
|
||||
<LoadingButton onClick={checkForUpdates} loading={loading}>
|
||||
Check for updates
|
||||
</LoadingButton>
|
||||
) : (
|
||||
@@ -258,11 +190,20 @@ export default function RowyRun({
|
||||
autoComplete="url"
|
||||
error={verified === false}
|
||||
helperText={
|
||||
verified === true
|
||||
? `${name} Run is set up correctly`
|
||||
: verified === false
|
||||
? `${name} Run is not set up correctly`
|
||||
: " "
|
||||
verified === true ? (
|
||||
<>
|
||||
<CheckCircleIcon
|
||||
color="success"
|
||||
style={{ fontSize: "1rem", verticalAlign: "text-top" }}
|
||||
/>
|
||||
|
||||
{name} Run is set up correctly
|
||||
</>
|
||||
) : verified === false ? (
|
||||
`${name} Run is not set up correctly`
|
||||
) : (
|
||||
" "
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Paper, PaperProps } from "@mui/material";
|
||||
|
||||
import SectionHeading from "components/SectionHeading";
|
||||
import SlideTransition from "components/Modal/SlideTransition";
|
||||
import SectionHeading from "@src/components/SectionHeading";
|
||||
import SlideTransition from "@src/components/Modal/SlideTransition";
|
||||
|
||||
export interface ISettingsSectionProps {
|
||||
children: React.ReactNode;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useTheme, Grid, Typography, Stack, Box, Button } from "@mui/material";
|
||||
import PassIcon from "@mui/icons-material/Check";
|
||||
import FailIcon from "@mui/icons-material/Error";
|
||||
|
||||
import { PRIMARY, DARK_PRIMARY } from "theme/colors";
|
||||
import { PRIMARY, DARK_PRIMARY } from "@src/theme/colors";
|
||||
import themes from "theme";
|
||||
|
||||
import { colord, extend } from "colord";
|
||||
|
||||
@@ -12,11 +12,11 @@ import {
|
||||
import AddIcon from "@mui/icons-material/PersonAddOutlined";
|
||||
|
||||
import MultiSelect from "@rowy/multiselect";
|
||||
import Modal from "components/Modal";
|
||||
import Modal from "@src/components/Modal";
|
||||
|
||||
import { useProjectContext } from "contexts/ProjectContext";
|
||||
import routes from "constants/routes";
|
||||
import { runRoutes } from "constants/runRoutes";
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
import routes from "@src/constants/routes";
|
||||
import { runRoutes } from "@src/constants/runRoutes";
|
||||
|
||||
export default function InviteUser() {
|
||||
const { roles: projectRoles, rowyRun } = useProjectContext();
|
||||
|
||||
@@ -10,16 +10,16 @@ import {
|
||||
IconButton,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import CopyIcon from "assets/icons/Copy";
|
||||
import CopyIcon from "@src/assets/icons/Copy";
|
||||
import DeleteIcon from "@mui/icons-material/DeleteOutlined";
|
||||
|
||||
import MultiSelect from "@rowy/multiselect";
|
||||
import { User } from "pages/Settings/UserManagement";
|
||||
import { User } from "@src/pages/Settings/UserManagement";
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
import { runRoutes } from "constants/runRoutes";
|
||||
import { runRoutes } from "@src/constants/runRoutes";
|
||||
import { db } from "@src/firebase";
|
||||
import { USERS } from "config/dbPaths";
|
||||
import { useConfirmation } from "components/ConfirmationDialog";
|
||||
import { USERS } from "@src/config/dbPaths";
|
||||
import { useConfirmation } from "@src/components/ConfirmationDialog";
|
||||
|
||||
export default function UserItem({ id, user, roles: rolesProp }: User) {
|
||||
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { IUserSettingsChildProps } from "pages/Settings/UserSettings";
|
||||
import { IUserSettingsChildProps } from "@src/pages/Settings/UserSettings";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { Grid, Avatar, Typography, Button } from "@mui/material";
|
||||
|
||||
import routes from "constants/routes";
|
||||
import routes from "@src/constants/routes";
|
||||
|
||||
export default function Account({ settings }: IUserSettingsChildProps) {
|
||||
return (
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { lazy, Suspense, useState } from "react";
|
||||
import { IUserSettingsChildProps } from "pages/Settings/UserSettings";
|
||||
import { IUserSettingsChildProps } from "@src/pages/Settings/UserSettings";
|
||||
import _merge from "lodash/merge";
|
||||
import _unset from "lodash/unset";
|
||||
|
||||
import { FormControlLabel, Checkbox, Collapse } from "@mui/material";
|
||||
import Loading from "components/Loading";
|
||||
import Loading from "@src/components/Loading";
|
||||
|
||||
// prettier-ignore
|
||||
const ThemeColorPicker = lazy(() => import("components/Settings/ThemeColorPicker") /* webpackChunkName: "Settings/ThemeColorPicker" */);
|
||||
const ThemeColorPicker = lazy(() => import("@src/components/Settings/ThemeColorPicker") /* webpackChunkName: "Settings/ThemeColorPicker" */);
|
||||
|
||||
export default function Personalization({
|
||||
settings,
|
||||
@@ -29,23 +29,6 @@ export default function Personalization({
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={settings.theme?.dark?.palette?.darker}
|
||||
onChange={(e) => {
|
||||
updateSettings({
|
||||
theme: _merge(settings.theme, {
|
||||
dark: { palette: { darker: e.target.checked } },
|
||||
}),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="Darker dark theme"
|
||||
sx={{ my: -10 / 8 }}
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
@@ -62,7 +45,7 @@ export default function Personalization({
|
||||
/>
|
||||
}
|
||||
label="Customize theme colors"
|
||||
style={{ marginLeft: -11, marginBottom: -10 }}
|
||||
style={{ marginLeft: -11, marginBottom: -10, marginTop: -10 }}
|
||||
/>
|
||||
|
||||
<Collapse in={customizedThemeColor} style={{ marginTop: 0 }}>
|
||||
|
||||
@@ -1,39 +1,68 @@
|
||||
import { IUserSettingsChildProps } from "@src/pages/Settings/UserSettings";
|
||||
import _merge from "lodash/merge";
|
||||
|
||||
import {
|
||||
FormControl,
|
||||
RadioGroup,
|
||||
FormControlLabel,
|
||||
Radio,
|
||||
Divider,
|
||||
Checkbox,
|
||||
} from "@mui/material";
|
||||
|
||||
import { useAppContext } from "contexts/AppContext";
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
|
||||
export default function Theme() {
|
||||
export default function Theme({
|
||||
settings,
|
||||
updateSettings,
|
||||
}: IUserSettingsChildProps) {
|
||||
const { theme, themeOverridden, setTheme, setThemeOverridden } =
|
||||
useAppContext();
|
||||
|
||||
return (
|
||||
<FormControl component="fieldset" variant="standard" sx={{ my: -10 / 8 }}>
|
||||
<legend style={{ fontSize: 0 }}>Theme</legend>
|
||||
<>
|
||||
<FormControl component="fieldset" variant="standard" sx={{ my: -10 / 8 }}>
|
||||
<legend style={{ fontSize: 0 }}>Theme</legend>
|
||||
|
||||
<RadioGroup
|
||||
value={themeOverridden ? theme : "system"}
|
||||
onChange={(e) => {
|
||||
if (e.target.value === "system") {
|
||||
setThemeOverridden(false);
|
||||
} else {
|
||||
setTheme(e.target.value as typeof theme);
|
||||
setThemeOverridden(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FormControlLabel
|
||||
control={<Radio />}
|
||||
value="system"
|
||||
label="Match system theme"
|
||||
/>
|
||||
<FormControlLabel control={<Radio />} value="light" label="Light" />
|
||||
<FormControlLabel control={<Radio />} value="dark" label="Dark" />
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<RadioGroup
|
||||
value={themeOverridden ? theme : "system"}
|
||||
onChange={(e) => {
|
||||
if (e.target.value === "system") {
|
||||
setThemeOverridden(false);
|
||||
} else {
|
||||
setTheme(e.target.value as typeof theme);
|
||||
setThemeOverridden(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<FormControlLabel
|
||||
control={<Radio />}
|
||||
value="system"
|
||||
label="Match system theme"
|
||||
/>
|
||||
<FormControlLabel control={<Radio />} value="light" label="Light" />
|
||||
<FormControlLabel control={<Radio />} value="dark" label="Dark" />
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
|
||||
<Divider />
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={settings.theme?.dark?.palette?.darker}
|
||||
onChange={(e) => {
|
||||
updateSettings({
|
||||
theme: _merge(settings.theme, {
|
||||
dark: { palette: { darker: e.target.checked } },
|
||||
}),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="Darker dark theme"
|
||||
style={{ marginLeft: -11, marginBottom: -10, marginTop: 13 }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Stack, CircularProgress, Typography } from "@mui/material";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import CheckIcon from "@mui/icons-material/Check";
|
||||
import ArrowIcon from "@mui/icons-material/ArrowForward";
|
||||
import CircularProgressOptical from "@src/components/CircularProgressOptical";
|
||||
|
||||
export interface ISetupItemProps {
|
||||
status: "complete" | "loading" | "incomplete";
|
||||
@@ -25,17 +26,12 @@ export default function SetupItem({
|
||||
{status === "complete" ? (
|
||||
<CheckIcon aria-label="Item complete" color="action" />
|
||||
) : status === "loading" ? (
|
||||
<CircularProgress
|
||||
id="progress"
|
||||
size={20}
|
||||
thickness={5}
|
||||
sx={{ m: 0.25 }}
|
||||
/>
|
||||
<CircularProgressOptical id="progress" size={20} sx={{ m: 0.25 }} />
|
||||
) : (
|
||||
<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}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ISetupStepBodyProps } from "pages/Setup";
|
||||
import { ISetupStepBodyProps } from "@src/pages/Setup";
|
||||
|
||||
import { FormControlLabel, Checkbox, Typography, Link } from "@mui/material";
|
||||
|
||||
import { useAppContext } from "contexts/AppContext";
|
||||
import { EXTERNAL_LINKS } from "constants/externalLinks";
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
import { EXTERNAL_LINKS } from "@src/constants/externalLinks";
|
||||
|
||||
export default function Step0Welcome({
|
||||
completion,
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useLocation, useHistory } from "react-router-dom";
|
||||
import queryString from "query-string";
|
||||
import { ISetupStepBodyProps } from "pages/Setup";
|
||||
import { ISetupStepBodyProps } from "@src/pages/Setup";
|
||||
|
||||
import { Button, Typography, Stack, TextField } from "@mui/material";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import InlineOpenInNewIcon from "components/InlineOpenInNewIcon";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
|
||||
import SetupItem from "./SetupItem";
|
||||
|
||||
import { name } from "@root/package.json";
|
||||
import { rowyRun } from "utils/rowyRun";
|
||||
import { runRoutes } from "constants/runRoutes";
|
||||
import { EXTERNAL_LINKS } from "constants/externalLinks";
|
||||
import { rowyRun } from "@src/utils/rowyRun";
|
||||
import { runRoutes } from "@src/constants/runRoutes";
|
||||
import { EXTERNAL_LINKS, WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
|
||||
export default function Step1RowyRun({
|
||||
completion,
|
||||
@@ -79,11 +79,7 @@ export default function Step1RowyRun({
|
||||
/>
|
||||
</a>
|
||||
) : (
|
||||
<Button
|
||||
href={EXTERNAL_LINKS.rowyRunDocs}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button href={WIKI_LINKS.rowyRun} target="_blank" rel="noopener noreferrer">
|
||||
Deploy instructions
|
||||
<InlineOpenInNewIcon />
|
||||
</Button>
|
||||
@@ -178,7 +174,7 @@ export default function Step1RowyRun({
|
||||
}
|
||||
|
||||
export const checkRowyRun = async (
|
||||
rowyRunUrl: string,
|
||||
serviceUrl: string,
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
const result = {
|
||||
@@ -188,7 +184,7 @@ export const checkRowyRun = async (
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await rowyRun({ rowyRunUrl, route: runRoutes.version, signal });
|
||||
const res = await rowyRun({ serviceUrl, route: runRoutes.version, signal });
|
||||
if (!res.version) return result;
|
||||
|
||||
result.isValidRowyRunUrl = true;
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { ISetupStepBodyProps } from "pages/Setup";
|
||||
import { ISetupStepBodyProps } from "@src/pages/Setup";
|
||||
|
||||
import { Typography, Link, Stack } from "@mui/material";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import InlineOpenInNewIcon from "components/InlineOpenInNewIcon";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
|
||||
import SetupItem from "./SetupItem";
|
||||
|
||||
import { name } from "@root/package.json";
|
||||
import { useAppContext } from "contexts/AppContext";
|
||||
import { rowyRun } from "utils/rowyRun";
|
||||
import { runRoutes } from "constants/runRoutes";
|
||||
import { WIKI_LINKS } from "constants/externalLinks";
|
||||
import screenRecording from "assets/service-account.mp4";
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
import { rowyRun } from "@src/utils/rowyRun";
|
||||
import { runRoutes } from "@src/constants/runRoutes";
|
||||
import { WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
import screenRecording from "@src/assets/service-account.mp4";
|
||||
|
||||
export default function Step2ServiceAccount({
|
||||
rowyRunUrl,
|
||||
@@ -20,7 +20,7 @@ export default function Step2ServiceAccount({
|
||||
setCompletion,
|
||||
}: ISetupStepBodyProps) {
|
||||
const [hasAllRoles, setHasAllRoles] = useState(completion.serviceAccount);
|
||||
const [roles, setRoles] = useState<Record<string, any>>({});
|
||||
// const [roles, setRoles] = useState<Record<string, any>>({});
|
||||
const [verificationStatus, setVerificationStatus] = useState<
|
||||
"IDLE" | "LOADING" | "FAIL"
|
||||
>("IDLE");
|
||||
@@ -40,7 +40,7 @@ export default function Step2ServiceAccount({
|
||||
setVerificationStatus("LOADING");
|
||||
try {
|
||||
const result = await checkServiceAccount(rowyRunUrl);
|
||||
setRoles(result);
|
||||
// setRoles(result);
|
||||
if (result.hasAllRoles) {
|
||||
setVerificationStatus("IDLE");
|
||||
setHasAllRoles(true);
|
||||
@@ -150,12 +150,12 @@ export default function Step2ServiceAccount({
|
||||
}
|
||||
|
||||
export const checkServiceAccount = async (
|
||||
rowyRunUrl: string,
|
||||
serviceUrl: string,
|
||||
signal?: AbortSignal
|
||||
) => {
|
||||
try {
|
||||
const res = await rowyRun({
|
||||
rowyRunUrl,
|
||||
serviceUrl,
|
||||
route: runRoutes.serviceAccountAccess,
|
||||
signal,
|
||||
});
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { ISetupStepBodyProps } from "pages/Setup";
|
||||
import { ISetupStepBodyProps } from "@src/pages/Setup";
|
||||
|
||||
import { Typography, Stack, Button, IconButton } from "@mui/material";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import InlineOpenInNewIcon from "components/InlineOpenInNewIcon";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
|
||||
import SetupItem from "./SetupItem";
|
||||
import SignInWithGoogle from "./SignInWithGoogle";
|
||||
|
||||
import { useAppContext } from "contexts/AppContext";
|
||||
import { rowyRun } from "utils/rowyRun";
|
||||
import { runRoutes } from "constants/runRoutes";
|
||||
import CopyIcon from "assets/icons/Copy";
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
import { rowyRun } from "@src/utils/rowyRun";
|
||||
import { runRoutes } from "@src/constants/runRoutes";
|
||||
import CopyIcon from "@src/assets/icons/Copy";
|
||||
|
||||
export default function Step3ProjectOwner({
|
||||
rowyRunUrl,
|
||||
@@ -22,7 +22,7 @@ export default function Step3ProjectOwner({
|
||||
|
||||
const [email, setEmail] = useState("");
|
||||
useEffect(() => {
|
||||
rowyRun({ rowyRunUrl, route: runRoutes.projectOwner })
|
||||
rowyRun({ serviceUrl: rowyRunUrl, route: runRoutes.projectOwner })
|
||||
.then((data) => setEmail(data.email))
|
||||
.catch((e: any) => {
|
||||
console.error(e);
|
||||
@@ -44,7 +44,7 @@ export default function Step3ProjectOwner({
|
||||
const authToken = await getAuthToken();
|
||||
const res = await rowyRun({
|
||||
route: runRoutes.setOwnerRoles,
|
||||
rowyRunUrl,
|
||||
serviceUrl: rowyRunUrl,
|
||||
authToken,
|
||||
});
|
||||
|
||||
@@ -186,7 +186,7 @@ export const checkProjectOwner = async (
|
||||
|
||||
try {
|
||||
const res = await rowyRun({
|
||||
rowyRunUrl,
|
||||
serviceUrl: rowyRunUrl,
|
||||
route: runRoutes.projectOwner,
|
||||
signal,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { ISetupStepBodyProps } from "pages/Setup";
|
||||
import { ISetupStepBodyProps } from "@src/pages/Setup";
|
||||
|
||||
import {
|
||||
Typography,
|
||||
@@ -7,20 +7,28 @@ import {
|
||||
Checkbox,
|
||||
Button,
|
||||
Link,
|
||||
TextField,
|
||||
Grid,
|
||||
} from "@mui/material";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import CopyIcon from "assets/icons/Copy";
|
||||
import InlineOpenInNewIcon from "components/InlineOpenInNewIcon";
|
||||
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 "contexts/AppContext";
|
||||
import { CONFIG } from "config/dbPaths";
|
||||
import { requiredRules, adminRules, utilFns } from "config/firestoreRules";
|
||||
import { rowyRun } from "utils/rowyRun";
|
||||
import { runRoutes } from "constants/runRoutes";
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
import { CONFIG } from "@src/config/dbPaths";
|
||||
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";
|
||||
|
||||
export default function Step4Rules({
|
||||
rowyRunUrl,
|
||||
@@ -28,6 +36,7 @@ export default function Step4Rules({
|
||||
setCompletion,
|
||||
}: ISetupStepBodyProps) {
|
||||
const { projectId, getAuthToken } = useAppContext();
|
||||
// const { requestConfirmation } = useConfirmation();
|
||||
|
||||
const [hasRules, setHasRules] = useState(completion.rules);
|
||||
const [adminRule, setAdminRule] = useState(true);
|
||||
@@ -42,7 +51,7 @@ export default function Step4Rules({
|
||||
getAuthToken(true)
|
||||
.then((authToken) =>
|
||||
rowyRun({
|
||||
rowyRunUrl,
|
||||
serviceUrl: rowyRunUrl,
|
||||
route: runRoutes.firestoreRules,
|
||||
authToken,
|
||||
})
|
||||
@@ -50,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;
|
||||
@@ -61,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 () => {
|
||||
@@ -77,26 +99,39 @@ export default function Step4Rules({
|
||||
if (!authToken) throw new Error("Failed to generate auth token");
|
||||
|
||||
const res = await rowyRun({
|
||||
rowyRunUrl,
|
||||
serviceUrl: rowyRunUrl,
|
||||
route: runRoutes.setFirestoreRules,
|
||||
authToken,
|
||||
body: { ruleset: newRules },
|
||||
});
|
||||
if (!res.success) throw new Error(res.message);
|
||||
|
||||
const isSuccessful = await checkRules(rowyRunUrl, authToken);
|
||||
if (isSuccessful) {
|
||||
setCompletion((c) => ({ ...c, rules: true }));
|
||||
setHasRules(true);
|
||||
}
|
||||
|
||||
setRulesStatus("IDLE");
|
||||
setRulesStatus("");
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
setRulesStatus(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Typography variant="inherit">
|
||||
@@ -126,95 +161,105 @@ 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>
|
||||
Please verify the new rules first.
|
||||
</Typography>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={setRules}
|
||||
loading={rulesStatus === "LOADING"}
|
||||
>
|
||||
Set Firestore Rules
|
||||
</LoadingButton>
|
||||
{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 && (
|
||||
{!hasRules && showManualMode && (
|
||||
<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:
|
||||
</>
|
||||
}
|
||||
title="Alternatively, you can add these rules in the Firebase Console."
|
||||
>
|
||||
<TextField
|
||||
id="new-rules"
|
||||
label="New rules"
|
||||
value={newRules}
|
||||
onChange={(e) => setNewRules(e.target.value)}
|
||||
multiline
|
||||
rows={5}
|
||||
fullWidth
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="pre"
|
||||
sx={{
|
||||
"& .MuiInputBase-input": {
|
||||
fontFamily: "mono",
|
||||
letterSpacing: 0,
|
||||
resize: "vertical",
|
||||
},
|
||||
width: "100%",
|
||||
height: 400,
|
||||
resize: "both",
|
||||
overflow: "auto",
|
||||
|
||||
"& .comment": { color: "info.dark" },
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: rules.replace(
|
||||
/(\/\/.*$)/gm,
|
||||
`<span class="comment">$1</span>`
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<Typography
|
||||
variant="inherit"
|
||||
color={
|
||||
rulesStatus !== "LOADING" && rulesStatus ? "error" : undefined
|
||||
}
|
||||
>
|
||||
Please check the generated rules first.
|
||||
</Typography>
|
||||
<div>
|
||||
<Grid container spacing={1}>
|
||||
<Grid item>
|
||||
<Button
|
||||
startIcon={<CopyIcon />}
|
||||
onClick={() => navigator.clipboard.writeText(rules)}
|
||||
>
|
||||
Copy to clipboard
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={setRules}
|
||||
loading={rulesStatus === "LOADING"}
|
||||
>
|
||||
Set Firestore Rules
|
||||
</LoadingButton>
|
||||
|
||||
{rulesStatus !== "LOADING" && typeof rulesStatus === "string" && (
|
||||
<Typography variant="caption" color="error">
|
||||
{rulesStatus}
|
||||
</Typography>
|
||||
)}
|
||||
<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>
|
||||
</SetupItem>
|
||||
)}
|
||||
</>
|
||||
@@ -230,7 +275,7 @@ export const checkRules = async (
|
||||
|
||||
try {
|
||||
const res = await rowyRun({
|
||||
rowyRunUrl,
|
||||
serviceUrl: rowyRunUrl,
|
||||
route: runRoutes.firestoreRules,
|
||||
authToken,
|
||||
signal,
|
||||
@@ -246,7 +291,6 @@ export const checkRules = async (
|
||||
sanitizedRules.includes(
|
||||
utilFns.replace(/\s{2,}/g, " ").replace(/\n/g, " ")
|
||||
);
|
||||
|
||||
return hasRules;
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { ISetupStepBodyProps } from "pages/Setup";
|
||||
import { ISetupStepBodyProps } from "@src/pages/Setup";
|
||||
|
||||
import { Typography, Button } from "@mui/material";
|
||||
import LoadingButton from "@mui/lab/LoadingButton";
|
||||
@@ -7,10 +7,10 @@ import LoadingButton from "@mui/lab/LoadingButton";
|
||||
import SetupItem from "./SetupItem";
|
||||
|
||||
import { name } from "@root/package.json";
|
||||
import { useAppContext } from "contexts/AppContext";
|
||||
import { CONFIG } from "config/dbPaths";
|
||||
import { rowyRun } from "utils/rowyRun";
|
||||
import { runRoutes } from "constants/runRoutes";
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
import { CONFIG } from "@src/config/dbPaths";
|
||||
import { rowyRun } from "@src/utils/rowyRun";
|
||||
import { runRoutes } from "@src/constants/runRoutes";
|
||||
|
||||
export default function Step5Migrate({
|
||||
rowyRunUrl,
|
||||
@@ -30,7 +30,7 @@ export default function Step5Migrate({
|
||||
|
||||
const res = await rowyRun({
|
||||
route: runRoutes.migrateFT2Rowy,
|
||||
rowyRunUrl,
|
||||
serviceUrl: rowyRunUrl,
|
||||
authToken,
|
||||
});
|
||||
if (!res.success) throw new Error(res.message);
|
||||
@@ -100,7 +100,7 @@ export const checkMigrate = async (
|
||||
|
||||
try {
|
||||
const res = await rowyRun({
|
||||
rowyRunUrl,
|
||||
serviceUrl: rowyRunUrl,
|
||||
route: runRoutes.checkFT2Rowy,
|
||||
authToken,
|
||||
signal,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSnackbar } from "notistack";
|
||||
|
||||
import { Typography, Stack, RadioGroup, Radio } from "@mui/material";
|
||||
@@ -9,10 +9,14 @@ import ThumbDownOffIcon from "@mui/icons-material/ThumbDownOffAlt";
|
||||
|
||||
import { name } from "@root/package.json";
|
||||
import { analytics } from "analytics";
|
||||
import { db } from "@src/firebase";
|
||||
|
||||
export default function Step6Finish() {
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
useEffect(() => {
|
||||
db.doc("_rowy_/settings").update({ setupCompleted: true });
|
||||
}, []);
|
||||
const [rating, setRating] = useState<"up" | "down" | undefined>();
|
||||
|
||||
const handleRate = (e) => {
|
||||
|
||||
@@ -7,8 +7,8 @@ import _pickBy from "lodash/pickBy";
|
||||
import { Control, UseFormReturn, useWatch } from "react-hook-form";
|
||||
import { Values } from "./utils";
|
||||
|
||||
import { useProjectContext } from "contexts/ProjectContext";
|
||||
import { TableState } from "hooks/useTable";
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
import { TableState } from "@src/hooks/useTable";
|
||||
|
||||
export interface IAutosaveProps {
|
||||
control: Control;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { Suspense } from "react";
|
||||
|
||||
import { Stack, InputLabel, Typography, IconButton } from "@mui/material";
|
||||
import DocumentPathIcon from "assets/icons/DocumentPath";
|
||||
import DocumentPathIcon from "@src/assets/icons/DocumentPath";
|
||||
import LaunchIcon from "@mui/icons-material/Launch";
|
||||
import LockIcon from "@mui/icons-material/LockOutlined";
|
||||
|
||||
import ErrorBoundary from "components/ErrorBoundary";
|
||||
import ErrorBoundary from "@src/components/ErrorBoundary";
|
||||
import FieldSkeleton from "./FieldSkeleton";
|
||||
|
||||
import { FieldType } from "constants/fields";
|
||||
import { getFieldProp } from "components/fields";
|
||||
import { useAppContext } from "contexts/AppContext";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { getFieldProp } from "@src/components/fields";
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
|
||||
export interface IFieldWrapperProps {
|
||||
children?: React.ReactNode;
|
||||
|
||||
@@ -2,20 +2,24 @@ import { createElement, useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import _sortBy from "lodash/sortBy";
|
||||
import _isEmpty from "lodash/isEmpty";
|
||||
import _mapValues from "lodash/mapValues";
|
||||
import firebase from "firebase/app";
|
||||
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 "components/fields";
|
||||
import { IFieldConfig } from "components/fields/types";
|
||||
import { getFieldProp } from "@src/components/fields";
|
||||
import { IFieldConfig } from "@src/components/fields/types";
|
||||
import Autosave from "./Autosave";
|
||||
import Reset from "./Reset";
|
||||
import FieldWrapper from "./FieldWrapper";
|
||||
|
||||
import { useAppContext } from "contexts/AppContext";
|
||||
import { useProjectContext } from "contexts/ProjectContext";
|
||||
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;
|
||||
@@ -24,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
|
||||
@@ -38,21 +48,15 @@ export default function Form({ values }: IFormProps) {
|
||||
{}
|
||||
);
|
||||
const { ref: docRef, ...rowValues } = values;
|
||||
const safeRowValues = _mapValues(rowValues, (v) => {
|
||||
// If react-hook-form receives a Firestore document reference, it tries to
|
||||
// clone firebase.firestore and exceeds maximum call stack size.
|
||||
if (firebase.firestore.DocumentReference.prototype.isPrototypeOf(v))
|
||||
return v.path;
|
||||
return v;
|
||||
});
|
||||
const safeRowValues = sanitizeFirestoreRefs(rowValues);
|
||||
const defaultValues = { ...initialValues, ...safeRowValues };
|
||||
|
||||
const methods = useForm({ mode: "onBlur", defaultValues });
|
||||
const { control, reset, formState, getValues } = methods;
|
||||
const { dirtyFields } = formState;
|
||||
|
||||
const column = sideDrawerRef?.current?.cell?.column;
|
||||
useEffect(() => {
|
||||
const column = sideDrawerRef?.current?.cell?.column;
|
||||
if (!column) return;
|
||||
|
||||
const labelElem = document.getElementById(
|
||||
@@ -65,7 +69,7 @@ export default function Form({ values }: IFormProps) {
|
||||
if (labelElem) labelElem.scrollIntoView({ behavior: "smooth" });
|
||||
if (fieldElem) fieldElem.focus({ preventScroll: true });
|
||||
}, 200);
|
||||
}, [sideDrawerRef?.current]);
|
||||
}, [column]);
|
||||
|
||||
return (
|
||||
<form>
|
||||
@@ -128,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>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Control } from "react-hook-form";
|
||||
import { makeStyles, createStyles } from "@mui/styles";
|
||||
import { FieldType } from "constants/fields";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { colord } from "colord";
|
||||
|
||||
export interface IFieldProps {
|
||||
|
||||
@@ -10,11 +10,11 @@ import ChevronUpIcon from "@mui/icons-material/KeyboardArrowUp";
|
||||
import ChevronDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
|
||||
import Form from "./Form";
|
||||
import ErrorBoundary from "components/ErrorBoundary";
|
||||
import ErrorBoundary from "@src/components/ErrorBoundary";
|
||||
|
||||
import { useStyles } from "./useStyles";
|
||||
import { useProjectContext } from "contexts/ProjectContext";
|
||||
import useDoc from "hooks/useDoc";
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
import useDoc from "@src/hooks/useDoc";
|
||||
import { analytics } from "@src/analytics";
|
||||
|
||||
export const DRAWER_WIDTH = 512;
|
||||
@@ -46,18 +46,10 @@ export default function SideDrawer() {
|
||||
setCell!((cell) => ({ column: cell!.column, row }));
|
||||
|
||||
const idx = tableState?.columns[cell!.column]?.index;
|
||||
console.log(
|
||||
"selectCell",
|
||||
{ rowIdx: cell!.row, idx },
|
||||
dataGridRef?.current?.selectCell
|
||||
);
|
||||
dataGridRef?.current?.selectCell({ rowIdx: row, idx }, false);
|
||||
};
|
||||
|
||||
const [urlDocState, dispatchUrlDoc] = useDoc({});
|
||||
// useEffect(() => {
|
||||
// if (urlDocState.doc) setOpen(true);
|
||||
// }, [urlDocState]);
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(false);
|
||||
@@ -78,12 +70,11 @@ 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
|
||||
)}`
|
||||
);
|
||||
// console.log(tableState?.tablePath, tableState?.rows[cell.row].id);
|
||||
if (urlDocState.doc) {
|
||||
urlDocState.unsubscribe();
|
||||
dispatchUrlDoc({ path: "", doc: null });
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { makeStyles, createStyles } from "@mui/styles";
|
||||
import { DRAWER_WIDTH, DRAWER_COLLAPSED_WIDTH } from "./index";
|
||||
import { APP_BAR_HEIGHT } from "components/Navigation";
|
||||
import { TABLE_HEADER_HEIGHT } from "components/Table/TableHeader";
|
||||
import { APP_BAR_HEIGHT } from "@src/components/Navigation";
|
||||
import { TABLE_HEADER_HEIGHT } from "@src/components/TableHeader";
|
||||
|
||||
export const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useState, Dispatch, SetStateAction, MutableRefObject } from "react";
|
||||
import { Stack, CircularProgress } from "@mui/material";
|
||||
|
||||
import { Stack } from "@mui/material";
|
||||
import CircularProgressOptical from "@src/components/CircularProgressOptical";
|
||||
|
||||
export interface ISnackbarProgressRef {
|
||||
setProgress: Dispatch<SetStateAction<number>>;
|
||||
@@ -31,12 +33,11 @@ export default function SnackbarProgress({
|
||||
{progress}/{target}
|
||||
</span>
|
||||
|
||||
<CircularProgress
|
||||
<CircularProgressOptical
|
||||
value={(progress / target) * 100}
|
||||
variant="determinate"
|
||||
size={24}
|
||||
color="inherit"
|
||||
thickness={4}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
92
src/components/SteppedAccordion.tsx
Normal file
92
src/components/SteppedAccordion.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import {
|
||||
Stepper,
|
||||
StepperProps,
|
||||
Step,
|
||||
StepProps,
|
||||
StepButton,
|
||||
StepButtonProps,
|
||||
Typography,
|
||||
StepContent,
|
||||
StepContentProps,
|
||||
} from "@mui/material";
|
||||
import ExpandIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
|
||||
export interface ISteppedAccordionProps extends Partial<StepperProps> {
|
||||
steps: {
|
||||
id: string;
|
||||
title: React.ReactNode;
|
||||
optional?: boolean;
|
||||
content: React.ReactNode;
|
||||
|
||||
stepProps?: Partial<StepProps>;
|
||||
titleProps?: Partial<StepButtonProps>;
|
||||
contentProps?: Partial<StepContentProps>;
|
||||
}[];
|
||||
}
|
||||
|
||||
export default function SteppedAccordion({
|
||||
steps,
|
||||
...props
|
||||
}: ISteppedAccordionProps) {
|
||||
const [activeStep, setActiveStep] = useState(steps[0].id);
|
||||
|
||||
return (
|
||||
<Stepper
|
||||
nonLinear
|
||||
activeStep={steps.findIndex((x) => x.id === activeStep)}
|
||||
orientation="vertical"
|
||||
{...props}
|
||||
sx={{
|
||||
mt: 0,
|
||||
|
||||
"& .MuiStepLabel-root": { width: "100%" },
|
||||
"& .MuiStepLabel-label": {
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
typography: "subtitle2",
|
||||
"&.Mui-active": { typography: "subtitle2" },
|
||||
},
|
||||
"& .MuiStepLabel-label svg": {
|
||||
display: "block",
|
||||
marginLeft: "auto",
|
||||
my: ((24 - 18) / 2 / 8) * -1,
|
||||
transition: (theme) => theme.transitions.create("transform"),
|
||||
},
|
||||
"& .Mui-active svg": {
|
||||
transform: "rotate(180deg)",
|
||||
},
|
||||
|
||||
...props.sx,
|
||||
}}
|
||||
>
|
||||
{steps.map(
|
||||
({
|
||||
id,
|
||||
title,
|
||||
optional,
|
||||
content,
|
||||
stepProps,
|
||||
titleProps,
|
||||
contentProps,
|
||||
}) => (
|
||||
<Step key={id} {...stepProps}>
|
||||
<StepButton
|
||||
onClick={() => setActiveStep((s) => (s === id ? "" : id))}
|
||||
optional={
|
||||
optional && <Typography variant="caption">Optional</Typography>
|
||||
}
|
||||
{...titleProps}
|
||||
>
|
||||
{title}
|
||||
<ExpandIcon />
|
||||
</StepButton>
|
||||
|
||||
<StepContent {...contentProps}>{content}</StepContent>
|
||||
</Step>
|
||||
)
|
||||
)}
|
||||
</Stepper>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import _find from "lodash/find";
|
||||
// import { useSnackbar } from "notistack";
|
||||
import { useSnackbar } from "notistack";
|
||||
|
||||
import { makeStyles, createStyles } from "@mui/styles";
|
||||
import {
|
||||
@@ -13,16 +13,22 @@ import {
|
||||
Typography,
|
||||
TextField,
|
||||
MenuItem,
|
||||
Button,
|
||||
} from "@mui/material";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
|
||||
import CopyCellsIcon from "assets/icons/CopyCells";
|
||||
import CopyCellsIcon from "@src/assets/icons/CopyCells";
|
||||
import ClearSelectionIcon from "@mui/icons-material/IndeterminateCheckBox";
|
||||
import DeleteIcon from "@mui/icons-material/DeleteForever";
|
||||
import ArrowDropUpIcon from "@mui/icons-material/ArrowDropUp";
|
||||
|
||||
import { useConfirmation } from "components/ConfirmationDialog/Context";
|
||||
import { useProjectContext } from "contexts/ProjectContext";
|
||||
import { formatPath } from "utils/fns";
|
||||
import { useConfirmation } from "@src/components/ConfirmationDialog/Context";
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
import { formatPath, asyncForEach } from "@src/utils/fns";
|
||||
// import routes from "@src/constants/routes";
|
||||
import { runRoutes } from "@src/constants/runRoutes";
|
||||
// import { config } from "process";
|
||||
import { WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
@@ -82,8 +88,6 @@ const useStyles = makeStyles((theme) =>
|
||||
},
|
||||
dropdownLabel: {
|
||||
left: theme.spacing(1.5),
|
||||
top: "50%",
|
||||
transform: "translateY(-50%) !important",
|
||||
|
||||
...theme.typography.body1,
|
||||
},
|
||||
@@ -91,20 +95,29 @@ const useStyles = makeStyles((theme) =>
|
||||
"$dropdownLabel&": { color: theme.palette.text.primary },
|
||||
},
|
||||
select: {
|
||||
paddingTop: "6px !important",
|
||||
paddingBottom: "7px !important",
|
||||
// paddingTop: "6px !important",
|
||||
// paddingBottom: "7px !important",
|
||||
},
|
||||
dropdownMenu: {
|
||||
// marginTop: theme.spacing(-3)
|
||||
},
|
||||
dropdownMenu: { marginTop: theme.spacing(-3) },
|
||||
})
|
||||
);
|
||||
|
||||
export default function BulkActions({ selectedRows, columns, clearSelection }) {
|
||||
const classes = useStyles();
|
||||
const [, setLoading] = useState<Boolean>();
|
||||
const { tableActions, addRow, tableState } = useProjectContext();
|
||||
const {
|
||||
tableActions,
|
||||
addRow,
|
||||
tableState,
|
||||
deleteRow,
|
||||
rowyRun,
|
||||
compatibleRowyRunVersion,
|
||||
} = useProjectContext();
|
||||
|
||||
const { requestConfirmation } = useConfirmation();
|
||||
// const { enqueueSnackbar } = useSnackbar();
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const actionColumns: { name: string; key: string; config: any }[] = columns
|
||||
.filter((column) => column.type === "ACTION")
|
||||
@@ -115,7 +128,7 @@ export default function BulkActions({ selectedRows, columns, clearSelection }) {
|
||||
}));
|
||||
|
||||
const handleDuplicate = () => {
|
||||
selectedRows.forEach((row) => {
|
||||
asyncForEach(selectedRows, async (row) => {
|
||||
const clonedRow = { ...row };
|
||||
// remove metadata
|
||||
delete clonedRow.ref;
|
||||
@@ -123,60 +136,80 @@ export default function BulkActions({ selectedRows, columns, clearSelection }) {
|
||||
Object.keys(clonedRow).forEach((key) => {
|
||||
if (clonedRow[key] === undefined) delete clonedRow[key];
|
||||
});
|
||||
if (tableActions) addRow!(clonedRow);
|
||||
await addRow!(clonedRow, undefined, { type: "smaller" });
|
||||
//sleep 1 sec
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
});
|
||||
clearSelection();
|
||||
};
|
||||
const handleDelete = () => {
|
||||
selectedRows.forEach((row) => row.ref.delete());
|
||||
deleteRow!(selectedRows.map((row) => row.ref.id));
|
||||
clearSelection();
|
||||
};
|
||||
|
||||
const handleActionScript = async (actionColumn, actionType) => {
|
||||
const requiredVersion = "1.2.0";
|
||||
if (!compatibleRowyRunVersion!({ minVersion: requiredVersion })) {
|
||||
enqueueSnackbar(
|
||||
`Upgrade your Rowy run to ${requiredVersion} or above, to run bulk actions`,
|
||||
{
|
||||
variant: "warning",
|
||||
action: (
|
||||
<Button
|
||||
href={WIKI_LINKS.rowyRun}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Docs
|
||||
<InlineOpenInNewIcon />
|
||||
</Button>
|
||||
),
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
const refs = selectedRows.map((row) => {
|
||||
const { ref } = row;
|
||||
return {
|
||||
path: ref.path,
|
||||
id: ref.id,
|
||||
tablePath: window.location.pathname,
|
||||
};
|
||||
});
|
||||
const data = {
|
||||
refs,
|
||||
column: actionColumn,
|
||||
action: actionType,
|
||||
schemaDocPath: formatPath(tableState?.config.id ?? ""),
|
||||
actionParams: {},
|
||||
};
|
||||
setLoading(true);
|
||||
const result = await rowyRun!({
|
||||
route: runRoutes.actionScript,
|
||||
body: data,
|
||||
});
|
||||
Array.isArray(result)
|
||||
? result.map((res) =>
|
||||
enqueueSnackbar(res.message, {
|
||||
variant: res.success ? "success" : "error",
|
||||
})
|
||||
)
|
||||
: enqueueSnackbar(result.message, {
|
||||
variant: result.success ? "success" : "error",
|
||||
});
|
||||
setLoading(false);
|
||||
clearSelection();
|
||||
};
|
||||
const executeAction = async (key: string, actionType: string) => {
|
||||
const actionColumn = _find(actionColumns, { key });
|
||||
if (!actionColumn) return;
|
||||
const callableName = actionColumn.config.callableName ?? "actionScript";
|
||||
|
||||
const calls = selectedRows.map((row) => {
|
||||
const { ref } = row;
|
||||
const data = {
|
||||
ref: {
|
||||
path: ref.path,
|
||||
id: ref.id,
|
||||
tablePath: window.location.pathname,
|
||||
},
|
||||
column: actionColumn,
|
||||
action: actionType,
|
||||
schemaDocPath: formatPath(tableState?.tablePath ?? ""),
|
||||
actionParams: {},
|
||||
};
|
||||
return true;
|
||||
// cloudFunction(
|
||||
// callableName,
|
||||
// data,
|
||||
// async (response) => {
|
||||
// const { message, cellValue, success } = response.data;
|
||||
// // setIsRunning(false);
|
||||
// enqueueSnackbar(JSON.stringify(message), {
|
||||
// variant: success ? "success" : "error",
|
||||
// });
|
||||
// if (cellValue && cellValue.status) {
|
||||
// return ref.update({ [actionColumn.key]: cellValue });
|
||||
// }
|
||||
// },
|
||||
// (error) => {
|
||||
// console.error("ERROR", callableName, error);
|
||||
// //setIsRunning(false);
|
||||
// enqueueSnackbar(JSON.stringify(error), { variant: "error" });
|
||||
// }
|
||||
// );
|
||||
});
|
||||
setLoading(true);
|
||||
const result = await Promise.all(calls);
|
||||
await Promise.all(result);
|
||||
console.log(result);
|
||||
setLoading(false);
|
||||
clearSelection();
|
||||
if (actionColumn.config.isActionScript) {
|
||||
handleActionScript(actionColumn, actionType);
|
||||
} else {
|
||||
enqueueSnackbar("Callable actions not implemented yet", {
|
||||
variant: "warning",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const numSelected = selectedRows.length;
|
||||
@@ -210,6 +243,11 @@ export default function BulkActions({ selectedRows, columns, clearSelection }) {
|
||||
<Grid item className={classes.spacer} />
|
||||
|
||||
<Grid item>
|
||||
{/* <Typography>
|
||||
{`${actionColumns.length} action${
|
||||
actionColumns.length !== 1 ? "s" : ""
|
||||
}`}
|
||||
</Typography> */}
|
||||
<TextField
|
||||
select
|
||||
variant="filled"
|
||||
@@ -228,7 +266,7 @@ export default function BulkActions({ selectedRows, columns, clearSelection }) {
|
||||
},
|
||||
}}
|
||||
SelectProps={{
|
||||
classes: { root: classes.select },
|
||||
classes: { select: classes.select },
|
||||
displayEmpty: true,
|
||||
MenuProps: {
|
||||
anchorOrigin: { vertical: "top", horizontal: "left" },
|
||||
|
||||
@@ -2,7 +2,7 @@ import { styled } from "@mui/material/styles";
|
||||
import ErrorIcon from "@mui/icons-material/ErrorOutline";
|
||||
import WarningIcon from "@mui/icons-material/WarningAmber";
|
||||
|
||||
import RichTooltip from "components/RichTooltip";
|
||||
import RichTooltip from "@src/components/RichTooltip";
|
||||
|
||||
const Root = styled("div", { shouldForwardProp: (prop) => prop !== "error" })(
|
||||
({ theme, ...props }) => ({
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useRef } from "react";
|
||||
import clsx from "clsx";
|
||||
import { HeaderRendererProps } from "react-data-grid";
|
||||
import { useDrag, useDrop, DragObjectWithType } from "react-dnd";
|
||||
import useCombinedRefs from "hooks/useCombinedRefs";
|
||||
import useCombinedRefs from "@src/hooks/useCombinedRefs";
|
||||
|
||||
import { makeStyles, createStyles } from "@mui/styles";
|
||||
import {
|
||||
@@ -17,10 +17,10 @@ import SortDescIcon from "@mui/icons-material/ArrowDownward";
|
||||
import DropdownIcon from "@mui/icons-material/MoreHoriz";
|
||||
import LockIcon from "@mui/icons-material/LockOutlined";
|
||||
|
||||
import { FieldType } from "constants/fields";
|
||||
import { getFieldProp } from "components/fields";
|
||||
import { useAppContext } from "contexts/AppContext";
|
||||
import { useProjectContext } from "contexts/ProjectContext";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { getFieldProp } from "@src/components/fields";
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
import { TableOrder } from "@src/hooks/useTable";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
|
||||
@@ -1,35 +1,22 @@
|
||||
import React from "react";
|
||||
import { lazy, Suspense, createElement } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { IMenuModalProps } from "..";
|
||||
|
||||
import { makeStyles, createStyles } from "@mui/styles";
|
||||
import Checkbox from "@mui/material/Checkbox";
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import { Typography, TextField, MenuItem, ListItemText } from "@mui/material";
|
||||
import Subheading from "../Subheading";
|
||||
|
||||
import { getFieldProp } from "components/fields";
|
||||
import CodeEditorHelper from "components/CodeEditorHelper";
|
||||
import CodeEditor from "components/Table/editors/CodeEditor";
|
||||
import { getFieldProp } from "@src/components/fields";
|
||||
import FieldSkeleton from "@src/components/SideDrawer/Form/FieldSkeleton";
|
||||
import CodeEditorHelper from "@src/components/CodeEditor/CodeEditorHelper";
|
||||
import FormAutosave from "./FormAutosave";
|
||||
import { FieldType } from "constants/fields";
|
||||
import { WIKI_LINKS } from "constants/externalLinks";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
import { name } from "@root/package.json";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
typeSelect: { marginBottom: theme.spacing(1) },
|
||||
typeSelectItem: { whiteSpace: "normal" },
|
||||
|
||||
codeEditorContainer: {
|
||||
border: `1px solid ${theme.palette.divider}`,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
|
||||
mono: {
|
||||
fontFamily: theme.typography.fontFamilyMono,
|
||||
},
|
||||
})
|
||||
const CodeEditor = lazy(
|
||||
() =>
|
||||
import("@src/components/CodeEditor" /* webpackChunkName: "CodeEditor" */)
|
||||
);
|
||||
|
||||
export interface IDefaultValueInputProps extends IMenuModalProps {
|
||||
@@ -43,7 +30,6 @@ export default function DefaultValueInput({
|
||||
fieldName,
|
||||
...props
|
||||
}: IDefaultValueInputProps) {
|
||||
const classes = useStyles();
|
||||
const _type =
|
||||
type !== FieldType.derivative
|
||||
? type
|
||||
@@ -64,13 +50,17 @@ export default function DefaultValueInput({
|
||||
value={config.defaultValue?.type ?? "undefined"}
|
||||
onChange={(e) => handleChange("defaultValue.type")(e.target.value)}
|
||||
fullWidth
|
||||
className={classes.typeSelect}
|
||||
sx={{ mb: 1 }}
|
||||
SelectProps={{
|
||||
MenuProps: {
|
||||
sx: { "& .MuiListItemText-root": { whiteSpace: "normal" } },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MenuItem value="undefined">
|
||||
<ListItemText
|
||||
primary="Undefined"
|
||||
secondary="No default value. The field will not appear in the row’s corresponding Firestore document by default."
|
||||
className={classes.typeSelectItem}
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuItem value="null">
|
||||
@@ -78,24 +68,21 @@ export default function DefaultValueInput({
|
||||
primary="Null"
|
||||
secondary={
|
||||
<>
|
||||
Initialise as <span className={classes.mono}>null</span>.
|
||||
Initialise as <code>null</code>.
|
||||
</>
|
||||
}
|
||||
className={classes.typeSelectItem}
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuItem value="static">
|
||||
<ListItemText
|
||||
primary="Static"
|
||||
secondary="Set a specific default value for all cells in this column."
|
||||
className={classes.typeSelectItem}
|
||||
/>
|
||||
</MenuItem>
|
||||
<MenuItem value="dynamic">
|
||||
<ListItemText
|
||||
primary={`Dynamic (Requires ${name} Cloud Functions)`}
|
||||
secondary={`Write code to set the default value using this table’s ${name} Cloud Function. Setup is required.`}
|
||||
className={classes.typeSelectItem}
|
||||
/>
|
||||
</MenuItem>
|
||||
</TextField>
|
||||
@@ -135,7 +122,7 @@ export default function DefaultValueInput({
|
||||
}
|
||||
/>
|
||||
|
||||
{React.createElement(customFieldInput, {
|
||||
{createElement(customFieldInput, {
|
||||
column: { type, key: fieldName, config, ...props, ...config },
|
||||
control,
|
||||
docRef: {},
|
||||
@@ -147,18 +134,12 @@ export default function DefaultValueInput({
|
||||
{config.defaultValue?.type === "dynamic" && (
|
||||
<>
|
||||
<CodeEditorHelper docLink={WIKI_LINKS.howToDefaultValues} />
|
||||
<div className={classes.codeEditorContainer}>
|
||||
<Suspense fallback={<FieldSkeleton height={100} />}>
|
||||
<CodeEditor
|
||||
height={120}
|
||||
script={config.defaultValue?.script}
|
||||
handleChange={handleChange("defaultValue.script")}
|
||||
editorOptions={{
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
}}
|
||||
value={config.defaultValue?.script}
|
||||
onChange={handleChange("defaultValue.script")}
|
||||
/>
|
||||
</div>
|
||||
</Suspense>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -2,18 +2,19 @@ import { useState, Suspense, useMemo, createElement } from "react";
|
||||
import _set from "lodash/set";
|
||||
import { IMenuModalProps } from "..";
|
||||
|
||||
import { Typography, Divider, Stack } from "@mui/material";
|
||||
import { Typography, Stack } from "@mui/material";
|
||||
|
||||
import Modal from "components/Modal";
|
||||
import { getFieldProp } from "components/fields";
|
||||
import Modal from "@src/components/Modal";
|
||||
import { getFieldProp } from "@src/components/fields";
|
||||
import DefaultValueInput from "./DefaultValueInput";
|
||||
import ErrorBoundary from "components/ErrorBoundary";
|
||||
import Loading from "components/Loading";
|
||||
import ErrorBoundary from "@src/components/ErrorBoundary";
|
||||
import Loading from "@src/components/Loading";
|
||||
|
||||
import { useProjectContext } from "contexts/ProjectContext";
|
||||
import { useConfirmation } from "components/ConfirmationDialog";
|
||||
import { FieldType } from "constants/fields";
|
||||
import { runRoutes } from "constants/runRoutes";
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
import { useConfirmation } from "@src/components/ConfirmationDialog";
|
||||
import { useSnackLogContext } from "@src/contexts/SnackLogContext";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { runRoutes } from "@src/constants/runRoutes";
|
||||
|
||||
export default function FieldSettings(props: IMenuModalProps) {
|
||||
const { name, fieldName, type, open, config, handleClose, handleSave } =
|
||||
@@ -22,10 +23,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 snackLogContext = useSnackLogContext();
|
||||
|
||||
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 (
|
||||
@@ -37,22 +63,16 @@ 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
|
||||
maxWidth="md"
|
||||
onClose={handleClose}
|
||||
title={`${name}: Settings`}
|
||||
disableBackdropClick
|
||||
disableEscapeKeyDown
|
||||
children={
|
||||
<Suspense fallback={<Loading fullScreen={false} />}>
|
||||
<>
|
||||
@@ -78,7 +98,10 @@ export default function FieldSettings(props: IMenuModalProps) {
|
||||
>
|
||||
{createElement(customFieldSettings, {
|
||||
config: newConfig,
|
||||
handleChange,
|
||||
onChange: handleChange,
|
||||
fieldName,
|
||||
onBlur: validateSettings,
|
||||
errors,
|
||||
})}
|
||||
</Stack>
|
||||
)}
|
||||
@@ -93,7 +116,9 @@ export default function FieldSettings(props: IMenuModalProps) {
|
||||
</Typography>
|
||||
{createElement(rendedFieldSettings, {
|
||||
config: newConfig,
|
||||
handleChange,
|
||||
onChange: handleChange,
|
||||
onBlur: validateSettings,
|
||||
errors,
|
||||
})}
|
||||
</Stack>
|
||||
)}
|
||||
@@ -110,6 +135,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",
|
||||
@@ -118,6 +166,7 @@ export default function FieldSettings(props: IMenuModalProps) {
|
||||
cancel: "Later",
|
||||
handleConfirm: async () => {
|
||||
if (!rowyRun) return;
|
||||
snackLogContext.requestSnackLog();
|
||||
rowyRun({
|
||||
route: runRoutes.buildFunction,
|
||||
body: {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import MultiSelect from "@rowy/multiselect";
|
||||
import { ListItemIcon } from "@mui/material";
|
||||
|
||||
import { FIELDS } from "components/fields";
|
||||
import { FieldType } from "constants/fields";
|
||||
import { getFieldProp } from "components/fields";
|
||||
import { FIELDS } from "@src/components/fields";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { getFieldProp } from "@src/components/fields";
|
||||
|
||||
export interface IFieldsDropdownProps {
|
||||
value: FieldType;
|
||||
@@ -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,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { IMenuModalProps } from ".";
|
||||
|
||||
import { TextField } from "@mui/material";
|
||||
|
||||
import Modal from "components/Modal";
|
||||
import Modal from "@src/components/Modal";
|
||||
|
||||
export default function NameChange({
|
||||
name,
|
||||
|
||||
@@ -4,19 +4,24 @@ import { IMenuModalProps } from ".";
|
||||
|
||||
import { TextField, Typography, Button } from "@mui/material";
|
||||
|
||||
import Modal from "components/Modal";
|
||||
import Modal from "@src/components/Modal";
|
||||
import FieldsDropdown from "./FieldsDropdown";
|
||||
|
||||
import { FieldType } from "constants/fields";
|
||||
import { getFieldProp } from "components/fields";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { getFieldProp } from "@src/components/fields";
|
||||
import { analytics } from "analytics";
|
||||
import { useProjectContext } from "contexts/ProjectContext";
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
|
||||
const AUDIT_FIELD_TYPES = [
|
||||
FieldType.createdBy,
|
||||
FieldType.createdAt,
|
||||
FieldType.updatedBy,
|
||||
FieldType.updatedAt,
|
||||
];
|
||||
export interface INewColumnProps extends IMenuModalProps {
|
||||
data: Record<string, any>;
|
||||
openSettings: (column: any) => void;
|
||||
}
|
||||
|
||||
export default function NewColumn({
|
||||
open,
|
||||
data,
|
||||
@@ -31,16 +36,7 @@ export default function NewColumn({
|
||||
const [type, setType] = useState(FieldType.shortText);
|
||||
const requireConfiguration = getFieldProp("requireConfiguration", type);
|
||||
|
||||
const isAuditField =
|
||||
type === FieldType.createdBy ||
|
||||
type === FieldType.createdAt ||
|
||||
type === FieldType.updatedBy ||
|
||||
type === FieldType.updatedAt;
|
||||
|
||||
useEffect(() => {
|
||||
if (type !== FieldType.id && !isAuditField)
|
||||
setFieldKey(_camel(columnLabel));
|
||||
}, [columnLabel, type, isAuditField]);
|
||||
const isAuditField = AUDIT_FIELD_TYPES.includes(type);
|
||||
|
||||
useEffect(() => {
|
||||
switch (type) {
|
||||
@@ -90,7 +86,12 @@ export default function NewColumn({
|
||||
label="Column name"
|
||||
type="text"
|
||||
fullWidth
|
||||
onChange={(e) => setColumnLabel(e.target.value)}
|
||||
onChange={(e) => {
|
||||
setColumnLabel(e.target.value);
|
||||
if (type !== FieldType.id && !isAuditField) {
|
||||
setFieldKey(_camel(e.target.value));
|
||||
}
|
||||
}}
|
||||
helperText="Set the user-facing name for this column."
|
||||
/>
|
||||
</section>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { IMenuModalProps } from ".";
|
||||
import Modal from "components/Modal";
|
||||
import Modal from "@src/components/Modal";
|
||||
import FieldsDropdown from "./FieldsDropdown";
|
||||
import { analytics } from "analytics";
|
||||
export default function FormDialog({
|
||||
|
||||
@@ -11,32 +11,32 @@ import LockOpenIcon from "@mui/icons-material/LockOpen";
|
||||
import LockIcon from "@mui/icons-material/LockOutlined";
|
||||
// import VisibilityOffIcon from "@mui/icons-material/VisibilityOffOutlined";
|
||||
// import VisibilityIcon from "@mui/icons-material/VisibilityOutlined";
|
||||
import FreezeIcon from "assets/icons/Freeze";
|
||||
import UnfreezeIcon from "assets/icons/Unfreeze";
|
||||
import CellResizeIcon from "assets/icons/CellResize";
|
||||
import FreezeIcon from "@src/assets/icons/Freeze";
|
||||
import UnfreezeIcon from "@src/assets/icons/Unfreeze";
|
||||
import CellResizeIcon from "@src/assets/icons/CellResize";
|
||||
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
|
||||
import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward";
|
||||
import EditIcon from "@mui/icons-material/EditOutlined";
|
||||
// import ReorderIcon from "@mui/icons-material/Reorder";
|
||||
import SettingsIcon from "@mui/icons-material/SettingsOutlined";
|
||||
import ColumnPlusBeforeIcon from "assets/icons/ColumnPlusBefore";
|
||||
import ColumnPlusAfterIcon from "assets/icons/ColumnPlusAfter";
|
||||
import ColumnRemoveIcon from "assets/icons/ColumnRemove";
|
||||
import ColumnPlusBeforeIcon from "@src/assets/icons/ColumnPlusBefore";
|
||||
import ColumnPlusAfterIcon from "@src/assets/icons/ColumnPlusAfter";
|
||||
import ColumnRemoveIcon from "@src/assets/icons/ColumnRemove";
|
||||
|
||||
import MenuContents from "./MenuContents";
|
||||
import NameChange from "./NameChange";
|
||||
import NewColumn from "./NewColumn";
|
||||
import TypeChange from "./TypeChange";
|
||||
import FieldSettings from "./FieldSettings";
|
||||
import ColumnHeader from "components/Wizards/Column";
|
||||
import ColumnHeader from "@src/components/Wizards/Column";
|
||||
|
||||
import { useProjectContext } from "contexts/ProjectContext";
|
||||
import { FieldType } from "constants/fields";
|
||||
import { getFieldProp } from "components/fields";
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { getFieldProp } from "@src/components/fields";
|
||||
|
||||
import { Column } from "react-data-grid";
|
||||
import { PopoverProps } from "@mui/material";
|
||||
import { useConfirmation } from "components/ConfirmationDialog";
|
||||
import { useConfirmation } from "@src/components/ConfirmationDialog";
|
||||
|
||||
const INITIAL_MODAL = { type: "", data: {} };
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Grid, Stack, Typography, Button, Divider } from "@mui/material";
|
||||
import ImportIcon from "assets/icons/Import";
|
||||
import AddColumnIcon from "assets/icons/AddColumn";
|
||||
import ImportIcon from "@src/assets/icons/Import";
|
||||
import AddColumnIcon from "@src/assets/icons/AddColumn";
|
||||
|
||||
import { APP_BAR_HEIGHT } from "components/Navigation";
|
||||
import { APP_BAR_HEIGHT } from "@src/components/Navigation";
|
||||
|
||||
import { useProjectContext } from "contexts/ProjectContext";
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
import ColumnMenu from "./ColumnMenu";
|
||||
import ImportWizard from "components/Wizards/ImportWizard";
|
||||
import ImportCSV from "./TableHeader/ImportCsv";
|
||||
import ImportWizard from "@src/components/Wizards/ImportWizard";
|
||||
import ImportCSV from "@src/components/TableHeader/ImportCsv";
|
||||
|
||||
export default function EmptyTable() {
|
||||
const { tableState, importWizardRef, columnMenuRef } = useProjectContext();
|
||||
|
||||
@@ -17,16 +17,16 @@ import {
|
||||
import FilterIcon from "@mui/icons-material/FilterList";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
|
||||
import ButtonWithStatus from "components/ButtonWithStatus";
|
||||
import FormAutosave from "components/Table/ColumnMenu/FieldSettings/FormAutosave";
|
||||
import FieldSkeleton from "components/SideDrawer/Form/FieldSkeleton";
|
||||
import ButtonWithStatus from "@src/components/ButtonWithStatus";
|
||||
import FormAutosave from "@src/components/Table/ColumnMenu/FieldSettings/FormAutosave";
|
||||
import FieldSkeleton from "@src/components/SideDrawer/Form/FieldSkeleton";
|
||||
|
||||
import { FieldType } from "constants/fields";
|
||||
import { TableFilter } from "hooks/useTable";
|
||||
import { useProjectContext } from "contexts/ProjectContext";
|
||||
import { useAppContext } from "contexts/AppContext";
|
||||
import { DocActions } from "hooks/useDoc";
|
||||
import { getFieldProp } from "components/fields";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { TableFilter } from "@src/hooks/useTable";
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
import { DocActions } from "@src/hooks/useDoc";
|
||||
import { getFieldProp } from "@src/components/fields";
|
||||
|
||||
const getType = (column) =>
|
||||
column.type === FieldType.derivative
|
||||
@@ -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>
|
||||
|
||||
@@ -2,9 +2,9 @@ import { Column } from "react-data-grid";
|
||||
|
||||
import { makeStyles, createStyles } from "@mui/styles";
|
||||
import { Grid, Button } from "@mui/material";
|
||||
import AddColumnIcon from "assets/icons/AddColumn";
|
||||
import AddColumnIcon from "@src/assets/icons/AddColumn";
|
||||
|
||||
import { useProjectContext } from "contexts/ProjectContext";
|
||||
import { useProjectContext } from "@src/contexts/ProjectContext";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
|
||||
@@ -6,13 +6,14 @@ import { makeStyles, createStyles } from "@mui/styles";
|
||||
import VisibilityOffIcon from "@mui/icons-material/VisibilityOffOutlined";
|
||||
|
||||
import MultiSelect from "@rowy/multiselect";
|
||||
import ButtonWithStatus from "components/ButtonWithStatus";
|
||||
import Column from "components/Wizards/Column";
|
||||
import ButtonWithStatus from "@src/components/ButtonWithStatus";
|
||||
import Column from "@src/components/Wizards/Column";
|
||||
|
||||
import { useProjectContext } from "contexts/ProjectContext";
|
||||
import { useAppContext } from "contexts/AppContext";
|
||||
import { DocActions } from "hooks/useDoc";
|
||||
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 },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import useHotkeys from "../../hooks/useHotkeys";
|
||||
import { FieldType } from "constants/fields";
|
||||
import { useAppContext } from "contexts/AppContext";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { useAppContext } from "@src/contexts/AppContext";
|
||||
|
||||
// TODO: Hook up to ProjectContext
|
||||
const onSubmit: any = () => () => {};
|
||||
|
||||
59
src/components/Table/OutOfOrderIndicator.tsx
Normal file
59
src/components/Table/OutOfOrderIndicator.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import createPersistedState from "use-persisted-state";
|
||||
|
||||
import { styled } from "@mui/material/styles";
|
||||
import RichTooltip from "@src/components/RichTooltip";
|
||||
import WarningIcon from "@mui/icons-material/WarningAmber";
|
||||
import { OUT_OF_ORDER_MARGIN } from "./TableContainer";
|
||||
|
||||
const useOutOfOrderTooltipDismissedState = createPersistedState(
|
||||
"__ROWY__OUT_OF_ORDER_TOOLTIP_DISMISSED"
|
||||
);
|
||||
|
||||
const Dot = styled("div")(({ theme }) => ({
|
||||
position: "absolute",
|
||||
left: -6,
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
zIndex: 1,
|
||||
|
||||
width: 12,
|
||||
height: 12,
|
||||
|
||||
borderRadius: "50%",
|
||||
backgroundColor: theme.palette.warning.main,
|
||||
}));
|
||||
|
||||
export interface IOutOfOrderIndicatorProps {
|
||||
top: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export default function OutOfOrderIndicator({
|
||||
top,
|
||||
height,
|
||||
}: IOutOfOrderIndicatorProps) {
|
||||
const [dismissed, setDismissed] = useOutOfOrderTooltipDismissedState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="out-of-order-dot"
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: top,
|
||||
height: height - OUT_OF_ORDER_MARGIN - 2,
|
||||
marginLeft: `max(env(safe-area-inset-left), 16px)`,
|
||||
width: 12,
|
||||
}}
|
||||
>
|
||||
<RichTooltip
|
||||
icon={<WarningIcon fontSize="inherit" color="warning" />}
|
||||
title="Row out of order"
|
||||
message="This row will not appear on the top of the table after you reload this page"
|
||||
placement="right"
|
||||
render={({ openTooltip }) => <Dot onClick={openTooltip} />}
|
||||
defaultOpen={!dismissed}
|
||||
onClose={() => setDismissed(true)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import React from "react";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import MoreVertIcon from "@mui/icons-material/MoreVert";
|
||||
|
||||
const options = ["Webhooks", "Rules", "Algolia", "CollectionSync"];
|
||||
|
||||
const ITEM_HEIGHT = 48;
|
||||
|
||||
export default function SettingsMenu({ modal, setModal }) {
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = (option: string) => () => {
|
||||
setModal(option);
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<IconButton
|
||||
aria-label="More"
|
||||
aria-controls="long-menu"
|
||||
aria-haspopup="true"
|
||||
onClick={handleClick}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
<Menu
|
||||
id="long-menu"
|
||||
anchorEl={anchorEl}
|
||||
keepMounted
|
||||
open={open}
|
||||
onClose={handleClose("")}
|
||||
PaperProps={{
|
||||
style: {
|
||||
maxHeight: ITEM_HEIGHT * 4.5,
|
||||
width: "20ch",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<MenuItem
|
||||
key={option}
|
||||
value={option}
|
||||
selected={option === modal}
|
||||
onClick={handleClose(option)}
|
||||
disabled={["Rules", "Algolia", "CollectionSync"].includes(option)}
|
||||
>
|
||||
{option}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Menu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { makeStyles, createStyles } from "@mui/styles";
|
||||
|
||||
import Button from "@mui/material/Button";
|
||||
import Dialog, { DialogProps } from "@mui/material/Dialog";
|
||||
import DialogActions from "@mui/material/DialogActions";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import FormControl from "@mui/material/FormControl";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
import FormControlLabel from "@mui/material/FormControlLabel";
|
||||
import InputLabel from "@mui/material/InputLabel";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Select from "@mui/material/Select";
|
||||
import Switch from "@mui/material/Switch";
|
||||
import CodeEditor from "../editors/CodeEditor";
|
||||
import { useProjectContext } from "contexts/ProjectContext";
|
||||
import { makeId } from "../../../utils/fns";
|
||||
|
||||
const useStyles = makeStyles((theme) =>
|
||||
createStyles({
|
||||
form: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
margin: "auto",
|
||||
width: "fit-content",
|
||||
},
|
||||
formControl: {
|
||||
marginTop: theme.spacing(2),
|
||||
minWidth: 120,
|
||||
},
|
||||
formControlLabel: {
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
enum WebhookTypes {
|
||||
custom = "CUSTOM",
|
||||
typeForm = "TYPE_FORM",
|
||||
}
|
||||
const EmptyState = {
|
||||
enabled: false,
|
||||
type: WebhookTypes.custom,
|
||||
secret: "",
|
||||
customParser: "",
|
||||
};
|
||||
export default function WebhooksDialog({ open, handleClose }) {
|
||||
const classes = useStyles();
|
||||
|
||||
const { tableState, tableActions } = useProjectContext();
|
||||
|
||||
const [state, setState] = useState<{
|
||||
enabled: boolean;
|
||||
type: WebhookTypes;
|
||||
secret: string;
|
||||
customParser: string;
|
||||
}>(EmptyState);
|
||||
const tableFields = Object.keys(tableState?.columns as any);
|
||||
const fullWidth = true;
|
||||
const maxWidth: DialogProps["maxWidth"] = "xl";
|
||||
const handleChange = (key: string) => (value: any) => {
|
||||
setState((s) => ({ ...s, [key]: value }));
|
||||
};
|
||||
const initializeWebhooksConfig = () => {
|
||||
const secret = makeId(32);
|
||||
handleChange("secret")(secret);
|
||||
setState({ ...EmptyState, secret });
|
||||
tableActions?.table.updateConfig("webhooks", {
|
||||
enabled: false,
|
||||
type: WebhookTypes.custom,
|
||||
secret,
|
||||
customParser: "", // TODO: add a boilerplate/example
|
||||
});
|
||||
};
|
||||
useEffect(() => {
|
||||
if (
|
||||
tableState &&
|
||||
!tableState.config.tableConfig.loading &&
|
||||
!tableState?.config.webhooks &&
|
||||
!state.secret
|
||||
) {
|
||||
initializeWebhooksConfig();
|
||||
} else if (tableState?.config.webhooks) {
|
||||
setState({ ...tableState?.config.webhooks });
|
||||
}
|
||||
}, [tableState?.config]);
|
||||
|
||||
const handleWebhookTypeChange = (
|
||||
event: React.ChangeEvent<{ value: unknown }>
|
||||
) => {
|
||||
handleChange("type")(event.target.value as WebhookTypes);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
handleClose();
|
||||
await tableActions?.table.updateConfig("webhooks", {
|
||||
...state,
|
||||
});
|
||||
};
|
||||
const handleCancel = () => {
|
||||
handleClose();
|
||||
setState({ ...tableState?.config.webhooks });
|
||||
};
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Dialog
|
||||
fullWidth={fullWidth}
|
||||
maxWidth={maxWidth}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
aria-labelledby="max-width-dialog-title"
|
||||
>
|
||||
<DialogTitle id="max-width-dialog-title">Webhooks</DialogTitle>
|
||||
<DialogContent>
|
||||
<FormControl className={classes.formControl}>
|
||||
<FormControlLabel
|
||||
control={<Switch />}
|
||||
label={"Enable webhooks for this table"}
|
||||
labelPlacement="end"
|
||||
checked={state.enabled}
|
||||
onChange={() => {
|
||||
handleChange("enabled")(!state.enabled);
|
||||
}}
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
"& .MuiFormControlLabel-label": { mt: 0 },
|
||||
}}
|
||||
// classes={{ root: classes.formControlLabel, label: classes.label }}
|
||||
/>
|
||||
<InputLabel htmlFor="webhook-type">Webhook type</InputLabel>
|
||||
<Select
|
||||
autoFocus
|
||||
value={state.type}
|
||||
onChange={handleWebhookTypeChange as any}
|
||||
inputProps={{
|
||||
name: "webhook-type",
|
||||
id: "webhook-type",
|
||||
}}
|
||||
>
|
||||
<MenuItem value={WebhookTypes.typeForm}>Typeform</MenuItem>
|
||||
<MenuItem value={WebhookTypes.custom}>Custom</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
{state.type === WebhookTypes.custom && (
|
||||
<CodeEditor
|
||||
script={state.customParser}
|
||||
handleChange={handleChange("customParser")}
|
||||
/>
|
||||
)}
|
||||
<br />
|
||||
{state.type === WebhookTypes.typeForm && (
|
||||
<>
|
||||
<Typography variant="overline">Web hook url:</Typography>
|
||||
<Typography variant="body1">
|
||||
{/* {WEBHOOK_URL}?tablePath={tableState?.tablePath}
|
||||
&type=TYPE_FORM&secret={state.secret} */}
|
||||
</Typography>
|
||||
<Typography variant="overline">instructions:</Typography>
|
||||
<Typography variant="body1">
|
||||
please set the question reference in typeform to the following
|
||||
field keys :{" "}
|
||||
{tableFields.map((key) => (
|
||||
<>
|
||||
{" "}
|
||||
<b key={key}>{key}</b>,
|
||||
</>
|
||||
))}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleSave} color="primary">
|
||||
Save
|
||||
</Button>
|
||||
<Button onClick={handleCancel} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user