mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-29 00:16:39 +01:00
add tables page, CodeEditor, TableSettingsDialog
This commit is contained in:
11
package.json
11
package.json
@@ -11,9 +11,12 @@
|
||||
"@emotion/react": "^11.9.0",
|
||||
"@emotion/styled": "^11.8.1",
|
||||
"@mdi/js": "^6.6.96",
|
||||
"@monaco-editor/react": "^4.4.4",
|
||||
"@mui/icons-material": "^5.6.0",
|
||||
"@mui/lab": "^5.0.0-alpha.76",
|
||||
"@mui/material": "^5.6.0",
|
||||
"@mui/styles": "^5.6.2",
|
||||
"@rowy/form-builder": "^0.5.5",
|
||||
"@rowy/multiselect": "^0.3.0",
|
||||
"compare-versions": "^4.1.3",
|
||||
"date-fns": "^2.28.0",
|
||||
@@ -21,9 +24,11 @@
|
||||
"firebase": "^9.6.11",
|
||||
"firebaseui": "^6.0.1",
|
||||
"jotai": "^1.6.5",
|
||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"match-sorter": "^6.3.1",
|
||||
"notistack": "^2.0.4",
|
||||
"quicktype-core": "^6.0.71",
|
||||
"react": "^18.0.0",
|
||||
"react-color-palette": "^6.2.0",
|
||||
"react-data-grid": "7.0.0-beta.5",
|
||||
@@ -32,9 +37,13 @@
|
||||
"react-element-scroll-hook": "^1.1.0",
|
||||
"react-error-boundary": "^3.1.4",
|
||||
"react-helmet-async": "^1.3.0",
|
||||
"react-hook-form": "^7.30.0",
|
||||
"react-markdown": "^8.0.3",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-router-hash-link": "^2.4.3",
|
||||
"react-scripts": "^5.0.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"swr": "^1.3.0",
|
||||
"tss-react": "^3.6.2",
|
||||
"typescript": "^4.6.3",
|
||||
"use-debounce": "^7.0.1",
|
||||
@@ -132,7 +141,9 @@
|
||||
"eslint-plugin-tsdoc": "^0.2.16",
|
||||
"husky": ">=7.0.4",
|
||||
"lint-staged": ">=12.3.7",
|
||||
"monaco-editor": "^0.33.0",
|
||||
"prettier": "^2.6.2",
|
||||
"raw-loader": "^4.0.2",
|
||||
"source-map-explorer": "^2.5.2"
|
||||
},
|
||||
"resolutions": {
|
||||
|
||||
30
src/App.tsx
30
src/App.tsx
@@ -9,6 +9,7 @@ import RowyRunModal from "@src/components/RowyRunModal";
|
||||
import NotFound from "@src/pages/NotFound";
|
||||
import RequireAuth from "@src/layouts/RequireAuth";
|
||||
import Navigation from "@src/layouts/Navigation";
|
||||
import TableSettingsDialog from "@src/components/TableSettingsDialog";
|
||||
|
||||
import { globalScope, currentUserAtom } from "@src/atoms/globalScope";
|
||||
import { ROUTES } from "@src/constants/routes";
|
||||
@@ -28,13 +29,15 @@ const ImpersonatorAuthPage = lazy(() => import("@src/pages/Auth/ImpersonatorAuth
|
||||
// prettier-ignore
|
||||
const SetupPage = lazy(() => import("@src/pages/Setup" /* webpackChunkName: "SetupPage" */));
|
||||
|
||||
// prettier-ignore
|
||||
const TablesPage = lazy(() => import("@src/pages/Tables" /* webpackChunkName: "TablesPage" */));
|
||||
|
||||
// prettier-ignore
|
||||
const UserSettingsPage = lazy(() => import("@src/pages/Settings/UserSettings" /* webpackChunkName: "UserSettingsPage" */));
|
||||
// prettier-ignore
|
||||
const ProjectSettingsPage = lazy(() => import("@src/pages/Settings/ProjectSettings" /* webpackChunkName: "ProjectSettingsPage" */));
|
||||
// prettier-ignore
|
||||
const UserManagementPage = lazy(() => import("@src/pages/Settings/UserManagement" /* webpackChunkName: "UserManagementPage" */));
|
||||
// prettier-ignore
|
||||
// const RowyRunTestPage = lazy(() => import("@src/pages/RowyRunTest" /* webpackChunkName: "RowyRunTestPage" */));
|
||||
|
||||
export default function App() {
|
||||
@@ -44,7 +47,7 @@ export default function App() {
|
||||
<Suspense fallback={<Loading fullScreen />}>
|
||||
<ProjectSourceFirebase />
|
||||
<ConfirmDialog />
|
||||
<RowyRunModal/>
|
||||
<RowyRunModal />
|
||||
|
||||
{currentUser === undefined ? (
|
||||
<Loading fullScreen message="Authenticating" />
|
||||
@@ -71,21 +74,34 @@ export default function App() {
|
||||
path="/"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<Navigation />
|
||||
<Navigation>
|
||||
<TableSettingsDialog />
|
||||
</Navigation>
|
||||
</RequireAuth>
|
||||
}
|
||||
>
|
||||
<Route
|
||||
path={ROUTES.home}
|
||||
element={<Navigate to={ROUTES.tables} replace />}
|
||||
/>
|
||||
<Route path={ROUTES.tables} element={<TablesPage />} />
|
||||
|
||||
<Route
|
||||
path={ROUTES.settings}
|
||||
element={<Navigate to={ROUTES.userSettings} replace />}
|
||||
/>
|
||||
<Route path={ROUTES.userSettings} element={<UserSettingsPage />} />
|
||||
<Route path={ROUTES.projectSettings} element={<ProjectSettingsPage />} />
|
||||
<Route path={ROUTES.userManagement} element={<UserManagementPage />} />
|
||||
<Route
|
||||
path={ROUTES.projectSettings}
|
||||
element={<ProjectSettingsPage />}
|
||||
/>
|
||||
<Route
|
||||
path={ROUTES.userManagement}
|
||||
element={<UserManagementPage />}
|
||||
/>
|
||||
{/* <Route path={ROUTES.rowyRunTest} element={<RowyRunTestPage />} /> */}
|
||||
|
||||
<Route path="/jotaiTest" element={<JotaiTestPage />} />
|
||||
|
||||
<Route path="/jotaiTest" element={<JotaiTestPage />} />
|
||||
</Route>
|
||||
|
||||
{/* <Route path="/jotaiTest" element={<JotaiTestPage />} /> */}
|
||||
|
||||
120
src/components/CodeEditor/CodeEditor.tsx
Normal file
120
src/components/CodeEditor/CodeEditor.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
import { useState } from "react";
|
||||
import Editor, { EditorProps } from "@monaco-editor/react";
|
||||
import type { editor } from "monaco-editor/esm/vs/editor/editor.api";
|
||||
|
||||
import { useTheme, Box, BoxProps } from "@mui/material";
|
||||
import TrapFocus from "@mui/material/Unstable_TrapFocus";
|
||||
import CircularProgressOptical from "@src/components/CircularProgressOptical";
|
||||
import ResizeBottomRightIcon from "@src/assets/icons/ResizeBottomRight";
|
||||
|
||||
import useMonacoCustomizations, {
|
||||
IUseMonacoCustomizationsProps,
|
||||
} from "./useMonacoCustomizations";
|
||||
import FullScreenButton from "./FullScreenButton";
|
||||
|
||||
export interface ICodeEditorProps
|
||||
extends Partial<EditorProps>,
|
||||
Omit<IUseMonacoCustomizationsProps, "fullScreen"> {
|
||||
value: string;
|
||||
containerProps?: Partial<BoxProps>;
|
||||
|
||||
onValidate?: EditorProps["onValidate"];
|
||||
onValidStatusUpdate?: (result: {
|
||||
isValid: boolean;
|
||||
markers: editor.IMarker[];
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export default function CodeEditor({
|
||||
value,
|
||||
minHeight = 100,
|
||||
disabled,
|
||||
error,
|
||||
containerProps,
|
||||
|
||||
onValidate,
|
||||
onValidStatusUpdate,
|
||||
|
||||
extraLibs,
|
||||
diagnosticsOptions,
|
||||
onUnmount,
|
||||
defaultLanguage = "javascript",
|
||||
...props
|
||||
}: ICodeEditorProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
// Store editor value to prevent code editor values not being saved when
|
||||
// Side Drawer is in the middle of a refresh
|
||||
const [initialEditorValue] = useState(value ?? "");
|
||||
const [fullScreen, setFullScreen] = useState(false);
|
||||
|
||||
const { boxSx } = useMonacoCustomizations({
|
||||
minHeight,
|
||||
disabled,
|
||||
error,
|
||||
extraLibs,
|
||||
diagnosticsOptions,
|
||||
onUnmount,
|
||||
fullScreen,
|
||||
});
|
||||
|
||||
const onValidate_: EditorProps["onValidate"] = (markers) => {
|
||||
onValidStatusUpdate?.({ isValid: markers.length <= 0, markers });
|
||||
onValidate?.(markers);
|
||||
};
|
||||
|
||||
return (
|
||||
<TrapFocus open={fullScreen}>
|
||||
<Box
|
||||
sx={[
|
||||
boxSx,
|
||||
...(Array.isArray(containerProps?.sx)
|
||||
? containerProps!.sx
|
||||
: containerProps?.sx
|
||||
? [containerProps.sx]
|
||||
: []),
|
||||
]}
|
||||
style={fullScreen ? { height: "100%" } : {}}
|
||||
>
|
||||
<Editor
|
||||
defaultLanguage={defaultLanguage}
|
||||
value={initialEditorValue}
|
||||
loading={<CircularProgressOptical size={20} sx={{ m: 2 }} />}
|
||||
className="editor"
|
||||
{...props}
|
||||
onValidate={onValidate_}
|
||||
options={{
|
||||
readOnly: disabled,
|
||||
fontFamily: theme.typography.fontFamilyMono,
|
||||
rulers: [80],
|
||||
minimap: { enabled: false },
|
||||
lineNumbersMinChars: 4,
|
||||
lineDecorationsWidth: 0,
|
||||
automaticLayout: true,
|
||||
fixedOverflowWidgets: true,
|
||||
tabSize: 2,
|
||||
...props.options,
|
||||
}}
|
||||
/>
|
||||
|
||||
<FullScreenButton
|
||||
onClick={() => setFullScreen((f) => !f)}
|
||||
active={fullScreen}
|
||||
/>
|
||||
|
||||
{!fullScreen && (
|
||||
<ResizeBottomRightIcon
|
||||
aria-label="Resize code editor"
|
||||
color="action"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 1,
|
||||
right: 1,
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</TrapFocus>
|
||||
);
|
||||
}
|
||||
122
src/components/CodeEditor/CodeEditorHelper.tsx
Normal file
122
src/components/CodeEditor/CodeEditorHelper.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import { useAtom } from "jotai";
|
||||
|
||||
import { Stack, Typography, Grid, Tooltip, IconButton } from "@mui/material";
|
||||
import SecretsIcon from "@mui/icons-material/VpnKeyOutlined";
|
||||
import FunctionsIcon from "@mui/icons-material/CloudOutlined";
|
||||
import DocsIcon from "@mui/icons-material/DescriptionOutlined";
|
||||
|
||||
import { globalScope, projectIdAtom } from "@src/atoms/globalScope";
|
||||
|
||||
export interface ICodeEditorHelperProps {
|
||||
docLink: string;
|
||||
additionalVariables?: {
|
||||
key: string;
|
||||
description: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export default function CodeEditorHelper({
|
||||
docLink,
|
||||
additionalVariables,
|
||||
}: ICodeEditorHelperProps) {
|
||||
const [projectId] = useAtom(projectIdAtom, globalScope);
|
||||
|
||||
const availableVariables = [
|
||||
{
|
||||
key: "row",
|
||||
description: `row has the value of doc.data() it has type definitions using this table's schema, but you can access any field in the document.`,
|
||||
},
|
||||
{
|
||||
key: "db",
|
||||
description: `db object provides access to firestore database instance of this project. giving you access to any collection or document in this firestore instance`,
|
||||
},
|
||||
{
|
||||
key: "ref",
|
||||
description: `ref object that represents the reference to the current row in firestore db (ie: doc.ref).`,
|
||||
},
|
||||
{
|
||||
key: "auth",
|
||||
description: `auth provides access to a firebase auth instance, can be used to manage auth users or generate tokens.`,
|
||||
},
|
||||
{
|
||||
key: "storage",
|
||||
description: `firebase Storage can be accessed through this, storage.bucket() returns default storage bucket of the firebase project.`,
|
||||
},
|
||||
{
|
||||
key: "rowy",
|
||||
description: `rowy provides a set of functions that are commonly used, such as easy access to GCP Secret Manager`,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="flex-start"
|
||||
justifyItems="space-between"
|
||||
spacing={1}
|
||||
justifyContent="space-between"
|
||||
sx={{ my: 1 }}
|
||||
>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
Available:
|
||||
</Typography>
|
||||
|
||||
<Grid
|
||||
container
|
||||
spacing={1}
|
||||
style={{ flexGrow: 1, marginTop: -8, marginLeft: 0 }}
|
||||
>
|
||||
{availableVariables.concat(additionalVariables ?? []).map((v) => (
|
||||
<Grid item key={v.key}>
|
||||
<Tooltip title={v.description}>
|
||||
<code>{v.key}</code>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
spacing={1}
|
||||
style={{ marginTop: -4 }}
|
||||
>
|
||||
<Tooltip title="Secret Manager ↗">
|
||||
<IconButton
|
||||
size="small"
|
||||
color="primary"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={`https://console.cloud.google.com/security/secret-manager?project=${projectId}`}
|
||||
>
|
||||
<SecretsIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="Configure Cloud Function ↗">
|
||||
<IconButton
|
||||
size="small"
|
||||
color="primary"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={`https://console.cloud.google.com/functions/list?project=${projectId}`}
|
||||
>
|
||||
<FunctionsIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title="Examples & documentation ↗">
|
||||
<IconButton
|
||||
size="small"
|
||||
color="primary"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={docLink}
|
||||
>
|
||||
<DocsIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
117
src/components/CodeEditor/DiffEditor.tsx
Normal file
117
src/components/CodeEditor/DiffEditor.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { useState } from "react";
|
||||
import {
|
||||
DiffEditor as MonacoDiffEditor,
|
||||
DiffEditorProps,
|
||||
EditorProps,
|
||||
} from "@monaco-editor/react";
|
||||
|
||||
import { useTheme, Box, BoxProps } from "@mui/material";
|
||||
import TrapFocus from "@mui/material/Unstable_TrapFocus";
|
||||
import CircularProgressOptical from "@src/components/CircularProgressOptical";
|
||||
import ResizeBottomRightIcon from "@src/assets/icons/ResizeBottomRight";
|
||||
|
||||
import useMonacoCustomizations, {
|
||||
IUseMonacoCustomizationsProps,
|
||||
} from "./useMonacoCustomizations";
|
||||
import FullScreenButton from "./FullScreenButton";
|
||||
|
||||
export interface IDiffEditorProps
|
||||
extends Partial<DiffEditorProps>,
|
||||
Omit<IUseMonacoCustomizationsProps, "fullScreen"> {
|
||||
onChange?: EditorProps["onChange"];
|
||||
containerProps?: Partial<BoxProps>;
|
||||
}
|
||||
|
||||
export default function DiffEditor({
|
||||
onChange,
|
||||
minHeight = 100,
|
||||
disabled,
|
||||
error,
|
||||
containerProps,
|
||||
|
||||
extraLibs,
|
||||
diagnosticsOptions,
|
||||
onUnmount,
|
||||
|
||||
...props
|
||||
}: IDiffEditorProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
const [fullScreen, setFullScreen] = useState(false);
|
||||
|
||||
const { boxSx } = useMonacoCustomizations({
|
||||
minHeight,
|
||||
disabled,
|
||||
error,
|
||||
extraLibs,
|
||||
diagnosticsOptions,
|
||||
onUnmount,
|
||||
fullScreen,
|
||||
});
|
||||
|
||||
// Needs manual patch since `onMount` prop is not available in `DiffEditor`
|
||||
// https://github.com/suren-atoyan/monaco-react/issues/281
|
||||
const handleEditorMount: DiffEditorProps["onMount"] = (editor, monaco) => {
|
||||
const modifiedEditor = editor.getModifiedEditor();
|
||||
modifiedEditor.onDidChangeModelContent((ev) => {
|
||||
onChange?.(modifiedEditor.getValue(), ev);
|
||||
});
|
||||
|
||||
props.onMount?.(editor, monaco);
|
||||
};
|
||||
|
||||
return (
|
||||
<TrapFocus open={fullScreen}>
|
||||
<Box
|
||||
sx={[
|
||||
boxSx,
|
||||
...(Array.isArray(containerProps?.sx)
|
||||
? containerProps!.sx
|
||||
: containerProps?.sx
|
||||
? [containerProps.sx]
|
||||
: []),
|
||||
]}
|
||||
style={fullScreen ? { height: "100%" } : {}}
|
||||
>
|
||||
<MonacoDiffEditor
|
||||
language="javascript"
|
||||
loading={<CircularProgressOptical size={20} sx={{ m: 2 }} />}
|
||||
className="editor"
|
||||
{...props}
|
||||
onMount={handleEditorMount}
|
||||
options={
|
||||
{
|
||||
readOnly: disabled,
|
||||
fontFamily: theme.typography.fontFamilyMono,
|
||||
rulers: [80],
|
||||
minimap: { enabled: false },
|
||||
lineNumbersMinChars: 4,
|
||||
lineDecorationsWidth: "18",
|
||||
automaticLayout: true,
|
||||
fixedOverflowWidgets: true,
|
||||
tabSize: 2,
|
||||
...props.options,
|
||||
} as any
|
||||
}
|
||||
/>
|
||||
|
||||
<FullScreenButton
|
||||
onClick={() => setFullScreen((f) => !f)}
|
||||
active={fullScreen}
|
||||
style={{ right: 32 }}
|
||||
/>
|
||||
|
||||
<ResizeBottomRightIcon
|
||||
aria-label="Resize code editor"
|
||||
color="action"
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 1,
|
||||
right: 1,
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</TrapFocus>
|
||||
);
|
||||
}
|
||||
32
src/components/CodeEditor/FullScreenButton.tsx
Normal file
32
src/components/CodeEditor/FullScreenButton.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Button, ButtonProps } from "@mui/material";
|
||||
import FullscreenIcon from "@mui/icons-material/Fullscreen";
|
||||
import FullscreenExitIcon from "@mui/icons-material/FullscreenExit";
|
||||
|
||||
export interface IFullScreenButtonProps extends ButtonProps {
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export default function FullScreenButton({
|
||||
active,
|
||||
...props
|
||||
}: IFullScreenButtonProps) {
|
||||
return (
|
||||
<Button
|
||||
aria-label={`${active ? "Exit" : "Enter"} full screen`}
|
||||
variant={active ? "contained" : "outlined"}
|
||||
color={active ? "secondary" : undefined}
|
||||
{...props}
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 4,
|
||||
right: 16,
|
||||
zIndex: 2,
|
||||
minWidth: 32,
|
||||
padding: 0,
|
||||
...props.style,
|
||||
}}
|
||||
>
|
||||
{active ? <FullscreenExitIcon /> : <FullscreenIcon />}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
98
src/components/CodeEditor/extensions.d.ts
vendored
Normal file
98
src/components/CodeEditor/extensions.d.ts
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
/* eslint-disable tsdoc/syntax */
|
||||
type Trigger = "create" | "update" | "delete";
|
||||
type Triggers = Trigger[];
|
||||
|
||||
// function types that defines extension body and should run
|
||||
type Condition =
|
||||
| boolean
|
||||
| ((data: ExtensionContext) => boolean | Promise<boolean>);
|
||||
|
||||
// the argument that the extension body takes in
|
||||
type ExtensionContext = {
|
||||
row: Row;
|
||||
ref: FirebaseFirestore.DocumentReference;
|
||||
storage: firebasestorage.Storage;
|
||||
db: FirebaseFirestore.Firestore;
|
||||
auth: firebaseauth.BaseAuth;
|
||||
change: any;
|
||||
triggerType: Triggers;
|
||||
fieldTypes: any;
|
||||
extensionConfig: {
|
||||
label: string;
|
||||
type: string;
|
||||
triggers: Trigger[];
|
||||
conditions: Condition;
|
||||
requiredFields: string[];
|
||||
extensionBody: any;
|
||||
};
|
||||
RULES_UTILS: any;
|
||||
};
|
||||
|
||||
// extension body definition
|
||||
type slackEmailBody = {
|
||||
channels?: string[];
|
||||
text?: string;
|
||||
emails: string[];
|
||||
blocks?: object[];
|
||||
attachments?: any;
|
||||
};
|
||||
|
||||
type slackChannelBody = {
|
||||
channels: string[];
|
||||
text?: string;
|
||||
emails?: string[];
|
||||
blocks?: object[];
|
||||
attachments?: any;
|
||||
};
|
||||
|
||||
type DocSyncBody = (context: ExtensionContext) => Promise<{
|
||||
fieldsToSync: Fields;
|
||||
row: Row;
|
||||
targetPath: string;
|
||||
}>;
|
||||
|
||||
type HistorySnapshotBody = (context: ExtensionContext) => Promise<{
|
||||
trackedFields: Fields;
|
||||
}>;
|
||||
|
||||
type AlgoliaIndexBody = (context: ExtensionContext) => Promise<{
|
||||
fieldsToSync: Fields;
|
||||
index: string;
|
||||
row: Row;
|
||||
objectID: string;
|
||||
}>;
|
||||
|
||||
type MeiliIndexBody = (context: ExtensionContext) => Promise<{
|
||||
fieldsToSync: Fields;
|
||||
index: string;
|
||||
row: Row;
|
||||
objectID: string;
|
||||
}>;
|
||||
|
||||
type BigqueryIndexBody = (context: ExtensionContext) => Promise<{
|
||||
fieldsToSync: Fields;
|
||||
index: string;
|
||||
row: Row;
|
||||
objectID: string;
|
||||
}>;
|
||||
|
||||
type SlackMessageBody = (
|
||||
context: ExtensionContext
|
||||
) => Promise<slackEmailBody | slackChannelBody>;
|
||||
|
||||
type SendgridEmailBody = (context: ExtensionContext) => Promise<any>;
|
||||
|
||||
type ApiCallBody = (context: ExtensionContext) => Promise<{
|
||||
body: string;
|
||||
url: string;
|
||||
method: string;
|
||||
callback: any;
|
||||
}>;
|
||||
|
||||
type TwilioMessageBody = (context: ExtensionContext) => Promise<{
|
||||
body: string;
|
||||
from: string;
|
||||
to: string;
|
||||
}>;
|
||||
|
||||
type TaskBody = (context: ExtensionContext) => Promise<any>;
|
||||
1834
src/components/CodeEditor/firebaseAuth.d.ts
vendored
Normal file
1834
src/components/CodeEditor/firebaseAuth.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
447
src/components/CodeEditor/firebaseStorage.d.ts
vendored
Normal file
447
src/components/CodeEditor/firebaseStorage.d.ts
vendored
Normal file
@@ -0,0 +1,447 @@
|
||||
/* eslint-disable tsdoc/syntax */
|
||||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
|
||||
// node_modules/@google-cloud/storage/build/src/bucket.d.ts
|
||||
declare class Bucket {
|
||||
/**
|
||||
* The bucket's name.
|
||||
* @name Bucket#name
|
||||
* @type {string}
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* A reference to the {@link Storage} associated with this {@link Bucket}
|
||||
* instance.
|
||||
* @name Bucket#storage
|
||||
* @type {Storage}
|
||||
*/
|
||||
storage: Storage;
|
||||
/**
|
||||
* A user project to apply to each request from this bucket.
|
||||
* @name Bucket#userProject
|
||||
* @type {string}
|
||||
*/
|
||||
userProject?: string;
|
||||
/**
|
||||
* Cloud Storage uses access control lists (ACLs) to manage object and
|
||||
* bucket access. ACLs are the mechanism you use to share objects with other
|
||||
* users and allow other users to access your buckets and objects.
|
||||
*
|
||||
* An ACL consists of one or more entries, where each entry grants permissions
|
||||
* to an entity. Permissions define the actions that can be performed against
|
||||
* an object or bucket (for example, `READ` or `WRITE`); the entity defines
|
||||
* who the permission applies to (for example, a specific user or group of
|
||||
* users).
|
||||
*
|
||||
* The `acl` object on a Bucket instance provides methods to get you a list of
|
||||
* the ACLs defined on your bucket, as well as set, update, and delete them.
|
||||
*
|
||||
* Buckets also have
|
||||
* [default
|
||||
* ACLs](https://cloud.google.com/storage/docs/access-control/lists#default)
|
||||
* for all created files. Default ACLs specify permissions that all new
|
||||
* objects added to the bucket will inherit by default. You can add, delete,
|
||||
* get, and update entities and permissions for these as well with
|
||||
* {@link Bucket#acl.default}.
|
||||
*
|
||||
* @see [About Access Control Lists]{@link http://goo.gl/6qBBPO}
|
||||
* @see [Default ACLs]{@link https://cloud.google.com/storage/docs/access-control/lists#default}
|
||||
*
|
||||
* @name Bucket#acl
|
||||
* @mixes Acl
|
||||
* @property {Acl} default Cloud Storage Buckets have
|
||||
* [default
|
||||
* ACLs](https://cloud.google.com/storage/docs/access-control/lists#default)
|
||||
* for all created files. You can add, delete, get, and update entities and
|
||||
* permissions for these as well. The method signatures and examples are all
|
||||
* the same, after only prefixing the method call with `default`.
|
||||
*
|
||||
* @example
|
||||
* const {Storage} = require('@google-cloud/storage');
|
||||
* const storage = new Storage();
|
||||
*
|
||||
* //-
|
||||
* // Make a bucket's contents publicly readable.
|
||||
* //-
|
||||
* const myBucket = storage.bucket('my-bucket');
|
||||
*
|
||||
* const options = {
|
||||
* entity: 'allUsers',
|
||||
* role: storage.acl.READER_ROLE
|
||||
* };
|
||||
*
|
||||
* myBucket.acl.add(options, function(err, aclObject) {});
|
||||
*
|
||||
* //-
|
||||
* // If the callback is omitted, we'll return a Promise.
|
||||
* //-
|
||||
* myBucket.acl.add(options).then(function(data) {
|
||||
* const aclObject = data[0];
|
||||
* const apiResponse = data[1];
|
||||
* });
|
||||
*
|
||||
* @example <caption>include:samples/acl.js</caption>
|
||||
* region_tag:storage_print_bucket_acl
|
||||
* Example of printing a bucket's ACL:
|
||||
*
|
||||
* @example <caption>include:samples/acl.js</caption>
|
||||
* region_tag:storage_print_bucket_acl_for_user
|
||||
* Example of printing a bucket's ACL for a specific user:
|
||||
*
|
||||
* @example <caption>include:samples/acl.js</caption>
|
||||
* region_tag:storage_add_bucket_owner
|
||||
* Example of adding an owner to a bucket:
|
||||
*
|
||||
* @example <caption>include:samples/acl.js</caption>
|
||||
* region_tag:storage_remove_bucket_owner
|
||||
* Example of removing an owner from a bucket:
|
||||
*
|
||||
* @example <caption>include:samples/acl.js</caption>
|
||||
* region_tag:storage_add_bucket_default_owner
|
||||
* Example of adding a default owner to a bucket:
|
||||
*
|
||||
* @example <caption>include:samples/acl.js</caption>
|
||||
* region_tag:storage_remove_bucket_default_owner
|
||||
* Example of removing a default owner from a bucket:
|
||||
*/
|
||||
acl: Acl;
|
||||
/**
|
||||
* Get and set IAM policies for your bucket.
|
||||
*
|
||||
* @name Bucket#iam
|
||||
* @mixes Iam
|
||||
*
|
||||
* @see [Cloud Storage IAM Management](https://cloud.google.com/storage/docs/access-control/iam#short_title_iam_management)
|
||||
* @see [Granting, Changing, and Revoking Access](https://cloud.google.com/iam/docs/granting-changing-revoking-access)
|
||||
* @see [IAM Roles](https://cloud.google.com/iam/docs/understanding-roles)
|
||||
*
|
||||
* @example
|
||||
* const {Storage} = require('@google-cloud/storage');
|
||||
* const storage = new Storage();
|
||||
* const bucket = storage.bucket('albums');
|
||||
*
|
||||
* //-
|
||||
* // Get the IAM policy for your bucket.
|
||||
* //-
|
||||
* bucket.iam.getPolicy(function(err, policy) {
|
||||
* console.log(policy);
|
||||
* });
|
||||
*
|
||||
* //-
|
||||
* // If the callback is omitted, we'll return a Promise.
|
||||
* //-
|
||||
* bucket.iam.getPolicy().then(function(data) {
|
||||
* const policy = data[0];
|
||||
* const apiResponse = data[1];
|
||||
* });
|
||||
*
|
||||
* @example <caption>include:samples/iam.js</caption>
|
||||
* region_tag:storage_view_bucket_iam_members
|
||||
* Example of retrieving a bucket's IAM policy:
|
||||
*
|
||||
* @example <caption>include:samples/iam.js</caption>
|
||||
* region_tag:storage_add_bucket_iam_member
|
||||
* Example of adding to a bucket's IAM policy:
|
||||
*
|
||||
* @example <caption>include:samples/iam.js</caption>
|
||||
* region_tag:storage_remove_bucket_iam_member
|
||||
* Example of removing from a bucket's IAM policy:
|
||||
*/
|
||||
iam: Iam;
|
||||
/**
|
||||
* Get {@link File} objects for the files currently in the bucket as a
|
||||
* readable object stream.
|
||||
*
|
||||
* @method Bucket#getFilesStream
|
||||
* @param {GetFilesOptions} [query] Query object for listing files.
|
||||
* @returns {ReadableStream} A readable stream that emits {@link File} instances.
|
||||
*
|
||||
* @example
|
||||
* const {Storage} = require('@google-cloud/storage');
|
||||
* const storage = new Storage();
|
||||
* const bucket = storage.bucket('albums');
|
||||
*
|
||||
* bucket.getFilesStream()
|
||||
* .on('error', console.error)
|
||||
* .on('data', function(file) {
|
||||
* // file is a File object.
|
||||
* })
|
||||
* .on('end', function() {
|
||||
* // All files retrieved.
|
||||
* });
|
||||
*
|
||||
* //-
|
||||
* // If you anticipate many results, you can end a stream early to prevent
|
||||
* // unnecessary processing and API requests.
|
||||
* //-
|
||||
* bucket.getFilesStream()
|
||||
* .on('data', function(file) {
|
||||
* this.end();
|
||||
* });
|
||||
*
|
||||
* //-
|
||||
* // If you're filtering files with a delimiter, you should use
|
||||
* // {@link Bucket#getFiles} and set `autoPaginate: false` in order to
|
||||
* // preserve the `apiResponse` argument.
|
||||
* //-
|
||||
* const prefixes = [];
|
||||
*
|
||||
* function callback(err, files, nextQuery, apiResponse) {
|
||||
* prefixes = prefixes.concat(apiResponse.prefixes);
|
||||
*
|
||||
* if (nextQuery) {
|
||||
* bucket.getFiles(nextQuery, callback);
|
||||
* } else {
|
||||
* // prefixes = The finished array of prefixes.
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* bucket.getFiles({
|
||||
* autoPaginate: false,
|
||||
* delimiter: '/'
|
||||
* }, callback);
|
||||
*/
|
||||
getFilesStream: Function;
|
||||
signer?: URLSigner;
|
||||
constructor(storage: Storage, name: string, options?: BucketOptions);
|
||||
addLifecycleRule(
|
||||
rule: LifecycleRule,
|
||||
options?: AddLifecycleRuleOptions
|
||||
): Promise<SetBucketMetadataResponse>;
|
||||
addLifecycleRule(
|
||||
rule: LifecycleRule,
|
||||
options: AddLifecycleRuleOptions,
|
||||
callback: SetBucketMetadataCallback
|
||||
): void;
|
||||
addLifecycleRule(
|
||||
rule: LifecycleRule,
|
||||
callback: SetBucketMetadataCallback
|
||||
): void;
|
||||
combine(
|
||||
sources: string[] | File[],
|
||||
destination: string | File,
|
||||
options?: CombineOptions
|
||||
): Promise<CombineResponse>;
|
||||
combine(
|
||||
sources: string[] | File[],
|
||||
destination: string | File,
|
||||
options: CombineOptions,
|
||||
callback: CombineCallback
|
||||
): void;
|
||||
combine(
|
||||
sources: string[] | File[],
|
||||
destination: string | File,
|
||||
callback: CombineCallback
|
||||
): void;
|
||||
createChannel(
|
||||
id: string,
|
||||
config: CreateChannelConfig,
|
||||
options?: CreateChannelOptions
|
||||
): Promise<CreateChannelResponse>;
|
||||
createChannel(
|
||||
id: string,
|
||||
config: CreateChannelConfig,
|
||||
callback: CreateChannelCallback
|
||||
): void;
|
||||
createChannel(
|
||||
id: string,
|
||||
config: CreateChannelConfig,
|
||||
options: CreateChannelOptions,
|
||||
callback: CreateChannelCallback
|
||||
): void;
|
||||
createNotification(
|
||||
topic: string,
|
||||
options?: CreateNotificationOptions
|
||||
): Promise<CreateNotificationResponse>;
|
||||
createNotification(
|
||||
topic: string,
|
||||
options: CreateNotificationOptions,
|
||||
callback: CreateNotificationCallback
|
||||
): void;
|
||||
createNotification(topic: string, callback: CreateNotificationCallback): void;
|
||||
deleteFiles(query?: DeleteFilesOptions): Promise<void>;
|
||||
deleteFiles(callback: DeleteFilesCallback): void;
|
||||
deleteFiles(query: DeleteFilesOptions, callback: DeleteFilesCallback): void;
|
||||
deleteLabels(labels?: string | string[]): Promise<DeleteLabelsResponse>;
|
||||
deleteLabels(callback: DeleteLabelsCallback): void;
|
||||
deleteLabels(labels: string | string[], callback: DeleteLabelsCallback): void;
|
||||
disableRequesterPays(): Promise<DisableRequesterPaysResponse>;
|
||||
disableRequesterPays(callback: DisableRequesterPaysCallback): void;
|
||||
enableLogging(
|
||||
config: EnableLoggingOptions
|
||||
): Promise<SetBucketMetadataResponse>;
|
||||
enableLogging(
|
||||
config: EnableLoggingOptions,
|
||||
callback: SetBucketMetadataCallback
|
||||
): void;
|
||||
enableRequesterPays(): Promise<EnableRequesterPaysResponse>;
|
||||
enableRequesterPays(callback: EnableRequesterPaysCallback): void;
|
||||
/**
|
||||
* Create a {@link File} object. See {@link File} to see how to handle
|
||||
* the different use cases you may have.
|
||||
*
|
||||
* @param {string} name The name of the file in this bucket.
|
||||
* @param {object} [options] Configuration options.
|
||||
* @param {string|number} [options.generation] Only use a specific revision of
|
||||
* this file.
|
||||
* @param {string} [options.encryptionKey] A custom encryption key. See
|
||||
* [Customer-supplied Encryption
|
||||
* Keys](https://cloud.google.com/storage/docs/encryption#customer-supplied).
|
||||
* @param {string} [options.kmsKeyName] The name of the Cloud KMS key that will
|
||||
* be used to encrypt the object. Must be in the format:
|
||||
* `projects/my-project/locations/location/keyRings/my-kr/cryptoKeys/my-key`.
|
||||
* KMS key ring must use the same location as the bucket.
|
||||
* @returns {File}
|
||||
*
|
||||
* @example
|
||||
* const {Storage} = require('@google-cloud/storage');
|
||||
* const storage = new Storage();
|
||||
* const bucket = storage.bucket('albums');
|
||||
* const file = bucket.file('my-existing-file.png');
|
||||
*/
|
||||
file(name: string, options?: FileOptions): File;
|
||||
getFiles(query?: GetFilesOptions): Promise<GetFilesResponse>;
|
||||
getFiles(query: GetFilesOptions, callback: GetFilesCallback): void;
|
||||
getFiles(callback: GetFilesCallback): void;
|
||||
getLabels(options: GetLabelsOptions): Promise<GetLabelsResponse>;
|
||||
getLabels(callback: GetLabelsCallback): void;
|
||||
getLabels(options: GetLabelsOptions, callback: GetLabelsCallback): void;
|
||||
getNotifications(
|
||||
options?: GetNotificationsOptions
|
||||
): Promise<GetNotificationsResponse>;
|
||||
getNotifications(callback: GetNotificationsCallback): void;
|
||||
getNotifications(
|
||||
options: GetNotificationsOptions,
|
||||
callback: GetNotificationsCallback
|
||||
): void;
|
||||
getSignedUrl(cfg: GetBucketSignedUrlConfig): Promise<GetSignedUrlResponse>;
|
||||
getSignedUrl(
|
||||
cfg: GetBucketSignedUrlConfig,
|
||||
callback: GetSignedUrlCallback
|
||||
): void;
|
||||
lock(metageneration: number | string): Promise<BucketLockResponse>;
|
||||
lock(metageneration: number | string, callback: BucketLockCallback): void;
|
||||
makePrivate(
|
||||
options?: MakeBucketPrivateOptions
|
||||
): Promise<MakeBucketPrivateResponse>;
|
||||
makePrivate(callback: MakeBucketPrivateCallback): void;
|
||||
makePrivate(
|
||||
options: MakeBucketPrivateOptions,
|
||||
callback: MakeBucketPrivateCallback
|
||||
): void;
|
||||
makePublic(
|
||||
options?: MakeBucketPublicOptions
|
||||
): Promise<MakeBucketPublicResponse>;
|
||||
makePublic(callback: MakeBucketPublicCallback): void;
|
||||
makePublic(
|
||||
options: MakeBucketPublicOptions,
|
||||
callback: MakeBucketPublicCallback
|
||||
): void;
|
||||
/**
|
||||
* Get a reference to a Cloud Pub/Sub Notification.
|
||||
*
|
||||
* @param {string} id ID of notification.
|
||||
* @returns {Notification}
|
||||
* @see Notification
|
||||
*
|
||||
* @example
|
||||
* const {Storage} = require('@google-cloud/storage');
|
||||
* const storage = new Storage();
|
||||
* const bucket = storage.bucket('my-bucket');
|
||||
* const notification = bucket.notification('1');
|
||||
*/
|
||||
notification(id: string): Notification;
|
||||
removeRetentionPeriod(): Promise<SetBucketMetadataResponse>;
|
||||
removeRetentionPeriod(callback: SetBucketMetadataCallback): void;
|
||||
request(reqOpts: DecorateRequestOptions): Promise<[ResponseBody, Metadata]>;
|
||||
request(
|
||||
reqOpts: DecorateRequestOptions,
|
||||
callback: BodyResponseCallback
|
||||
): void;
|
||||
setLabels(
|
||||
labels: Labels,
|
||||
options?: SetLabelsOptions
|
||||
): Promise<SetLabelsResponse>;
|
||||
setLabels(labels: Labels, callback: SetLabelsCallback): void;
|
||||
setLabels(
|
||||
labels: Labels,
|
||||
options: SetLabelsOptions,
|
||||
callback: SetLabelsCallback
|
||||
): void;
|
||||
setRetentionPeriod(duration: number): Promise<SetBucketMetadataResponse>;
|
||||
setRetentionPeriod(
|
||||
duration: number,
|
||||
callback: SetBucketMetadataCallback
|
||||
): void;
|
||||
setCorsConfiguration(
|
||||
corsConfiguration: Cors[]
|
||||
): Promise<SetBucketMetadataResponse>;
|
||||
setCorsConfiguration(
|
||||
corsConfiguration: Cors[],
|
||||
callback: SetBucketMetadataCallback
|
||||
): void;
|
||||
setStorageClass(
|
||||
storageClass: string,
|
||||
options?: SetBucketStorageClassOptions
|
||||
): Promise<SetBucketMetadataResponse>;
|
||||
setStorageClass(
|
||||
storageClass: string,
|
||||
callback: SetBucketStorageClassCallback
|
||||
): void;
|
||||
setStorageClass(
|
||||
storageClass: string,
|
||||
options: SetBucketStorageClassOptions,
|
||||
callback: SetBucketStorageClassCallback
|
||||
): void;
|
||||
/**
|
||||
* Set a user project to be billed for all requests made from this Bucket
|
||||
* object and any files referenced from this Bucket object.
|
||||
*
|
||||
* @param {string} userProject The user project.
|
||||
*
|
||||
* @example
|
||||
* const {Storage} = require('@google-cloud/storage');
|
||||
* const storage = new Storage();
|
||||
* const bucket = storage.bucket('albums');
|
||||
*
|
||||
* bucket.setUserProject('grape-spaceship-123');
|
||||
*/
|
||||
setUserProject(userProject: string): void;
|
||||
upload(pathString: string, options?: UploadOptions): Promise<UploadResponse>;
|
||||
upload(
|
||||
pathString: string,
|
||||
options: UploadOptions,
|
||||
callback: UploadCallback
|
||||
): void;
|
||||
upload(pathString: string, callback: UploadCallback): void;
|
||||
makeAllFilesPublicPrivate_(
|
||||
options?: MakeAllFilesPublicPrivateOptions
|
||||
): Promise<MakeAllFilesPublicPrivateResponse>;
|
||||
makeAllFilesPublicPrivate_(callback: MakeAllFilesPublicPrivateCallback): void;
|
||||
makeAllFilesPublicPrivate_(
|
||||
options: MakeAllFilesPublicPrivateOptions,
|
||||
callback: MakeAllFilesPublicPrivateCallback
|
||||
): void;
|
||||
getId(): string;
|
||||
}
|
||||
|
||||
/*! firebase-admin v9.4.2 */
|
||||
declare namespace firebasestorage {
|
||||
/**
|
||||
* The default `Storage` service if no
|
||||
* app is provided or the `Storage` service associated with the provided
|
||||
* app.
|
||||
*/
|
||||
export class Storage {
|
||||
/**
|
||||
* Optional app whose `Storage` service to
|
||||
* return. If not provided, the default `Storage` service will be returned.
|
||||
*/
|
||||
app: app.App;
|
||||
/**
|
||||
* @returns A [Bucket](https://cloud.google.com/nodejs/docs/reference/storage/latest/Bucket)
|
||||
* instance as defined in the `@google-cloud/storage` package.
|
||||
*/
|
||||
bucket(name?: string): Bucket;
|
||||
}
|
||||
}
|
||||
1557
src/components/CodeEditor/firestore.d.ts
vendored
Normal file
1557
src/components/CodeEditor/firestore.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
535
src/components/CodeEditor/github-dark-default.json
Normal file
535
src/components/CodeEditor/github-dark-default.json
Normal file
@@ -0,0 +1,535 @@
|
||||
{
|
||||
"inherit": true,
|
||||
"base": "vs-dark",
|
||||
"colors": {
|
||||
"focusBorder": "#1f6feb",
|
||||
"foreground": "#c9d1d9",
|
||||
"descriptionForeground": "#8b949e",
|
||||
"errorForeground": "#f85149",
|
||||
"textLink.foreground": "#58a6ff",
|
||||
"textLink.activeForeground": "#58a6ff",
|
||||
"textBlockQuote.background": "#010409",
|
||||
"textBlockQuote.border": "#30363d",
|
||||
"textCodeBlock.background": "#6e768166",
|
||||
"textPreformat.foreground": "#8b949e",
|
||||
"textSeparator.foreground": "#21262d",
|
||||
"button.background": "#238636",
|
||||
"button.foreground": "#ffffff",
|
||||
"button.hoverBackground": "#2ea043",
|
||||
"button.secondaryBackground": "#282e33",
|
||||
"button.secondaryForeground": "#c9d1d9",
|
||||
"button.secondaryHoverBackground": "#30363d",
|
||||
"checkbox.background": "#161b22",
|
||||
"checkbox.border": "#30363d",
|
||||
"dropdown.background": "#161b22",
|
||||
"dropdown.border": "#30363d",
|
||||
"dropdown.foreground": "#c9d1d9",
|
||||
"dropdown.listBackground": "#161b22",
|
||||
"input.background": "#0d1117",
|
||||
"input.border": "#30363d",
|
||||
"input.foreground": "#c9d1d9",
|
||||
"input.placeholderForeground": "#484f58",
|
||||
"badge.foreground": "#f0f6fc",
|
||||
"badge.background": "#1f6feb",
|
||||
"progressBar.background": "#1f6feb",
|
||||
"titleBar.activeForeground": "#8b949e",
|
||||
"titleBar.activeBackground": "#0d1117",
|
||||
"titleBar.inactiveForeground": "#8b949e",
|
||||
"titleBar.inactiveBackground": "#010409",
|
||||
"titleBar.border": "#30363d",
|
||||
"activityBar.foreground": "#c9d1d9",
|
||||
"activityBar.inactiveForeground": "#8b949e",
|
||||
"activityBar.background": "#0d1117",
|
||||
"activityBarBadge.foreground": "#f0f6fc",
|
||||
"activityBarBadge.background": "#1f6feb",
|
||||
"activityBar.activeBorder": "#f78166",
|
||||
"activityBar.border": "#30363d",
|
||||
"sideBar.foreground": "#c9d1d9",
|
||||
"sideBar.background": "#010409",
|
||||
"sideBar.border": "#30363d",
|
||||
"sideBarTitle.foreground": "#c9d1d9",
|
||||
"sideBarSectionHeader.foreground": "#c9d1d9",
|
||||
"sideBarSectionHeader.background": "#010409",
|
||||
"sideBarSectionHeader.border": "#30363d",
|
||||
"list.hoverForeground": "#c9d1d9",
|
||||
"list.inactiveSelectionForeground": "#c9d1d9",
|
||||
"list.activeSelectionForeground": "#c9d1d9",
|
||||
"list.hoverBackground": "#6e76811a",
|
||||
"list.inactiveSelectionBackground": "#6e768166",
|
||||
"list.activeSelectionBackground": "#6e768166",
|
||||
"list.focusForeground": "#c9d1d9",
|
||||
"list.focusBackground": "#388bfd26",
|
||||
"list.inactiveFocusBackground": "#388bfd26",
|
||||
"list.highlightForeground": "#58a6ff",
|
||||
"tree.indentGuidesStroke": "#21262d",
|
||||
"notificationCenterHeader.foreground": "#8b949e",
|
||||
"notificationCenterHeader.background": "#161b22",
|
||||
"notifications.foreground": "#c9d1d9",
|
||||
"notifications.background": "#161b22",
|
||||
"notifications.border": "#30363d",
|
||||
"notificationsErrorIcon.foreground": "#f85149",
|
||||
"notificationsWarningIcon.foreground": "#d29922",
|
||||
"notificationsInfoIcon.foreground": "#58a6ff",
|
||||
"pickerGroup.border": "#30363d",
|
||||
"pickerGroup.foreground": "#8b949e",
|
||||
"quickInput.background": "#161b22",
|
||||
"quickInput.foreground": "#c9d1d9",
|
||||
"statusBar.foreground": "#8b949e",
|
||||
"statusBar.background": "#0d1117",
|
||||
"statusBar.border": "#30363d",
|
||||
"statusBar.noFolderBackground": "#0d1117",
|
||||
"statusBar.debuggingBackground": "#da3633",
|
||||
"statusBar.debuggingForeground": "#f0f6fc",
|
||||
"statusBarItem.prominentBackground": "#161b22",
|
||||
"editorGroupHeader.tabsBackground": "#010409",
|
||||
"editorGroupHeader.tabsBorder": "#30363d",
|
||||
"editorGroup.border": "#30363d",
|
||||
"tab.activeForeground": "#c9d1d9",
|
||||
"tab.inactiveForeground": "#8b949e",
|
||||
"tab.inactiveBackground": "#010409",
|
||||
"tab.activeBackground": "#0d1117",
|
||||
"tab.hoverBackground": "#0d1117",
|
||||
"tab.unfocusedHoverBackground": "#6e76811a",
|
||||
"tab.border": "#30363d",
|
||||
"tab.unfocusedActiveBorderTop": "#30363d",
|
||||
"tab.activeBorder": "#0d1117",
|
||||
"tab.unfocusedActiveBorder": "#0d1117",
|
||||
"tab.activeBorderTop": "#f78166",
|
||||
"breadcrumb.foreground": "#8b949e",
|
||||
"breadcrumb.focusForeground": "#c9d1d9",
|
||||
"breadcrumb.activeSelectionForeground": "#8b949e",
|
||||
"breadcrumbPicker.background": "#161b22",
|
||||
"editor.foreground": "#c9d1d9",
|
||||
"editor.background": "#0d1117",
|
||||
"editorWidget.background": "#161b22",
|
||||
"editor.foldBackground": "#6e76811a",
|
||||
"editor.lineHighlightBackground": "#6e76811a",
|
||||
"editorLineNumber.foreground": "#8b949e",
|
||||
"editorLineNumber.activeForeground": "#c9d1d9",
|
||||
"editorIndentGuide.background": "#21262d",
|
||||
"editorIndentGuide.activeBackground": "#30363d",
|
||||
"editorWhitespace.foreground": "#484f58",
|
||||
"editorCursor.foreground": "#58a6ff",
|
||||
"editor.findMatchBackground": "#ffd33d44",
|
||||
"editor.findMatchHighlightBackground": "#ffd33d22",
|
||||
"editor.linkedEditingBackground": "#3392FF22",
|
||||
"editor.inactiveSelectionBackground": "#3392FF22",
|
||||
"editor.selectionBackground": "#3392FF44",
|
||||
"editor.selectionHighlightBackground": "#17E5E633",
|
||||
"editor.selectionHighlightBorder": "#17E5E600",
|
||||
"editor.wordHighlightBackground": "#17E5E600",
|
||||
"editor.wordHighlightStrongBackground": "#17E5E600",
|
||||
"editor.wordHighlightBorder": "#17E5E699",
|
||||
"editor.wordHighlightStrongBorder": "#17E5E666",
|
||||
"editorBracketMatch.background": "#17E5E650",
|
||||
"editorBracketMatch.border": "#17E5E600",
|
||||
"editorGutter.modifiedBackground": "#bb800966",
|
||||
"editorGutter.addedBackground": "#2ea04366",
|
||||
"editorGutter.deletedBackground": "#f8514966",
|
||||
"diffEditor.insertedTextBackground": "#2ea04326",
|
||||
"diffEditor.removedTextBackground": "#f8514926",
|
||||
"scrollbar.shadow": "#0008",
|
||||
"scrollbarSlider.background": "#484F5833",
|
||||
"scrollbarSlider.hoverBackground": "#484F5844",
|
||||
"scrollbarSlider.activeBackground": "#484F5888",
|
||||
"editorOverviewRuler.border": "#010409",
|
||||
"panel.background": "#010409",
|
||||
"panel.border": "#30363d",
|
||||
"panelTitle.activeBorder": "#f78166",
|
||||
"panelTitle.activeForeground": "#c9d1d9",
|
||||
"panelTitle.inactiveForeground": "#8b949e",
|
||||
"panelInput.border": "#30363d",
|
||||
"terminal.foreground": "#8b949e",
|
||||
"terminal.ansiBlack": "#484f58",
|
||||
"terminal.ansiRed": "#ff7b72",
|
||||
"terminal.ansiGreen": "#3fb950",
|
||||
"terminal.ansiYellow": "#d29922",
|
||||
"terminal.ansiBlue": "#58a6ff",
|
||||
"terminal.ansiMagenta": "#bc8cff",
|
||||
"terminal.ansiCyan": "#39c5cf",
|
||||
"terminal.ansiWhite": "#b1bac4",
|
||||
"terminal.ansiBrightBlack": "#6e7681",
|
||||
"terminal.ansiBrightRed": "#ffa198",
|
||||
"terminal.ansiBrightGreen": "#56d364",
|
||||
"terminal.ansiBrightYellow": "#e3b341",
|
||||
"terminal.ansiBrightBlue": "#79c0ff",
|
||||
"terminal.ansiBrightMagenta": "#d2a8ff",
|
||||
"terminal.ansiBrightCyan": "#56d4dd",
|
||||
"terminal.ansiBrightWhite": "#f0f6fc",
|
||||
"gitDecoration.addedResourceForeground": "#3fb950",
|
||||
"gitDecoration.modifiedResourceForeground": "#d29922",
|
||||
"gitDecoration.deletedResourceForeground": "#f85149",
|
||||
"gitDecoration.untrackedResourceForeground": "#3fb950",
|
||||
"gitDecoration.ignoredResourceForeground": "#484f58",
|
||||
"gitDecoration.conflictingResourceForeground": "#db6d28",
|
||||
"gitDecoration.submoduleResourceForeground": "#8b949e",
|
||||
"debugToolBar.background": "#161b22",
|
||||
"editor.stackFrameHighlightBackground": "#D2992225",
|
||||
"editor.focusedStackFrameHighlightBackground": "#3FB95025",
|
||||
"peekViewEditor.matchHighlightBackground": "#ffd33d33",
|
||||
"peekViewResult.matchHighlightBackground": "#ffd33d33",
|
||||
"peekViewEditor.background": "#0d111788",
|
||||
"peekViewResult.background": "#0d1117",
|
||||
"settings.headerForeground": "#8b949e",
|
||||
"settings.modifiedItemIndicator": "#bb800966",
|
||||
"welcomePage.buttonBackground": "#21262d",
|
||||
"welcomePage.buttonHoverBackground": "#30363d"
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"foreground": "#8b949e",
|
||||
"token": "comment"
|
||||
},
|
||||
{
|
||||
"foreground": "#8b949e",
|
||||
"token": "punctuation.definition.comment"
|
||||
},
|
||||
{
|
||||
"foreground": "#8b949e",
|
||||
"token": "string.comment"
|
||||
},
|
||||
{
|
||||
"foreground": "#79c0ff",
|
||||
"token": "constant"
|
||||
},
|
||||
{
|
||||
"foreground": "#79c0ff",
|
||||
"token": "entity.name.constant"
|
||||
},
|
||||
{
|
||||
"foreground": "#79c0ff",
|
||||
"token": "variable.other.constant"
|
||||
},
|
||||
{
|
||||
"foreground": "#79c0ff",
|
||||
"token": "variable.language"
|
||||
},
|
||||
{
|
||||
"foreground": "#79c0ff",
|
||||
"token": "entity"
|
||||
},
|
||||
{
|
||||
"foreground": "#ffa657",
|
||||
"token": "entity.name"
|
||||
},
|
||||
{
|
||||
"foreground": "#ffa657",
|
||||
"token": "meta.export.default"
|
||||
},
|
||||
{
|
||||
"foreground": "#ffa657",
|
||||
"token": "meta.definition.variable"
|
||||
},
|
||||
{
|
||||
"foreground": "#c9d1d9",
|
||||
"token": "variable.parameter.function"
|
||||
},
|
||||
{
|
||||
"foreground": "#c9d1d9",
|
||||
"token": "meta.jsx.children"
|
||||
},
|
||||
{
|
||||
"foreground": "#c9d1d9",
|
||||
"token": "meta.block"
|
||||
},
|
||||
{
|
||||
"foreground": "#c9d1d9",
|
||||
"token": "meta.tag.attributes"
|
||||
},
|
||||
{
|
||||
"foreground": "#c9d1d9",
|
||||
"token": "entity.name.constant"
|
||||
},
|
||||
{
|
||||
"foreground": "#c9d1d9",
|
||||
"token": "meta.object.member"
|
||||
},
|
||||
{
|
||||
"foreground": "#c9d1d9",
|
||||
"token": "meta.embedded.expression"
|
||||
},
|
||||
{
|
||||
"foreground": "#d2a8ff",
|
||||
"token": "entity.name.function"
|
||||
},
|
||||
{
|
||||
"foreground": "#7ee787",
|
||||
"token": "entity.name.tag"
|
||||
},
|
||||
{
|
||||
"foreground": "#7ee787",
|
||||
"token": "support.class.component"
|
||||
},
|
||||
{
|
||||
"foreground": "#ff7b72",
|
||||
"token": "keyword"
|
||||
},
|
||||
{
|
||||
"foreground": "#ff7b72",
|
||||
"token": "storage"
|
||||
},
|
||||
{
|
||||
"foreground": "#ff7b72",
|
||||
"token": "storage.type"
|
||||
},
|
||||
{
|
||||
"foreground": "#c9d1d9",
|
||||
"token": "storage.modifier.package"
|
||||
},
|
||||
{
|
||||
"foreground": "#c9d1d9",
|
||||
"token": "storage.modifier.import"
|
||||
},
|
||||
{
|
||||
"foreground": "#c9d1d9",
|
||||
"token": "storage.type.java"
|
||||
},
|
||||
{
|
||||
"foreground": "#a5d6ff",
|
||||
"token": "string"
|
||||
},
|
||||
{
|
||||
"foreground": "#a5d6ff",
|
||||
"token": "punctuation.definition.string"
|
||||
},
|
||||
{
|
||||
"foreground": "#a5d6ff",
|
||||
"token": "string punctuation.section.embedded source"
|
||||
},
|
||||
{
|
||||
"foreground": "#79c0ff",
|
||||
"token": "support"
|
||||
},
|
||||
{
|
||||
"foreground": "#79c0ff",
|
||||
"token": "meta.property-name"
|
||||
},
|
||||
{
|
||||
"foreground": "#ffa657",
|
||||
"token": "variable"
|
||||
},
|
||||
{
|
||||
"foreground": "#c9d1d9",
|
||||
"token": "variable.other"
|
||||
},
|
||||
{
|
||||
"fontStyle": "italic",
|
||||
"foreground": "#ffa198",
|
||||
"token": "invalid.broken"
|
||||
},
|
||||
{
|
||||
"fontStyle": "italic",
|
||||
"foreground": "#ffa198",
|
||||
"token": "invalid.deprecated"
|
||||
},
|
||||
{
|
||||
"fontStyle": "italic",
|
||||
"foreground": "#ffa198",
|
||||
"token": "invalid.illegal"
|
||||
},
|
||||
{
|
||||
"fontStyle": "italic",
|
||||
"foreground": "#ffa198",
|
||||
"token": "invalid.unimplemented"
|
||||
},
|
||||
{
|
||||
"fontStyle": "italic underline",
|
||||
"background": "#ff7b72",
|
||||
"foreground": "#0d1117",
|
||||
"content": "^M",
|
||||
"token": "carriage-return"
|
||||
},
|
||||
{
|
||||
"foreground": "#ffa198",
|
||||
"token": "message.error"
|
||||
},
|
||||
{
|
||||
"foreground": "#c9d1d9",
|
||||
"token": "string source"
|
||||
},
|
||||
{
|
||||
"foreground": "#79c0ff",
|
||||
"token": "string variable"
|
||||
},
|
||||
{
|
||||
"foreground": "#a5d6ff",
|
||||
"token": "source.regexp"
|
||||
},
|
||||
{
|
||||
"foreground": "#a5d6ff",
|
||||
"token": "string.regexp"
|
||||
},
|
||||
{
|
||||
"foreground": "#a5d6ff",
|
||||
"token": "string.regexp.character-class"
|
||||
},
|
||||
{
|
||||
"foreground": "#a5d6ff",
|
||||
"token": "string.regexp constant.character.escape"
|
||||
},
|
||||
{
|
||||
"foreground": "#a5d6ff",
|
||||
"token": "string.regexp source.ruby.embedded"
|
||||
},
|
||||
{
|
||||
"foreground": "#a5d6ff",
|
||||
"token": "string.regexp string.regexp.arbitrary-repitition"
|
||||
},
|
||||
{
|
||||
"fontStyle": "bold",
|
||||
"foreground": "#7ee787",
|
||||
"token": "string.regexp constant.character.escape"
|
||||
},
|
||||
{
|
||||
"foreground": "#79c0ff",
|
||||
"token": "support.constant"
|
||||
},
|
||||
{
|
||||
"foreground": "#79c0ff",
|
||||
"token": "support.variable"
|
||||
},
|
||||
{
|
||||
"foreground": "#79c0ff",
|
||||
"token": "meta.module-reference"
|
||||
},
|
||||
{
|
||||
"foreground": "#ffa657",
|
||||
"token": "punctuation.definition.list.begin.markdown"
|
||||
},
|
||||
{
|
||||
"fontStyle": "bold",
|
||||
"foreground": "#79c0ff",
|
||||
"token": "markup.heading"
|
||||
},
|
||||
{
|
||||
"fontStyle": "bold",
|
||||
"foreground": "#79c0ff",
|
||||
"token": "markup.heading entity.name"
|
||||
},
|
||||
{
|
||||
"foreground": "#7ee787",
|
||||
"token": "markup.quote"
|
||||
},
|
||||
{
|
||||
"fontStyle": "italic",
|
||||
"foreground": "#c9d1d9",
|
||||
"token": "markup.italic"
|
||||
},
|
||||
{
|
||||
"fontStyle": "bold",
|
||||
"foreground": "#c9d1d9",
|
||||
"token": "markup.bold"
|
||||
},
|
||||
{
|
||||
"foreground": "#79c0ff",
|
||||
"token": "markup.raw"
|
||||
},
|
||||
{
|
||||
"background": "#490202",
|
||||
"foreground": "#ffa198",
|
||||
"token": "markup.deleted"
|
||||
},
|
||||
{
|
||||
"background": "#490202",
|
||||
"foreground": "#ffa198",
|
||||
"token": "meta.diff.header.from-file"
|
||||
},
|
||||
{
|
||||
"background": "#490202",
|
||||
"foreground": "#ffa198",
|
||||
"token": "punctuation.definition.deleted"
|
||||
},
|
||||
{
|
||||
"background": "#04260f",
|
||||
"foreground": "#7ee787",
|
||||
"token": "markup.inserted"
|
||||
},
|
||||
{
|
||||
"background": "#04260f",
|
||||
"foreground": "#7ee787",
|
||||
"token": "meta.diff.header.to-file"
|
||||
},
|
||||
{
|
||||
"background": "#04260f",
|
||||
"foreground": "#7ee787",
|
||||
"token": "punctuation.definition.inserted"
|
||||
},
|
||||
{
|
||||
"background": "#5a1e02",
|
||||
"foreground": "#ffa657",
|
||||
"token": "markup.changed"
|
||||
},
|
||||
{
|
||||
"background": "#5a1e02",
|
||||
"foreground": "#ffa657",
|
||||
"token": "punctuation.definition.changed"
|
||||
},
|
||||
{
|
||||
"foreground": "#161b22",
|
||||
"background": "#79c0ff",
|
||||
"token": "markup.ignored"
|
||||
},
|
||||
{
|
||||
"foreground": "#161b22",
|
||||
"background": "#79c0ff",
|
||||
"token": "markup.untracked"
|
||||
},
|
||||
{
|
||||
"foreground": "#d2a8ff",
|
||||
"fontStyle": "bold",
|
||||
"token": "meta.diff.range"
|
||||
},
|
||||
{
|
||||
"foreground": "#79c0ff",
|
||||
"token": "meta.diff.header"
|
||||
},
|
||||
{
|
||||
"fontStyle": "bold",
|
||||
"foreground": "#79c0ff",
|
||||
"token": "meta.separator"
|
||||
},
|
||||
{
|
||||
"foreground": "#79c0ff",
|
||||
"token": "meta.output"
|
||||
},
|
||||
{
|
||||
"foreground": "#8b949e",
|
||||
"token": "brackethighlighter.tag"
|
||||
},
|
||||
{
|
||||
"foreground": "#8b949e",
|
||||
"token": "brackethighlighter.curly"
|
||||
},
|
||||
{
|
||||
"foreground": "#8b949e",
|
||||
"token": "brackethighlighter.round"
|
||||
},
|
||||
{
|
||||
"foreground": "#8b949e",
|
||||
"token": "brackethighlighter.square"
|
||||
},
|
||||
{
|
||||
"foreground": "#8b949e",
|
||||
"token": "brackethighlighter.angle"
|
||||
},
|
||||
{
|
||||
"foreground": "#8b949e",
|
||||
"token": "brackethighlighter.quote"
|
||||
},
|
||||
{
|
||||
"foreground": "#ffa198",
|
||||
"token": "brackethighlighter.unmatched"
|
||||
},
|
||||
{
|
||||
"foreground": "#a5d6ff",
|
||||
"fontStyle": "underline",
|
||||
"token": "constant.other.reference.link"
|
||||
},
|
||||
{
|
||||
"foreground": "#a5d6ff",
|
||||
"fontStyle": "underline",
|
||||
"token": "string.other.link"
|
||||
}
|
||||
],
|
||||
"encodedTokensColors": []
|
||||
}
|
||||
531
src/components/CodeEditor/github-light-default.json
Normal file
531
src/components/CodeEditor/github-light-default.json
Normal file
@@ -0,0 +1,531 @@
|
||||
{
|
||||
"inherit": true,
|
||||
"base": "vs",
|
||||
"colors": {
|
||||
"focusBorder": "#0969da",
|
||||
"foreground": "#24292f",
|
||||
"descriptionForeground": "#57606a",
|
||||
"errorForeground": "#cf222e",
|
||||
"textLink.foreground": "#0969da",
|
||||
"textLink.activeForeground": "#0969da",
|
||||
"textBlockQuote.background": "#f6f8fa",
|
||||
"textBlockQuote.border": "#d0d7de",
|
||||
"textCodeBlock.background": "#afb8c133",
|
||||
"textPreformat.foreground": "#57606a",
|
||||
"textSeparator.foreground": "#d8dee4",
|
||||
"button.background": "#2da44e",
|
||||
"button.foreground": "#ffffff",
|
||||
"button.hoverBackground": "#2c974b",
|
||||
"button.secondaryBackground": "#ebecf0",
|
||||
"button.secondaryForeground": "#24292f",
|
||||
"button.secondaryHoverBackground": "#f3f4f6",
|
||||
"checkbox.background": "#f6f8fa",
|
||||
"checkbox.border": "#d0d7de",
|
||||
"dropdown.background": "#ffffff",
|
||||
"dropdown.border": "#d0d7de",
|
||||
"dropdown.foreground": "#24292f",
|
||||
"dropdown.listBackground": "#ffffff",
|
||||
"input.background": "#ffffff",
|
||||
"input.border": "#d0d7de",
|
||||
"input.foreground": "#24292f",
|
||||
"input.placeholderForeground": "#6e7781",
|
||||
"badge.foreground": "#ffffff",
|
||||
"badge.background": "#0969da",
|
||||
"progressBar.background": "#0969da",
|
||||
"titleBar.activeForeground": "#57606a",
|
||||
"titleBar.activeBackground": "#ffffff",
|
||||
"titleBar.inactiveForeground": "#57606a",
|
||||
"titleBar.inactiveBackground": "#f6f8fa",
|
||||
"titleBar.border": "#d0d7de",
|
||||
"activityBar.foreground": "#24292f",
|
||||
"activityBar.inactiveForeground": "#57606a",
|
||||
"activityBar.background": "#ffffff",
|
||||
"activityBarBadge.foreground": "#ffffff",
|
||||
"activityBarBadge.background": "#0969da",
|
||||
"activityBar.activeBorder": "#fd8c73",
|
||||
"activityBar.border": "#d0d7de",
|
||||
"sideBar.foreground": "#24292f",
|
||||
"sideBar.background": "#f6f8fa",
|
||||
"sideBar.border": "#d0d7de",
|
||||
"sideBarTitle.foreground": "#24292f",
|
||||
"sideBarSectionHeader.foreground": "#24292f",
|
||||
"sideBarSectionHeader.background": "#f6f8fa",
|
||||
"sideBarSectionHeader.border": "#d0d7de",
|
||||
"list.hoverForeground": "#24292f",
|
||||
"list.inactiveSelectionForeground": "#24292f",
|
||||
"list.activeSelectionForeground": "#24292f",
|
||||
"list.hoverBackground": "#eaeef280",
|
||||
"list.inactiveSelectionBackground": "#afb8c133",
|
||||
"list.activeSelectionBackground": "#afb8c133",
|
||||
"list.focusForeground": "#24292f",
|
||||
"list.focusBackground": "#ddf4ff",
|
||||
"list.inactiveFocusBackground": "#ddf4ff",
|
||||
"list.highlightForeground": "#0969da",
|
||||
"tree.indentGuidesStroke": "#d8dee4",
|
||||
"notificationCenterHeader.foreground": "#57606a",
|
||||
"notificationCenterHeader.background": "#f6f8fa",
|
||||
"notifications.foreground": "#24292f",
|
||||
"notifications.background": "#ffffff",
|
||||
"notifications.border": "#d0d7de",
|
||||
"notificationsErrorIcon.foreground": "#cf222e",
|
||||
"notificationsWarningIcon.foreground": "#9a6700",
|
||||
"notificationsInfoIcon.foreground": "#0969da",
|
||||
"pickerGroup.border": "#d0d7de",
|
||||
"pickerGroup.foreground": "#57606a",
|
||||
"quickInput.background": "#ffffff",
|
||||
"quickInput.foreground": "#24292f",
|
||||
"statusBar.foreground": "#57606a",
|
||||
"statusBar.background": "#ffffff",
|
||||
"statusBar.border": "#d0d7de",
|
||||
"statusBar.noFolderBackground": "#ffffff",
|
||||
"statusBar.debuggingBackground": "#cf222e",
|
||||
"statusBar.debuggingForeground": "#ffffff",
|
||||
"statusBarItem.prominentBackground": "#f6f8fa",
|
||||
"editorGroupHeader.tabsBackground": "#f6f8fa",
|
||||
"editorGroupHeader.tabsBorder": "#d0d7de",
|
||||
"editorGroup.border": "#d0d7de",
|
||||
"tab.activeForeground": "#24292f",
|
||||
"tab.inactiveForeground": "#57606a",
|
||||
"tab.inactiveBackground": "#f6f8fa",
|
||||
"tab.activeBackground": "#ffffff",
|
||||
"tab.hoverBackground": "#ffffff",
|
||||
"tab.unfocusedHoverBackground": "#eaeef280",
|
||||
"tab.border": "#d0d7de",
|
||||
"tab.unfocusedActiveBorderTop": "#d0d7de",
|
||||
"tab.activeBorder": "#ffffff",
|
||||
"tab.unfocusedActiveBorder": "#ffffff",
|
||||
"tab.activeBorderTop": "#fd8c73",
|
||||
"breadcrumb.foreground": "#57606a",
|
||||
"breadcrumb.focusForeground": "#24292f",
|
||||
"breadcrumb.activeSelectionForeground": "#57606a",
|
||||
"breadcrumbPicker.background": "#ffffff",
|
||||
"editor.foreground": "#24292f",
|
||||
"editor.background": "#ffffff",
|
||||
"editorWidget.background": "#ffffff",
|
||||
"editor.foldBackground": "#6e77811a",
|
||||
"editor.lineHighlightBackground": "#eaeef280",
|
||||
"editorLineNumber.foreground": "#57606a",
|
||||
"editorLineNumber.activeForeground": "#24292f",
|
||||
"editorIndentGuide.background": "#d8dee4",
|
||||
"editorIndentGuide.activeBackground": "#d0d7de",
|
||||
"editorWhitespace.foreground": "#6e7781",
|
||||
"editorCursor.foreground": "#0969da",
|
||||
"editor.findMatchBackground": "#bf8700",
|
||||
"editor.findMatchHighlightBackground": "#ffdf5d66",
|
||||
"editor.linkedEditingBackground": "#0366d611",
|
||||
"editor.inactiveSelectionBackground": "#0366d611",
|
||||
"editor.selectionBackground": "#0366d625",
|
||||
"editor.selectionHighlightBackground": "#34d05840",
|
||||
"editor.selectionHighlightBorder": "#34d05800",
|
||||
"editor.wordHighlightBackground": "#34d05800",
|
||||
"editor.wordHighlightStrongBackground": "#34d05800",
|
||||
"editor.wordHighlightBorder": "#24943e99",
|
||||
"editor.wordHighlightStrongBorder": "#24943e50",
|
||||
"editorBracketMatch.background": "#34d05840",
|
||||
"editorBracketMatch.border": "#34d05800",
|
||||
"editorGutter.modifiedBackground": "#d4a72c66",
|
||||
"editorGutter.addedBackground": "#4ac26b66",
|
||||
"editorGutter.deletedBackground": "#ff818266",
|
||||
"diffEditor.insertedTextBackground": "#85e89d33",
|
||||
"diffEditor.removedTextBackground": "#f9758326",
|
||||
"scrollbar.shadow": "#6a737d33",
|
||||
"scrollbarSlider.background": "#959da533",
|
||||
"scrollbarSlider.hoverBackground": "#959da544",
|
||||
"scrollbarSlider.activeBackground": "#959da588",
|
||||
"editorOverviewRuler.border": "#ffffff",
|
||||
"panel.background": "#f6f8fa",
|
||||
"panel.border": "#d0d7de",
|
||||
"panelTitle.activeBorder": "#fd8c73",
|
||||
"panelTitle.activeForeground": "#24292f",
|
||||
"panelTitle.inactiveForeground": "#57606a",
|
||||
"panelInput.border": "#d0d7de",
|
||||
"terminal.foreground": "#57606a",
|
||||
"terminal.ansiBlack": "#24292f",
|
||||
"terminal.ansiRed": "#cf222e",
|
||||
"terminal.ansiGreen": "#116329",
|
||||
"terminal.ansiYellow": "#4d2d00",
|
||||
"terminal.ansiBlue": "#0969da",
|
||||
"terminal.ansiMagenta": "#8250df",
|
||||
"terminal.ansiCyan": "#1b7c83",
|
||||
"terminal.ansiWhite": "#6e7781",
|
||||
"terminal.ansiBrightBlack": "#57606a",
|
||||
"terminal.ansiBrightRed": "#a40e26",
|
||||
"terminal.ansiBrightGreen": "#1a7f37",
|
||||
"terminal.ansiBrightYellow": "#633c01",
|
||||
"terminal.ansiBrightBlue": "#218bff",
|
||||
"terminal.ansiBrightMagenta": "#a475f9",
|
||||
"terminal.ansiBrightCyan": "#3192aa",
|
||||
"terminal.ansiBrightWhite": "#8c959f",
|
||||
"gitDecoration.addedResourceForeground": "#1a7f37",
|
||||
"gitDecoration.modifiedResourceForeground": "#9a6700",
|
||||
"gitDecoration.deletedResourceForeground": "#cf222e",
|
||||
"gitDecoration.untrackedResourceForeground": "#1a7f37",
|
||||
"gitDecoration.ignoredResourceForeground": "#6e7781",
|
||||
"gitDecoration.conflictingResourceForeground": "#bc4c00",
|
||||
"gitDecoration.submoduleResourceForeground": "#57606a",
|
||||
"debugToolBar.background": "#ffffff",
|
||||
"editor.stackFrameHighlightBackground": "#ffd33d33",
|
||||
"editor.focusedStackFrameHighlightBackground": "#28a74525",
|
||||
"settings.headerForeground": "#57606a",
|
||||
"settings.modifiedItemIndicator": "#d4a72c66",
|
||||
"welcomePage.buttonBackground": "#f6f8fa",
|
||||
"welcomePage.buttonHoverBackground": "#f3f4f6"
|
||||
},
|
||||
"rules": [
|
||||
{
|
||||
"foreground": "#6e7781",
|
||||
"token": "comment"
|
||||
},
|
||||
{
|
||||
"foreground": "#6e7781",
|
||||
"token": "punctuation.definition.comment"
|
||||
},
|
||||
{
|
||||
"foreground": "#6e7781",
|
||||
"token": "string.comment"
|
||||
},
|
||||
{
|
||||
"foreground": "#0550ae",
|
||||
"token": "constant"
|
||||
},
|
||||
{
|
||||
"foreground": "#0550ae",
|
||||
"token": "entity.name.constant"
|
||||
},
|
||||
{
|
||||
"foreground": "#0550ae",
|
||||
"token": "variable.other.constant"
|
||||
},
|
||||
{
|
||||
"foreground": "#0550ae",
|
||||
"token": "variable.language"
|
||||
},
|
||||
{
|
||||
"foreground": "#0550ae",
|
||||
"token": "entity"
|
||||
},
|
||||
{
|
||||
"foreground": "#953800",
|
||||
"token": "entity.name"
|
||||
},
|
||||
{
|
||||
"foreground": "#953800",
|
||||
"token": "meta.export.default"
|
||||
},
|
||||
{
|
||||
"foreground": "#953800",
|
||||
"token": "meta.definition.variable"
|
||||
},
|
||||
{
|
||||
"foreground": "#24292f",
|
||||
"token": "variable.parameter.function"
|
||||
},
|
||||
{
|
||||
"foreground": "#24292f",
|
||||
"token": "meta.jsx.children"
|
||||
},
|
||||
{
|
||||
"foreground": "#24292f",
|
||||
"token": "meta.block"
|
||||
},
|
||||
{
|
||||
"foreground": "#24292f",
|
||||
"token": "meta.tag.attributes"
|
||||
},
|
||||
{
|
||||
"foreground": "#24292f",
|
||||
"token": "entity.name.constant"
|
||||
},
|
||||
{
|
||||
"foreground": "#24292f",
|
||||
"token": "meta.object.member"
|
||||
},
|
||||
{
|
||||
"foreground": "#24292f",
|
||||
"token": "meta.embedded.expression"
|
||||
},
|
||||
{
|
||||
"foreground": "#8250df",
|
||||
"token": "entity.name.function"
|
||||
},
|
||||
{
|
||||
"foreground": "#116329",
|
||||
"token": "entity.name.tag"
|
||||
},
|
||||
{
|
||||
"foreground": "#116329",
|
||||
"token": "support.class.component"
|
||||
},
|
||||
{
|
||||
"foreground": "#cf222e",
|
||||
"token": "keyword"
|
||||
},
|
||||
{
|
||||
"foreground": "#cf222e",
|
||||
"token": "storage"
|
||||
},
|
||||
{
|
||||
"foreground": "#cf222e",
|
||||
"token": "storage.type"
|
||||
},
|
||||
{
|
||||
"foreground": "#24292f",
|
||||
"token": "storage.modifier.package"
|
||||
},
|
||||
{
|
||||
"foreground": "#24292f",
|
||||
"token": "storage.modifier.import"
|
||||
},
|
||||
{
|
||||
"foreground": "#24292f",
|
||||
"token": "storage.type.java"
|
||||
},
|
||||
{
|
||||
"foreground": "#0a3069",
|
||||
"token": "string"
|
||||
},
|
||||
{
|
||||
"foreground": "#0a3069",
|
||||
"token": "punctuation.definition.string"
|
||||
},
|
||||
{
|
||||
"foreground": "#0a3069",
|
||||
"token": "string punctuation.section.embedded source"
|
||||
},
|
||||
{
|
||||
"foreground": "#0550ae",
|
||||
"token": "support"
|
||||
},
|
||||
{
|
||||
"foreground": "#0550ae",
|
||||
"token": "meta.property-name"
|
||||
},
|
||||
{
|
||||
"foreground": "#953800",
|
||||
"token": "variable"
|
||||
},
|
||||
{
|
||||
"foreground": "#24292f",
|
||||
"token": "variable.other"
|
||||
},
|
||||
{
|
||||
"fontStyle": "italic",
|
||||
"foreground": "#82071e",
|
||||
"token": "invalid.broken"
|
||||
},
|
||||
{
|
||||
"fontStyle": "italic",
|
||||
"foreground": "#82071e",
|
||||
"token": "invalid.deprecated"
|
||||
},
|
||||
{
|
||||
"fontStyle": "italic",
|
||||
"foreground": "#82071e",
|
||||
"token": "invalid.illegal"
|
||||
},
|
||||
{
|
||||
"fontStyle": "italic",
|
||||
"foreground": "#82071e",
|
||||
"token": "invalid.unimplemented"
|
||||
},
|
||||
{
|
||||
"fontStyle": "italic underline",
|
||||
"background": "#cf222e",
|
||||
"foreground": "#f6f8fa",
|
||||
"content": "^M",
|
||||
"token": "carriage-return"
|
||||
},
|
||||
{
|
||||
"foreground": "#82071e",
|
||||
"token": "message.error"
|
||||
},
|
||||
{
|
||||
"foreground": "#24292f",
|
||||
"token": "string source"
|
||||
},
|
||||
{
|
||||
"foreground": "#0550ae",
|
||||
"token": "string variable"
|
||||
},
|
||||
{
|
||||
"foreground": "#0a3069",
|
||||
"token": "source.regexp"
|
||||
},
|
||||
{
|
||||
"foreground": "#0a3069",
|
||||
"token": "string.regexp"
|
||||
},
|
||||
{
|
||||
"foreground": "#0a3069",
|
||||
"token": "string.regexp.character-class"
|
||||
},
|
||||
{
|
||||
"foreground": "#0a3069",
|
||||
"token": "string.regexp constant.character.escape"
|
||||
},
|
||||
{
|
||||
"foreground": "#0a3069",
|
||||
"token": "string.regexp source.ruby.embedded"
|
||||
},
|
||||
{
|
||||
"foreground": "#0a3069",
|
||||
"token": "string.regexp string.regexp.arbitrary-repitition"
|
||||
},
|
||||
{
|
||||
"fontStyle": "bold",
|
||||
"foreground": "#116329",
|
||||
"token": "string.regexp constant.character.escape"
|
||||
},
|
||||
{
|
||||
"foreground": "#0550ae",
|
||||
"token": "support.constant"
|
||||
},
|
||||
{
|
||||
"foreground": "#0550ae",
|
||||
"token": "support.variable"
|
||||
},
|
||||
{
|
||||
"foreground": "#0550ae",
|
||||
"token": "meta.module-reference"
|
||||
},
|
||||
{
|
||||
"foreground": "#953800",
|
||||
"token": "punctuation.definition.list.begin.markdown"
|
||||
},
|
||||
{
|
||||
"fontStyle": "bold",
|
||||
"foreground": "#0550ae",
|
||||
"token": "markup.heading"
|
||||
},
|
||||
{
|
||||
"fontStyle": "bold",
|
||||
"foreground": "#0550ae",
|
||||
"token": "markup.heading entity.name"
|
||||
},
|
||||
{
|
||||
"foreground": "#116329",
|
||||
"token": "markup.quote"
|
||||
},
|
||||
{
|
||||
"fontStyle": "italic",
|
||||
"foreground": "#24292f",
|
||||
"token": "markup.italic"
|
||||
},
|
||||
{
|
||||
"fontStyle": "bold",
|
||||
"foreground": "#24292f",
|
||||
"token": "markup.bold"
|
||||
},
|
||||
{
|
||||
"foreground": "#0550ae",
|
||||
"token": "markup.raw"
|
||||
},
|
||||
{
|
||||
"background": "#FFEBE9",
|
||||
"foreground": "#82071e",
|
||||
"token": "markup.deleted"
|
||||
},
|
||||
{
|
||||
"background": "#FFEBE9",
|
||||
"foreground": "#82071e",
|
||||
"token": "meta.diff.header.from-file"
|
||||
},
|
||||
{
|
||||
"background": "#FFEBE9",
|
||||
"foreground": "#82071e",
|
||||
"token": "punctuation.definition.deleted"
|
||||
},
|
||||
{
|
||||
"background": "#dafbe1",
|
||||
"foreground": "#116329",
|
||||
"token": "markup.inserted"
|
||||
},
|
||||
{
|
||||
"background": "#dafbe1",
|
||||
"foreground": "#116329",
|
||||
"token": "meta.diff.header.to-file"
|
||||
},
|
||||
{
|
||||
"background": "#dafbe1",
|
||||
"foreground": "#116329",
|
||||
"token": "punctuation.definition.inserted"
|
||||
},
|
||||
{
|
||||
"background": "#ffd8b5",
|
||||
"foreground": "#953800",
|
||||
"token": "markup.changed"
|
||||
},
|
||||
{
|
||||
"background": "#ffd8b5",
|
||||
"foreground": "#953800",
|
||||
"token": "punctuation.definition.changed"
|
||||
},
|
||||
{
|
||||
"foreground": "#eaeef2",
|
||||
"background": "#0550ae",
|
||||
"token": "markup.ignored"
|
||||
},
|
||||
{
|
||||
"foreground": "#eaeef2",
|
||||
"background": "#0550ae",
|
||||
"token": "markup.untracked"
|
||||
},
|
||||
{
|
||||
"foreground": "#8250df",
|
||||
"fontStyle": "bold",
|
||||
"token": "meta.diff.range"
|
||||
},
|
||||
{
|
||||
"foreground": "#0550ae",
|
||||
"token": "meta.diff.header"
|
||||
},
|
||||
{
|
||||
"fontStyle": "bold",
|
||||
"foreground": "#0550ae",
|
||||
"token": "meta.separator"
|
||||
},
|
||||
{
|
||||
"foreground": "#0550ae",
|
||||
"token": "meta.output"
|
||||
},
|
||||
{
|
||||
"foreground": "#57606a",
|
||||
"token": "brackethighlighter.tag"
|
||||
},
|
||||
{
|
||||
"foreground": "#57606a",
|
||||
"token": "brackethighlighter.curly"
|
||||
},
|
||||
{
|
||||
"foreground": "#57606a",
|
||||
"token": "brackethighlighter.round"
|
||||
},
|
||||
{
|
||||
"foreground": "#57606a",
|
||||
"token": "brackethighlighter.square"
|
||||
},
|
||||
{
|
||||
"foreground": "#57606a",
|
||||
"token": "brackethighlighter.angle"
|
||||
},
|
||||
{
|
||||
"foreground": "#57606a",
|
||||
"token": "brackethighlighter.quote"
|
||||
},
|
||||
{
|
||||
"foreground": "#82071e",
|
||||
"token": "brackethighlighter.unmatched"
|
||||
},
|
||||
{
|
||||
"foreground": "#0a3069",
|
||||
"fontStyle": "underline",
|
||||
"token": "constant.other.reference.link"
|
||||
},
|
||||
{
|
||||
"foreground": "#0a3069",
|
||||
"fontStyle": "underline",
|
||||
"token": "string.other.link"
|
||||
}
|
||||
],
|
||||
"encodedTokensColors": []
|
||||
}
|
||||
2
src/components/CodeEditor/index.ts
Normal file
2
src/components/CodeEditor/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./CodeEditor";
|
||||
export { default } from "./CodeEditor";
|
||||
101
src/components/CodeEditor/rowy.d.ts
vendored
Normal file
101
src/components/CodeEditor/rowy.d.ts
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
type RowyFile = {
|
||||
downloadURL: string;
|
||||
name: string;
|
||||
type: string;
|
||||
lastModifiedTS: number;
|
||||
};
|
||||
type RowyUser = {
|
||||
email: any;
|
||||
emailVerified: boolean;
|
||||
displayName: string;
|
||||
photoURL: string;
|
||||
uid: string;
|
||||
timestamp: number;
|
||||
};
|
||||
type uploadOptions = {
|
||||
bucket?: string;
|
||||
folderPath?: string;
|
||||
fileName?: string;
|
||||
};
|
||||
interface Rowy {
|
||||
metadata: {
|
||||
/**
|
||||
* The project ID of the project running this function.
|
||||
*/
|
||||
projectId: () => Promise<string>;
|
||||
/**
|
||||
* The numeric project ID of the project running this function.
|
||||
*/
|
||||
projectNumber: () => Promise<string>;
|
||||
/**
|
||||
* The email address of service account running this function.
|
||||
* This is the service account that is used to call other APIs.
|
||||
* Ensure that the service account has the correct permissions.
|
||||
*/
|
||||
serviceAccountEmail: () => Promise<string>;
|
||||
/**
|
||||
* a user object of the service account running this function.
|
||||
* Compatible with Rowy audit fields
|
||||
* Can be used to add createdBy or updatedBy fields to a document.
|
||||
*/
|
||||
serviceAccountUser: () => Promise<RowyUser>;
|
||||
};
|
||||
/**
|
||||
* Gives access to the Secret Manager.
|
||||
* manage your secrets in the Google Cloud Console.
|
||||
*/
|
||||
secrets: {
|
||||
/**
|
||||
* Get an existing secret from the secret manager.
|
||||
*/
|
||||
get: (
|
||||
name: SecretNames,
|
||||
version?: string
|
||||
) => Promise<string | any | undefined>;
|
||||
};
|
||||
/**
|
||||
* Gives access to the Cloud Storage.
|
||||
*/
|
||||
storage: {
|
||||
upload: {
|
||||
/**
|
||||
* uploads a file to storage bucket from an external url.
|
||||
*/
|
||||
url: (
|
||||
url: string,
|
||||
options?: uploadOptions
|
||||
) => Promise<RowyFile | undefined>;
|
||||
/**
|
||||
* uploads a file to storage bucket from a buffer or string
|
||||
*/
|
||||
data: (
|
||||
data: Buffer | string,
|
||||
options?: uploadOptions
|
||||
) => Promise<RowyFile | undefined>;
|
||||
};
|
||||
};
|
||||
/**
|
||||
* @deprecated will be removed in version 2.0.
|
||||
* use rowy.secrets.get instead.
|
||||
* Get an existing secret from the secret manager.
|
||||
*/
|
||||
getSecret: (
|
||||
name: SecretNames,
|
||||
version?: string
|
||||
) => Promise<string | undefined>;
|
||||
/**
|
||||
* @deprecated will be removed in version 2.0.
|
||||
* use rowy.metadata.serviceAccountUser instead.
|
||||
* Compatible with Rowy audit fields
|
||||
* Can be used to add createdBy or updatedBy fields to a document.
|
||||
*/
|
||||
getServiceAccountUser: () => Promise<RowyUser>;
|
||||
/**
|
||||
* @deprecated will be removed in version 2.0.
|
||||
* use rowy.storage.upload.url instead.
|
||||
* uploads a file to storage bucket from an external url.
|
||||
*/
|
||||
url2storage: (url: string) => Promise<RowyFile | undefined>;
|
||||
}
|
||||
|
||||
declare const rowy: Rowy;
|
||||
327
src/components/CodeEditor/useMonacoCustomizations.ts
Normal file
327
src/components/CodeEditor/useMonacoCustomizations.ts
Normal file
@@ -0,0 +1,327 @@
|
||||
import { useEffect } from "react";
|
||||
import {
|
||||
quicktype,
|
||||
InputData,
|
||||
jsonInputForTargetLanguage,
|
||||
} from "quicktype-core";
|
||||
|
||||
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";
|
||||
|
||||
// TODO:
|
||||
// import { getColumnType, 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 rowyUtilsDefs from "!!raw-loader!./rowy.d.ts";
|
||||
import extensionsDefs from "!!raw-loader!./extensions.d.ts";
|
||||
import { runRoutes } from "@src/constants/runRoutes";
|
||||
|
||||
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 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"
|
||||
);
|
||||
monaco.languages.typescript.javascriptDefaults.addExtraLib(rowyUtilsDefs);
|
||||
} 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]);
|
||||
|
||||
// TODO:
|
||||
// const addJsonFieldDefinition = async (columnKey, interfaceName) => {
|
||||
// const samples = tableState?.rows
|
||||
// .map((row) => row[columnKey])
|
||||
// .filter((entry) => entry !== undefined)
|
||||
// .map((entry) => JSON.stringify(entry));
|
||||
// if (!samples || samples.length === 0) {
|
||||
// monaco?.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
// `type ${interfaceName} = any;`
|
||||
// );
|
||||
// return;
|
||||
// } else {
|
||||
// const jsonInput = jsonInputForTargetLanguage("typescript");
|
||||
// await jsonInput.addSource({
|
||||
// name: interfaceName,
|
||||
// samples,
|
||||
// });
|
||||
|
||||
// const inputData = new InputData();
|
||||
// inputData.addInput(jsonInput);
|
||||
// const result = await quicktype({
|
||||
// inputData,
|
||||
// lang: "typescript",
|
||||
// rendererOptions: { "just-types": "true" },
|
||||
// });
|
||||
// const newLib = result.lines.join("\n").replaceAll("export ", "");
|
||||
// monaco?.languages.typescript.javascriptDefaults.addExtraLib(newLib);
|
||||
// }
|
||||
// };
|
||||
|
||||
// TODO: types
|
||||
// const setSecrets = async (monaco, rowyRun) => {
|
||||
// // set secret options
|
||||
// try {
|
||||
// const listSecrets = await rowyRun({
|
||||
// route: runRoutes.listSecrets,
|
||||
// });
|
||||
// const secretsDef = `type SecretNames = ${listSecrets
|
||||
// .map((secret) => `"${secret}"`)
|
||||
// .join(" | ")}
|
||||
// enum secrets {
|
||||
// ${listSecrets.map((secret) => `${secret} = "${secret}"`).join("\n")}
|
||||
// }
|
||||
// `;
|
||||
// monaco.languages.typescript.javascriptDefaults.addExtraLib(secretsDef);
|
||||
// } catch (error) {
|
||||
// console.error("Could not set secret definitions: ", error);
|
||||
// }
|
||||
// };
|
||||
// TODO: types
|
||||
// const setBaseDefinitions = (monaco, columns) => {
|
||||
// const rowDefinition =
|
||||
// [
|
||||
// Object.keys(columns).map((columnKey: string) => {
|
||||
// const column = columns[columnKey];
|
||||
// const type = getColumnType(column);
|
||||
// if (type === "JSON") {
|
||||
// const interfaceName =
|
||||
// columnKey[0].toUpperCase() + columnKey.slice(1);
|
||||
// addJsonFieldDefinition(columnKey, interfaceName);
|
||||
// const def = `static "${columnKey}": ${interfaceName}`;
|
||||
// return def;
|
||||
// }
|
||||
// return `static "${columnKey}": ${getFieldProp("dataType", type)}`;
|
||||
// }),
|
||||
// ].join(";\n") + ";";
|
||||
|
||||
// const availableFields = Object.keys(columns)
|
||||
// .map((columnKey: string) => `"${columnKey}"`)
|
||||
// .join("|\n");
|
||||
|
||||
// monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
// ["/**", " * extensions type configuration", " */", extensionsDefs].join(
|
||||
// "\n"
|
||||
// ),
|
||||
// "ts:filename/extensions.d.ts"
|
||||
// );
|
||||
// monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
// [
|
||||
// "// basic types that are used in all places",
|
||||
// "declare var require: any;",
|
||||
// "declare var Buffer: any;",
|
||||
// "const ref: FirebaseFirestore.DocumentReference;",
|
||||
// "const storage: firebasestorage.Storage;",
|
||||
// "const db: FirebaseFirestore.Firestore;",
|
||||
// "const auth: firebaseauth.BaseAuth;",
|
||||
// `type Row = {${rowDefinition}};`,
|
||||
// `type Field = ${availableFields} | string | object;`,
|
||||
// `type Fields = Field[];`,
|
||||
// ].join("\n"),
|
||||
// "ts:filename/rowFields.d.ts"
|
||||
// );
|
||||
// };
|
||||
// TODO:
|
||||
// Set row definitions
|
||||
// useEffect(() => {
|
||||
// if (!monaco || !rowyRun || !tableState?.columns) return;
|
||||
// try {
|
||||
// setBaseDefinitions(monaco, tableState.columns);
|
||||
// } catch (error) {
|
||||
// console.error("Could not set basic", error);
|
||||
// }
|
||||
// // set available secrets from secretManager
|
||||
// try {
|
||||
// setSecrets(monaco, rowyRun);
|
||||
// } catch (error) {
|
||||
// console.error("Could not set secrets: ", error);
|
||||
// }
|
||||
// }, [monaco, tableState?.columns, rowyRun]);
|
||||
|
||||
let boxSx: SystemStyleObject<Theme> = {
|
||||
minWidth: 400,
|
||||
minHeight,
|
||||
height: minHeight,
|
||||
borderRadius: 1,
|
||||
resize: "vertical",
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
backgroundColor: disabled ? "transparent" : theme.palette.action.input,
|
||||
|
||||
"&::after": {
|
||||
content: '""',
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
pointerEvents: "none",
|
||||
borderRadius: "inherit",
|
||||
|
||||
boxShadow: `0 -1px 0 0 ${theme.palette.text.disabled} inset,
|
||||
0 0 0 1px ${theme.palette.action.inputOutline} inset`,
|
||||
transition: theme.transitions.create("box-shadow", {
|
||||
duration: theme.transitions.duration.short,
|
||||
}),
|
||||
},
|
||||
|
||||
"&:hover::after": {
|
||||
boxShadow: `0 -1px 0 0 ${theme.palette.text.primary} inset,
|
||||
0 0 0 1px ${theme.palette.action.inputOutline} inset`,
|
||||
},
|
||||
"&:focus-within::after": {
|
||||
boxShadow: `0 -2px 0 0 ${theme.palette.primary.main} inset,
|
||||
0 0 0 1px ${theme.palette.action.inputOutline} inset`,
|
||||
},
|
||||
|
||||
...(error
|
||||
? {
|
||||
"&::after, &:hover::after, &:focus-within::after": {
|
||||
boxShadow: `0 -2px 0 0 ${theme.palette.error.main} inset,
|
||||
0 0 0 1px ${theme.palette.action.inputOutline} inset`,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
|
||||
"& .editor": {
|
||||
// Overwrite user-select: none that causes editor
|
||||
// to not be focusable in Safari
|
||||
userSelect: "auto",
|
||||
height: "100%",
|
||||
},
|
||||
|
||||
"& .monaco-editor, & .monaco-editor .margin, & .monaco-editor-background": {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
};
|
||||
|
||||
if (fullScreen)
|
||||
boxSx = {
|
||||
...boxSx,
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: theme.zIndex.tooltip + 1,
|
||||
m: "0 !important",
|
||||
resize: "none",
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
|
||||
borderRadius: 0,
|
||||
"&::after": { display: "none" },
|
||||
};
|
||||
|
||||
return { boxSx };
|
||||
}
|
||||
42
src/components/CodeEditor/utils.d.ts
vendored
Normal file
42
src/components/CodeEditor/utils.d.ts
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/**
|
||||
* utility functions
|
||||
*/
|
||||
declare namespace RULES_UTILS {
|
||||
/**
|
||||
* 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 {}
|
||||
}
|
||||
41
src/components/RenderedMarkdown.tsx
Normal file
41
src/components/RenderedMarkdown.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import type { ReactMarkdownOptions } from "react-markdown/lib/react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
|
||||
import { Typography, Link } from "@mui/material";
|
||||
|
||||
const remarkPlugins = [remarkGfm];
|
||||
const components: ReactMarkdownOptions["components"] = {
|
||||
a: (props) => <Link color="inherit" {...props} />,
|
||||
p: Typography,
|
||||
// eslint-disable-next-line jsx-a11y/alt-text
|
||||
img: (props) => (
|
||||
<img style={{ maxWidth: "100%", borderRadius: 4 }} alt="" {...props} />
|
||||
),
|
||||
};
|
||||
|
||||
const restrictionPresets = {
|
||||
singleLine: ["p", "em", "strong", "a", "code", "del"],
|
||||
};
|
||||
|
||||
export interface IRenderedMarkdownProps extends ReactMarkdownOptions {
|
||||
restrictionPreset?: keyof typeof restrictionPresets;
|
||||
}
|
||||
|
||||
export default function RenderedMarkdown({
|
||||
restrictionPreset,
|
||||
...props
|
||||
}: IRenderedMarkdownProps) {
|
||||
return (
|
||||
<ReactMarkdown
|
||||
{...props}
|
||||
allowedElements={
|
||||
restrictionPreset ? restrictionPresets[restrictionPreset] : undefined
|
||||
}
|
||||
unwrapDisallowed
|
||||
linkTarget="_blank"
|
||||
remarkPlugins={remarkPlugins}
|
||||
components={{ ...components, ...props.components }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
114
src/components/SteppedAccordion.tsx
Normal file
114
src/components/SteppedAccordion.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import {
|
||||
Stepper,
|
||||
StepperProps,
|
||||
Step,
|
||||
StepProps,
|
||||
StepButton,
|
||||
StepButtonProps,
|
||||
StepLabel,
|
||||
StepLabelProps,
|
||||
Typography,
|
||||
StepContent,
|
||||
StepContentProps,
|
||||
} from "@mui/material";
|
||||
import ExpandIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||
|
||||
export interface ISteppedAccordionProps extends Partial<StepperProps> {
|
||||
steps: {
|
||||
id: string;
|
||||
title: React.ReactNode;
|
||||
subtitle?: React.ReactNode;
|
||||
optional?: boolean;
|
||||
content: React.ReactNode;
|
||||
error?: boolean;
|
||||
|
||||
stepProps?: Partial<StepProps>;
|
||||
labelButtonProps?: Partial<StepButtonProps>;
|
||||
labelProps?: Partial<StepLabelProps>;
|
||||
contentProps?: Partial<StepContentProps>;
|
||||
}[];
|
||||
disableUnmount?: boolean;
|
||||
}
|
||||
|
||||
export default function SteppedAccordion({
|
||||
steps,
|
||||
disableUnmount,
|
||||
...props
|
||||
}: ISteppedAccordionProps) {
|
||||
const [activeStep, setActiveStep] = useState(steps[0].id);
|
||||
|
||||
return (
|
||||
<Stepper
|
||||
nonLinear
|
||||
activeStep={steps.findIndex((x) => x.id === activeStep)}
|
||||
orientation="vertical"
|
||||
{...props}
|
||||
sx={{
|
||||
mt: 0,
|
||||
|
||||
"& .MuiStepLabel-root": { width: "100%" },
|
||||
"& .MuiStepLabel-label": {
|
||||
display: "flex",
|
||||
width: "100%",
|
||||
typography: "subtitle2",
|
||||
"&.Mui-active": { typography: "subtitle2" },
|
||||
},
|
||||
"& .MuiStepLabel-label svg": {
|
||||
display: "block",
|
||||
marginLeft: "auto",
|
||||
my: ((24 - 18) / 2 / 8) * -1,
|
||||
transition: (theme) => theme.transitions.create("transform"),
|
||||
},
|
||||
"& .Mui-active svg": {
|
||||
transform: "rotate(180deg)",
|
||||
},
|
||||
|
||||
...props.sx,
|
||||
}}
|
||||
>
|
||||
{steps.map(
|
||||
({
|
||||
id,
|
||||
title,
|
||||
subtitle,
|
||||
optional,
|
||||
content,
|
||||
error,
|
||||
stepProps,
|
||||
labelButtonProps,
|
||||
labelProps,
|
||||
contentProps,
|
||||
}) => (
|
||||
<Step key={id} {...stepProps}>
|
||||
<StepButton
|
||||
onClick={() => setActiveStep((s) => (s === id ? "" : id))}
|
||||
optional={
|
||||
subtitle ||
|
||||
(optional && (
|
||||
<Typography variant="caption">Optional</Typography>
|
||||
))
|
||||
}
|
||||
{...labelButtonProps}
|
||||
>
|
||||
<StepLabel error={error} {...labelProps}>
|
||||
{title}
|
||||
<ExpandIcon />
|
||||
</StepLabel>
|
||||
</StepButton>
|
||||
|
||||
<StepContent
|
||||
TransitionProps={
|
||||
disableUnmount ? { unmountOnExit: false } : undefined
|
||||
}
|
||||
{...contentProps}
|
||||
>
|
||||
{content}
|
||||
</StepContent>
|
||||
</Step>
|
||||
)
|
||||
)}
|
||||
</Stepper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { useState } from "react";
|
||||
import { Control } from "react-hook-form";
|
||||
import type { UseFormReturn, FieldValues } from "react-hook-form";
|
||||
|
||||
import { IconButton, Menu } from "@mui/material";
|
||||
import ExportIcon from "assets/icons/Export";
|
||||
import ImportIcon from "assets/icons/Import";
|
||||
|
||||
import ImportSettings from "./ImportSettings";
|
||||
import ExportSettings from "./ExportSettings";
|
||||
|
||||
import { TableSettingsDialogState } from "@src/atoms/globalScope";
|
||||
|
||||
export interface IActionsMenuProps {
|
||||
mode: TableSettingsDialogState["mode"];
|
||||
control: Control;
|
||||
useFormMethods: UseFormReturn<FieldValues, object>;
|
||||
}
|
||||
|
||||
export default function ActionsMenu({
|
||||
mode,
|
||||
control,
|
||||
useFormMethods,
|
||||
}: IActionsMenuProps) {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const handleClose = () => setAnchorEl(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
aria-label="Actions…"
|
||||
id="table-settings-actions-button"
|
||||
aria-controls="table-settings-actions-menu"
|
||||
aria-haspopup="true"
|
||||
aria-expanded={open ? "true" : undefined}
|
||||
onClick={(e) => setAnchorEl(e.currentTarget)}
|
||||
>
|
||||
{mode === "create" ? <ImportIcon /> : <ExportIcon />}
|
||||
</IconButton>
|
||||
|
||||
<Menu
|
||||
id="table-settings-actions-menu"
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
MenuListProps={{ "aria-labelledby": "table-settings-actions-button" }}
|
||||
disablePortal
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||
transformOrigin={{ vertical: "top", horizontal: "right" }}
|
||||
>
|
||||
<ImportSettings
|
||||
closeMenu={handleClose}
|
||||
control={control}
|
||||
useFormMethods={useFormMethods}
|
||||
/>
|
||||
<ExportSettings closeMenu={handleClose} control={control} />
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { useState } from "react";
|
||||
import { Control, useWatch } from "react-hook-form";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
import { isEmpty } from "lodash-es";
|
||||
import { useSnackbar } from "notistack";
|
||||
|
||||
import { MenuItem, DialogContentText, LinearProgress } from "@mui/material";
|
||||
|
||||
import { analytics, logEvent } from "@src/analytics";
|
||||
import Modal from "@src/components/Modal";
|
||||
import CodeEditor from "@src/components/CodeEditor";
|
||||
|
||||
export interface IExportSettingsProps {
|
||||
closeMenu: () => void;
|
||||
control: Control;
|
||||
}
|
||||
|
||||
export default function ExportSettings({
|
||||
closeMenu,
|
||||
control,
|
||||
}: IExportSettingsProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { _suggestedRules, ...values } = useWatch({ control });
|
||||
// TODO:
|
||||
const tableConfigState = {} as any;
|
||||
// const [tableConfigState] = useTableConfig(values.id);
|
||||
const { id, ref, ..._schema } = tableConfigState.doc ?? {};
|
||||
|
||||
const formattedJson = stringify(
|
||||
// Allow values._schema to take priority if user imported _schema before
|
||||
"_schema" in values || isEmpty(_schema) ? values : { ...values, _schema },
|
||||
{
|
||||
space: 2,
|
||||
// TODO: types
|
||||
cmp: (a: any, b: any) =>
|
||||
// Sort _schema at the end
|
||||
a.key.startsWith("_")
|
||||
? 1
|
||||
: // Otherwise, sort alphabetically
|
||||
a.key > b.key
|
||||
? 1
|
||||
: -1,
|
||||
}
|
||||
);
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
closeMenu();
|
||||
};
|
||||
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const handleExport = () => {
|
||||
logEvent(analytics, "export_tableSettings");
|
||||
navigator.clipboard.writeText(formattedJson);
|
||||
enqueueSnackbar("Copied to clipboard");
|
||||
handleClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItem onClick={() => setOpen(true)}>Export table settings…</MenuItem>
|
||||
|
||||
{open && (
|
||||
<Modal
|
||||
onClose={handleClose}
|
||||
title="Export table settings"
|
||||
header={
|
||||
<>
|
||||
{tableConfigState.loading && values.id && (
|
||||
<LinearProgress
|
||||
style={{ position: "absolute", top: 0, left: 0, right: 0 }}
|
||||
/>
|
||||
)}
|
||||
<DialogContentText style={{ margin: "0 var(--dialog-spacing)" }}>
|
||||
Export table settings and columns in JSON format
|
||||
</DialogContentText>
|
||||
</>
|
||||
}
|
||||
body={
|
||||
<div style={{ marginTop: "var(--dialog-contents-spacing)" }}>
|
||||
<CodeEditor
|
||||
disabled
|
||||
value={formattedJson}
|
||||
defaultLanguage="json"
|
||||
minHeight={300}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
actions={{
|
||||
primary: {
|
||||
children: "Copy to clipboard",
|
||||
onClick: handleExport,
|
||||
},
|
||||
secondary: {
|
||||
children: "Cancel",
|
||||
onClick: handleClose,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
import { useState } from "react";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { Control, useWatch } from "react-hook-form";
|
||||
import type { UseFormReturn, FieldValues } from "react-hook-form";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
import { isEmpty, get } from "lodash-es";
|
||||
import { useSnackbar } from "notistack";
|
||||
|
||||
import { MenuItem, DialogContentText, FormHelperText } from "@mui/material";
|
||||
|
||||
import Modal from "@src/components/Modal";
|
||||
import DiffEditor from "@src/components/CodeEditor/DiffEditor";
|
||||
|
||||
// import useTableConfig from "@src/hooks/useTable/useTableConfig";
|
||||
import { globalScope, confirmDialogAtom } from "@src/atoms/globalScope";
|
||||
import { analytics, logEvent } from "@src/analytics";
|
||||
|
||||
export interface IImportSettingsProps {
|
||||
closeMenu: () => void;
|
||||
control: Control;
|
||||
useFormMethods: UseFormReturn<FieldValues, object>;
|
||||
}
|
||||
|
||||
export default function ImportSettings({
|
||||
closeMenu,
|
||||
control,
|
||||
useFormMethods,
|
||||
}: IImportSettingsProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const [newSettings, setNewSettings] = useState("");
|
||||
const [valid, setValid] = useState(true);
|
||||
|
||||
const { _suggestedRules, ...values } = useWatch({ control });
|
||||
// TODO:
|
||||
const tableConfigState = {} as any;
|
||||
// const [tableConfigState] = useTableConfig(values.id);
|
||||
const { id, ref, ..._schema } = tableConfigState.doc ?? {};
|
||||
|
||||
const formattedJson = stringify(
|
||||
// Allow values._schema to take priority if user imported _schema before
|
||||
"_schema" in values || isEmpty(_schema) ? values : { ...values, _schema },
|
||||
{
|
||||
space: 2,
|
||||
// TODO: types
|
||||
cmp: (a: any, b: any) =>
|
||||
// Sort _schema at the end
|
||||
a.key.startsWith("_")
|
||||
? 1
|
||||
: // Otherwise, sort alphabetically
|
||||
a.key > b.key
|
||||
? 1
|
||||
: -1,
|
||||
}
|
||||
);
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
closeMenu();
|
||||
};
|
||||
|
||||
const confirm = useSetAtom(confirmDialogAtom, globalScope);
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const { setValue } = useFormMethods;
|
||||
|
||||
const handleImport = () => {
|
||||
logEvent(analytics, "import_tableSettings");
|
||||
const { id, collection, ...newValues } = JSON.parse(newSettings);
|
||||
for (const key in newValues) {
|
||||
setValue(key, newValues[key], {
|
||||
shouldDirty: true,
|
||||
shouldValidate: true,
|
||||
});
|
||||
}
|
||||
|
||||
enqueueSnackbar("Imported settings");
|
||||
handleClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuItem onClick={() => setOpen(true)}>Import table settings…</MenuItem>
|
||||
|
||||
{open && (
|
||||
<Modal
|
||||
onClose={handleClose}
|
||||
title="Import table settings"
|
||||
header={
|
||||
<DialogContentText style={{ margin: "0 var(--dialog-spacing)" }}>
|
||||
Import table settings in JSON format. This will overwrite any
|
||||
existing settings, except for the table ID and collection.
|
||||
</DialogContentText>
|
||||
}
|
||||
body={
|
||||
<div style={{ marginTop: "var(--dialog-contents-spacing)" }}>
|
||||
<DiffEditor
|
||||
original={formattedJson}
|
||||
modified={newSettings}
|
||||
language="json"
|
||||
onChange={(v) => {
|
||||
try {
|
||||
if (v) {
|
||||
JSON.parse(v);
|
||||
setNewSettings(v);
|
||||
setValid(true);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`Failed to parse JSON: ${e}`);
|
||||
setValid(false);
|
||||
}
|
||||
}}
|
||||
error={!valid}
|
||||
minHeight={300}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
footer={
|
||||
!valid && (
|
||||
<FormHelperText
|
||||
error
|
||||
variant="filled"
|
||||
sx={{ mx: "auto", mt: 1, mb: -1 }}
|
||||
>
|
||||
Invalid JSON
|
||||
</FormHelperText>
|
||||
)
|
||||
}
|
||||
actions={{
|
||||
primary: {
|
||||
children: "Import",
|
||||
onClick: () => {
|
||||
const parsedJson = JSON.parse(newSettings);
|
||||
const hasExtensions = Boolean(
|
||||
get(parsedJson, "_schema.extensionObjects")
|
||||
);
|
||||
const hasWebhooks = Boolean(
|
||||
get(parsedJson, "_schema.webhooks")
|
||||
);
|
||||
|
||||
confirm({
|
||||
title: "Import settings?",
|
||||
body: (
|
||||
<>
|
||||
<DialogContentText paragraph>
|
||||
You will overwrite any existing settings for this table,{" "}
|
||||
<b>except for the table ID and collection</b>.
|
||||
</DialogContentText>
|
||||
|
||||
{(hasExtensions || hasWebhooks) && (
|
||||
<DialogContentText paragraph>
|
||||
You’re importing new{" "}
|
||||
<b>
|
||||
{[
|
||||
hasExtensions && "extensions",
|
||||
hasWebhooks && "webhooks",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" and ")}
|
||||
</b>{" "}
|
||||
for this table. You’ll be prompted to <b>deploy</b>{" "}
|
||||
them when you save the table settings.
|
||||
</DialogContentText>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
confirm: "Import",
|
||||
handleConfirm: handleImport,
|
||||
});
|
||||
},
|
||||
disabled: !valid,
|
||||
},
|
||||
secondary: {
|
||||
children: "Cancel",
|
||||
onClick: handleClose,
|
||||
},
|
||||
}}
|
||||
maxWidth="lg"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
2
src/components/TableSettingsDialog/ActionsMenu/index.ts
Normal file
2
src/components/TableSettingsDialog/ActionsMenu/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./ActionsMenu";
|
||||
export { default } from "./ActionsMenu";
|
||||
143
src/components/TableSettingsDialog/DeleteMenu.tsx
Normal file
143
src/components/TableSettingsDialog/DeleteMenu.tsx
Normal file
@@ -0,0 +1,143 @@
|
||||
import { useState } from "react";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useSnackbar } from "notistack";
|
||||
|
||||
import { IconButton, Menu, MenuItem, DialogContentText } from "@mui/material";
|
||||
import DeleteIcon from "@mui/icons-material/DeleteOutlined";
|
||||
|
||||
import {
|
||||
globalScope,
|
||||
confirmDialogAtom,
|
||||
TableSettings,
|
||||
} from "@src/atoms/globalScope";
|
||||
import { ROUTES } from "@src/constants/routes";
|
||||
import { analytics, logEvent } from "@src/analytics";
|
||||
|
||||
export interface IDeleteMenuProps {
|
||||
clearDialog: () => void;
|
||||
data: TableSettings | null;
|
||||
}
|
||||
|
||||
export default function DeleteMenu({ clearDialog, data }: IDeleteMenuProps) {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const handleClose = () => setAnchorEl(null);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const confirm = useSetAtom(confirmDialogAtom, globalScope);
|
||||
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
|
||||
|
||||
const handleResetStructure = async () => {
|
||||
const snack = enqueueSnackbar("Resetting columns…", { persist: true });
|
||||
|
||||
// TODO:
|
||||
// const schemaDocRef = db.doc(`${TABLE_SCHEMAS}/${data!.id}`);
|
||||
// await schemaDocRef.update({ columns: {} });
|
||||
|
||||
clearDialog();
|
||||
closeSnackbar(snack);
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
const snack = enqueueSnackbar("Deleting table…", { persist: true });
|
||||
|
||||
// TODO:
|
||||
// const tablesDocRef = db.doc(SETTINGS);
|
||||
// const tableData = (await tablesDocRef.get()).data();
|
||||
// const updatedTables = tableData?.tables.filter(
|
||||
// (table) => table.id !== data?.id || table.tableType !== data?.tableType
|
||||
// );
|
||||
// tablesDocRef.update({ tables: updatedTables });
|
||||
// await db
|
||||
// .collection(
|
||||
// data?.tableType === "primaryCollection"
|
||||
// ? TABLE_SCHEMAS
|
||||
// : TABLE_GROUP_SCHEMAS
|
||||
// )
|
||||
// .doc(data?.id)
|
||||
// .delete();
|
||||
|
||||
logEvent(analytics, "delete_table");
|
||||
clearDialog();
|
||||
closeSnackbar(snack);
|
||||
navigate(ROUTES.home);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
aria-label="Delete table…"
|
||||
id="table-settings-delete-button"
|
||||
aria-controls="table-settings-delete-menu"
|
||||
aria-haspopup="true"
|
||||
aria-expanded={open ? "true" : undefined}
|
||||
onClick={(e) => setAnchorEl(e.currentTarget)}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
|
||||
<Menu
|
||||
id="table-settings-delete-menu"
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
MenuListProps={{ "aria-labelledby": "table-settings-delete-button" }}
|
||||
disablePortal
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||
transformOrigin={{ vertical: "top", horizontal: "right" }}
|
||||
>
|
||||
<MenuItem
|
||||
color="error"
|
||||
onClick={() =>
|
||||
confirm({
|
||||
title: `Reset columns of “${data?.name}”?`,
|
||||
body: (
|
||||
<>
|
||||
<DialogContentText paragraph>
|
||||
This will only reset the columns of this column so you can
|
||||
set up the columns again.
|
||||
</DialogContentText>
|
||||
<DialogContentText>
|
||||
You will not lose any data in your Firestore collection{" "}
|
||||
<code>{data?.collection}</code>.
|
||||
</DialogContentText>
|
||||
</>
|
||||
),
|
||||
confirm: "Reset",
|
||||
confirmColor: "error",
|
||||
handleConfirm: handleResetStructure,
|
||||
})
|
||||
}
|
||||
>
|
||||
Reset columns…
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
color="error"
|
||||
onClick={() =>
|
||||
confirm({
|
||||
title: `Delete the table “${data?.name}”?`,
|
||||
body: (
|
||||
<>
|
||||
<DialogContentText paragraph>
|
||||
This will only delete the Rowy configuration data.
|
||||
</DialogContentText>
|
||||
<DialogContentText>
|
||||
You will not lose any data in your Firestore collection{" "}
|
||||
<code>{data?.collection}</code>.
|
||||
</DialogContentText>
|
||||
</>
|
||||
),
|
||||
confirm: "Delete",
|
||||
confirmColor: "error",
|
||||
handleConfirm: handleDelete,
|
||||
})
|
||||
}
|
||||
>
|
||||
Delete table…
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
}
|
||||
148
src/components/TableSettingsDialog/SuggestedRules.tsx
Normal file
148
src/components/TableSettingsDialog/SuggestedRules.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import { useState } from "react";
|
||||
import { useAtom } from "jotai";
|
||||
import { useWatch } from "react-hook-form";
|
||||
|
||||
import {
|
||||
InputLabel,
|
||||
Collapse,
|
||||
FormControlLabel,
|
||||
Checkbox,
|
||||
Grid,
|
||||
Button,
|
||||
} from "@mui/material";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
import { IFieldComponentProps } from "@rowy/form-builder";
|
||||
|
||||
import { globalScope, projectIdAtom } from "@src/atoms/globalScope";
|
||||
|
||||
type customizationOptions = "allRead" | "authRead" | "subcollections" | "user";
|
||||
|
||||
export interface ISuggestedRulesProps extends IFieldComponentProps {}
|
||||
|
||||
export default function SuggestedRules({
|
||||
useFormMethods: { control },
|
||||
label,
|
||||
}: ISuggestedRulesProps) {
|
||||
const [projectId] = useAtom(projectIdAtom, globalScope);
|
||||
|
||||
const watched = useWatch({
|
||||
control,
|
||||
name: ["collection", "roles", "readOnly"],
|
||||
} as any);
|
||||
const [collection, roles, readOnly] = Array.isArray(watched) ? watched : [];
|
||||
|
||||
const [customized, setCustomized] = useState<boolean>(false);
|
||||
const [customizations, setCustomizations] = useState<customizationOptions[]>(
|
||||
[]
|
||||
);
|
||||
const handleChange =
|
||||
(option: customizationOptions) =>
|
||||
(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setCustomizations((prev) => {
|
||||
const set = new Set(prev || []);
|
||||
if (e.target.checked) set.add(option);
|
||||
else set.delete(option);
|
||||
return Array.from(set);
|
||||
});
|
||||
|
||||
const generatedRules = `match /${collection}/{${
|
||||
customizations.includes("subcollections") ? "document=**" : "docId"
|
||||
}} {
|
||||
allow read, write: if hasAnyRole(${
|
||||
readOnly ? `["ADMIN"]` : JSON.stringify(roles)
|
||||
});${
|
||||
customizations.includes("allRead")
|
||||
? "\n allow read: if true;"
|
||||
: customizations.includes("authRead")
|
||||
? "\n allow read: if request.auth != null;"
|
||||
: ""
|
||||
}${
|
||||
customizations.includes("user")
|
||||
? `\n
|
||||
allow create: if request.auth != null;
|
||||
allow get, update, delete: if isDocOwner(userId);`
|
||||
: ""
|
||||
}
|
||||
}`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<InputLabel sx={{ mb: 0.5, ml: 0.25 }}>{label}</InputLabel>
|
||||
<pre style={{ margin: 0, userSelect: "all", whiteSpace: "pre-wrap" }}>
|
||||
{generatedRules}
|
||||
</pre>
|
||||
|
||||
<Collapse in={customized}>
|
||||
<Grid container>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={customizations.includes("allRead")}
|
||||
onChange={handleChange("allRead")}
|
||||
/>
|
||||
}
|
||||
label="Anyone can read"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={customizations.includes("authRead")}
|
||||
onChange={handleChange("authRead")}
|
||||
/>
|
||||
}
|
||||
label="All signed-in users can read"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={customizations.includes("user")}
|
||||
onChange={handleChange("user")}
|
||||
/>
|
||||
}
|
||||
label="Users can create and edit docs"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} sm={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={customizations.includes("subcollections")}
|
||||
onChange={handleChange("subcollections")}
|
||||
/>
|
||||
}
|
||||
label="Same rules for all subcollections"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Collapse>
|
||||
|
||||
<Grid container spacing={1} style={{ marginTop: 0 }}>
|
||||
{!customized && (
|
||||
<Grid item>
|
||||
<Button onClick={() => setCustomized(true)}>Customize…</Button>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item>
|
||||
<Button onClick={() => navigator.clipboard.writeText(generatedRules)}>
|
||||
Copy to clipboard
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button
|
||||
href={`https://console.firebase.google.com/u/0/project/${projectId}/firestore/rules`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Set rules in Firebase Console
|
||||
<InlineOpenInNewIcon />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
32
src/components/TableSettingsDialog/TableId.tsx
Normal file
32
src/components/TableSettingsDialog/TableId.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { useEffect } from "react";
|
||||
import { useWatch } from "react-hook-form";
|
||||
import { camelCase } from "lodash-es";
|
||||
import {
|
||||
ShortTextComponent,
|
||||
IShortTextComponentProps,
|
||||
} from "@rowy/form-builder";
|
||||
|
||||
export interface ITableIdProps extends IShortTextComponentProps {
|
||||
watchedField?: string;
|
||||
}
|
||||
|
||||
export default function TableId({ watchedField, ...props }: ITableIdProps) {
|
||||
const {
|
||||
field: { onChange },
|
||||
useFormMethods: { control },
|
||||
disabled,
|
||||
} = props;
|
||||
|
||||
const watchedValue = useWatch({ control, name: watchedField } as any);
|
||||
useEffect(() => {
|
||||
if (!disabled && typeof watchedValue === "string" && !!watchedValue)
|
||||
onChange(camelCase(watchedValue));
|
||||
}, [watchedValue, disabled]);
|
||||
|
||||
return (
|
||||
<ShortTextComponent
|
||||
{...props}
|
||||
sx={{ "& .MuiInputBase-input": { fontFamily: "mono" } }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
27
src/components/TableSettingsDialog/TableName.tsx
Normal file
27
src/components/TableSettingsDialog/TableName.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useEffect } from "react";
|
||||
import { useWatch } from "react-hook-form";
|
||||
import { startCase } from "lodash-es";
|
||||
import {
|
||||
ShortTextComponent,
|
||||
IShortTextComponentProps,
|
||||
} from "@rowy/form-builder";
|
||||
|
||||
export interface ITableNameProps extends IShortTextComponentProps {
|
||||
watchedField?: string;
|
||||
}
|
||||
|
||||
export default function TableName({ watchedField, ...props }: ITableNameProps) {
|
||||
const {
|
||||
field: { onChange },
|
||||
useFormMethods: { control },
|
||||
disabled,
|
||||
} = props;
|
||||
|
||||
const watchedValue = useWatch({ control, name: watchedField } as any);
|
||||
useEffect(() => {
|
||||
if (!disabled && typeof watchedValue === "string" && !!watchedValue)
|
||||
onChange(startCase(watchedValue));
|
||||
}, [watchedValue, disabled]);
|
||||
|
||||
return <ShortTextComponent {...props} />;
|
||||
}
|
||||
461
src/components/TableSettingsDialog/TableSettingsDialog.tsx
Normal file
461
src/components/TableSettingsDialog/TableSettingsDialog.tsx
Normal file
@@ -0,0 +1,461 @@
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import useSWR from "swr";
|
||||
import { useSnackbar } from "notistack";
|
||||
import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { find, sortBy, get, isEmpty } from "lodash-es";
|
||||
import { FieldValues } from "react-hook-form";
|
||||
|
||||
import { DialogContentText, Stack, Typography } from "@mui/material";
|
||||
|
||||
import { FormDialog, FormFields } from "@rowy/form-builder";
|
||||
import { tableSettings } from "./form";
|
||||
import TableName from "./TableName";
|
||||
import TableId from "./TableId";
|
||||
import SuggestedRules from "./SuggestedRules";
|
||||
import SteppedAccordion from "@src/components/SteppedAccordion";
|
||||
import ActionsMenu from "./ActionsMenu";
|
||||
import DeleteMenu from "./DeleteMenu";
|
||||
|
||||
import {
|
||||
globalScope,
|
||||
tableSettingsDialogAtom,
|
||||
tablesAtom,
|
||||
rolesAtom,
|
||||
rowyRunAtom,
|
||||
confirmDialogAtom,
|
||||
TableSettings,
|
||||
} from "@src/atoms/globalScope";
|
||||
import { analytics, logEvent } from "@src/analytics";
|
||||
|
||||
// TODO:
|
||||
// import { useSnackLogContext } from "@src/contexts/SnackLogContext";
|
||||
import { runRoutes } from "@src/constants/runRoutes";
|
||||
import {
|
||||
CONFIG,
|
||||
TABLE_GROUP_SCHEMAS,
|
||||
TABLE_SCHEMAS,
|
||||
} from "@src/config/dbPaths";
|
||||
import { Controller } from "react-hook-form";
|
||||
import { ROUTES } from "@src/constants/routes";
|
||||
|
||||
const customComponents = {
|
||||
tableName: {
|
||||
component: TableName,
|
||||
defaultValue: "",
|
||||
validation: [["string"]],
|
||||
},
|
||||
tableId: {
|
||||
component: TableId,
|
||||
defaultValue: "",
|
||||
validation: [["string"]],
|
||||
},
|
||||
suggestedRules: {
|
||||
component: SuggestedRules,
|
||||
defaultValue: "",
|
||||
validation: [["string"]],
|
||||
},
|
||||
};
|
||||
|
||||
export default function TableSettingsDialog() {
|
||||
const [{ open, mode, data }, setTableSettingsDialog] = useAtom(
|
||||
tableSettingsDialogAtom,
|
||||
globalScope
|
||||
);
|
||||
const clearDialog = () => setTableSettingsDialog({ open: false });
|
||||
|
||||
const [roles] = useAtom(rolesAtom, globalScope);
|
||||
const [tables] = useAtom(tablesAtom, globalScope);
|
||||
const [rowyRun] = useAtom(rowyRunAtom, globalScope);
|
||||
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const confirm = useSetAtom(confirmDialogAtom, globalScope);
|
||||
// const snackLogContext = useSnackLogContext();
|
||||
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
|
||||
|
||||
const sectionNames = Array.from(
|
||||
new Set((tables ?? []).map((t) => t.section))
|
||||
);
|
||||
|
||||
const { data: collections } = useSWR(
|
||||
"firebaseCollections",
|
||||
() => rowyRun({ route: runRoutes.listCollections }),
|
||||
{
|
||||
revalidateOnMount: true,
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
dedupingInterval: 60_000 * 60,
|
||||
}
|
||||
);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
// TODO: types
|
||||
const handleSubmit = async (v: FieldValues) => {
|
||||
const { _suggestedRules, ...values } = v;
|
||||
const data = { ...values };
|
||||
|
||||
if (values.schemaSource)
|
||||
data.schemaSource = find(tables, { id: values.schemaSource });
|
||||
|
||||
const hasExtensions = !isEmpty(get(data, "_schema.extensionObjects"));
|
||||
const hasWebhooks = !isEmpty(get(data, "_schema.webhooks"));
|
||||
const deployExtensionsWebhooks = (onComplete?: () => void) => {
|
||||
if (rowyRun && (hasExtensions || hasWebhooks)) {
|
||||
confirm({
|
||||
title: `Deploy ${[
|
||||
hasExtensions && "extensions",
|
||||
hasWebhooks && "webhooks",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" and ")}?`,
|
||||
body: "You can also deploy later from the table page",
|
||||
confirm: "Deploy",
|
||||
cancel: "Later",
|
||||
handleConfirm: async () => {
|
||||
const tablePath = data.collection;
|
||||
const tableConfigPath = `${
|
||||
data.tableType !== "collectionGroup"
|
||||
? TABLE_SCHEMAS
|
||||
: TABLE_GROUP_SCHEMAS
|
||||
}/${data.id}`;
|
||||
|
||||
if (hasExtensions) {
|
||||
// find derivative, default value
|
||||
// TODO:
|
||||
// snackLogContext.requestSnackLog();
|
||||
rowyRun({
|
||||
route: runRoutes.buildFunction,
|
||||
body: {
|
||||
tablePath,
|
||||
pathname: `/${
|
||||
data.tableType === "collectionGroup"
|
||||
? "tableGroup"
|
||||
: "table"
|
||||
}/${data.id}`,
|
||||
tableConfigPath,
|
||||
},
|
||||
});
|
||||
logEvent(analytics, "deployed_extensions");
|
||||
}
|
||||
|
||||
if (hasWebhooks) {
|
||||
const resp = await rowyRun({
|
||||
service: "hooks",
|
||||
route: runRoutes.publishWebhooks,
|
||||
body: {
|
||||
tableConfigPath,
|
||||
tablePath,
|
||||
},
|
||||
});
|
||||
enqueueSnackbar(resp.message, {
|
||||
variant: resp.success ? "success" : "error",
|
||||
});
|
||||
logEvent(analytics, "published_webhooks");
|
||||
}
|
||||
|
||||
if (onComplete) onComplete();
|
||||
},
|
||||
handleCancel: async () => {
|
||||
let _schema: Record<string, any> = {};
|
||||
if (hasExtensions) {
|
||||
_schema.extensionObjects = get(
|
||||
data,
|
||||
"_schema.extensionObjects"
|
||||
// TODO: types
|
||||
)!.map((x: any) => ({
|
||||
...x,
|
||||
active: false,
|
||||
}));
|
||||
}
|
||||
if (hasWebhooks) {
|
||||
// TODO: types
|
||||
_schema.webhooks = get(data, "_schema.webhooks")!.map(
|
||||
(x: any) => ({
|
||||
...x,
|
||||
active: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// await settingsActions?.updateTable({
|
||||
// id: data.id,
|
||||
// tableType: data.tableType,
|
||||
// _schema,
|
||||
// });
|
||||
if (onComplete) onComplete();
|
||||
},
|
||||
});
|
||||
} else {
|
||||
if (onComplete) onComplete();
|
||||
}
|
||||
};
|
||||
|
||||
if (mode === "update") {
|
||||
// TODO:
|
||||
// await settingsActions?.updateTable(data);
|
||||
deployExtensionsWebhooks();
|
||||
clearDialog();
|
||||
logEvent(analytics, "update_table", { type: values.tableType });
|
||||
enqueueSnackbar("Updated table");
|
||||
} else {
|
||||
const creatingSnackbar = enqueueSnackbar("Creating table…", {
|
||||
persist: true,
|
||||
});
|
||||
// TODO:
|
||||
// await settingsActions?.createTable(data);
|
||||
await logEvent(analytics, "create_table", { type: values.tableType });
|
||||
deployExtensionsWebhooks(() => {
|
||||
if (location.pathname === ROUTES.tables) {
|
||||
navigate(
|
||||
`${
|
||||
values.tableType === "collectionGroup"
|
||||
? ROUTES.tableGroup
|
||||
: ROUTES.table
|
||||
}/${values.id}`
|
||||
);
|
||||
} else {
|
||||
navigate(values.id);
|
||||
}
|
||||
clearDialog();
|
||||
closeSnackbar(creatingSnackbar);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const fields = tableSettings(
|
||||
mode,
|
||||
roles,
|
||||
sectionNames,
|
||||
sortBy(
|
||||
tables?.map((table) => ({
|
||||
label: table.name,
|
||||
value: table.id,
|
||||
section: table.section,
|
||||
collection: table.collection,
|
||||
})),
|
||||
["section", "label"]
|
||||
),
|
||||
Array.isArray(collections) ? collections.filter((x) => x !== CONFIG) : null
|
||||
);
|
||||
|
||||
return (
|
||||
<FormDialog
|
||||
onClose={clearDialog}
|
||||
title={
|
||||
(mode === "create" ? "Create table" : "Table settings") +
|
||||
" (INCOMPLETE)"
|
||||
}
|
||||
fields={fields}
|
||||
customBody={(formFieldsProps) => {
|
||||
const { errors } = formFieldsProps.useFormMethods.formState;
|
||||
const groupedErrors: Record<string, string> = Object.entries(
|
||||
errors
|
||||
).reduce((acc, [name, err]) => {
|
||||
const match = find(fields, ["name", name])?.step;
|
||||
if (!match) return acc;
|
||||
acc[match] = err.message;
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Controller
|
||||
control={formFieldsProps.control}
|
||||
name="_schema"
|
||||
defaultValue={{}}
|
||||
render={() => <></>}
|
||||
/>
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={1}
|
||||
sx={{
|
||||
display: "flex",
|
||||
height: "var(--dialog-title-height)",
|
||||
alignItems: "center",
|
||||
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
right: 40 + 12 + 8,
|
||||
}}
|
||||
>
|
||||
<ActionsMenu
|
||||
mode={mode}
|
||||
control={formFieldsProps.control}
|
||||
useFormMethods={formFieldsProps.useFormMethods}
|
||||
/>
|
||||
{mode === "update" && (
|
||||
<DeleteMenu clearDialog={clearDialog} data={data} />
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<SteppedAccordion
|
||||
disableUnmount
|
||||
steps={
|
||||
[
|
||||
{
|
||||
id: "collection",
|
||||
title: "Collection",
|
||||
content: (
|
||||
<>
|
||||
<DialogContentText paragraph>
|
||||
Connect this table to a new or existing Firestore
|
||||
collection
|
||||
</DialogContentText>
|
||||
<FormFields
|
||||
{...formFieldsProps}
|
||||
fields={fields.filter((f) => f.step === "collection")}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
optional: false,
|
||||
error: Boolean(groupedErrors.collection),
|
||||
subtitle: groupedErrors.collection && (
|
||||
<Typography variant="caption" color="error">
|
||||
{groupedErrors.collection}
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "display",
|
||||
title: "Display",
|
||||
content: (
|
||||
<>
|
||||
<DialogContentText paragraph>
|
||||
Set how this table is displayed to users
|
||||
</DialogContentText>
|
||||
<FormFields
|
||||
{...formFieldsProps}
|
||||
fields={fields.filter((f) => f.step === "display")}
|
||||
customComponents={customComponents}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
optional: false,
|
||||
error: Boolean(groupedErrors.display),
|
||||
subtitle: groupedErrors.display && (
|
||||
<Typography variant="caption" color="error">
|
||||
{groupedErrors.display}
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "accessControls",
|
||||
title: "Access controls",
|
||||
content: (
|
||||
<>
|
||||
<DialogContentText paragraph>
|
||||
Set who can view and edit this table. Only ADMIN users
|
||||
can edit table settings or add, edit, and delete
|
||||
columns.
|
||||
</DialogContentText>
|
||||
<FormFields
|
||||
{...formFieldsProps}
|
||||
fields={fields.filter(
|
||||
(f) => f.step === "accessControls"
|
||||
)}
|
||||
customComponents={customComponents}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
optional: false,
|
||||
error: Boolean(groupedErrors.accessControls),
|
||||
subtitle: groupedErrors.accessControls && (
|
||||
<Typography variant="caption" color="error">
|
||||
{groupedErrors.accessControls}
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "auditing",
|
||||
title: "Auditing",
|
||||
content: (
|
||||
<>
|
||||
<DialogContentText paragraph>
|
||||
Track when users create or update rows
|
||||
</DialogContentText>
|
||||
<FormFields
|
||||
{...formFieldsProps}
|
||||
fields={fields.filter((f) => f.step === "auditing")}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
optional: true,
|
||||
error: Boolean(groupedErrors.auditing),
|
||||
subtitle: groupedErrors.auditing && (
|
||||
<Typography variant="caption" color="error">
|
||||
{groupedErrors.auditing}
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
/*
|
||||
* TODO: Figure out where to store this settings
|
||||
|
||||
{
|
||||
id: "function",
|
||||
title: "Cloud Function",
|
||||
content: (
|
||||
<>
|
||||
<DialogContentText paragraph>
|
||||
Configure cloud function settings, this setting is shared across all tables connected to the same collection
|
||||
</DialogContentText>
|
||||
<FormFields
|
||||
{...formFieldsProps}
|
||||
fields={fields.filter((f) => f.step === "function")}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
optional: true,
|
||||
error: Boolean(groupedErrors.function),
|
||||
subtitle: groupedErrors.auditing && (
|
||||
<Typography variant="caption" color="error">
|
||||
{groupedErrors.function}
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
*/
|
||||
mode === "create"
|
||||
? {
|
||||
id: "columns",
|
||||
title: "Columns",
|
||||
content: (
|
||||
<>
|
||||
<DialogContentText paragraph>
|
||||
Initialize table with columns
|
||||
</DialogContentText>
|
||||
<FormFields
|
||||
{...formFieldsProps}
|
||||
fields={fields.filter(
|
||||
(f) => f.step === "columns"
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
optional: true,
|
||||
error: Boolean(groupedErrors.columns),
|
||||
subtitle: groupedErrors.columns && (
|
||||
<Typography variant="caption" color="error">
|
||||
{groupedErrors.columns}
|
||||
</Typography>
|
||||
),
|
||||
}
|
||||
: null,
|
||||
].filter(Boolean) as any
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
customComponents={customComponents}
|
||||
values={{ ...data }}
|
||||
onSubmit={handleSubmit}
|
||||
SubmitButtonProps={{
|
||||
children: mode === "create" ? "Create" : "Update",
|
||||
// TODO:
|
||||
disabled: true,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
512
src/components/TableSettingsDialog/form.tsx
Normal file
512
src/components/TableSettingsDialog/form.tsx
Normal file
@@ -0,0 +1,512 @@
|
||||
import { find } from "lodash-es";
|
||||
import { Field, FieldType } from "@rowy/form-builder";
|
||||
import { TableSettingsDialogState } from "@src/atoms/globalScope";
|
||||
|
||||
import { Link, ListItemText, Typography } from "@mui/material";
|
||||
import OpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
import WarningIcon from "@mui/icons-material/WarningAmber";
|
||||
|
||||
import { WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
import { FieldType as TableFieldType } from "@src/constants/fields";
|
||||
|
||||
export const tableSettings = (
|
||||
mode: TableSettingsDialogState["mode"],
|
||||
roles: string[] | undefined,
|
||||
sections: string[] | undefined,
|
||||
tables:
|
||||
| { label: string; value: any; section: string; collection: string }[]
|
||||
| undefined,
|
||||
collections: string[] | null
|
||||
): Field[] =>
|
||||
[
|
||||
// Step 1: Collection
|
||||
{
|
||||
step: "collection",
|
||||
type: FieldType.singleSelect,
|
||||
name: "tableType",
|
||||
label: "Table type",
|
||||
defaultValue: "primaryCollection",
|
||||
options: [
|
||||
{
|
||||
label: (
|
||||
<ListItemText
|
||||
primary="Primary collection"
|
||||
secondary={
|
||||
<>
|
||||
Connect this table to the <b>single collection</b> matching
|
||||
the collection name entered below
|
||||
</>
|
||||
}
|
||||
style={{ maxWidth: 470 }}
|
||||
/>
|
||||
),
|
||||
value: "primaryCollection",
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<ListItemText
|
||||
primary="Collection group"
|
||||
secondary={
|
||||
<>
|
||||
Connect this table to{" "}
|
||||
<b>all collections and subcollections</b> matching the
|
||||
collection name entered below
|
||||
</>
|
||||
}
|
||||
style={{ maxWidth: 470 }}
|
||||
/>
|
||||
),
|
||||
value: "collectionGroup",
|
||||
},
|
||||
],
|
||||
required: true,
|
||||
disabled: mode === "update",
|
||||
assistiveText: (
|
||||
<>
|
||||
Cannot be edited
|
||||
{mode === "create" && " later"}.{" "}
|
||||
<Link
|
||||
href="https://firebase.googleblog.com/2019/06/understanding-collection-group-queries.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn more about collection groups
|
||||
<OpenInNewIcon />
|
||||
</Link>
|
||||
</>
|
||||
),
|
||||
},
|
||||
Array.isArray(collections)
|
||||
? {
|
||||
step: "collection",
|
||||
type: FieldType.singleSelect,
|
||||
name: "collection",
|
||||
label: "Collection",
|
||||
labelPlural: "collections",
|
||||
options: collections,
|
||||
itemRenderer: (option: any) => (
|
||||
<code key={option.value}>{option.label}</code>
|
||||
),
|
||||
freeText: true,
|
||||
required: true,
|
||||
assistiveText: (
|
||||
<>
|
||||
{mode === "update" ? (
|
||||
<>
|
||||
<WarningIcon
|
||||
color="warning"
|
||||
aria-label="Warning"
|
||||
sx={{ fontSize: 16, mr: 0.5, verticalAlign: "middle" }}
|
||||
/>
|
||||
You can change which Firestore collection to display. Data in
|
||||
the new collection must be compatible with the existing
|
||||
columns.
|
||||
</>
|
||||
) : (
|
||||
"Choose which Firestore collection to display."
|
||||
)}{" "}
|
||||
<Link
|
||||
href={`https://console.firebase.google.com/project/_/firestore/data`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Your collections
|
||||
<OpenInNewIcon />
|
||||
</Link>
|
||||
</>
|
||||
),
|
||||
AddButtonProps: {
|
||||
children: "Create collection or use custom path…",
|
||||
},
|
||||
AddDialogProps: {
|
||||
title: "Create collection or use custom path",
|
||||
textFieldLabel: (
|
||||
<>
|
||||
Collection name
|
||||
<Typography variant="caption" display="block">
|
||||
If this collection does not exist, it won’t be created until
|
||||
you add a row to the table
|
||||
</Typography>
|
||||
</>
|
||||
),
|
||||
},
|
||||
TextFieldProps: {
|
||||
sx: { "& .MuiInputBase-input": { fontFamily: "mono" } },
|
||||
},
|
||||
// https://firebase.google.com/docs/firestore/quotas#collections_documents_and_fields
|
||||
validation: [
|
||||
["matches", /^[^\s]+$/, "Collection name cannot have spaces"],
|
||||
["notOneOf", [".", ".."], "Collection name cannot be . or .."],
|
||||
[
|
||||
"test",
|
||||
"double-underscore",
|
||||
"Collection name cannot begin and end with __",
|
||||
(value: any) => !value.startsWith("__") && !value.endsWith("__"),
|
||||
],
|
||||
],
|
||||
}
|
||||
: {
|
||||
step: "collection",
|
||||
type: FieldType.shortText,
|
||||
name: "collection",
|
||||
label: "Collection name",
|
||||
required: true,
|
||||
assistiveText: (
|
||||
<>
|
||||
{mode === "update" ? (
|
||||
<>
|
||||
<WarningIcon
|
||||
color="warning"
|
||||
aria-label="Warning"
|
||||
sx={{ fontSize: 16, mr: 0.5, verticalAlign: "middle" }}
|
||||
/>
|
||||
You can change which Firestore collection to display. Data in
|
||||
the new collection must be compatible with the existing
|
||||
columns.
|
||||
</>
|
||||
) : (
|
||||
"Type the name of the Firestore collection to display."
|
||||
)}{" "}
|
||||
<Link
|
||||
href={`https://console.firebase.google.com/project/_/firestore/data`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Your collections
|
||||
<OpenInNewIcon />
|
||||
</Link>
|
||||
</>
|
||||
),
|
||||
sx: { "& .MuiInputBase-input": { fontFamily: "mono" } },
|
||||
// https://firebase.google.com/docs/firestore/quotas#collections_documents_and_fields
|
||||
validation: [
|
||||
["matches", /^[^\s]+$/, "Collection name cannot have spaces"],
|
||||
["notOneOf", [".", ".."], "Collection name cannot be . or .."],
|
||||
[
|
||||
"test",
|
||||
"double-underscore",
|
||||
"Collection name cannot begin and end with __",
|
||||
(value: any) => !value.startsWith("__") && !value.endsWith("__"),
|
||||
],
|
||||
],
|
||||
},
|
||||
|
||||
// Step 2: Display
|
||||
{
|
||||
step: "display",
|
||||
type: "tableName",
|
||||
name: "name",
|
||||
label: "Table name",
|
||||
required: true,
|
||||
watchedField: "collection",
|
||||
assistiveText: "User-facing name for this table",
|
||||
autoFocus: true,
|
||||
gridCols: { xs: 12, sm: 6 },
|
||||
},
|
||||
{
|
||||
step: "display",
|
||||
type: "tableId",
|
||||
name: "id",
|
||||
label: "Table ID",
|
||||
required: true,
|
||||
watchedField: "name",
|
||||
assistiveText: `Unique ID used to store this table’s configuration. Cannot be edited${
|
||||
mode === "create" ? " later" : ""
|
||||
}.`,
|
||||
disabled: mode === "update",
|
||||
gridCols: { xs: 12, sm: 6 },
|
||||
validation:
|
||||
mode === "create"
|
||||
? [
|
||||
[
|
||||
"test",
|
||||
"unique",
|
||||
"Another table exists with this ID",
|
||||
(value: any) => !find(tables, ["value", value]),
|
||||
],
|
||||
]
|
||||
: [],
|
||||
},
|
||||
{
|
||||
step: "display",
|
||||
type: FieldType.singleSelect,
|
||||
name: "section",
|
||||
label: "Section (optional)",
|
||||
labelPlural: "sections",
|
||||
freeText: true,
|
||||
options: sections,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
step: "display",
|
||||
type: FieldType.paragraph,
|
||||
name: "description",
|
||||
label: "Description (optional)",
|
||||
minRows: 2,
|
||||
},
|
||||
|
||||
// Step 3: Access controls
|
||||
{
|
||||
step: "accessControls",
|
||||
type: FieldType.multiSelect,
|
||||
name: "roles",
|
||||
label: "Accessed by",
|
||||
labelPlural: "roles",
|
||||
options: roles ?? [],
|
||||
defaultValue: ["ADMIN"],
|
||||
required: true,
|
||||
freeText: true,
|
||||
},
|
||||
{
|
||||
step: "accessControls",
|
||||
type: FieldType.checkbox,
|
||||
name: "readOnly",
|
||||
label: "Read-only for non-ADMIN users",
|
||||
assistiveText:
|
||||
"Disable all editing functionality. Locks all columns and disables adding and deleting rows and columns.",
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
step: "accessControls",
|
||||
type: FieldType.contentParagraph,
|
||||
name: "_contentParagraph_rules",
|
||||
label: (
|
||||
<>
|
||||
To enable access controls for this table, you must set the
|
||||
corresponding Firestore Security Rules.{" "}
|
||||
<Link
|
||||
href={WIKI_LINKS.setupRoles + "#table-rules"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
style={{ position: "relative", zIndex: 1 }}
|
||||
>
|
||||
Learn how to write rules
|
||||
<OpenInNewIcon />
|
||||
</Link>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
step: "accessControls",
|
||||
type: "suggestedRules",
|
||||
name: "_suggestedRules",
|
||||
label: "Suggested Firestore Rules",
|
||||
watchedField: "collection",
|
||||
},
|
||||
|
||||
// Step 4: Auditing
|
||||
{
|
||||
step: "auditing",
|
||||
type: FieldType.checkbox,
|
||||
name: "audit",
|
||||
label: "Enable auditing for this table",
|
||||
defaultValue: true,
|
||||
},
|
||||
{
|
||||
step: "auditing",
|
||||
type: FieldType.shortText,
|
||||
name: "auditFieldCreatedBy",
|
||||
label: "Created By field key (optional)",
|
||||
defaultValue: "_createdBy",
|
||||
displayCondition: "return values.audit",
|
||||
assistiveText: "Optionally, change the field key",
|
||||
gridCols: { xs: 12, sm: 6 },
|
||||
sx: { "& .MuiInputBase-input": { fontFamily: "mono" } },
|
||||
},
|
||||
{
|
||||
step: "auditing",
|
||||
type: FieldType.shortText,
|
||||
name: "auditFieldUpdatedBy",
|
||||
label: "Updated By field key (optional)",
|
||||
defaultValue: "_updatedBy",
|
||||
displayCondition: "return values.audit",
|
||||
assistiveText: "Optionally, change the field key",
|
||||
gridCols: { xs: 12, sm: 6 },
|
||||
sx: { "& .MuiInputBase-input": { fontFamily: "mono" } },
|
||||
},
|
||||
// Step 5:Cloud functions
|
||||
/*
|
||||
{
|
||||
step: "function",
|
||||
type: FieldType.slider,
|
||||
name: "triggerDepth",
|
||||
defaultValue: 1,
|
||||
min: 1,
|
||||
max: 5,
|
||||
label: "Collection depth",
|
||||
displayCondition: "return values.tableType === 'collectionGroup'",
|
||||
assistiveText: (
|
||||
<>
|
||||
{name} Cloud Functions that rely on{" "}
|
||||
<Link
|
||||
href="https://firebase.google.com/docs/functions/firestore-events#function_triggers"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Firestore triggers
|
||||
</Link>{" "}
|
||||
on this table require you to manually set the depth of this collection
|
||||
group.
|
||||
<br />
|
||||
<Link
|
||||
href="https://stackoverflow.com/questions/58186741/watch-a-collectiongroup-with-firestore-using-cloud-functions"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn more about this requirement
|
||||
<OpenInNewIcon />
|
||||
</Link>
|
||||
</>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
step: "function",
|
||||
type: FieldType.singleSelect,
|
||||
name: "function.memory",
|
||||
label: "Memory Allocation",
|
||||
defaultValue: "256MB",
|
||||
options: ["128MB", "256MB", "512MB", "1GB", "2GB", "4GB", "8GB"],
|
||||
required: true,
|
||||
gridCols: { xs: 12, sm: 6 },
|
||||
},
|
||||
{
|
||||
step: "function",
|
||||
type: FieldType.shortText,
|
||||
name: "function.timeout",
|
||||
label: "Timeout",
|
||||
defaultValue: 60,
|
||||
InputProps: {
|
||||
type: "number",
|
||||
endAdornment: <InputAdornment position="end">seconds</InputAdornment>,
|
||||
},
|
||||
gridCols: { xs: 12, sm: 6 },
|
||||
},
|
||||
{
|
||||
step: "function",
|
||||
type: FieldType.contentSubHeader,
|
||||
name: "functionHeader",
|
||||
label: "Auto scaling",
|
||||
|
||||
assistiveText: (
|
||||
<>
|
||||
<Link
|
||||
href="https://firebase.google.com/docs/functions/autoscaling"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn more about auto scaling
|
||||
<OpenInNewIcon />
|
||||
</Link>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
step: "function",
|
||||
type: FieldType.shortText,
|
||||
name: "function.minInstances",
|
||||
label: "Minimum Instances",
|
||||
defaultValue: 0,
|
||||
InputProps: {
|
||||
type: "number",
|
||||
},
|
||||
gridCols: { xs: 12, sm: 6 },
|
||||
},
|
||||
{
|
||||
step: "function",
|
||||
type: FieldType.shortText,
|
||||
name: "function.maxInstances",
|
||||
label: "Maximum Instances",
|
||||
defaultValue: 1000,
|
||||
InputProps: {
|
||||
type: "number",
|
||||
},
|
||||
gridCols: { xs: 12, sm: 6 },
|
||||
},
|
||||
*/
|
||||
mode === "create" && tables && tables?.length !== 0
|
||||
? {
|
||||
step: "columns",
|
||||
type: FieldType.singleSelect,
|
||||
name: "schemaSource",
|
||||
label: "Copy columns from existing table (optional)",
|
||||
labelPlural: "tables",
|
||||
options: tables,
|
||||
clearable: true,
|
||||
freeText: false,
|
||||
itemRenderer: (option: {
|
||||
value: string;
|
||||
label: string;
|
||||
section: string;
|
||||
collection: string;
|
||||
}) => (
|
||||
<>
|
||||
{option.section} > {option.label}{" "}
|
||||
<code style={{ marginLeft: "auto" }}>{option.collection}</code>
|
||||
</>
|
||||
),
|
||||
}
|
||||
: null,
|
||||
mode === "create"
|
||||
? {
|
||||
step: "columns",
|
||||
type: FieldType.contentSubHeader,
|
||||
name: "_contentSubHeader_initialColumns",
|
||||
label: "Initial columns",
|
||||
sx: { "&&": { mb: 1 }, typography: "button", ml: 2 / 8 },
|
||||
}
|
||||
: null,
|
||||
mode === "create"
|
||||
? {
|
||||
step: "columns",
|
||||
type: FieldType.checkbox,
|
||||
name: `_initialColumns.${TableFieldType.createdBy}`,
|
||||
label: "Created By",
|
||||
displayCondition: "return values.audit",
|
||||
gridCols: 6,
|
||||
disablePaddingTop: true,
|
||||
}
|
||||
: null,
|
||||
mode === "create"
|
||||
? {
|
||||
step: "columns",
|
||||
type: FieldType.checkbox,
|
||||
name: `_initialColumns.${TableFieldType.updatedBy}`,
|
||||
label: "Updated By",
|
||||
displayCondition: "return values.audit",
|
||||
gridCols: 6,
|
||||
disablePaddingTop: true,
|
||||
}
|
||||
: null,
|
||||
mode === "create"
|
||||
? {
|
||||
step: "columns",
|
||||
type: FieldType.checkbox,
|
||||
name: `_initialColumns.${TableFieldType.createdAt}`,
|
||||
label: "Created At",
|
||||
displayCondition: "return values.audit",
|
||||
gridCols: 6,
|
||||
disablePaddingTop: true,
|
||||
}
|
||||
: null,
|
||||
mode === "create"
|
||||
? {
|
||||
step: "columns",
|
||||
type: FieldType.checkbox,
|
||||
name: `_initialColumns.${TableFieldType.updatedAt}`,
|
||||
label: "Updated At",
|
||||
displayCondition: "return values.audit",
|
||||
gridCols: 6,
|
||||
disablePaddingTop: true,
|
||||
}
|
||||
: null,
|
||||
mode === "create"
|
||||
? {
|
||||
step: "columns",
|
||||
type: FieldType.checkbox,
|
||||
name: `_initialColumns.${TableFieldType.id}`,
|
||||
label: "Row ID",
|
||||
disablePaddingTop: true,
|
||||
}
|
||||
: null,
|
||||
].filter((field) => field !== null) as Field[];
|
||||
2
src/components/TableSettingsDialog/index.ts
Normal file
2
src/components/TableSettingsDialog/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./TableSettingsDialog";
|
||||
export { default } from "./TableSettingsDialog";
|
||||
33
src/components/Tables/HomeWelcomePrompt.tsx
Normal file
33
src/components/Tables/HomeWelcomePrompt.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Zoom, Stack, Typography } from "@mui/material";
|
||||
|
||||
export default function HomeWelcomePrompt() {
|
||||
return (
|
||||
<Zoom in style={{ transformOrigin: `${320 - 52}px ${320 - 52}px` }}>
|
||||
<Stack
|
||||
justifyContent="center"
|
||||
sx={{
|
||||
bgcolor: "primary.main",
|
||||
color: "primary.contrastText",
|
||||
boxShadow: 24,
|
||||
|
||||
width: 320,
|
||||
height: 320,
|
||||
p: 5,
|
||||
borderRadius: "50% 50% 0 50%",
|
||||
|
||||
position: "fixed",
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
}}
|
||||
>
|
||||
<Typography variant="overline" component="h1" gutterBottom>
|
||||
Get started
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h5" component="p">
|
||||
Create a table from a new or existing Firestore collection
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Zoom>
|
||||
);
|
||||
}
|
||||
79
src/components/Tables/TableGrid/TableCard.tsx
Normal file
79
src/components/Tables/TableGrid/TableCard.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardActionArea,
|
||||
CardContent,
|
||||
Typography,
|
||||
CardActions,
|
||||
Button,
|
||||
} from "@mui/material";
|
||||
import GoIcon from "@src/assets/icons/Go";
|
||||
|
||||
import RenderedMarkdown from "@src/components/RenderedMarkdown";
|
||||
import { TableSettings } from "@src/atoms/globalScope";
|
||||
|
||||
export interface ITableCardProps extends TableSettings {
|
||||
link: string;
|
||||
actions?: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function TableCard({
|
||||
section,
|
||||
name,
|
||||
description,
|
||||
link,
|
||||
actions,
|
||||
}: ITableCardProps) {
|
||||
return (
|
||||
<Card style={{ height: "100%", display: "flex", flexDirection: "column" }}>
|
||||
<CardActionArea component={Link} to={link}>
|
||||
<CardContent style={{ paddingBottom: 0 }}>
|
||||
<Typography variant="overline" component="p">
|
||||
{section}
|
||||
</Typography>
|
||||
<Typography variant="h6" component="h3" gutterBottom>
|
||||
{name}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
|
||||
<CardContent style={{ flexGrow: 1, paddingTop: 0 }}>
|
||||
<Typography
|
||||
color="textSecondary"
|
||||
sx={{
|
||||
minHeight: (theme) =>
|
||||
(theme.typography.body2.lineHeight as number) * 2 + "em",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 1,
|
||||
}}
|
||||
component="div"
|
||||
>
|
||||
{description && (
|
||||
<RenderedMarkdown
|
||||
children={description}
|
||||
//restrictionPreset="singleLine"
|
||||
/>
|
||||
)}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
|
||||
<CardActions>
|
||||
<Button
|
||||
variant="text"
|
||||
color="primary"
|
||||
endIcon={<GoIcon />}
|
||||
component={Link}
|
||||
to={link}
|
||||
>
|
||||
Open
|
||||
</Button>
|
||||
|
||||
<div style={{ flexGrow: 1 }} />
|
||||
|
||||
{actions}
|
||||
</CardActions>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
42
src/components/Tables/TableGrid/TableCardSkeleton.tsx
Normal file
42
src/components/Tables/TableGrid/TableCardSkeleton.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
Typography,
|
||||
CardActions,
|
||||
Skeleton,
|
||||
} from "@mui/material";
|
||||
|
||||
export default function TableCardSkeleton() {
|
||||
return (
|
||||
<Card style={{ height: "100%", display: "flex", flexDirection: "column" }}>
|
||||
<CardContent style={{ flexGrow: 1 }}>
|
||||
<Typography variant="overline">
|
||||
<Skeleton width={80} />
|
||||
</Typography>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
<Skeleton width={180} />
|
||||
</Typography>
|
||||
<Typography
|
||||
color="textSecondary"
|
||||
sx={{
|
||||
minHeight: (theme) =>
|
||||
(theme.typography.body2.lineHeight as number) * 2 + "em",
|
||||
}}
|
||||
>
|
||||
<Skeleton width={120} />
|
||||
</Typography>
|
||||
</CardContent>
|
||||
|
||||
<CardActions sx={{ mb: 1, mx: 1 }}>
|
||||
<Skeleton
|
||||
width={60}
|
||||
height={20}
|
||||
variant="rectangular"
|
||||
sx={{ borderRadius: 1, mr: "auto" }}
|
||||
/>
|
||||
|
||||
<Skeleton variant="circular" width={24} height={24} />
|
||||
</CardActions>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
73
src/components/Tables/TableGrid/TableGrid.tsx
Normal file
73
src/components/Tables/TableGrid/TableGrid.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { TransitionGroup } from "react-transition-group";
|
||||
|
||||
import { Box, Grid, Collapse } from "@mui/material";
|
||||
|
||||
import SectionHeading from "@src/components/SectionHeading";
|
||||
import TableCard from "./TableCard";
|
||||
import SlideTransition from "@src/components/Modal/SlideTransition";
|
||||
|
||||
import { TableSettings } from "@src/atoms/globalScope";
|
||||
|
||||
export interface ITableGridProps {
|
||||
sections: Record<string, TableSettings[]>;
|
||||
getLink: (table: TableSettings) => string;
|
||||
getActions?: (table: TableSettings) => React.ReactNode;
|
||||
}
|
||||
|
||||
export default function TableGrid({
|
||||
sections,
|
||||
getLink,
|
||||
getActions,
|
||||
}: ITableGridProps) {
|
||||
return (
|
||||
<TransitionGroup>
|
||||
{Object.entries(sections).map(
|
||||
([sectionName, sectionTables], sectionIndex) => {
|
||||
const tableItems = sectionTables
|
||||
.map((table, tableIndex) => {
|
||||
if (!table) return null;
|
||||
|
||||
return (
|
||||
<SlideTransition
|
||||
key={table.id}
|
||||
appear
|
||||
timeout={(sectionIndex + 1) * 100 + tableIndex * 50}
|
||||
>
|
||||
<Grid item xs={12} sm={6} md={4} lg={3}>
|
||||
<TableCard
|
||||
{...table}
|
||||
link={getLink(table)}
|
||||
actions={getActions ? getActions(table) : null}
|
||||
/>
|
||||
</Grid>
|
||||
</SlideTransition>
|
||||
);
|
||||
})
|
||||
.filter((item) => item !== null);
|
||||
|
||||
if (tableItems.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Collapse key={sectionName}>
|
||||
<Box component="section" sx={{ mt: 4 }}>
|
||||
<SlideTransition
|
||||
key={"grid-section-" + sectionName}
|
||||
in
|
||||
timeout={(sectionIndex + 1) * 100}
|
||||
>
|
||||
<SectionHeading sx={{ pl: 2, pr: 1.5 }}>
|
||||
{sectionName}
|
||||
</SectionHeading>
|
||||
</SlideTransition>
|
||||
|
||||
<Grid component={TransitionGroup} container spacing={2}>
|
||||
{tableItems}
|
||||
</Grid>
|
||||
</Box>
|
||||
</Collapse>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</TransitionGroup>
|
||||
);
|
||||
}
|
||||
2
src/components/Tables/TableGrid/index.ts
Normal file
2
src/components/Tables/TableGrid/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./TableGrid";
|
||||
export { default } from "./TableGrid";
|
||||
71
src/components/Tables/TableList/TableList.tsx
Normal file
71
src/components/Tables/TableList/TableList.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { TransitionGroup } from "react-transition-group";
|
||||
|
||||
import { Box, Paper, Collapse, List } from "@mui/material";
|
||||
|
||||
import SectionHeading from "@src/components/SectionHeading";
|
||||
import TableListItem from "./TableListItem";
|
||||
import SlideTransition from "@src/components/Modal/SlideTransition";
|
||||
|
||||
import { TableSettings } from "@src/atoms/globalScope";
|
||||
|
||||
export interface ITableListProps {
|
||||
sections: Record<string, TableSettings[]>;
|
||||
getLink: (table: TableSettings) => string;
|
||||
getActions?: (table: TableSettings) => React.ReactNode;
|
||||
}
|
||||
|
||||
export default function TableList({
|
||||
sections,
|
||||
getLink,
|
||||
getActions,
|
||||
}: ITableListProps) {
|
||||
return (
|
||||
<TransitionGroup>
|
||||
{Object.entries(sections).map(
|
||||
([sectionName, sectionTables], sectionIndex) => {
|
||||
const tableItems = sectionTables
|
||||
.map((table) => {
|
||||
if (!table) return null;
|
||||
|
||||
return (
|
||||
<Collapse key={table.id}>
|
||||
<TableListItem
|
||||
{...table}
|
||||
link={getLink(table)}
|
||||
actions={getActions ? getActions(table) : null}
|
||||
/>
|
||||
</Collapse>
|
||||
);
|
||||
})
|
||||
.filter((item) => item !== null);
|
||||
|
||||
if (tableItems.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Collapse key={sectionName}>
|
||||
<Box component="section" sx={{ mt: 4 }}>
|
||||
<SlideTransition
|
||||
key={"list-section-" + sectionName}
|
||||
in
|
||||
timeout={(sectionIndex + 1) * 100}
|
||||
>
|
||||
<SectionHeading sx={{ pl: 2, pr: 1 }}>
|
||||
{sectionName}
|
||||
</SectionHeading>
|
||||
</SlideTransition>
|
||||
|
||||
<SlideTransition in timeout={(sectionIndex + 1) * 100}>
|
||||
<Paper>
|
||||
<List disablePadding>
|
||||
<TransitionGroup>{tableItems}</TransitionGroup>
|
||||
</List>
|
||||
</Paper>
|
||||
</SlideTransition>
|
||||
</Box>
|
||||
</Collapse>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</TransitionGroup>
|
||||
);
|
||||
}
|
||||
81
src/components/Tables/TableList/TableListItem.tsx
Normal file
81
src/components/Tables/TableList/TableListItem.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import {
|
||||
ListItem,
|
||||
ListItemButton,
|
||||
Typography,
|
||||
IconButton,
|
||||
} from "@mui/material";
|
||||
import GoIcon from "@mui/icons-material/ArrowForward";
|
||||
|
||||
import RenderedMarkdown from "@src/components/RenderedMarkdown";
|
||||
import { TableSettings } from "@src/atoms/globalScope";
|
||||
|
||||
export interface ITableListItemProps extends TableSettings {
|
||||
link: string;
|
||||
actions?: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function TableListItem({
|
||||
// section,
|
||||
name,
|
||||
description,
|
||||
link,
|
||||
actions,
|
||||
}: ITableListItemProps) {
|
||||
return (
|
||||
<ListItem disableGutters disablePadding>
|
||||
<ListItemButton
|
||||
component={Link}
|
||||
to={link}
|
||||
sx={{
|
||||
alignItems: "baseline",
|
||||
height: 48,
|
||||
py: 0,
|
||||
pr: 0,
|
||||
borderRadius: 2,
|
||||
"& > *": { lineHeight: "48px !important" },
|
||||
flexWrap: "nowrap",
|
||||
overflow: "hidden",
|
||||
|
||||
flexBasis: 160 + 16,
|
||||
flexGrow: 0,
|
||||
flexShrink: 0,
|
||||
mr: 2,
|
||||
}}
|
||||
>
|
||||
<Typography component="h3" variant="button" noWrap>
|
||||
{name}
|
||||
</Typography>
|
||||
</ListItemButton>
|
||||
|
||||
<Typography
|
||||
color="textSecondary"
|
||||
component="div"
|
||||
noWrap
|
||||
sx={{ flexGrow: 1, "& *": { display: "inline" } }}
|
||||
>
|
||||
{description && (
|
||||
<RenderedMarkdown
|
||||
children={description}
|
||||
restrictionPreset="singleLine"
|
||||
/>
|
||||
)}
|
||||
</Typography>
|
||||
|
||||
<div style={{ flexShrink: 0 }}>
|
||||
{actions}
|
||||
|
||||
<IconButton
|
||||
size="large"
|
||||
color="primary"
|
||||
component={Link}
|
||||
to={link}
|
||||
sx={{ display: { xs: "none", sm: "inline-flex" } }}
|
||||
>
|
||||
<GoIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
23
src/components/Tables/TableList/TableListItemSkeleton.tsx
Normal file
23
src/components/Tables/TableList/TableListItemSkeleton.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { ListItem, Skeleton } from "@mui/material";
|
||||
|
||||
export default function TableListItemSkeleton() {
|
||||
return (
|
||||
<ListItem disableGutters disablePadding style={{ height: 48 }}>
|
||||
<Skeleton width={160} sx={{ mx: 2, flexShrink: 0 }} />
|
||||
<Skeleton sx={{ mr: 2, flexBasis: 240, flexShrink: 1 }} />
|
||||
|
||||
<Skeleton
|
||||
variant="circular"
|
||||
width={24}
|
||||
height={24}
|
||||
sx={{ ml: "auto", mr: 3, flexShrink: 0 }}
|
||||
/>
|
||||
<Skeleton
|
||||
variant="circular"
|
||||
width={24}
|
||||
height={24}
|
||||
sx={{ mr: 1.5, flexShrink: 0 }}
|
||||
/>
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
2
src/components/Tables/TableList/index.ts
Normal file
2
src/components/Tables/TableList/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./TableList";
|
||||
export { default } from "./TableList";
|
||||
@@ -14,6 +14,9 @@ export enum ROUTES {
|
||||
setup = "/setup",
|
||||
pageNotFound = "/404",
|
||||
|
||||
tables = "/tables",
|
||||
automations = "/automations",
|
||||
|
||||
table = "/table",
|
||||
tableWithId = "/table/:id",
|
||||
tableGroup = "/tableGroup",
|
||||
@@ -27,8 +30,8 @@ export enum ROUTES {
|
||||
}
|
||||
|
||||
export const ROUTE_TITLES = {
|
||||
[ROUTES.home]: {
|
||||
title: "Home",
|
||||
[ROUTES.tables]: {
|
||||
title: "Tables",
|
||||
titleComponent: (open, pinned) =>
|
||||
!(open && pinned) && (
|
||||
<Logo
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useAtom } from "jotai";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import { find, groupBy } from "lodash-es";
|
||||
|
||||
import {
|
||||
@@ -11,13 +11,14 @@ import {
|
||||
ListItemText,
|
||||
Divider,
|
||||
} from "@mui/material";
|
||||
import CloseIcon from "@mui/icons-material/MenuOpen";
|
||||
import PinIcon from "@mui/icons-material/PushPinOutlined";
|
||||
import UnpinIcon from "@mui/icons-material/PushPin";
|
||||
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 "@mui/icons-material/MenuOpen";
|
||||
import PinIcon from "@mui/icons-material/PushPinOutlined";
|
||||
import UnpinIcon from "@mui/icons-material/PushPin";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
|
||||
import { APP_BAR_HEIGHT } from ".";
|
||||
import Logo from "@src/assets/Logo";
|
||||
@@ -31,6 +32,7 @@ import {
|
||||
userSettingsAtom,
|
||||
tablesAtom,
|
||||
TableSettings,
|
||||
tableSettingsDialogAtom,
|
||||
} from "@src/atoms/globalScope";
|
||||
import { ROUTES } from "@src/constants/routes";
|
||||
|
||||
@@ -54,6 +56,10 @@ export default function NavDrawer({
|
||||
const [tables] = useAtom(tablesAtom, globalScope);
|
||||
const [userSettings] = useAtom(userSettingsAtom, globalScope);
|
||||
const [userRoles] = useAtom(userRolesAtom, globalScope);
|
||||
const openTableSettingsDialog = useSetAtom(
|
||||
tableSettingsDialogAtom,
|
||||
globalScope
|
||||
);
|
||||
|
||||
const favorites = Array.isArray(userSettings.favoriteTables)
|
||||
? userSettings.favoriteTables
|
||||
@@ -65,7 +71,9 @@ export default function NavDrawer({
|
||||
...groupBy(tables, "section"),
|
||||
};
|
||||
|
||||
const closeDrawer = (e: {}) => props.onClose(e, "escapeKeyDown");
|
||||
const closeDrawer = pinned
|
||||
? undefined
|
||||
: (e: {}) => props.onClose(e, "escapeKeyDown");
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
@@ -134,21 +142,20 @@ export default function NavDrawer({
|
||||
<nav>
|
||||
<List disablePadding>
|
||||
<li>
|
||||
<NavItem
|
||||
to={ROUTES.home}
|
||||
onClick={pinned ? undefined : closeDrawer}
|
||||
>
|
||||
<NavItem to={ROUTES.home} onClick={closeDrawer}>
|
||||
<ListItemIcon>
|
||||
<HomeIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Home" />
|
||||
</NavItem>
|
||||
</li>
|
||||
|
||||
{userRoles.includes("ADMIN") && (
|
||||
<Divider variant="middle" sx={{ my: 1 }} />
|
||||
)}
|
||||
|
||||
<li>
|
||||
<NavItem
|
||||
to={ROUTES.userSettings}
|
||||
onClick={pinned ? undefined : closeDrawer}
|
||||
>
|
||||
<NavItem to={ROUTES.userSettings} onClick={closeDrawer}>
|
||||
<ListItemIcon>
|
||||
<SettingsIcon />
|
||||
</ListItemIcon>
|
||||
@@ -157,10 +164,7 @@ export default function NavDrawer({
|
||||
</li>
|
||||
{userRoles.includes("ADMIN") && (
|
||||
<li>
|
||||
<NavItem
|
||||
to={ROUTES.projectSettings}
|
||||
onClick={pinned ? undefined : closeDrawer}
|
||||
>
|
||||
<NavItem to={ROUTES.projectSettings} onClick={closeDrawer}>
|
||||
<ListItemIcon>
|
||||
<ProjectSettingsIcon />
|
||||
</ListItemIcon>
|
||||
@@ -171,10 +175,7 @@ export default function NavDrawer({
|
||||
)}
|
||||
{userRoles.includes("ADMIN") && (
|
||||
<li>
|
||||
<NavItem
|
||||
to={ROUTES.userManagement}
|
||||
onClick={pinned ? undefined : closeDrawer}
|
||||
>
|
||||
<NavItem to={ROUTES.userManagement} onClick={closeDrawer}>
|
||||
<ListItemIcon>
|
||||
<UserManagementIcon />
|
||||
</ListItemIcon>
|
||||
@@ -185,6 +186,24 @@ export default function NavDrawer({
|
||||
|
||||
<Divider variant="middle" sx={{ my: 1 }} />
|
||||
|
||||
<li>
|
||||
<NavItem
|
||||
{...({ component: "button" } as any)}
|
||||
style={{ textAlign: "left" }}
|
||||
onClick={(e) => {
|
||||
if (closeDrawer) closeDrawer(e);
|
||||
openTableSettingsDialog({});
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<AddIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Create table…" />
|
||||
</NavItem>
|
||||
</li>
|
||||
|
||||
<Divider variant="middle" sx={{ my: 1 }} />
|
||||
|
||||
{sections &&
|
||||
Object.entries(sections)
|
||||
.filter(([, tables]) => tables.length > 0)
|
||||
@@ -193,7 +212,7 @@ export default function NavDrawer({
|
||||
key={section}
|
||||
section={section}
|
||||
tables={tables}
|
||||
closeDrawer={pinned ? undefined : closeDrawer}
|
||||
closeDrawer={closeDrawer}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
|
||||
@@ -184,7 +184,8 @@ export default function Navigation({ children }: React.PropsWithChildren<{}>) {
|
||||
: "100%",
|
||||
}}
|
||||
>
|
||||
{children || <Outlet />}
|
||||
<Outlet />
|
||||
{children}
|
||||
</div>
|
||||
</Suspense>
|
||||
</ErrorBoundary>
|
||||
|
||||
228
src/pages/Tables.tsx
Normal file
228
src/pages/Tables.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import { find, groupBy } from "lodash-es";
|
||||
|
||||
import {
|
||||
Container,
|
||||
Stack,
|
||||
Typography,
|
||||
ToggleButtonGroup,
|
||||
ToggleButton,
|
||||
Tooltip,
|
||||
Fab,
|
||||
Checkbox,
|
||||
IconButton,
|
||||
Zoom,
|
||||
} from "@mui/material";
|
||||
import ViewListIcon from "@mui/icons-material/ViewListOutlined";
|
||||
import ViewGridIcon from "@mui/icons-material/ViewModuleOutlined";
|
||||
import FavoriteBorderIcon from "@mui/icons-material/FavoriteBorder";
|
||||
import FavoriteIcon from "@mui/icons-material/Favorite";
|
||||
import EditIcon from "@mui/icons-material/EditOutlined";
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
|
||||
import FloatingSearch from "@src/components/FloatingSearch";
|
||||
import SlideTransition from "@src/components/Modal/SlideTransition";
|
||||
import TableGrid from "@src/components/Tables/TableGrid";
|
||||
import TableList from "@src/components/Tables/TableList";
|
||||
import HomeWelcomePrompt from "@src/components/Tables/HomeWelcomePrompt";
|
||||
import EmptyState from "@src/components/EmptyState";
|
||||
|
||||
import {
|
||||
globalScope,
|
||||
userRolesAtom,
|
||||
userSettingsAtom,
|
||||
updateUserSettingsAtom,
|
||||
tablesAtom,
|
||||
tablesViewAtom,
|
||||
tableSettingsDialogAtom,
|
||||
TableSettings,
|
||||
} from "@src/atoms/globalScope";
|
||||
import { ROUTES } from "@src/constants/routes";
|
||||
import useBasicSearch from "@src/hooks/useBasicSearch";
|
||||
import { APP_BAR_HEIGHT } from "@src/layouts/Navigation";
|
||||
|
||||
const SEARCH_KEYS = ["id", "name", "section", "description"];
|
||||
|
||||
export default function HomePage() {
|
||||
const [userRoles] = useAtom(userRolesAtom, globalScope);
|
||||
const [userSettings] = useAtom(userSettingsAtom, globalScope);
|
||||
const [updateUserSettings] = useAtom(updateUserSettingsAtom, globalScope);
|
||||
const [tables] = useAtom(tablesAtom, globalScope);
|
||||
const [view, setView] = useAtom(tablesViewAtom, globalScope);
|
||||
const openTableSettingsDialog = useSetAtom(
|
||||
tableSettingsDialogAtom,
|
||||
globalScope
|
||||
);
|
||||
|
||||
const [results, query, handleQuery] = useBasicSearch(
|
||||
tables ?? [],
|
||||
SEARCH_KEYS
|
||||
);
|
||||
|
||||
const favorites = Array.isArray(userSettings.favoriteTables)
|
||||
? userSettings.favoriteTables
|
||||
: [];
|
||||
const sections: Record<string, TableSettings[]> = {
|
||||
Favorites: favorites.map((id) => find(results, { id })) as TableSettings[],
|
||||
...groupBy(results, "section"),
|
||||
};
|
||||
|
||||
if (!Array.isArray(tables))
|
||||
throw new Error(
|
||||
"Project settings are not configured correctly. `tables` is not an array."
|
||||
);
|
||||
|
||||
const createTableFab = (
|
||||
<Tooltip title="Create table">
|
||||
<Zoom in>
|
||||
<Fab
|
||||
color="secondary"
|
||||
aria-label="Create table"
|
||||
onClick={() => openTableSettingsDialog({ mode: "create" })}
|
||||
sx={{
|
||||
zIndex: "speedDial",
|
||||
position: "fixed",
|
||||
bottom: (theme) => ({
|
||||
xs: `max(${theme.spacing(2)}, env(safe-area-inset-bottom))`,
|
||||
sm: `max(${theme.spacing(3)}, env(safe-area-inset-bottom))`,
|
||||
}),
|
||||
right: (theme) => ({
|
||||
xs: `max(${theme.spacing(2)}, env(safe-area-inset-right))`,
|
||||
sm: `max(${theme.spacing(3)}, env(safe-area-inset-right))`,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<AddIcon />
|
||||
</Fab>
|
||||
</Zoom>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
if (tables.length === 0) {
|
||||
if (userRoles.includes("ADMIN"))
|
||||
return (
|
||||
<>
|
||||
<HomeWelcomePrompt />
|
||||
{createTableFab}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<EmptyState
|
||||
message="No tables"
|
||||
description="There are no tables in this project. Sign in with an ADMIN account to create tables."
|
||||
fullScreen
|
||||
style={{ marginTop: -APP_BAR_HEIGHT }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const getLink = (table: TableSettings) =>
|
||||
`${
|
||||
table.tableType === "primaryCollection" ? ROUTES.table : ROUTES.tableGroup
|
||||
}/${table.id.replace(/\//g, "~2F")}`;
|
||||
|
||||
const handleFavorite =
|
||||
(id: string) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const favoriteTables = e.target.checked
|
||||
? [...favorites, id]
|
||||
: favorites.filter((f) => f !== id);
|
||||
|
||||
if (updateUserSettings) updateUserSettings({ favoriteTables });
|
||||
};
|
||||
|
||||
const getActions = (table: TableSettings) => (
|
||||
<>
|
||||
{userRoles.includes("ADMIN") && (
|
||||
<IconButton
|
||||
aria-label="Edit table"
|
||||
onClick={() =>
|
||||
openTableSettingsDialog({ mode: "update", data: table })
|
||||
}
|
||||
size={view === "list" ? "large" : undefined}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
<Checkbox
|
||||
onChange={handleFavorite(table.id)}
|
||||
checked={favorites.includes(table.id)}
|
||||
icon={<FavoriteBorderIcon />}
|
||||
checkedIcon={
|
||||
<Zoom in>
|
||||
<FavoriteIcon />
|
||||
</Zoom>
|
||||
}
|
||||
name={`favorite-${table.id}`}
|
||||
inputProps={{ "aria-label": "Favorite" }}
|
||||
sx={view === "list" ? { p: 1.5 } : undefined}
|
||||
color="secondary"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Container component="main" sx={{ px: 1, pt: 1, pb: 7 + 3 + 3 }}>
|
||||
<FloatingSearch
|
||||
label="Search tables"
|
||||
onChange={(e) => handleQuery(e.target.value)}
|
||||
paperSx={{
|
||||
maxWidth: (theme) => ({ md: theme.breakpoints.values.sm - 48 }),
|
||||
mb: { xs: 2, md: -6 },
|
||||
}}
|
||||
/>
|
||||
|
||||
<SlideTransition in timeout={50}>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
>
|
||||
<Typography
|
||||
variant="h6"
|
||||
component="h1"
|
||||
sx={{ pl: 2, cursor: "default" }}
|
||||
>
|
||||
{query ? `${results.length} of ${tables.length}` : tables.length}{" "}
|
||||
tables
|
||||
</Typography>
|
||||
|
||||
<ToggleButtonGroup
|
||||
value={view}
|
||||
size="large"
|
||||
exclusive
|
||||
onChange={(_, v) => {
|
||||
if (v !== null) setView(v);
|
||||
}}
|
||||
aria-label="Table view"
|
||||
sx={{ "& .MuiToggleButton-root": { borderRadius: 2 } }}
|
||||
>
|
||||
<ToggleButton value="list" aria-label="List view">
|
||||
<ViewListIcon style={{ transform: "rotate(180deg)" }} />
|
||||
</ToggleButton>
|
||||
<ToggleButton value="grid" aria-label="Grid view">
|
||||
<ViewGridIcon />
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Stack>
|
||||
</SlideTransition>
|
||||
|
||||
{view === "list" ? (
|
||||
<TableList
|
||||
sections={sections}
|
||||
getLink={getLink}
|
||||
getActions={getActions}
|
||||
/>
|
||||
) : (
|
||||
<TableGrid
|
||||
sections={sections}
|
||||
getLink={getLink}
|
||||
getActions={getActions}
|
||||
/>
|
||||
)}
|
||||
|
||||
{userRoles.includes("ADMIN") && createTableFab}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
4
src/types/json-stable-stringify-without-jsonify.d.ts
vendored
Normal file
4
src/types/json-stable-stringify-without-jsonify.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
declare module "json-stable-stringify-without-jsonify" {
|
||||
const stringify: any;
|
||||
export default stringify;
|
||||
}
|
||||
Reference in New Issue
Block a user