Merge pull request #590 from rowyio/rc

v2.2.0
This commit is contained in:
Shams
2021-11-30 14:31:19 +11:00
committed by GitHub
321 changed files with 9281 additions and 4505 deletions

View File

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

View File

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

View File

@@ -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();

View 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>
);
}

View File

@@ -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>
);
}

View 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.

View File

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

View File

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

View File

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

View 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}
/>
);
}

View File

@@ -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>
);
}

View File

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

View 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>
);
}

View 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>
);
}

View 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>;

View File

@@ -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();
/**

View 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": []
}

View 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": []
}

View 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>
);
}

View 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
View 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 {}
}

View File

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

View File

@@ -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();

View File

@@ -4,6 +4,7 @@ export type confirmationProps =
customBody?: React.ReactNode;
body?: string;
cancel?: string;
hideCancel?: boolean;
confirm?: string | JSX.Element;
confirmationCommand?: string;
handleConfirm: () => void;

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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>
);
}

View File

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

View File

@@ -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]),
]}
/>
)}
</>

View File

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

View File

@@ -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();

View File

@@ -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>
)}

View File

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

View 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,
}}
/>
);
}

View File

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

View File

@@ -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>
)}

View File

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

View File

@@ -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 })}

View File

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

View File

@@ -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>
) : (

View File

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

View File

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

View File

@@ -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" }}
/>
&nbsp;
{name} Run is set up correctly
</>
) : verified === false ? (
`${name} Run is not set up correctly`
) : (
" "
)
}
/>
</Grid>

View File

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

View File

@@ -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";

View File

@@ -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();

View File

@@ -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();

View File

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

View File

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

View File

@@ -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 }}
/>
</>
);
}

View File

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

View File

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

View File

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

View File

@@ -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,
});

View File

@@ -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,
});

View File

@@ -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);

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 wont 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>
);

View File

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

View File

@@ -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 });

View File

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

View File

@@ -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>
);

View 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>
);
}

View File

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

View File

@@ -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 }) => ({

View File

@@ -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) =>

View File

@@ -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 rows 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 tables ${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>
</>
)}
</>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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: {} };

View File

@@ -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();

View File

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

View File

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

View File

@@ -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 },
},
},
});

View File

@@ -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 = () => () => {};

View 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>
);
}

View File

@@ -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>
);
}

View File

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