diff --git a/package.json b/package.json index b30884a2..5e4a1714 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Rowy", - "version": "2.1.0", + "version": "2.2.0", "homepage": "https://rowy.io", "repository": { "type": "git", @@ -13,18 +13,19 @@ "@emotion/react": "^11.4.0", "@emotion/styled": "^11.3.0", "@hookform/resolvers": "^2.8.1", - "@mdi/js": "^6.2.95", - "@monaco-editor/react": "^4.1.0", - "@mui/icons-material": "^5.0.0", - "@mui/lab": "^5.0.0-alpha.50", - "@mui/material": "^5.0.0", - "@mui/styles": "^5.0.0", - "@rowy/form-builder": "^0.3.1", + "@mdi/js": "^6.5.95", + "@monaco-editor/react": "^4.3.1", + "@mui/icons-material": "^5.2.0", + "@mui/lab": "^5.0.0-alpha.58", + "@mui/material": "^5.2.2", + "@mui/styles": "^5.2.2", + "@rowy/form-builder": "^0.4.2", "@rowy/multiselect": "^0.2.3", "@tinymce/tinymce-react": "^3.12.6", "algoliasearch": "^4.8.6", "ansi-to-react": "^6.1.5", "colord": "^2.7.0", + "compare-versions": "^4.1.1", "craco-swc": "^0.1.3", "csv-parse": "^4.15.3", "date-fns": "^2.19.0", @@ -32,13 +33,14 @@ "file-saver": "^2.0.5", "firebase": "8.6.8", "hotkeys-js": "^3.7.2", - "json-format": "^1.0.1", + "jotai": "^1.4.2", + "json-stable-stringify-without-jsonify": "^1.0.1", "json2csv": "^5.0.6", "jszip": "^3.6.0", "jwt-decode": "^3.1.2", "lodash": "^4.17.21", - "moment": "^2.29.1", "notistack": "^2.0.2", + "pb-util": "^1.0.1", "query-string": "^6.8.3", "react": "^17.0.2", "react-beautiful-dnd": "^13.0.0", @@ -67,8 +69,7 @@ "use-algolia": "^1.4.1", "use-debounce": "^3.3.0", "use-persisted-state": "^0.3.3", - "yarn": "^1.22.10", - "yup": "^0.32.9" + "yarn": "^1.22.10" }, "scripts": { "upstream": "git fetch upstream;git merge upstream/main;git commit -m'merge upstream';git push", diff --git a/src/App.tsx b/src/App.tsx index a78c8762..ba5d5041 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -6,34 +6,34 @@ import AdapterDateFns from "@mui/lab/AdapterDateFns"; import { StyledEngineProvider } from "@mui/material/styles"; import "./space-grotesk.css"; -import CustomBrowserRouter from "utils/CustomBrowserRouter"; -import PrivateRoute from "utils/PrivateRoute"; -import ErrorBoundary from "components/ErrorBoundary"; -import Loading from "components/Loading"; -import Navigation from "components/Navigation"; -import Logo from "assets/Logo"; +import CustomBrowserRouter from "@src/utils/CustomBrowserRouter"; +import PrivateRoute from "@src/utils/PrivateRoute"; +import ErrorBoundary from "@src/components/ErrorBoundary"; +import Loading from "@src/components/Loading"; +import Navigation from "@src/components/Navigation"; +import Logo from "@src/assets/Logo"; -import SwrProvider from "contexts/SwrContext"; -import ConfirmationProvider from "components/ConfirmationDialog/Provider"; -import { AppProvider } from "contexts/AppContext"; -import { ProjectContextProvider } from "contexts/ProjectContext"; -import { SnackbarProvider } from "contexts/SnackbarContext"; -import { SnackLogProvider } from "contexts/SnackLogContext"; -import routes from "constants/routes"; +import SwrProvider from "@src/contexts/SwrContext"; +import ConfirmationProvider from "@src/components/ConfirmationDialog/Provider"; +import { AppProvider } from "@src/contexts/AppContext"; +import { ProjectContextProvider } from "@src/contexts/ProjectContext"; +import { SnackbarProvider } from "@src/contexts/SnackbarContext"; +import { SnackLogProvider } from "@src/contexts/SnackLogContext"; +import routes from "@src/constants/routes"; -import AuthPage from "pages/Auth"; -import SignOutPage from "pages/Auth/SignOut"; -import SignUpPage from "pages/Auth/SignUp"; -import DeployPage from "pages/Deploy"; -import TestPage from "pages/Test"; -import RowyRunTestPage from "pages/RowyRunTest"; -import PageNotFound from "pages/PageNotFound"; +import AuthPage from "@src/pages/Auth"; +import SignOutPage from "@src/pages/Auth/SignOut"; +import SignUpPage from "@src/pages/Auth/SignUp"; +import DeployPage from "@src/pages/Deploy"; +import TestPage from "@src/pages/Test"; +import RowyRunTestPage from "@src/pages/RowyRunTest"; +import PageNotFound from "@src/pages/PageNotFound"; -import Favicon from "assets/Favicon"; -import "analytics"; +import Favicon from "@src/assets/Favicon"; +import "@src/analytics"; // prettier-ignore -const AuthSetupGuidePage = lazy(() => import("pages/Auth/SetupGuide" /* webpackChunkName: "AuthSetupGuide" */)); +const AuthSetupGuidePage = lazy(() => import("@src/pages/Auth/SetupGuide" /* webpackChunkName: "AuthSetupGuide" */)); // prettier-ignore const ImpersonatorAuthPage = lazy(() => import("./pages/Auth/ImpersonatorAuth" /* webpackChunkName: "ImpersonatorAuthPage" */)); // prettier-ignore @@ -51,7 +51,7 @@ const UserSettingsPage = lazy(() => import("./pages/Settings/UserSettings" /* we // prettier-ignore const UserManagementPage = lazy(() => import("./pages/Settings/UserManagement" /* webpackChunkName: "UserManagementPage" */)); // prettier-ignore -const SetupPage = lazy(() => import("pages/Setup" /* webpackChunkName: "SetupPage" */)); +const SetupPage = lazy(() => import("@src/pages/Setup" /* webpackChunkName: "SetupPage" */)); export default function App() { return ( diff --git a/src/assets/BrandedBackground.tsx b/src/assets/BrandedBackground.tsx index 0d506fef..febb3cd2 100644 --- a/src/assets/BrandedBackground.tsx +++ b/src/assets/BrandedBackground.tsx @@ -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(); diff --git a/src/assets/icons/ResizeBottomRight.tsx b/src/assets/icons/ResizeBottomRight.tsx new file mode 100644 index 00000000..83e967bc --- /dev/null +++ b/src/assets/icons/ResizeBottomRight.tsx @@ -0,0 +1,10 @@ +import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon"; +import { mdiResizeBottomRight } from "@mdi/js"; + +export default function ResizeBottomRight(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/assets/icons/UpdatedAt.tsx b/src/assets/icons/UpdatedAt.tsx index 754a89ca..2652282e 100644 --- a/src/assets/icons/UpdatedAt.tsx +++ b/src/assets/icons/UpdatedAt.tsx @@ -1,9 +1,10 @@ import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon"; +import { mdiClockEditOutline } from "@mdi/js"; export default function UpdatedAt(props: SvgIconProps) { return ( - + ); } diff --git a/src/assets/icons/Webhook.tsx b/src/assets/icons/Webhook.tsx new file mode 100644 index 00000000..3e0a79a0 --- /dev/null +++ b/src/assets/icons/Webhook.tsx @@ -0,0 +1,10 @@ +import SvgIcon, { SvgIconProps } from "@mui/material/SvgIcon"; +import { mdiWebhook } from "@mdi/js"; + +export default function Webhook(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/assets/service-account.mp4 b/src/assets/service-account.mp4 index 0e9a2c3b..99054667 100644 Binary files a/src/assets/service-account.mp4 and b/src/assets/service-account.mp4 differ diff --git a/src/components/Auth/AuthLayout.tsx b/src/components/Auth/AuthLayout.tsx index 04a0ad88..858fd16c 100644 --- a/src/components/Auth/AuthLayout.tsx +++ b/src/components/Auth/AuthLayout.tsx @@ -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; diff --git a/src/components/Auth/FirebaseUi.tsx b/src/components/Auth/FirebaseUi.tsx index 4b58335e..2870fc32 100644 --- a/src/components/Auth/FirebaseUi.tsx +++ b/src/components/Auth/FirebaseUi.tsx @@ -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({ diff --git a/src/components/Auth/MarketingBanner.tsx b/src/components/Auth/MarketingBanner.tsx index 20021334..ea48c865 100644 --- a/src/components/Auth/MarketingBanner.tsx +++ b/src/components/Auth/MarketingBanner.tsx @@ -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 ( diff --git a/src/components/CircularProgressOptical.tsx b/src/components/CircularProgressOptical.tsx new file mode 100644 index 00000000..be71800d --- /dev/null +++ b/src/components/CircularProgressOptical.tsx @@ -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 ( + + ); +} diff --git a/src/components/CodeEditor.tsx b/src/components/CodeEditor.tsx deleted file mode 100644 index 0c02862b..00000000 --- a/src/components/CodeEditor.tsx +++ /dev/null @@ -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>; - 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(); - - 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 ( -
- -
- ); -} diff --git a/src/components/CodeEditorHelper.tsx b/src/components/CodeEditor/CodeEditorHelper.tsx similarity index 91% rename from src/components/CodeEditorHelper.tsx rename to src/components/CodeEditor/CodeEditorHelper.tsx index 553f2994..4c79f215 100644 --- a/src/components/CodeEditorHelper.tsx +++ b/src/components/CodeEditor/CodeEditorHelper.tsx @@ -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 }} > - - You can access: + + Available: @@ -63,6 +63,7 @@ export default function CodeEditorHelper({ + ); +} diff --git a/src/components/CodeEditor/extensions.d.ts b/src/components/CodeEditor/extensions.d.ts new file mode 100644 index 00000000..7f357e8d --- /dev/null +++ b/src/components/CodeEditor/extensions.d.ts @@ -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); + +// 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; + +type SendgridEmailBody = (context: ExtensionContext) => Promise; + +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; diff --git a/public/auth.d.ts b/src/components/CodeEditor/firebaseAuth.d.ts similarity index 100% rename from public/auth.d.ts rename to src/components/CodeEditor/firebaseAuth.d.ts diff --git a/public/storage.d.ts b/src/components/CodeEditor/firebaseStorage.d.ts similarity index 100% rename from public/storage.d.ts rename to src/components/CodeEditor/firebaseStorage.d.ts diff --git a/public/firestore.d.ts b/src/components/CodeEditor/firestore.d.ts similarity index 99% rename from public/firestore.d.ts rename to src/components/CodeEditor/firestore.d.ts index c840e33d..dc561cff 100644 --- a/public/firestore.d.ts +++ b/src/components/CodeEditor/firestore.d.ts @@ -844,9 +844,9 @@ declare namespace FirebaseFirestore { * `exists` property will always be true and `data()` will never return * 'undefined'. */ - export class QueryDocumentSnapshot extends DocumentSnapshot< - T - > { + export class QueryDocumentSnapshot< + T = DocumentData + > extends DocumentSnapshot { private constructor(); /** diff --git a/src/components/CodeEditor/github-dark-default.json b/src/components/CodeEditor/github-dark-default.json new file mode 100644 index 00000000..c4dd4815 --- /dev/null +++ b/src/components/CodeEditor/github-dark-default.json @@ -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": [] +} diff --git a/src/components/CodeEditor/github-light-default.json b/src/components/CodeEditor/github-light-default.json new file mode 100644 index 00000000..e27c1a01 --- /dev/null +++ b/src/components/CodeEditor/github-light-default.json @@ -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": [] +} diff --git a/src/components/CodeEditor/index.tsx b/src/components/CodeEditor/index.tsx new file mode 100644 index 00000000..742c9347 --- /dev/null +++ b/src/components/CodeEditor/index.tsx @@ -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, + Omit { + value: string; + containerProps?: Partial; + + 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 ( + + + } + 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, + }} + /> + + setFullScreen((f) => !f)} + active={fullScreen} + /> + + {!fullScreen && ( + + )} + + + ); +} diff --git a/src/components/CodeEditor/useMonacoCustomizations.ts b/src/components/CodeEditor/useMonacoCustomizations.ts new file mode 100644 index 00000000..74931c6c --- /dev/null +++ b/src/components/CodeEditor/useMonacoCustomizations.ts @@ -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 = { + 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 }; +} diff --git a/src/components/CodeEditor/utils.d.ts b/src/components/CodeEditor/utils.d.ts new file mode 100644 index 00000000..38a02852 --- /dev/null +++ b/src/components/CodeEditor/utils.d.ts @@ -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 {} +} diff --git a/src/components/Confirmation.tsx b/src/components/Confirmation.tsx index 5de653a6..9702452b 100644 --- a/src/components/Confirmation.tsx +++ b/src/components/Confirmation.tsx @@ -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({ diff --git a/src/components/ConfirmationDialog/Dialog.tsx b/src/components/ConfirmationDialog/Dialog.tsx index 799ce336..d32a6088 100644 --- a/src/components/ConfirmationDialog/Dialog.tsx +++ b/src/components/ConfirmationDialog/Dialog.tsx @@ -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({ - + {!hideCancel && ( + + )} - - - )} - - ); -} diff --git a/src/components/ConnectServiceSelect/index.tsx b/src/components/ConnectServiceSelect/index.tsx deleted file mode 100644 index 4bcb56bf..00000000 --- a/src/components/ConnectServiceSelect/index.tsx +++ /dev/null @@ -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; - docRef: firebase.default.firestore.DocumentReference; -} - -export default function ConnectServiceSelect({ - value = [], - className, - TextFieldProps = {}, - ...props -}: IConnectServiceSelectProps) { - const classes = useStyles(); - - const sanitisedValue = Array.isArray(value) ? value : []; - - return ( - `${(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, - }, - }} - > - - }> - - - - - ); -} diff --git a/src/components/ConnectServiceSelect/styles.ts b/src/components/ConnectServiceSelect/styles.ts deleted file mode 100644 index 72b9b4cd..00000000 --- a/src/components/ConnectServiceSelect/styles.ts +++ /dev/null @@ -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; diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index f3e60c26..53da9f0c 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -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 ( - - {this.state.errorMessage} - {this.state.errorMessage.startsWith("Loading chunk") ? ( + if (this.state.errorMessage.startsWith("Loading chunk")) + return ( + + Reload this page to get the latest update - ) : ( - - )} + + } + fullScreen + /> + ); + + return ( + + {this.state.errorMessage} + } fullScreen diff --git a/src/components/FloatingSearch.tsx b/src/components/FloatingSearch.tsx index 6783f6dc..83ddf636 100644 --- a/src/components/FloatingSearch.tsx +++ b/src/components/FloatingSearch.tsx @@ -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 { label: string; diff --git a/src/components/HelperText.tsx b/src/components/HelperText.tsx index dab222ac..714e0255 100644 --- a/src/components/HelperText.tsx +++ b/src/components/HelperText.tsx @@ -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, }} /> diff --git a/src/components/Home/AccessDenied.tsx b/src/components/Home/AccessDenied.tsx index 4af8d5ce..5c6b9024 100644 --- a/src/components/Home/AccessDenied.tsx +++ b/src/components/Home/AccessDenied.tsx @@ -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() { diff --git a/src/components/Home/HomeWelcomePrompt.tsx b/src/components/Home/HomeWelcomePrompt.tsx index f4212070..d8220549 100644 --- a/src/components/Home/HomeWelcomePrompt.tsx +++ b/src/components/Home/HomeWelcomePrompt.tsx @@ -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, }} > - - Welcome! -
- Create a table to get started. + + Get started - - Tables connect to your Firestore collections and display their data. + + Create a table from a new or existing Firestore collection diff --git a/src/components/Home/TableGrid/TableCard.tsx b/src/components/Home/TableGrid/TableCard.tsx index e2e76d6c..0b8ae9ff 100644 --- a/src/components/Home/TableGrid/TableCard.tsx +++ b/src/components/Home/TableGrid/TableCard.tsx @@ -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; diff --git a/src/components/Home/TableGrid/TableGridSkeleton.tsx b/src/components/Home/TableGrid/TableGridSkeleton.tsx index 8f9dd62c..ae3ec490 100644 --- a/src/components/Home/TableGrid/TableGridSkeleton.tsx +++ b/src/components/Home/TableGrid/TableGridSkeleton.tsx @@ -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() { diff --git a/src/components/Home/TableGrid/index.tsx b/src/components/Home/TableGrid/index.tsx index 493116b2..05fc1326 100644 --- a/src/components/Home/TableGrid/index.tsx +++ b/src/components/Home/TableGrid/index.tsx @@ -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; diff --git a/src/components/Home/TableList/TableListItem.tsx b/src/components/Home/TableList/TableListItem.tsx index a427f30f..327c48c6 100644 --- a/src/components/Home/TableList/TableListItem.tsx +++ b/src/components/Home/TableList/TableListItem.tsx @@ -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; diff --git a/src/components/Home/TableList/TableListSkeleton.tsx b/src/components/Home/TableList/TableListSkeleton.tsx index 5bb44174..8289032d 100644 --- a/src/components/Home/TableList/TableListSkeleton.tsx +++ b/src/components/Home/TableList/TableListSkeleton.tsx @@ -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() { diff --git a/src/components/Home/TableList/index.tsx b/src/components/Home/TableList/index.tsx index 991f2555..f8247fdf 100644 --- a/src/components/Home/TableList/index.tsx +++ b/src/components/Home/TableList/index.tsx @@ -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; diff --git a/src/components/InlineOpenInNewIcon.tsx b/src/components/InlineOpenInNewIcon.tsx index 33869d4c..fabcea43 100644 --- a/src/components/InlineOpenInNewIcon.tsx +++ b/src/components/InlineOpenInNewIcon.tsx @@ -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", diff --git a/src/components/KeyValueInput.tsx b/src/components/KeyValueInput.tsx new file mode 100644 index 00000000..da043790 --- /dev/null +++ b/src/components/KeyValueInput.tsx @@ -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; + onChange: (value: Record) => 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) + ); + }; + + 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) => + setValue((v) => { + const newValue = [...v]; + newValue[i][j] = e.target.value; + saveValue(newValue); + return newValue; + }); + + return ( + + + {label} + + + + {value.map(([propKey, propValue], i) => ( + + + + + + + + ))} + + + + + ); +} diff --git a/src/components/Loading.tsx b/src/components/Loading.tsx index fbe4127e..91a5592e 100644 --- a/src/components/Loading.tsx +++ b/src/components/Loading.tsx @@ -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 { message?: string; fullScreen?: boolean; @@ -33,7 +29,7 @@ export default function Loading({ ...props.style, }} > - + 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]), + ]} /> )} diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx index e8a55dae..9ce60cee 100644 --- a/src/components/Modal/index.tsx +++ b/src/components/Modal/index.tsx @@ -22,8 +22,9 @@ import ScrollableDialogContent, { } from "./ScrollableDialogContent"; export interface IModalProps extends Partial> { - onClose: () => void; + onClose: (setOpen: React.Dispatch>) => void; disableBackdropClick?: boolean; + disableEscapeKeyDown?: boolean; title: ReactNode; header?: ReactNode; @@ -45,6 +46,7 @@ export interface IModalProps extends Partial> { 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 = (_, 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 ( diff --git a/src/components/Navigation/Breadcrumbs.tsx b/src/components/Navigation/Breadcrumbs.tsx index b12898f8..de2fd7e6 100644 --- a/src/components/Navigation/Breadcrumbs.tsx +++ b/src/components/Navigation/Breadcrumbs.tsx @@ -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(); diff --git a/src/components/Navigation/NavDrawer.tsx b/src/components/Navigation/NavDrawer.tsx index b3371efa..2bcdeb14 100644 --- a/src/components/Navigation/NavDrawer.tsx +++ b/src/components/Navigation/NavDrawer.tsx @@ -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({ + )} diff --git a/src/components/Navigation/NavTableSection.tsx b/src/components/Navigation/NavTableSection.tsx index 3a4d2323..3f733358 100644 --- a/src/components/Navigation/NavTableSection.tsx +++ b/src/components/Navigation/NavTableSection.tsx @@ -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; diff --git a/src/components/Navigation/UpdateCheckBadge.tsx b/src/components/Navigation/UpdateCheckBadge.tsx new file mode 100644 index 00000000..e41ade67 --- /dev/null +++ b/src/components/Navigation/UpdateCheckBadge.tsx @@ -0,0 +1,22 @@ +import { Badge, BadgeProps } from "@mui/material"; +import useUpdateCheck from "@src/hooks/useUpdateCheck"; + +export default function UpdateCheckBadge(props: Partial) { + const [latestUpdate] = useUpdateCheck(); + + if (!latestUpdate.rowy && !latestUpdate.rowyRun) return <>{props.children}; + + return ( + + ); +} diff --git a/src/components/Navigation/UserMenu.tsx b/src/components/Navigation/UserMenu.tsx index dc1d1976..6cdc74c7 100644 --- a/src/components/Navigation/UserMenu.tsx +++ b/src/components/Navigation/UserMenu.tsx @@ -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(null); const [open, setOpen] = useState(false); const [themeSubMenu, setThemeSubMenu] = useState(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) { + {email} +
+ Project: {projectId} + + } primaryTypographyProps={{ variant: "subtitle1" }} /> diff --git a/src/components/Navigation/index.tsx b/src/components/Navigation/index.tsx index 1284a3b5..26b1ceb4 100644 --- a/src/components/Navigation/index.tsx +++ b/src/components/Navigation/index.tsx @@ -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" > - + {userRoles.includes("ADMIN") ? ( + + + + ) : ( + + )} )} diff --git a/src/components/RichTextEditor.tsx b/src/components/RichTextEditor.tsx index 051edd36..a0f4111a 100644 --- a/src/components/RichTextEditor.tsx +++ b/src/components/RichTextEditor.tsx @@ -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`, }, }, diff --git a/src/components/RichTooltip.tsx b/src/components/RichTooltip.tsx index 5c333fc3..b699efe1 100644 --- a/src/components/RichTooltip.tsx +++ b/src/components/RichTooltip.tsx @@ -81,6 +81,10 @@ export interface IRichTooltipProps message?: React.ReactNode; dismissButtonText?: React.ReactNode; dismissButtonProps?: Partial; + 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 ( } + PopperProps={{ + modifiers: [ + { + name: "preventOverflow", + enabled: true, + options: { + altAxis: true, + altBoundary: true, + tether: false, + rootBoundary: "document", + padding: 8, + }, + }, + ], + }} {...props} > {render({ openTooltip, closeTooltip, toggleTooltip })} diff --git a/src/components/SectionHeading.tsx b/src/components/SectionHeading.tsx index ab90df48..965b9f22 100644 --- a/src/components/SectionHeading.tsx +++ b/src/components/SectionHeading.tsx @@ -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 { children: string; diff --git a/src/components/Settings/ProjectSettings/About.tsx b/src/components/Settings/ProjectSettings/About.tsx index ec38e045..4cdd6c5c 100644 --- a/src/components/Settings/ProjectSettings/About.tsx +++ b/src/components/Settings/ProjectSettings/About.tsx @@ -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(); - const [latestUpdate, setLatestUpdate] = useLatestUpdateState>(null); - - const [checkState, setCheckState] = useState( - 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() {
- {checkState === "LOADING" ? ( + {loading ? ( Checking for updates… - ) : latestUpdate === null ? ( + ) : latestUpdate.rowy === null ? ( Up to date ) : ( + Update available:{" "} - {latestUpdate.tag_name} + {latestUpdate.rowy.tag_name} @@ -156,11 +105,8 @@ export default function About() { - {latestUpdate === null ? ( - + {latestUpdate.rowy === null ? ( + Check for updates ) : ( diff --git a/src/components/Settings/ProjectSettings/Authentication.tsx b/src/components/Settings/ProjectSettings/Authentication.tsx index f4a7af5c..d0759c2a 100644 --- a/src/components/Settings/ProjectSettings/Authentication.tsx +++ b/src/components/Settings/ProjectSettings/Authentication.tsx @@ -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, diff --git a/src/components/Settings/ProjectSettings/Customization.tsx b/src/components/Settings/ProjectSettings/Customization.tsx index 53fe2c30..92c7fa61 100644 --- a/src/components/Settings/ProjectSettings/Customization.tsx +++ b/src/components/Settings/ProjectSettings/Customization.tsx @@ -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, diff --git a/src/components/Settings/ProjectSettings/RowyRun.tsx b/src/components/Settings/ProjectSettings/RowyRun.tsx index 5d51d20d..901adf51 100644 --- a/src/components/Settings/ProjectSettings/RowyRun.tsx +++ b/src/components/Settings/ProjectSettings/RowyRun.tsx @@ -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(); - const [latestUpdate, setLatestUpdate] = useLatestUpdateState>(null); - - const [checkState, setCheckState] = useState( - 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({ ) ) : ( - ); @@ -168,7 +93,7 @@ export default function RowyRun({ such as table action scripts, user management, and easy Cloud Function deployment.{" "} @@ -183,35 +108,42 @@ export default function RowyRun({
- {checkState === "LOADING" ? ( + {loading ? ( Checking for updates… - ) : latestUpdate === null ? ( + ) : latestUpdate.rowyRun === null ? ( Up to date ) : ( + Update available:{" "} - {latestUpdate.tag_name} + {latestUpdate.rowyRun.tag_name} )} - {name} Run v{version} + {name} Run v{latestUpdate.deployedRowyRun} - {latestUpdate === null ? ( - + {latestUpdate.rowyRun === null ? ( + Check for updates ) : ( @@ -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 ? ( + <> + +   + {name} Run is set up correctly + + ) : verified === false ? ( + `${name} Run is not set up correctly` + ) : ( + " " + ) } /> diff --git a/src/components/Settings/SettingsSection.tsx b/src/components/Settings/SettingsSection.tsx index d24f1fcc..f4b69303 100644 --- a/src/components/Settings/SettingsSection.tsx +++ b/src/components/Settings/SettingsSection.tsx @@ -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; diff --git a/src/components/Settings/ThemeColorPicker.tsx b/src/components/Settings/ThemeColorPicker.tsx index c22900d9..441cb976 100644 --- a/src/components/Settings/ThemeColorPicker.tsx +++ b/src/components/Settings/ThemeColorPicker.tsx @@ -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"; diff --git a/src/components/Settings/UserManagement/InviteUser.tsx b/src/components/Settings/UserManagement/InviteUser.tsx index 0c581c75..0f534dcd 100644 --- a/src/components/Settings/UserManagement/InviteUser.tsx +++ b/src/components/Settings/UserManagement/InviteUser.tsx @@ -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(); diff --git a/src/components/Settings/UserManagement/UserItem.tsx b/src/components/Settings/UserManagement/UserItem.tsx index 3fdc086c..6a373d84 100644 --- a/src/components/Settings/UserManagement/UserItem.tsx +++ b/src/components/Settings/UserManagement/UserItem.tsx @@ -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(); diff --git a/src/components/Settings/UserSettings/Account.tsx b/src/components/Settings/UserSettings/Account.tsx index 96e6958b..a65b1a39 100644 --- a/src/components/Settings/UserSettings/Account.tsx +++ b/src/components/Settings/UserSettings/Account.tsx @@ -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 ( diff --git a/src/components/Settings/UserSettings/Personalization.tsx b/src/components/Settings/UserSettings/Personalization.tsx index b461d28f..8d9da6f5 100644 --- a/src/components/Settings/UserSettings/Personalization.tsx +++ b/src/components/Settings/UserSettings/Personalization.tsx @@ -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 ( <> - { - updateSettings({ - theme: _merge(settings.theme, { - dark: { palette: { darker: e.target.checked } }, - }), - }); - }} - /> - } - label="Darker dark theme" - sx={{ my: -10 / 8 }} - /> - } label="Customize theme colors" - style={{ marginLeft: -11, marginBottom: -10 }} + style={{ marginLeft: -11, marginBottom: -10, marginTop: -10 }} /> diff --git a/src/components/Settings/UserSettings/Theme.tsx b/src/components/Settings/UserSettings/Theme.tsx index 999d8143..f6ae12ff 100644 --- a/src/components/Settings/UserSettings/Theme.tsx +++ b/src/components/Settings/UserSettings/Theme.tsx @@ -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 ( - - Theme + <> + + Theme - { - if (e.target.value === "system") { - setThemeOverridden(false); - } else { - setTheme(e.target.value as typeof theme); - setThemeOverridden(true); - } - }} - > - } - value="system" - label="Match system theme" - /> - } value="light" label="Light" /> - } value="dark" label="Dark" /> - - + { + if (e.target.value === "system") { + setThemeOverridden(false); + } else { + setTheme(e.target.value as typeof theme); + setThemeOverridden(true); + } + }} + > + } + value="system" + label="Match system theme" + /> + } value="light" label="Light" /> + } value="dark" label="Dark" /> + + + + + + { + updateSettings({ + theme: _merge(settings.theme, { + dark: { palette: { darker: e.target.checked } }, + }), + }); + }} + /> + } + label="Darker dark theme" + style={{ marginLeft: -11, marginBottom: -10, marginTop: 13 }} + /> + ); } diff --git a/src/components/Setup/SetupItem.tsx b/src/components/Setup/SetupItem.tsx index 43be2dd6..ea099950 100644 --- a/src/components/Setup/SetupItem.tsx +++ b/src/components/Setup/SetupItem.tsx @@ -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" ? ( ) : status === "loading" ? ( - + ) : ( )} - + {title} {children} diff --git a/src/components/Setup/Step0Welcome.tsx b/src/components/Setup/Step0Welcome.tsx index 10c47253..da1c6d0b 100644 --- a/src/components/Setup/Step0Welcome.tsx +++ b/src/components/Setup/Step0Welcome.tsx @@ -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, diff --git a/src/components/Setup/Step1RowyRun.tsx b/src/components/Setup/Step1RowyRun.tsx index 8ce6eb1e..250e7515 100644 --- a/src/components/Setup/Step1RowyRun.tsx +++ b/src/components/Setup/Step1RowyRun.tsx @@ -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({ /> ) : ( - @@ -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; diff --git a/src/components/Setup/Step2ServiceAccount.tsx b/src/components/Setup/Step2ServiceAccount.tsx index 43036836..3e193e84 100644 --- a/src/components/Setup/Step2ServiceAccount.tsx +++ b/src/components/Setup/Step2ServiceAccount.tsx @@ -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>({}); + // const [roles, setRoles] = useState>({}); 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, }); diff --git a/src/components/Setup/Step3ProjectOwner.tsx b/src/components/Setup/Step3ProjectOwner.tsx index e102058a..bd3b5670 100644 --- a/src/components/Setup/Step3ProjectOwner.tsx +++ b/src/components/Setup/Step3ProjectOwner.tsx @@ -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, }); diff --git a/src/components/Setup/Step4Rules.tsx b/src/components/Setup/Step4Rules.tsx index 4ceecd06..14666220 100644 --- a/src/components/Setup/Step4Rules.tsx +++ b/src/components/Setup/Step4Rules.tsx @@ -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 ( <> @@ -126,95 +161,105 @@ export default function Step4Rules({ sx={{ "&&": { ml: -11 / 8, mb: -11 / 8 }, width: "100%" }} /> - + + We removed an insecure rule that allows anyone to access any part + of your database + - "& .comment": { color: "info.dark" }, - }} - dangerouslySetInnerHTML={{ - __html: rules.replace( - /(\/\/.*$)/gm, - `$1` - ), - }} + - - + Please verify the new rules first. + + + Set Firestore Rules + + {rulesStatus !== "LOADING" && typeof rulesStatus === "string" && ( + + {rulesStatus} + + )} + {!showManualMode && ( + setShowManualMode(true)} + > + Alternatively, add these rules in the Firebase Console + + )} )} - {!hasRules && ( + {!hasRules && showManualMode && ( - You can add these rules{" "} - - in the Firebase Console - - {" "} - or directly below: - - } + title="Alternatively, you can add these rules in the Firebase Console." > - setNewRules(e.target.value)} - multiline - rows={5} - fullWidth + $1` + ), }} /> - - Please check the generated rules first. - +
+ + + + - - Set Firestore Rules - - - {rulesStatus !== "LOADING" && typeof rulesStatus === "string" && ( - - {rulesStatus} - - )} + + + + +
)} @@ -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); diff --git a/src/components/Setup/Step5Migrate.tsx b/src/components/Setup/Step5Migrate.tsx index f6ff0110..64488109 100644 --- a/src/components/Setup/Step5Migrate.tsx +++ b/src/components/Setup/Step5Migrate.tsx @@ -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, diff --git a/src/components/Setup/Step6Finish.tsx b/src/components/Setup/Step6Finish.tsx index c059422d..aaa0b0eb 100644 --- a/src/components/Setup/Step6Finish.tsx +++ b/src/components/Setup/Step6Finish.tsx @@ -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) => { diff --git a/src/components/SideDrawer/Form/Autosave.tsx b/src/components/SideDrawer/Form/Autosave.tsx index 75d2c739..bd69748e 100644 --- a/src/components/SideDrawer/Form/Autosave.tsx +++ b/src/components/SideDrawer/Form/Autosave.tsx @@ -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; diff --git a/src/components/SideDrawer/Form/FieldWrapper.tsx b/src/components/SideDrawer/Form/FieldWrapper.tsx index ceb93c89..a6952631 100644 --- a/src/components/SideDrawer/Form/FieldWrapper.tsx +++ b/src/components/SideDrawer/Form/FieldWrapper.tsx @@ -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; diff --git a/src/components/SideDrawer/Form/index.tsx b/src/components/SideDrawer/Form/index.tsx index 7852882e..a64facfc 100644 --- a/src/components/SideDrawer/Form/index.tsx +++ b/src/components/SideDrawer/Form/index.tsx @@ -2,20 +2,24 @@ import { createElement, useEffect } from "react"; import { useForm } from "react-hook-form"; import _sortBy from "lodash/sortBy"; import _isEmpty from "lodash/isEmpty"; -import _mapValues from "lodash/mapValues"; -import firebase from "firebase/app"; +import createPersistedState from "use-persisted-state"; -import { Stack } from "@mui/material"; +import { Stack, FormControlLabel, Switch } from "@mui/material"; import { Values } from "./utils"; -import { getFieldProp } from "components/fields"; -import { IFieldConfig } from "components/fields/types"; +import { getFieldProp } from "@src/components/fields"; +import { IFieldConfig } from "@src/components/fields/types"; import Autosave from "./Autosave"; import Reset from "./Reset"; import FieldWrapper from "./FieldWrapper"; -import { useAppContext } from "contexts/AppContext"; -import { useProjectContext } from "contexts/ProjectContext"; +import { useAppContext } from "@src/contexts/AppContext"; +import { useProjectContext } from "@src/contexts/ProjectContext"; +import { sanitizeFirestoreRefs } from "@src/utils/fns"; + +const useSideDrawerShowHiddenFieldsState = createPersistedState( + "__ROWY__SIDE_DRAWER_SHOW_HIDDEN_FIELDS" +); export interface IFormProps { values: Values; @@ -24,12 +28,18 @@ export interface IFormProps { export default function Form({ values }: IFormProps) { const { tableState, sideDrawerRef } = useProjectContext(); const { userDoc } = useAppContext(); - const userDocHiddenFields = - userDoc.state.doc?.tables?.[`${tableState!.tablePath}`]?.hiddenFields ?? []; - const fields = _sortBy(Object.values(tableState!.columns), "index").filter( - (f) => !userDocHiddenFields.includes(f.name) - ); + const userDocHiddenFields = + userDoc.state.doc?.tables?.[`${tableState!.config.id}`]?.hiddenFields ?? []; + + const [showHiddenFields, setShowHiddenFields] = + useSideDrawerShowHiddenFieldsState(false); + + const fields = showHiddenFields + ? _sortBy(Object.values(tableState!.columns), "index") + : _sortBy(Object.values(tableState!.columns), "index").filter( + (f) => !userDocHiddenFields.includes(f.key) + ); // Get initial values from fields config. This won’t be written to the db // when the SideDrawer is opened. Only dirty fields will be written @@ -38,21 +48,15 @@ export default function Form({ values }: IFormProps) { {} ); const { ref: docRef, ...rowValues } = values; - const safeRowValues = _mapValues(rowValues, (v) => { - // If react-hook-form receives a Firestore document reference, it tries to - // clone firebase.firestore and exceeds maximum call stack size. - if (firebase.firestore.DocumentReference.prototype.isPrototypeOf(v)) - return v.path; - return v; - }); + const safeRowValues = sanitizeFirestoreRefs(rowValues); const defaultValues = { ...initialValues, ...safeRowValues }; const methods = useForm({ mode: "onBlur", defaultValues }); const { control, reset, formState, getValues } = methods; const { dirtyFields } = formState; + const column = sideDrawerRef?.current?.cell?.column; useEffect(() => { - const column = sideDrawerRef?.current?.cell?.column; if (!column) return; const labelElem = document.getElementById( @@ -65,7 +69,7 @@ export default function Form({ values }: IFormProps) { if (labelElem) labelElem.scrollIntoView({ behavior: "smooth" }); if (fieldElem) fieldElem.focus({ preventScroll: true }); }, 200); - }, [sideDrawerRef?.current]); + }, [column]); return (
@@ -128,6 +132,24 @@ export default function Form({ values }: IFormProps) { label="Document path" debugText={values.ref?.path ?? values.id ?? "No ref"} /> + + {userDocHiddenFields.length > 0 && ( + setShowHiddenFields(e.target.checked)} + /> + } + sx={{ + borderTop: 1, + borderColor: "divider", + pt: 3, + "& .MuiSwitch-root": { ml: -0.5 }, + }} + /> + )} ); diff --git a/src/components/SideDrawer/Form/utils.ts b/src/components/SideDrawer/Form/utils.ts index 86242961..c7ff6e37 100644 --- a/src/components/SideDrawer/Form/utils.ts +++ b/src/components/SideDrawer/Form/utils.ts @@ -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 { diff --git a/src/components/SideDrawer/index.tsx b/src/components/SideDrawer/index.tsx index 0cd164b0..a7ebedad 100644 --- a/src/components/SideDrawer/index.tsx +++ b/src/components/SideDrawer/index.tsx @@ -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 }); diff --git a/src/components/SideDrawer/useStyles.ts b/src/components/SideDrawer/useStyles.ts index 3595aa3f..ff1eb50c 100644 --- a/src/components/SideDrawer/useStyles.ts +++ b/src/components/SideDrawer/useStyles.ts @@ -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({ diff --git a/src/components/SnackbarProgress.tsx b/src/components/SnackbarProgress.tsx index 28cf9cd9..5ff8f64f 100644 --- a/src/components/SnackbarProgress.tsx +++ b/src/components/SnackbarProgress.tsx @@ -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>; @@ -31,12 +33,11 @@ export default function SnackbarProgress({ {progress}/{target} -
); diff --git a/src/components/SteppedAccordion.tsx b/src/components/SteppedAccordion.tsx new file mode 100644 index 00000000..932bc10e --- /dev/null +++ b/src/components/SteppedAccordion.tsx @@ -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 { + steps: { + id: string; + title: React.ReactNode; + optional?: boolean; + content: React.ReactNode; + + stepProps?: Partial; + titleProps?: Partial; + contentProps?: Partial; + }[]; +} + +export default function SteppedAccordion({ + steps, + ...props +}: ISteppedAccordionProps) { + const [activeStep, setActiveStep] = useState(steps[0].id); + + return ( + 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, + }) => ( + + setActiveStep((s) => (s === id ? "" : id))} + optional={ + optional && Optional + } + {...titleProps} + > + {title} + + + + {content} + + ) + )} + + ); +} diff --git a/src/components/Table/BulkActions/index.tsx b/src/components/Table/BulkActions/index.tsx index 686182a3..cbea8cab 100644 --- a/src/components/Table/BulkActions/index.tsx +++ b/src/components/Table/BulkActions/index.tsx @@ -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(); - 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: ( + + ), + } + ); + 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 }) { + {/* + {`${actionColumns.length} action${ + actionColumns.length !== 1 ? "s" : "" + }`} + */} prop !== "error" })( ({ theme, ...props }) => ({ diff --git a/src/components/Table/ColumnHeader.tsx b/src/components/Table/ColumnHeader.tsx index 817ab7a7..e407c3d7 100644 --- a/src/components/Table/ColumnHeader.tsx +++ b/src/components/Table/ColumnHeader.tsx @@ -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) => diff --git a/src/components/Table/ColumnMenu/FieldSettings/DefaultValueInput.tsx b/src/components/Table/ColumnMenu/FieldSettings/DefaultValueInput.tsx index 05b45960..78e74b71 100644 --- a/src/components/Table/ColumnMenu/FieldSettings/DefaultValueInput.tsx +++ b/src/components/Table/ColumnMenu/FieldSettings/DefaultValueInput.tsx @@ -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" } }, + }, + }} > @@ -78,24 +68,21 @@ export default function DefaultValueInput({ primary="Null" secondary={ <> - Initialise as null. + Initialise as null. } - className={classes.typeSelectItem} /> @@ -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" && ( <> -
+ }> -
+ )} diff --git a/src/components/Table/ColumnMenu/FieldSettings/index.tsx b/src/components/Table/ColumnMenu/FieldSettings/index.tsx index 0012fd21..34121f10 100644 --- a/src/components/Table/ColumnMenu/FieldSettings/index.tsx +++ b/src/components/Table/ColumnMenu/FieldSettings/index.tsx @@ -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 ( }> <> @@ -78,7 +98,10 @@ export default function FieldSettings(props: IMenuModalProps) { > {createElement(customFieldSettings, { config: newConfig, - handleChange, + onChange: handleChange, + fieldName, + onBlur: validateSettings, + errors, })}
)} @@ -93,7 +116,9 @@ export default function FieldSettings(props: IMenuModalProps) { {createElement(rendedFieldSettings, { config: newConfig, - handleChange, + onChange: handleChange, + onBlur: validateSettings, + errors, })} )} @@ -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: ( + <> + Please fix the following settings: +
    + {Object.entries(errors).map(([key, message]) => ( +
  • + {key}: {message} +
  • + ))} +
+ + ), + 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: { diff --git a/src/components/Table/ColumnMenu/FieldsDropdown.tsx b/src/components/Table/ColumnMenu/FieldsDropdown.tsx index 43fc7702..a25020ff 100644 --- a/src/components/Table/ColumnMenu/FieldsDropdown.tsx +++ b/src/components/Table/ColumnMenu/FieldsDropdown.tsx @@ -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 ( ({ @@ -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, }, }} /> diff --git a/src/components/Table/ColumnMenu/NameChange.tsx b/src/components/Table/ColumnMenu/NameChange.tsx index a2320672..28d20af1 100644 --- a/src/components/Table/ColumnMenu/NameChange.tsx +++ b/src/components/Table/ColumnMenu/NameChange.tsx @@ -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, diff --git a/src/components/Table/ColumnMenu/NewColumn.tsx b/src/components/Table/ColumnMenu/NewColumn.tsx index eb4c0e54..b59c0cc8 100644 --- a/src/components/Table/ColumnMenu/NewColumn.tsx +++ b/src/components/Table/ColumnMenu/NewColumn.tsx @@ -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; 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." /> diff --git a/src/components/Table/ColumnMenu/TypeChange.tsx b/src/components/Table/ColumnMenu/TypeChange.tsx index 93a2fafb..9d1e9b1a 100644 --- a/src/components/Table/ColumnMenu/TypeChange.tsx +++ b/src/components/Table/ColumnMenu/TypeChange.tsx @@ -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({ diff --git a/src/components/Table/ColumnMenu/index.tsx b/src/components/Table/ColumnMenu/index.tsx index 3cdd0e01..a9a7ed89 100644 --- a/src/components/Table/ColumnMenu/index.tsx +++ b/src/components/Table/ColumnMenu/index.tsx @@ -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: {} }; diff --git a/src/components/Table/EmptyTable.tsx b/src/components/Table/EmptyTable.tsx index 0fb9944d..2787abc8 100644 --- a/src/components/Table/EmptyTable.tsx +++ b/src/components/Table/EmptyTable.tsx @@ -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(); diff --git a/src/components/Table/Filters/index.tsx b/src/components/Table/Filters/index.tsx index ebf5b932..15e5cc94 100644 --- a/src/components/Table/Filters/index.tsx +++ b/src/components/Table/Filters/index.tsx @@ -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); 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: () => {}, })} diff --git a/src/components/Table/FinalColumnHeader.tsx b/src/components/Table/FinalColumnHeader.tsx index 75eb32a9..cc2cf653 100644 --- a/src/components/Table/FinalColumnHeader.tsx +++ b/src/components/Table/FinalColumnHeader.tsx @@ -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({ diff --git a/src/components/Table/HiddenFields.tsx b/src/components/Table/HiddenFields.tsx index 75a7077b..7676591f 100644 --- a/src/components/Table/HiddenFields.tsx +++ b/src/components/Table/HiddenFields.tsx @@ -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 }, }, }, }); diff --git a/src/components/Table/HotKeys.tsx b/src/components/Table/HotKeys.tsx index 9aaa07e1..c359fd55 100644 --- a/src/components/Table/HotKeys.tsx +++ b/src/components/Table/HotKeys.tsx @@ -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 = () => () => {}; diff --git a/src/components/Table/OutOfOrderIndicator.tsx b/src/components/Table/OutOfOrderIndicator.tsx new file mode 100644 index 00000000..0f3ece9e --- /dev/null +++ b/src/components/Table/OutOfOrderIndicator.tsx @@ -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 ( +
+ } + 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 }) => } + defaultOpen={!dismissed} + onClose={() => setDismissed(true)} + /> +
+ ); +} diff --git a/src/components/Table/Settings/Menu.tsx b/src/components/Table/Settings/Menu.tsx deleted file mode 100644 index 010a03d8..00000000 --- a/src/components/Table/Settings/Menu.tsx +++ /dev/null @@ -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); - const open = Boolean(anchorEl); - - const handleClick = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); - }; - - const handleClose = (option: string) => () => { - setModal(option); - setAnchorEl(null); - }; - - return ( -
- - - - - {options.map((option) => ( - - {option} - - ))} - -
- ); -} diff --git a/src/components/Table/Settings/Webhooks.tsx b/src/components/Table/Settings/Webhooks.tsx deleted file mode 100644 index bb00aa4e..00000000 --- a/src/components/Table/Settings/Webhooks.tsx +++ /dev/null @@ -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 ( - - - Webhooks - - - } - 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 }} - /> - Webhook type - - - {state.type === WebhookTypes.custom && ( - - )} -
- {state.type === WebhookTypes.typeForm && ( - <> - Web hook url: - - {/* {WEBHOOK_URL}?tablePath={tableState?.tablePath} - &type=TYPE_FORM&secret={state.secret} */} - - instructions: - - please set the question reference in typeform to the following - field keys :{" "} - {tableFields.map((key) => ( - <> - {" "} - {key}, - - ))} - - - )} -
- - - - -
-
- ); -} diff --git a/src/components/Table/Settings/index.tsx b/src/components/Table/Settings/index.tsx deleted file mode 100644 index fedfbda9..00000000 --- a/src/components/Table/Settings/index.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { useState } from "react"; -import SettingsMenu from "./Menu"; -//import Webhooks from "./Webhooks"; -export default function Settings() { - const [modal, setModal] = useState(""); - return ( - <> - - {/* { - setModal(""); - }} - /> */} - - ); -} diff --git a/src/components/Table/Skeleton/HeaderRowSkeleton.tsx b/src/components/Table/Skeleton/HeaderRowSkeleton.tsx index 03439c40..ec416c85 100644 --- a/src/components/Table/Skeleton/HeaderRowSkeleton.tsx +++ b/src/components/Table/Skeleton/HeaderRowSkeleton.tsx @@ -1,5 +1,5 @@ import { Fade, Stack, Skeleton, Button } from "@mui/material"; -import AddColumnIcon from "assets/icons/AddColumn"; +import AddColumnIcon from "@src/assets/icons/AddColumn"; const NUM_CELLS = 5; diff --git a/src/components/Table/Skeleton/TableHeaderSkeleton.tsx b/src/components/Table/Skeleton/TableHeaderSkeleton.tsx index 5709b638..e6f12ecb 100644 --- a/src/components/Table/Skeleton/TableHeaderSkeleton.tsx +++ b/src/components/Table/Skeleton/TableHeaderSkeleton.tsx @@ -1,8 +1,8 @@ import { Fade, Stack, Button } from "@mui/material"; import Skeleton from "@mui/material/Skeleton"; -import AddRowIcon from "assets/icons/AddRow"; +import AddRowIcon from "@src/assets/icons/AddRow"; -import { TABLE_HEADER_HEIGHT } from "components/Table/TableHeader"; +import { TABLE_HEADER_HEIGHT } from "@src/components/TableHeader"; const ButtonSkeleton = (props) => ( diff --git a/src/components/Table/TableContainer.tsx b/src/components/Table/TableContainer.tsx new file mode 100644 index 00000000..be070644 --- /dev/null +++ b/src/components/Table/TableContainer.tsx @@ -0,0 +1,197 @@ +import { styled, alpha, darken, lighten } from "@mui/material"; +import { APP_BAR_HEIGHT } from "@src/components/Navigation"; +import { DRAWER_COLLAPSED_WIDTH } from "@src/components/SideDrawer"; + +import { colord, extend } from "colord"; +import mixPlugin from "colord/plugins/lch"; +extend([mixPlugin]); + +export const OUT_OF_ORDER_MARGIN = 8; + +export const TableContainer = styled("div", { + shouldForwardProp: (prop) => prop !== "rowHeight", +})<{ rowHeight: number }>(({ theme, rowHeight }) => ({ + display: "flex", + flexDirection: "column", + height: `calc(100vh - ${APP_BAR_HEIGHT}px)`, + + "& > .rdg": { + width: `calc(100% - ${DRAWER_COLLAPSED_WIDTH}px)`, + flex: 1, + paddingBottom: `max(env(safe-area-inset-bottom), ${theme.spacing(2)})`, + }, + + [theme.breakpoints.down("sm")]: { width: "100%" }, + + "& .rdg": { + "--color": theme.palette.text.primary, + "--border-color": theme.palette.divider, + // "--summary-border-color": "#aaa", + "--background-color": + theme.palette.mode === "light" + ? theme.palette.background.paper + : colord(theme.palette.background.paper) + .mix("#fff", 0.04) + .alpha(1) + .toHslString(), + "--header-background-color": theme.palette.background.default, + "--row-hover-background-color": colord(theme.palette.background.paper) + .mix(theme.palette.action.hover, theme.palette.action.hoverOpacity) + .alpha(1) + .toHslString(), + "--row-selected-background-color": + theme.palette.mode === "light" + ? lighten(theme.palette.primary.main, 0.9) + : darken(theme.palette.primary.main, 0.8), + "--row-selected-hover-background-color": + theme.palette.mode === "light" + ? lighten(theme.palette.primary.main, 0.8) + : darken(theme.palette.primary.main, 0.7), + "--checkbox-color": theme.palette.primary.main, + "--checkbox-focus-color": theme.palette.primary.main, + "--checkbox-disabled-border-color": "#ccc", + "--checkbox-disabled-background-color": "#ddd", + "--selection-color": theme.palette.primary.main, + "--font-size": "0.75rem", + "--cell-padding": theme.spacing(0, 1.25), + + border: "none", + backgroundColor: "transparent", + + ...(theme.typography.caption as any), + // fontSize: "0.8125rem", + lineHeight: "inherit !important", + + "& .rdg-cell": { + display: "flex", + alignItems: "center", + padding: 0, + + overflow: "visible", + contain: "none", + position: "relative", + + lineHeight: "calc(var(--row-height) - 1px)", + }, + + "& .rdg-cell-frozen": { + position: "sticky", + }, + "& .rdg-cell-frozen-last": { + boxShadow: theme.shadows[2] + .replace(/, 0 (\d+px)/g, ", $1 0") + .split("),") + .slice(1) + .join("),"), + + "&[aria-selected=true]": { + boxShadow: + theme.shadows[2] + .replace(/, 0 (\d+px)/g, ", $1 0") + .split("),") + .slice(1) + .join("),") + ", inset 0 0 0 2px var(--selection-color)", + }, + }, + + "& .rdg-cell-copied": { + backgroundColor: + theme.palette.mode === "light" + ? lighten(theme.palette.primary.main, 0.7) + : darken(theme.palette.primary.main, 0.6), + }, + }, + + ".rdg-row, .rdg-header-row": { + marginLeft: `max(env(safe-area-inset-left), ${theme.spacing(2)})`, + marginRight: `env(safe-area-inset-right)`, + }, + + ".rdg-header-row .rdg-cell:first-child": { + borderTopLeftRadius: theme.shape.borderRadius, + }, + + ".rdg-row .rdg-cell:first-child, .rdg-header-row .rdg-cell:first-child": { + borderLeft: "1px solid var(--border-color)", + }, + + ".rdg-row:last-child": { + borderBottomLeftRadius: theme.shape.borderRadius, + borderBottomRightRadius: theme.shape.borderRadius, + + "& .rdg-cell:first-child": { + borderBottomLeftRadius: theme.shape.borderRadius, + }, + "& .rdg-cell:nth-last-child(2)": { + borderBottomRightRadius: theme.shape.borderRadius, + }, + }, + + ".rdg-header-row .rdg-cell": { + borderTop: "1px solid var(--border-color)", + }, + + ".rdg-row:hover": { color: theme.palette.text.primary }, + + ".row-hover-iconButton": { + color: theme.palette.text.disabled, + transitionDuration: "0s", + }, + ".rdg-row:hover .row-hover-iconButton": { + color: theme.palette.text.primary, + backgroundColor: alpha( + theme.palette.action.hover, + theme.palette.action.hoverOpacity * 1.5 + ), + }, + + ".cell-collapse-padding": { + margin: theme.spacing(0, -1.25), + width: `calc(100% + ${theme.spacing(1.25 * 2)})`, + }, + + ".rdg-row.out-of-order": { + "--row-height": rowHeight + 1 + "px !important", + marginTop: -1, + marginBottom: OUT_OF_ORDER_MARGIN, + borderBottomLeftRadius: theme.shape.borderRadius, + + "& .rdg-cell:not(:last-child)": { + borderTop: `1px solid var(--border-color)`, + }, + "& .rdg-cell:first-child": { + borderBottomLeftRadius: theme.shape.borderRadius, + }, + "& .rdg-cell:nth-last-child(2)": { + borderBottomRightRadius: theme.shape.borderRadius, + }, + "&:not(:nth-child(4))": { + borderTopLeftRadius: theme.shape.borderRadius, + + "& .rdg-cell:first-child": { + borderTopLeftRadius: theme.shape.borderRadius, + }, + "& .rdg-cell:nth-last-child(2)": { + borderTopRightRadius: theme.shape.borderRadius, + }, + }, + + "& + .rdg-row:not(.out-of-order)": { + "--row-height": rowHeight + 1 + "px !important", + marginTop: -1, + borderTopLeftRadius: theme.shape.borderRadius, + + "& .rdg-cell:not(:last-child)": { + borderTop: `1px solid var(--border-color)`, + }, + "& .rdg-cell:first-child": { + borderTopLeftRadius: theme.shape.borderRadius, + }, + "& .rdg-cell:nth-last-child(2)": { + borderTopRightRadius: theme.shape.borderRadius, + }, + }, + }, +})); + +export default TableContainer; diff --git a/src/components/Table/TableHeader/Extensions/ExtensionList.tsx b/src/components/Table/TableHeader/Extensions/ExtensionList.tsx deleted file mode 100644 index 267211d0..00000000 --- a/src/components/Table/TableHeader/Extensions/ExtensionList.tsx +++ /dev/null @@ -1,231 +0,0 @@ -import { useState, useRef } from "react"; -import { format, formatRelative } from "date-fns"; - -import { - Stack, - ButtonBase, - List, - ListItem, - ListItemText, - Avatar, - Button, - IconButton, - Menu, - MenuItem, - Switch, - Tooltip, - Typography, -} from "@mui/material"; -import AddIcon from "@mui/icons-material/Add"; -import ExtensionIcon from "assets/icons/Extension"; -import DuplicateIcon from "assets/icons/Copy"; -import EditIcon from "@mui/icons-material/EditOutlined"; -import DeleteIcon from "@mui/icons-material/DeleteOutlined"; - -import EmptyState from "components/EmptyState"; -import { - extensionTypes, - extensionNames, - IExtension, - ExtensionType, -} from "./utils"; -import { DATE_TIME_FORMAT } from "constants/dates"; - -export interface IExtensionListProps { - extensions: IExtension[]; - handleAddExtension: (type: ExtensionType) => void; - handleUpdateActive: (index: number, active: boolean) => void; - handleDuplicate: (index: number) => void; - handleEdit: (index: number) => void; - handleDelete: (index: number) => void; -} - -export default function ExtensionList({ - extensions, - handleAddExtension, - handleUpdateActive, - handleDuplicate, - handleEdit, - handleDelete, -}: IExtensionListProps) { - const [anchorEl, setAnchorEl] = useState(null); - const addButtonRef = useRef(null); - - const activeExtensionCount = extensions.filter( - (extension) => extension.active - ).length; - - const handleAddButton = () => { - setAnchorEl(addButtonRef.current); - }; - - const handleChooseAddType = (type: ExtensionType) => { - handleClose(); - handleAddExtension(type); - }; - - const handleClose = () => { - setAnchorEl(null); - }; - - return ( - <> - - - Extensions ({activeExtensionCount} / {extensions.length}) - - - - - {extensionTypes.map((type) => ( - handleChooseAddType(type)}> - {extensionNames[type]} - - ))} - - - - {extensions.length === 0 ? ( - - - - ) : ( - - {extensions.map((extensionObject, index) => ( - - } - secondaryAction={ - - - - - handleUpdateActive(index, !extensionObject.active) - } - inputProps={{ "aria-label": "Activate" }} - sx={{ mr: 1 }} - /> - - - - handleDuplicate(index)} - > - - - - - handleEdit(index)} - > - - - - - handleDelete(index)} - sx={{ "&&": { mr: -1.5 } }} - > - - - - - - - Last updated -
- by {extensionObject.lastEditor.displayName} -
- at{" "} - {format( - extensionObject.lastEditor.lastUpdate, - DATE_TIME_FORMAT - )} - - } - > - - - {formatRelative( - extensionObject.lastEditor.lastUpdate, - new Date() - )} - - - -
-
- } - /> - ))} -
- )} - - ); -} diff --git a/src/components/Table/TableHeader/Extensions/Step1Triggers.tsx b/src/components/Table/TableHeader/Extensions/Step1Triggers.tsx deleted file mode 100644 index 7e630712..00000000 --- a/src/components/Table/TableHeader/Extensions/Step1Triggers.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { IExtensionModalStepProps } from "./ExtensionModal"; - -import { - FormControl, - FormLabel, - FormGroup, - FormControlLabel, - Checkbox, -} from "@mui/material"; - -import { triggerTypes } from "./utils"; - -export default function Step1Triggers({ - extensionObject, - setExtensionObject, -}: IExtensionModalStepProps) { - return ( - - - Triggers - - - - {triggerTypes.map((trigger) => ( - { - setExtensionObject((extensionObject) => { - if (extensionObject.triggers.includes(trigger)) { - return { - ...extensionObject, - triggers: extensionObject.triggers.filter( - (t) => t !== trigger - ), - }; - } else { - return { - ...extensionObject, - triggers: [...extensionObject.triggers, trigger], - }; - } - }); - }} - /> - } - /> - ))} - - - ); -} diff --git a/src/components/Table/TableHeader/Extensions/Step2RequiredFields.tsx b/src/components/Table/TableHeader/Extensions/Step2RequiredFields.tsx deleted file mode 100644 index 373487cc..00000000 --- a/src/components/Table/TableHeader/Extensions/Step2RequiredFields.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { IExtensionModalStepProps } from "./ExtensionModal"; -import _sortBy from "lodash/sortBy"; - -import MultiSelect from "@rowy/multiselect"; -import { ListItemIcon } from "@mui/material"; - -import { useProjectContext } from "contexts/ProjectContext"; -import { FieldType } from "constants/fields"; -import { getFieldProp } from "components/fields"; - -export default function Step2RequiredFields({ - extensionObject, - setExtensionObject, -}: IExtensionModalStepProps) { - const { tableState } = useProjectContext(); - - return ( - c.type !== FieldType.id) - .map((c) => ({ - value: c.key, - label: c.name, - type: c.type, - })) - : [] - } - onChange={(requiredFields) => - setExtensionObject((e) => ({ ...e, requiredFields })) - } - TextFieldProps={{ autoFocus: true }} - freeText - AddButtonProps={{ children: "Add other field…" }} - AddDialogProps={{ - title: "Add other field", - textFieldLabel: "Field key", - }} - itemRenderer={(option: { - value: string; - label: string; - type?: FieldType; - }) => ( - <> - - {option.type && getFieldProp("icon", option.type)} - - {option.label} - {option.value} - - )} - /> - ); -} diff --git a/src/components/Table/TableHeader/TableLogs.tsx b/src/components/Table/TableHeader/TableLogs.tsx deleted file mode 100644 index 5e2764b0..00000000 --- a/src/components/Table/TableHeader/TableLogs.tsx +++ /dev/null @@ -1,541 +0,0 @@ -import React, { useEffect, useRef, useState } from "react"; -import useRouter from "hooks/useRouter"; -import useCollection from "hooks/useCollection"; -import { useProjectContext } from "contexts/ProjectContext"; -import useStateRef from "react-usestateref"; -import { useSnackLogContext } from "contexts/SnackLogContext"; -import { isCollectionGroup } from "utils/fns"; -import _throttle from "lodash/throttle"; -import { format } from "date-fns"; -import moment from "moment"; - -import { - Chip, - Stack, - CircularProgress, - Typography, - Box, - Tabs, - Tab, - IconButton, - Button, -} from "@mui/material"; -import Modal from "components/Modal"; -import { makeStyles, createStyles } from "@mui/styles"; -import LogsIcon from "assets/icons/CloudLogs"; -import SuccessIcon from "@mui/icons-material/CheckCircle"; -import FailIcon from "@mui/icons-material/Cancel"; -import ExpandIcon from "@mui/icons-material/ExpandLess"; -import CollapseIcon from "@mui/icons-material/ExpandMore"; -import OpenIcon from "@mui/icons-material/OpenInNew"; -import CloseIcon from "@mui/icons-material/Close"; -import TableHeaderButton from "./TableHeaderButton"; -import Ansi from "ansi-to-react"; -import EmptyState from "components/EmptyState"; - -import PropTypes from "prop-types"; -import routes from "constants/routes"; -import { DATE_TIME_FORMAT } from "constants/dates"; -import { SETTINGS, TABLE_SCHEMAS, TABLE_GROUP_SCHEMAS } from "config/dbPaths"; - -function a11yProps(index) { - return { - id: `vertical-tab-${index}`, - "aria-controls": `vertical-tabpanel-${index}`, - }; -} - -const isTargetInsideBox = (target, box) => { - const targetRect = target.getBoundingClientRect(); - const boxRect = box.getBoundingClientRect(); - return targetRect.y < boxRect.y + boxRect.height; -}; - -const useStyles = makeStyles((theme) => - createStyles({ - toolbarStatusIcon: { - fontSize: 12, - - position: "absolute", - bottom: 2, - right: 5, - - backgroundColor: theme.palette.background.paper, - boxShadow: `0 0 0 1px ${theme.palette.background.paper}`, - borderRadius: "50%", - }, - - root: { - display: "flex", - height: "100%", - }, - - logPanel: { - width: "100%", - backgroundColor: "#1E1E1E", - }, - logPanelProgress: { - marginLeft: "2em", - marginTop: "1em", - }, - logEntryWrapper: { - overflowY: "scroll", - maxHeight: "100%", - }, - logNumber: { - float: "left", - width: "2em", - textAlign: "right", - paddingRight: "1em", - }, - logEntry: { - lineBreak: "anywhere", - paddingLeft: "2em", - whiteSpace: "break-spaces", - userSelect: "text", - }, - logTypeInfo: { - color: "green", - }, - logTypeError: { - color: "red", - }, - logFont: { - ...theme.typography.body2, - fontFamily: theme.typography.fontFamilyMono, - // TODO: - color: "#CCC", - - "& code": { - fontFamily: theme.typography.fontFamilyMono, - }, - }, - - snackLog: { - position: "absolute", - left: 40, - bottom: 40, - backgroundColor: "#282829", - width: "min(40vw, 640px)", - padding: theme.spacing(1, 2, 2, 2), - borderRadius: 4, - zIndex: 1, - height: 300, - transition: "height 300ms ease-out", - }, - snackLogExpanded: { - height: "calc(100% - 300px)", - }, - - whiteText: { - color: "white", - }, - }) -); - -LogPanel.propTypes = { - logs: PropTypes.array, - status: PropTypes.string, - index: PropTypes.any.isRequired, - value: PropTypes.any.isRequired, -}; - -function LogRow({ logRecord, index }) { - const classes = useStyles(); - - return ( - - - {index} - - - - {moment(logRecord.timestamp).format("LTS")} - - {" "} - - {logRecord.log - .replaceAll("\\n", "\n") - .replaceAll("\\t", "\t") - .replaceAll("\\", "")} - - - - ); -} - -function LogPanel(props) { - const { logs, status, value, index, ...other } = props; - const classes = useStyles(); - - // useStateRef is necessary to resolve the state syncing issue - // https://stackoverflow.com/a/63039797/12208834 - const [liveStreaming, setLiveStreaming, liveStreamingStateRef] = - useStateRef(true); - const liveStreamingRef = useRef(); - const isActive = value === index; - - const handleScroll = _throttle(() => { - const target = document.querySelector("#live-stream-target"); - const scrollBox = document.querySelector("#live-stream-scroll-box"); - const liveStreamTargetVisible = isTargetInsideBox(target, scrollBox); - if (liveStreamTargetVisible !== liveStreamingStateRef.current) { - setLiveStreaming(liveStreamTargetVisible); - } - }, 500); - - const scrollToLive = () => { - const liveStreamTarget = document.querySelector("#live-stream-target"); - liveStreamTarget?.scrollIntoView?.({ - behavior: "smooth", - }); - }; - - useEffect(() => { - if (liveStreaming && isActive && status === "BUILDING") { - if (!liveStreamingRef.current) { - scrollToLive(); - } else { - setTimeout(scrollToLive, 100); - } - } - }, [logs, value]); - - useEffect(() => { - if (isActive) { - const liveStreamScrollBox = document.querySelector( - "#live-stream-scroll-box" - ); - liveStreamScrollBox!.addEventListener("scroll", () => { - handleScroll(); - }); - } - }, [value]); - - return ( -