mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-29 00:16:39 +01:00
Merge branch 'develop' into rc
This commit is contained in:
6
.github/ISSUE_TEMPLATE/config.yml
vendored
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,8 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 🤔 Support & questions
|
||||
url: https://discord.com/invite/fjBugmvzZP
|
||||
about: Chat with us for live support on discord.
|
||||
- name: 🤔 Need support / Q&A
|
||||
url: https://github.com/rowyio/rowy/discussions/categories/support-q-a
|
||||
about: Raise a support query on Github Discussion
|
||||
- name: 🙌 Want to join our team?
|
||||
url: https://www.rowy.io/jobs
|
||||
about: Get in touch to contribute & work with Rowy
|
||||
|
||||
@@ -37,9 +37,9 @@ Connect to your database, manage data in table-UI with role based access control
|
||||
|
||||
Set up Rowy on your Google Cloud Platform project with this easy deploy button.
|
||||
|
||||
[](https://deploy.rowy.app/)
|
||||
[<img width="250" alt="Guided quick start button" src="https://user-images.githubusercontent.com/307298/185548050-e9208fb6-fe53-4c84-bbfa-53c08e03c15f.png">](https://rowy.app/)
|
||||
|
||||
https://deploy.rowy.app/
|
||||
https://rowy.app
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -94,7 +94,7 @@ https://user-images.githubusercontent.com/307298/157185793-f67511cd-7b7b-4229-95
|
||||
Set up Rowy on your Google Cloud project with this one-click deploy button. Your
|
||||
data and cloud functions stay on your own Firestore/GCP.
|
||||
|
||||
[](https://deploy.rowy.app/)
|
||||
[](https://rowy.app/)
|
||||
|
||||
The one-click deploy makes the process of setting up easy with a step by step
|
||||
guide and ensures your project is setup correctly.
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"file-saver": "^2.0.5",
|
||||
"firebase": "^9.6.11",
|
||||
"firebaseui": "^6.0.1",
|
||||
"jotai": "^1.7.2",
|
||||
"jotai": "^1.8.4",
|
||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||
"json2csv": "^5.0.7",
|
||||
"jszip": "^3.10.0",
|
||||
|
||||
23
src/App.tsx
23
src/App.tsx
@@ -9,6 +9,7 @@ import ConfirmDialog from "@src/components/ConfirmDialog";
|
||||
import RowyRunModal from "@src/components/RowyRunModal";
|
||||
import NotFound from "@src/pages/NotFoundPage";
|
||||
import RequireAuth from "@src/layouts/RequireAuth";
|
||||
import AdminRoute from "@src/layouts/AdminRoute";
|
||||
|
||||
import {
|
||||
projectScope,
|
||||
@@ -20,7 +21,6 @@ import { ROUTES } from "@src/constants/routes";
|
||||
import useKeyPressWithAtom from "@src/hooks/useKeyPressWithAtom";
|
||||
|
||||
import TableGroupRedirectPage from "./pages/TableGroupRedirectPage";
|
||||
import JotaiTestPage from "@src/pages/Test/JotaiTestPage";
|
||||
import SignOutPage from "@src/pages/Auth/SignOutPage";
|
||||
|
||||
// prettier-ignore
|
||||
@@ -60,10 +60,6 @@ const MembersPage = lazy(() => import("@src/pages/Settings/MembersPage" /* webpa
|
||||
// prettier-ignore
|
||||
const DebugSettingsPage = lazy(() => import("@src/pages/Settings/DebugSettingsPage" /* webpackChunkName: "DebugSettingsPage" */));
|
||||
|
||||
// prettier-ignore
|
||||
const ThemeTestPage = lazy(() => import("@src/pages/Test/ThemeTestPage" /* webpackChunkName: "ThemeTestPage" */));
|
||||
// const RowyRunTestPage = lazy(() => import("@src/pages/RowyRunTestPage" /* webpackChunkName: "RowyRunTestPage" */));
|
||||
|
||||
export default function App() {
|
||||
const [currentUser] = useAtom(currentUserAtom, projectScope);
|
||||
const [userRoles] = useAtom(userRolesAtom, projectScope);
|
||||
@@ -150,19 +146,22 @@ export default function App() {
|
||||
<Route path={ROUTES.userSettings} element={<UserSettingsPage />} />
|
||||
<Route
|
||||
path={ROUTES.projectSettings}
|
||||
element={<ProjectSettingsPage />}
|
||||
element={
|
||||
<AdminRoute>
|
||||
<ProjectSettingsPage />
|
||||
</AdminRoute>
|
||||
}
|
||||
/>
|
||||
<Route path={ROUTES.members} element={<MembersPage />} />
|
||||
<Route
|
||||
path={ROUTES.debugSettings}
|
||||
element={<DebugSettingsPage />}
|
||||
element={
|
||||
<AdminRoute>
|
||||
<DebugSettingsPage />
|
||||
</AdminRoute>
|
||||
}
|
||||
/>
|
||||
{/* <Route path={ROUTES.rowyRunTest} element={<RowyRunTestPage />} /> */}
|
||||
|
||||
<Route path="/test/jotai" element={<JotaiTestPage />} />
|
||||
</Route>
|
||||
|
||||
<Route path={ROUTES.themeTest} element={<ThemeTestPage />} />
|
||||
</Routes>
|
||||
)}
|
||||
</Suspense>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { atom } from "jotai";
|
||||
import { atomWithReducer, atomWithHash } from "jotai/utils";
|
||||
import { uniqBy, findIndex, cloneDeep, unset, orderBy } from "lodash-es";
|
||||
import { findIndex, cloneDeep, unset, orderBy } from "lodash-es";
|
||||
|
||||
import {
|
||||
TableSettings,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Scope } from "jotai/core/atom";
|
||||
import { useAtomsDebugValue } from "jotai/devtools";
|
||||
|
||||
export function DebugAtoms(
|
||||
options: { scope: Scope } & Parameters<typeof useAtomsDebugValue>[0]
|
||||
options: NonNullable<Parameters<typeof useAtomsDebugValue>[0]>
|
||||
) {
|
||||
useAtomsDebugValue(options);
|
||||
return null;
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
Link as MuiLink,
|
||||
Button,
|
||||
} from "@mui/material";
|
||||
import SecurityIcon from "@mui/icons-material/SecurityOutlined";
|
||||
import LockIcon from "@mui/icons-material/LockOutlined";
|
||||
|
||||
import EmptyState from "@src/components/EmptyState";
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function AccessDenied({ resetErrorBoundary }: FallbackProps) {
|
||||
<EmptyState
|
||||
role="alert"
|
||||
fullScreen
|
||||
Icon={SecurityIcon}
|
||||
Icon={LockIcon}
|
||||
message="Access denied"
|
||||
description={
|
||||
<>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Box } from "@mui/material";
|
||||
|
||||
export interface ICircularProgressTimedProps
|
||||
extends ICircularProgressOpticalProps {
|
||||
/** Duration in seconds */
|
||||
duration: number;
|
||||
complete: boolean;
|
||||
}
|
||||
|
||||
@@ -22,18 +22,10 @@ export default function CodeEditorHelper({
|
||||
const [projectId] = useAtom(projectIdAtom, projectScope);
|
||||
|
||||
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.`,
|
||||
@@ -44,7 +36,7 @@ export default function CodeEditorHelper({
|
||||
},
|
||||
{
|
||||
key: "rowy",
|
||||
description: `rowy provides a set of functions that are commonly used, such as easy access to GCP Secret Manager`,
|
||||
description: `rowy provides a set of functions that are commonly used, such as easy file uploads & access to GCP Secret Manager`,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -78,6 +78,8 @@ export default function useMonacoCustomizations({
|
||||
|
||||
try {
|
||||
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
|
||||
moduleResolution:
|
||||
monaco.languages.typescript.ModuleResolutionKind.NodeJs,
|
||||
target: monaco.languages.typescript.ScriptTarget.ES2020,
|
||||
allowNonTsExtensions: true,
|
||||
});
|
||||
@@ -125,6 +127,7 @@ export default function useMonacoCustomizations({
|
||||
...JSON.parse(stringifiedDiagnosticsOptions),
|
||||
diagnosticCodesToIgnore: [
|
||||
1323, // remove dynamic import error
|
||||
2307, // silence type declarations not found for dynamic import
|
||||
],
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -329,6 +329,7 @@ export default function ColumnMenu() {
|
||||
</>
|
||||
),
|
||||
handleConfirm: handleEvaluateAll,
|
||||
confirm: "Evaluate",
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -204,7 +204,19 @@ export default function DefaultValueInput({
|
||||
|
||||
{column.config?.defaultValue?.type === "dynamic" && (
|
||||
<>
|
||||
<CodeEditorHelper docLink={WIKI_LINKS.howToDefaultValues} />
|
||||
<CodeEditorHelper
|
||||
docLink={WIKI_LINKS.howToDefaultValues}
|
||||
additionalVariables={[
|
||||
{
|
||||
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: "ref",
|
||||
description: `reference object that represents the reference to the current row in firestore db (ie: doc.ref).`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Suspense fallback={<FieldSkeleton height={100} />}>
|
||||
<CodeEditor
|
||||
column={column}
|
||||
|
||||
@@ -11,14 +11,20 @@ import {
|
||||
Button,
|
||||
} from "@mui/material";
|
||||
|
||||
import { SlideTransitionMui } from "@src/components/Modal/SlideTransition";
|
||||
import { FadeTransitionMui } from "@src/components/Modal/FadeTransition";
|
||||
import { projectScope, confirmDialogAtom } from "@src/atoms/projectScope";
|
||||
|
||||
export interface IConfirmDialogProps {
|
||||
scope?: Parameters<typeof useAtom>[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a confirm dialog using `confirmDialogAtom` in `globalState`
|
||||
* @see {@link confirmDialogAtom | Usage example}
|
||||
*/
|
||||
export default function ConfirmDialog() {
|
||||
export default function ConfirmDialog({
|
||||
scope = projectScope,
|
||||
}: IConfirmDialogProps) {
|
||||
const [
|
||||
{
|
||||
open,
|
||||
@@ -39,8 +45,12 @@ export default function ConfirmDialog() {
|
||||
buttonLayout = "horizontal",
|
||||
},
|
||||
setState,
|
||||
] = useAtom(confirmDialogAtom, projectScope);
|
||||
const handleClose = () => setState({ open: false });
|
||||
] = useAtom(confirmDialogAtom, scope);
|
||||
|
||||
const handleClose = () => {
|
||||
setState({ open: false });
|
||||
setDryText("");
|
||||
};
|
||||
|
||||
const [dryText, setDryText] = useState("");
|
||||
|
||||
@@ -52,7 +62,7 @@ export default function ConfirmDialog() {
|
||||
else handleClose();
|
||||
}}
|
||||
maxWidth={maxWidth}
|
||||
TransitionComponent={SlideTransitionMui}
|
||||
TransitionComponent={FadeTransitionMui}
|
||||
sx={{ cursor: "default", zIndex: (theme) => theme.zIndex.modal + 50 }}
|
||||
>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
@@ -68,7 +78,7 @@ export default function ConfirmDialog() {
|
||||
value={dryText}
|
||||
onChange={(e) => setDryText(e.target.value)}
|
||||
autoFocus
|
||||
label={`Type ${confirmationCommand} below to continue:`}
|
||||
label={`Type “${confirmationCommand}” below to continue:`}
|
||||
placeholder={confirmationCommand}
|
||||
fullWidth
|
||||
id="dryText"
|
||||
|
||||
@@ -4,17 +4,28 @@ import { colord } from "colord";
|
||||
import { useTheme, Avatar, AvatarProps } from "@mui/material";
|
||||
import { spreadSx } from "@src/utils/ui";
|
||||
|
||||
// https://www.stefanjudis.com/snippets/how-to-detect-emojis-in-javascript-strings/
|
||||
const emojiRegex = /\p{Emoji}/u;
|
||||
|
||||
export const EMOJI_AVATAR_L_LIGHT = 90;
|
||||
export const EMOJI_AVATAR_L_DARK = 30;
|
||||
export const EMOJI_AVATAR_C_LIGHT = 15;
|
||||
export const EMOJI_AVATAR_C_DARK = 20;
|
||||
|
||||
export interface IEmojiAvatarProps extends Partial<AvatarProps> {
|
||||
/** CSS color string or a number (as a string). If number, used as hue */
|
||||
bgColor?: string;
|
||||
emoji?: string;
|
||||
fallback: string;
|
||||
uid?: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export default function EmojiAvatar({
|
||||
bgColor,
|
||||
bgColor: bgColorProp,
|
||||
emoji,
|
||||
fallback,
|
||||
uid,
|
||||
children,
|
||||
size = 40,
|
||||
...props
|
||||
@@ -22,7 +33,19 @@ export default function EmojiAvatar({
|
||||
const theme = useTheme();
|
||||
const darkMode = theme.palette.mode === "dark";
|
||||
|
||||
const bgcolor = bgColor || generateRandomColor(fallback, darkMode);
|
||||
let bgcolor: string;
|
||||
if (bgColorProp && !Number.isNaN(Number(bgColorProp))) {
|
||||
bgcolor = colord({
|
||||
l: darkMode ? EMOJI_AVATAR_L_DARK : EMOJI_AVATAR_L_LIGHT,
|
||||
c: darkMode ? EMOJI_AVATAR_C_DARK : EMOJI_AVATAR_C_LIGHT,
|
||||
h: Number(bgColorProp),
|
||||
}).toHslString();
|
||||
} else if (bgColorProp) {
|
||||
bgcolor = bgColorProp;
|
||||
} else {
|
||||
bgcolor = generateRandomColor(`${fallback}__${uid}`, darkMode);
|
||||
}
|
||||
|
||||
const bgcolorLch = colord(bgcolor).toLch();
|
||||
const textColor = colord({
|
||||
l:
|
||||
@@ -42,7 +65,7 @@ export default function EmojiAvatar({
|
||||
color: textColor,
|
||||
width: size,
|
||||
height: size,
|
||||
fontSize: size * 0.45,
|
||||
fontSize: size * (emojiRegex.test(emoji || "") ? 0.67 : 0.45),
|
||||
},
|
||||
props.variant === "rounded" && { borderRadius: size / 40 },
|
||||
...spreadSx(props.sx),
|
||||
@@ -61,6 +84,10 @@ export default function EmojiAvatar({
|
||||
|
||||
const generateRandomColor = (seed: string, darkMode: boolean) => {
|
||||
const rng = seedrandom(seed);
|
||||
const color = colord({ l: darkMode ? 30 : 90, c: 15, h: rng() * 360 });
|
||||
const color = colord({
|
||||
l: darkMode ? EMOJI_AVATAR_L_DARK : EMOJI_AVATAR_L_LIGHT,
|
||||
c: darkMode ? EMOJI_AVATAR_C_DARK : EMOJI_AVATAR_C_LIGHT,
|
||||
h: rng() * 360,
|
||||
});
|
||||
return color.toHslString();
|
||||
};
|
||||
|
||||
14
src/components/Modal/MemoizedText.tsx
Normal file
14
src/components/Modal/MemoizedText.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { memo } from "react";
|
||||
|
||||
/**
|
||||
* Used for global Modals that can have customizable text
|
||||
* so that the default text doesn’t appear as the modal closes.
|
||||
*/
|
||||
const MemoizedText = memo(
|
||||
function MemoizedTextComponent({ text }: { text: React.ReactNode }) {
|
||||
return <>{text}</>;
|
||||
},
|
||||
() => true
|
||||
);
|
||||
|
||||
export default MemoizedText;
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import LoadingButton, { LoadingButtonProps } from "@mui/lab/LoadingButton";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
|
||||
import { SlideTransitionMui } from "./SlideTransition";
|
||||
import { FadeTransitionMui } from "./FadeTransition";
|
||||
import ScrollableDialogContent, {
|
||||
IScrollableDialogContentProps,
|
||||
} from "./ScrollableDialogContent";
|
||||
@@ -86,7 +86,7 @@ export default function Modal({
|
||||
onClose={handleClose}
|
||||
fullWidth
|
||||
fullScreen={fullScreen}
|
||||
TransitionComponent={fullScreen ? Slide : SlideTransitionMui}
|
||||
TransitionComponent={fullScreen ? Slide : FadeTransitionMui}
|
||||
TransitionProps={fullScreen ? ({ direction: "up" } as any) : undefined}
|
||||
aria-labelledby="modal-title"
|
||||
{...props}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "@mui/material";
|
||||
import { DocumentPath as DocumentPathIcon } from "@src/assets/icons";
|
||||
import LaunchIcon from "@mui/icons-material/Launch";
|
||||
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
|
||||
import LockIcon from "@mui/icons-material/LockOutlined";
|
||||
import VisibilityOffIcon from "@mui/icons-material/VisibilityOffOutlined";
|
||||
|
||||
@@ -25,6 +26,8 @@ import {
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { getFieldProp } from "@src/components/fields";
|
||||
import { getLabelId, getFieldId } from "./utils";
|
||||
import { useSnackbar } from "notistack";
|
||||
import { copyToClipboard } from "@src/utils/ui";
|
||||
|
||||
export interface IFieldWrapperProps {
|
||||
children?: React.ReactNode;
|
||||
@@ -49,7 +52,7 @@ export default function FieldWrapper({
|
||||
}: IFieldWrapperProps) {
|
||||
const [projectId] = useAtom(projectIdAtom, projectScope);
|
||||
const [altPress] = useAtom(altPressAtom, projectScope);
|
||||
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
return (
|
||||
<div>
|
||||
<Stack
|
||||
@@ -126,7 +129,14 @@ export default function FieldWrapper({
|
||||
>
|
||||
{debugText}
|
||||
</Typography>
|
||||
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
copyToClipboard(debugText as string);
|
||||
enqueueSnackbar("Copied!");
|
||||
}}
|
||||
>
|
||||
<ContentCopyIcon />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
href={`https://console.firebase.google.com/project/${projectId}/firestore/data/~2F${(
|
||||
debugText as string
|
||||
|
||||
@@ -137,6 +137,12 @@ export default function MenuContents({ onClose }: IMenuContentsProps) {
|
||||
|
||||
// Row actions
|
||||
if (row) {
|
||||
const handleDuplicate = () => {
|
||||
addRow({
|
||||
row,
|
||||
setId: addRowIdType === "custom" ? "decrement" : addRowIdType,
|
||||
});
|
||||
};
|
||||
const handleDelete = () => deleteRow(row._rowy_ref.path);
|
||||
const rowActions = [
|
||||
{
|
||||
@@ -179,13 +185,28 @@ export default function MenuContents({ onClose }: IMenuContentsProps) {
|
||||
disabled:
|
||||
tableSettings.tableType === "collectionGroup" ||
|
||||
(!userRoles.includes("ADMIN") && tableSettings.readOnly),
|
||||
onClick: () => {
|
||||
addRow({
|
||||
row,
|
||||
setId: addRowIdType === "custom" ? "decrement" : addRowIdType,
|
||||
});
|
||||
onClose();
|
||||
},
|
||||
onClick: altPress
|
||||
? handleDuplicate
|
||||
: () => {
|
||||
confirm({
|
||||
title: "Duplicate row?",
|
||||
body: (
|
||||
<>
|
||||
Row path:
|
||||
<br />
|
||||
<code
|
||||
style={{ userSelect: "all", wordBreak: "break-all" }}
|
||||
>
|
||||
{row._rowy_ref.path}
|
||||
</code>
|
||||
</>
|
||||
),
|
||||
confirm: "Duplicate",
|
||||
confirmColor: "success",
|
||||
handleConfirm: handleDuplicate,
|
||||
});
|
||||
onClose();
|
||||
},
|
||||
},
|
||||
{
|
||||
label: altPress ? "Delete" : "Delete…",
|
||||
|
||||
@@ -114,11 +114,7 @@ export default function Table({
|
||||
tableSettings.readOnly && !userRoles.includes("ADMIN")
|
||||
? false
|
||||
: column.editable ?? true,
|
||||
width: (column.width as number)
|
||||
? (column.width as number) > MAX_COL_WIDTH
|
||||
? MAX_COL_WIDTH
|
||||
: (column.width as number)
|
||||
: DEFAULT_COL_WIDTH,
|
||||
width: column.width ?? 100,
|
||||
}));
|
||||
|
||||
if (userRoles.includes("ADMIN") || !tableSettings.readOnly) {
|
||||
|
||||
@@ -31,6 +31,12 @@ export default function FinalColumn({ row }: FormatterProps<TableRow, any>) {
|
||||
|
||||
const [altPress] = useAtom(altPressAtom, projectScope);
|
||||
const handleDelete = () => deleteRow(row._rowy_ref.path);
|
||||
const handleDuplicate = () => {
|
||||
addRow({
|
||||
row,
|
||||
setId: addRowIdType === "custom" ? "decrement" : addRowIdType,
|
||||
});
|
||||
};
|
||||
|
||||
if (!userRoles.includes("ADMIN") && tableSettings.readOnly === true)
|
||||
return null;
|
||||
@@ -42,11 +48,28 @@ export default function FinalColumn({ row }: FormatterProps<TableRow, any>) {
|
||||
size="small"
|
||||
color="inherit"
|
||||
disabled={tableSettings.tableType === "collectionGroup"}
|
||||
onClick={() =>
|
||||
addRow({
|
||||
row,
|
||||
setId: addRowIdType === "custom" ? "decrement" : addRowIdType,
|
||||
})
|
||||
onClick={
|
||||
altPress
|
||||
? handleDuplicate
|
||||
: () => {
|
||||
confirm({
|
||||
title: "Duplicate row?",
|
||||
body: (
|
||||
<>
|
||||
Row path:
|
||||
<br />
|
||||
<code
|
||||
style={{ userSelect: "all", wordBreak: "break-all" }}
|
||||
>
|
||||
{row._rowy_ref.path}
|
||||
</code>
|
||||
</>
|
||||
),
|
||||
confirm: "Duplicate",
|
||||
confirmColor: "success",
|
||||
handleConfirm: handleDuplicate,
|
||||
});
|
||||
}
|
||||
}
|
||||
aria-label="Duplicate row"
|
||||
className="row-hover-iconButton"
|
||||
|
||||
@@ -13,6 +13,14 @@ const CodeEditor = lazy(
|
||||
);
|
||||
|
||||
const additionalVariables = [
|
||||
{
|
||||
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: "ref",
|
||||
description: `reference object that represents the reference to the current row in firestore db (ie: doc.ref).`,
|
||||
},
|
||||
{
|
||||
key: "change",
|
||||
description:
|
||||
|
||||
@@ -14,6 +14,14 @@ const CodeEditor = lazy(
|
||||
);
|
||||
|
||||
const additionalVariables = [
|
||||
{
|
||||
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: "ref",
|
||||
description: `reference object that represents the reference to the current row in firestore db (ie: doc.ref).`,
|
||||
},
|
||||
{
|
||||
key: "change",
|
||||
description:
|
||||
|
||||
@@ -13,14 +13,17 @@ import {
|
||||
FormControl,
|
||||
RadioGroup,
|
||||
Radio,
|
||||
Stack,
|
||||
Box,
|
||||
} from "@mui/material";
|
||||
import ArrowIcon from "@mui/icons-material/ArrowForward";
|
||||
import { TableColumn as TableColumnIcon } from "@src/assets/icons";
|
||||
|
||||
import { IStepProps } from ".";
|
||||
import { AirtableConfig } from "./ImportAirtableWizard";
|
||||
import { AirtableConfig } from "@src/components/TableModals/ImportAirtableWizard";
|
||||
import FadeList from "@src/components/TableModals/ScrollableList";
|
||||
import Column, { COLUMN_HEADER_HEIGHT } from "@src/components/Table/Column";
|
||||
import MultiSelect from "@rowy/multiselect";
|
||||
import ColumnSelect from "@src/components/Table/ColumnSelect";
|
||||
|
||||
import {
|
||||
tableScope,
|
||||
@@ -28,7 +31,8 @@ import {
|
||||
tableColumnsOrderedAtom,
|
||||
} from "@src/atoms/tableScope";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { suggestType } from "./utils";
|
||||
import { getFieldProp } from "@src/components/fields";
|
||||
import { suggestType } from "@src/components/TableModals/ImportAirtableWizard/utils";
|
||||
|
||||
export default function Step1Columns({
|
||||
airtableData,
|
||||
@@ -51,6 +55,7 @@ export default function Step1Columns({
|
||||
config.pairs.map((pair) => pair.fieldKey)
|
||||
);
|
||||
|
||||
// When a field is selected to be imported
|
||||
const handleSelect =
|
||||
(field: string) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const checked = e.target.checked;
|
||||
@@ -98,7 +103,7 @@ export default function Step1Columns({
|
||||
// Delete matching newColumn if it was created
|
||||
if (configPair) {
|
||||
const newColumnIndex = findIndex(config.newColumns, {
|
||||
key: configPair.fieldKey,
|
||||
key: configPair.columnKey,
|
||||
});
|
||||
if (newColumnIndex > -1) {
|
||||
const newColumns = [...config.newColumns];
|
||||
@@ -116,14 +121,17 @@ export default function Step1Columns({
|
||||
}
|
||||
};
|
||||
|
||||
// When a field is mapped to a new column
|
||||
const handleChange = (fieldKey: string) => (value: string) => {
|
||||
if (!value) return;
|
||||
const columnKey = !!tableSchema.columns?.[value] ? value : camelCase(value);
|
||||
if (columnKey === "") return;
|
||||
// Check if this pair already exists in config
|
||||
const configIndex = findIndex(config.pairs, { fieldKey });
|
||||
console.log(columnKey, configIndex);
|
||||
if (configIndex > -1) {
|
||||
const pairs = [...config.pairs];
|
||||
pairs[configIndex].fieldKey = columnKey;
|
||||
pairs[configIndex].columnKey = columnKey;
|
||||
setConfig((config) => ({ ...config, pairs }));
|
||||
} else {
|
||||
updateConfig({
|
||||
@@ -217,9 +225,8 @@ export default function Step1Columns({
|
||||
|
||||
<Grid item xs>
|
||||
{selected && (
|
||||
<MultiSelect
|
||||
<ColumnSelect
|
||||
multiple={false}
|
||||
options={tableColumns}
|
||||
value={columnKey}
|
||||
onChange={handleChange(field) as any}
|
||||
TextFieldProps={{
|
||||
@@ -229,21 +236,34 @@ export default function Step1Columns({
|
||||
if (!columnKey) return "Select or add column";
|
||||
else
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={1}
|
||||
alignItems="center"
|
||||
>
|
||||
<Box sx={{ width: 24, height: 24 }}>
|
||||
{!isNewColumn ? (
|
||||
getFieldProp("icon", matchingColumn?.type)
|
||||
) : (
|
||||
<TableColumnIcon color="disabled" />
|
||||
)}
|
||||
</Box>
|
||||
{matchingColumn?.name}
|
||||
{isNewColumn && (
|
||||
<Chip
|
||||
label="New"
|
||||
color="primary"
|
||||
size="small"
|
||||
sx={{
|
||||
marginLeft: (theme) =>
|
||||
theme.spacing(1) + " !important",
|
||||
backgroundColor: "action.focus",
|
||||
variant="outlined"
|
||||
style={{
|
||||
marginLeft: "auto",
|
||||
pointerEvents: "none",
|
||||
height: 24,
|
||||
fontWeight: "normal",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</Stack>
|
||||
);
|
||||
},
|
||||
sx: [
|
||||
@@ -274,14 +294,14 @@ export default function Step1Columns({
|
||||
!columnKey && { color: "text.disabled" },
|
||||
],
|
||||
},
|
||||
sx: { "& .MuiInputLabel-root": { display: "none" } },
|
||||
}}
|
||||
clearable={false}
|
||||
displayEmpty
|
||||
labelPlural="columns"
|
||||
freeText
|
||||
AddButtonProps={{ children: "Add new column…" }}
|
||||
AddButtonProps={{ children: "Create column…" }}
|
||||
AddDialogProps={{
|
||||
title: "Add new column",
|
||||
title: "Create column",
|
||||
textFieldLabel: "Column name",
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -58,8 +58,8 @@ const inferTypeFromValue = (value: any) => {
|
||||
|
||||
if (typeof value === "string") {
|
||||
if (REGEX_EMAIL.test(value)) return FieldType.email;
|
||||
if (REGEX_PHONE.test(value)) return FieldType.phone;
|
||||
if (REGEX_URL.test(value)) return FieldType.url;
|
||||
if (REGEX_PHONE.test(value)) return FieldType.phone;
|
||||
if (REGEX_HTML.test(value)) return FieldType.richText;
|
||||
if (value.length >= 50) return FieldType.longText;
|
||||
return FieldType.shortText;
|
||||
|
||||
@@ -3,15 +3,6 @@ import WarningIcon from "@mui/icons-material/WarningAmber";
|
||||
import { TableSettings } from "@src/types/table";
|
||||
import { IWebhook } from "@src/components/TableModals/WebhooksModal/utils";
|
||||
|
||||
export const webhookTypes = [
|
||||
"basic",
|
||||
"typeform",
|
||||
"sendgrid",
|
||||
//"shopify",
|
||||
//"twitter",
|
||||
//"stripe",
|
||||
] as const;
|
||||
|
||||
const requestType = [
|
||||
"declare type WebHookRequest {",
|
||||
" /**",
|
||||
|
||||
@@ -2,5 +2,6 @@ import basic from "./basic";
|
||||
import typeform from "./typeform";
|
||||
import sendgrid from "./sendgrid";
|
||||
import webform from "./webform";
|
||||
import stripe from "./stripe";
|
||||
|
||||
export { basic, typeform, sendgrid, webform };
|
||||
export { basic, typeform, sendgrid, webform, stripe };
|
||||
|
||||
95
src/components/TableModals/WebhooksModal/Schemas/stripe.tsx
Normal file
95
src/components/TableModals/WebhooksModal/Schemas/stripe.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import { Typography, Link, TextField } from "@mui/material";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
import { TableSettings } from "@src/types/table";
|
||||
import { IWebhook } from "@src/components/TableModals/WebhooksModal/utils";
|
||||
|
||||
export const webhookStripe = {
|
||||
name: "Stripe",
|
||||
parser: {
|
||||
additionalVariables: null,
|
||||
extraLibs: null,
|
||||
template: (
|
||||
table: TableSettings
|
||||
) => `const sendgridParser: Parser = async ({ req, db, ref }) => {
|
||||
const event = req.body
|
||||
switch (event.type) {
|
||||
case "payment_intent.succeeded":
|
||||
break;
|
||||
case "payment_intent.payment_failed":
|
||||
break;
|
||||
default:
|
||||
// all other types
|
||||
}
|
||||
};`,
|
||||
},
|
||||
condition: {
|
||||
additionalVariables: null,
|
||||
extraLibs: null,
|
||||
template: (
|
||||
table: TableSettings
|
||||
) => `const condition: Condition = async({ref,req,db}) => {
|
||||
// feel free to add your own code logic here
|
||||
return true;
|
||||
}`,
|
||||
},
|
||||
auth: (webhookObject: IWebhook, setWebhookObject: (w: IWebhook) => void) => {
|
||||
return (
|
||||
<>
|
||||
<Typography gutterBottom>
|
||||
Get your{" "}
|
||||
<Link
|
||||
href="https://dashboard.stripe.com/apikeys"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="inherit"
|
||||
>
|
||||
secret key
|
||||
<InlineOpenInNewIcon />
|
||||
</Link>{" "}
|
||||
and{" "}
|
||||
<Link
|
||||
href="https://dashboard.stripe.com/webhooks"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
variant="inherit"
|
||||
>
|
||||
signing key
|
||||
<InlineOpenInNewIcon />
|
||||
</Link>{" "}
|
||||
from Stripe dashboard.
|
||||
<br />
|
||||
Then add the secret below.
|
||||
</Typography>
|
||||
|
||||
<TextField
|
||||
id="stripe-secret-key"
|
||||
label="Secret key"
|
||||
value={webhookObject.auth.secretKey}
|
||||
fullWidth
|
||||
multiline
|
||||
onChange={(e) => {
|
||||
setWebhookObject({
|
||||
...webhookObject,
|
||||
auth: { ...webhookObject.auth, secretKey: e.target.value },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
id="stripe-signing-secret"
|
||||
label="Signing key"
|
||||
value={webhookObject.auth.signingSecret}
|
||||
fullWidth
|
||||
multiline
|
||||
onChange={(e) => {
|
||||
setWebhookObject({
|
||||
...webhookObject,
|
||||
auth: { ...webhookObject.auth, signingSecret: e.target.value },
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export default webhookStripe;
|
||||
@@ -11,6 +11,10 @@ import { WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
import { parserExtraLibs } from "./utils";
|
||||
|
||||
const additionalVariables = [
|
||||
{
|
||||
key: "ref",
|
||||
description: `reference object that represents the reference to the current collection in firestore db.`,
|
||||
},
|
||||
{
|
||||
key: "req",
|
||||
description: "webhook request",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { TableSettings } from "@src/types/table";
|
||||
import { generateId } from "@src/utils/table";
|
||||
import { typeform, basic, sendgrid, webform } from "./Schemas";
|
||||
import { typeform, basic, sendgrid, webform, stripe } from "./Schemas";
|
||||
|
||||
export const webhookTypes = [
|
||||
"basic",
|
||||
"typeform",
|
||||
@@ -8,7 +9,7 @@ export const webhookTypes = [
|
||||
"webform",
|
||||
//"shopify",
|
||||
//"twitter",
|
||||
//"stripe",
|
||||
"stripe",
|
||||
] as const;
|
||||
|
||||
const requestType = [
|
||||
@@ -53,7 +54,7 @@ export const webhookNames: Record<WebhookType, string> = {
|
||||
//github:"GitHub",
|
||||
// shopify: "Shopify",
|
||||
// twitter: "Twitter",
|
||||
// stripe: "Stripe",
|
||||
stripe: "Stripe",
|
||||
basic: "Basic",
|
||||
webform: "Web form",
|
||||
};
|
||||
@@ -82,6 +83,7 @@ export const webhookSchemas = {
|
||||
typeform,
|
||||
sendgrid,
|
||||
webform,
|
||||
stripe,
|
||||
};
|
||||
|
||||
export function emptyWebhookObject(
|
||||
|
||||
@@ -300,7 +300,7 @@ export const tableSettings = (
|
||||
type: FieldType.checkbox,
|
||||
name: "audit",
|
||||
label: "Enable auditing for this table",
|
||||
defaultValue: true,
|
||||
defaultValue: false,
|
||||
},
|
||||
{
|
||||
step: "auditing",
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
|
||||
import { Button, Typography, TextField } from "@mui/material";
|
||||
import {
|
||||
Button,
|
||||
Typography,
|
||||
TextField,
|
||||
IconButton,
|
||||
Stack,
|
||||
InputLabel,
|
||||
} from "@mui/material";
|
||||
|
||||
import {
|
||||
tableModalAtom,
|
||||
@@ -11,6 +18,9 @@ import {
|
||||
import { analytics, logEvent } from "@src/analytics";
|
||||
import { find } from "lodash-es";
|
||||
|
||||
import { WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
import DocsIcon from "@mui/icons-material/ArrowUpward";
|
||||
|
||||
export default function ImportFromAirtable() {
|
||||
const [{ baseId, tableId, apiKey }, setImportAirtable] = useAtom(
|
||||
importAirtableAtom,
|
||||
@@ -108,10 +118,28 @@ export default function ImportFromAirtable() {
|
||||
manage with Rowy.
|
||||
</Typography>
|
||||
<TextField
|
||||
sx={{ marginBottom: 1 }}
|
||||
variant="filled"
|
||||
autoFocus
|
||||
fullWidth
|
||||
label="Airtable API Key"
|
||||
label={
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="end"
|
||||
>
|
||||
<InputLabel>Airtable API Key</InputLabel>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="primary"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={WIKI_LINKS.importAirtableApiKey}
|
||||
>
|
||||
API Key <DocsIcon sx={{ rotate: "45deg" }} />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
}
|
||||
placeholder="Insert your API key here"
|
||||
value={apiKey}
|
||||
onChange={(e) =>
|
||||
@@ -126,7 +154,24 @@ export default function ImportFromAirtable() {
|
||||
<TextField
|
||||
variant="filled"
|
||||
fullWidth
|
||||
label="Airtable Table URL"
|
||||
label={
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="end"
|
||||
>
|
||||
<InputLabel>Airtable Table URL</InputLabel>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="primary"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={WIKI_LINKS.importAirtableTableUrl}
|
||||
>
|
||||
Table URL <DocsIcon sx={{ rotate: "45deg" }} />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
}
|
||||
placeholder="Insert your Table URL here"
|
||||
value={tableUrl}
|
||||
onChange={(e) => {
|
||||
|
||||
@@ -28,6 +28,16 @@ const replacer = (data: any) => (m: string, key: string) => {
|
||||
};
|
||||
|
||||
const getStateIcon = (actionState: "undo" | "redo" | string, config: any) => {
|
||||
if (!get(config, "customIcons.enabled", false)) {
|
||||
switch (actionState) {
|
||||
case "undo":
|
||||
return <UndoIcon />;
|
||||
case "redo":
|
||||
return <RedoIcon />;
|
||||
default:
|
||||
return <RunIcon />;
|
||||
}
|
||||
}
|
||||
switch (actionState) {
|
||||
case "undo":
|
||||
return get(config, "customIcons.undo") || <UndoIcon />;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { lazy, Suspense, useState } from "react";
|
||||
import { lazy, Suspense, useState, useEffect } from "react";
|
||||
import { get } from "lodash-es";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
import { Link } from "react-router-dom";
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
projectRolesAtom,
|
||||
projectSettingsAtom,
|
||||
compatibleRowyRunVersionAtom,
|
||||
rowyRunModalAtom,
|
||||
} from "@src/atoms/projectScope";
|
||||
import { tableScope, tableColumnsOrderedAtom } from "@src/atoms/tableScope";
|
||||
import { WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
@@ -68,6 +69,11 @@ const Settings = ({ config, onChange, fieldName }: ISettingsProps) => {
|
||||
);
|
||||
const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope);
|
||||
|
||||
const openRowyRunModal = useSetAtom(rowyRunModalAtom, projectScope);
|
||||
useEffect(() => {
|
||||
if (!settings.rowyRunUrl) openRowyRunModal({ feature: "Action fields" });
|
||||
}, [settings.rowyRunUrl]);
|
||||
|
||||
// const [activeStep, setActiveStep] = useState<
|
||||
// "requirements" | "friction" | "action" | "undo" | "customization"
|
||||
// >("requirements");
|
||||
@@ -88,7 +94,7 @@ const Settings = ({ config, onChange, fieldName }: ISettingsProps) => {
|
||||
Array.isArray(config.params) ? config.params : [],
|
||||
{ space: 2 }
|
||||
);
|
||||
const [codeValid, setCodeValid] = useState(true);
|
||||
const [codeErrorMessage, setCodeErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const scriptExtraLibs = [
|
||||
[
|
||||
@@ -96,14 +102,16 @@ const Settings = ({ config, onChange, fieldName }: ISettingsProps) => {
|
||||
" /**",
|
||||
" * actionParams are provided by dialog popup form",
|
||||
" */",
|
||||
(config.params ?? []).filter(Boolean).map((param: any) => {
|
||||
const validationKeys = Object.keys(param.validation ?? {});
|
||||
if (validationKeys.includes("string")) {
|
||||
return `static ${param.name}: string`;
|
||||
} else if (validationKeys.includes("array")) {
|
||||
return `static ${param.name}: any[]`;
|
||||
} else return `static ${param.name}: any`;
|
||||
}),
|
||||
(Array.isArray(config.params) ? config.params : [])
|
||||
.filter(Boolean)
|
||||
.map((param: any) => {
|
||||
const validationKeys = Object.keys(param.validation ?? {});
|
||||
if (validationKeys.includes("string")) {
|
||||
return `static ${param.name}: string`;
|
||||
} else if (validationKeys.includes("array")) {
|
||||
return `static ${param.name}: any[]`;
|
||||
} else return `static ${param.name}: any`;
|
||||
}),
|
||||
"}",
|
||||
].join("\n"),
|
||||
actionDefs,
|
||||
@@ -256,25 +264,25 @@ const Settings = ({ config, onChange, fieldName }: ISettingsProps) => {
|
||||
value={formattedParamsJson}
|
||||
onChange={(v) => {
|
||||
try {
|
||||
if (v) {
|
||||
const parsed = JSON.parse(v);
|
||||
const parsed = JSON.parse(v ?? "");
|
||||
if (Array.isArray(parsed)) {
|
||||
onChange("params")(parsed);
|
||||
setCodeErrorMessage(null);
|
||||
} else {
|
||||
setCodeErrorMessage("Form fields must be array");
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`Failed to parse JSON: ${e}`);
|
||||
setCodeValid(false);
|
||||
setCodeErrorMessage("Invalid JSON");
|
||||
}
|
||||
}}
|
||||
onValidStatusUpdate={({ isValid }) =>
|
||||
setCodeValid(isValid)
|
||||
}
|
||||
error={!codeValid}
|
||||
error={!!codeErrorMessage}
|
||||
/>
|
||||
</Suspense>
|
||||
|
||||
{!codeValid && (
|
||||
{codeErrorMessage && (
|
||||
<FormHelperText error variant="filled">
|
||||
Invalid JSON
|
||||
{codeErrorMessage}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
@@ -406,7 +414,16 @@ const Settings = ({ config, onChange, fieldName }: ISettingsProps) => {
|
||||
</Suspense>
|
||||
<CodeEditorHelper
|
||||
docLink={WIKI_LINKS.fieldTypesAction + "#script"}
|
||||
additionalVariables={[]}
|
||||
additionalVariables={[
|
||||
{
|
||||
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: "ref",
|
||||
description: `reference object that represents the reference to the current row in firestore db (ie: doc.ref).`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -522,7 +539,16 @@ const Settings = ({ config, onChange, fieldName }: ISettingsProps) => {
|
||||
</Suspense>
|
||||
<CodeEditorHelper
|
||||
docLink={WIKI_LINKS.fieldTypesAction + "#script"}
|
||||
additionalVariables={[]}
|
||||
additionalVariables={[
|
||||
{
|
||||
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: "ref",
|
||||
description: `reference object that represents the reference to the current row in firestore db (ie: doc.ref).`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { lazy, Suspense, useState } from "react";
|
||||
import { get } from "lodash-es";
|
||||
import { lazy, Suspense, useEffect } from "react";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
|
||||
import {
|
||||
Grid,
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
Link,
|
||||
} from "@mui/material";
|
||||
|
||||
import SteppedAccordion from "@src/components/SteppedAccordion";
|
||||
import FieldSkeleton from "@src/components/SideDrawer/FieldSkeleton";
|
||||
import CodeEditorHelper from "@src/components/CodeEditor/CodeEditorHelper";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
@@ -21,6 +20,11 @@ import connectorDefs from "!!raw-loader!./connector.d.ts";
|
||||
import { WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
import { baseFunction } from "./utils";
|
||||
import { ISettingsProps } from "@src/components/fields/types";
|
||||
import {
|
||||
projectScope,
|
||||
projectSettingsAtom,
|
||||
rowyRunModalAtom,
|
||||
} from "@src/atoms/projectScope";
|
||||
|
||||
//import typeDefs from "!!raw-loader!./types.d.ts";
|
||||
const CodeEditor = lazy(
|
||||
@@ -46,6 +50,13 @@ const diagnosticsOptions = {
|
||||
};
|
||||
|
||||
export default function Settings({ config, onChange }: ISettingsProps) {
|
||||
const [projectSettings] = useAtom(projectSettingsAtom, projectScope);
|
||||
const openRowyRunModal = useSetAtom(rowyRunModalAtom, projectScope);
|
||||
useEffect(() => {
|
||||
if (!projectSettings.rowyRunUrl)
|
||||
openRowyRunModal({ feature: "Connector fields" });
|
||||
}, [projectSettings.rowyRunUrl]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
@@ -61,7 +72,16 @@ export default function Settings({ config, onChange }: ISettingsProps) {
|
||||
</Suspense>
|
||||
<CodeEditorHelper
|
||||
docLink={WIKI_LINKS.fieldTypesConnector + "#examples"}
|
||||
additionalVariables={[]}
|
||||
additionalVariables={[
|
||||
{
|
||||
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: "ref",
|
||||
description: `reference object that represents the reference to the current row in firestore db (ie: doc.ref).`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<FormControl>
|
||||
|
||||
@@ -135,7 +135,19 @@ export default function Settings({
|
||||
|
||||
<div>
|
||||
<InputLabel>Derivative script</InputLabel>
|
||||
<CodeEditorHelper docLink={WIKI_LINKS.fieldTypesDerivative} />
|
||||
<CodeEditorHelper
|
||||
docLink={WIKI_LINKS.fieldTypesDerivative}
|
||||
additionalVariables={[
|
||||
{
|
||||
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: "ref",
|
||||
description: `reference object that represents the reference to the current row in firestore db (ie: doc.ref).`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Suspense fallback={<FieldSkeleton height={200} />}>
|
||||
<CodeEditor
|
||||
diagnosticsOptions={
|
||||
|
||||
75
src/components/fields/Json/ContextMenuActions.tsx
Normal file
75
src/components/fields/Json/ContextMenuActions.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import { find, get } from "lodash-es";
|
||||
import { useSnackbar } from "notistack";
|
||||
import { Copy } from "@src/assets/icons";
|
||||
import Paste from "@mui/icons-material/ContentPaste";
|
||||
|
||||
import {
|
||||
tableScope,
|
||||
tableSchemaAtom,
|
||||
tableRowsAtom,
|
||||
updateFieldAtom,
|
||||
} from "@src/atoms/tableScope";
|
||||
import { IFieldConfig } from "@src/components/fields/types";
|
||||
|
||||
export interface IContextMenuActions {
|
||||
label: string;
|
||||
icon: React.ReactNode;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const ContextMenuActions: IFieldConfig["contextMenuActions"] = (
|
||||
selectedCell,
|
||||
reset
|
||||
) => {
|
||||
const [tableSchema] = useAtom(tableSchemaAtom, tableScope);
|
||||
const [tableRows] = useAtom(tableRowsAtom, tableScope);
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
const updateField = useSetAtom(updateFieldAtom, tableScope);
|
||||
const selectedCol = tableSchema.columns?.[selectedCell.columnKey];
|
||||
if (!selectedCol) return [];
|
||||
|
||||
const selectedRow = find(tableRows, ["_rowy_ref.path", selectedCell.path]);
|
||||
const cellValue = get(selectedRow, selectedCol.fieldName) || [];
|
||||
|
||||
const isEmpty =
|
||||
cellValue === "" ||
|
||||
cellValue === null ||
|
||||
cellValue === undefined ||
|
||||
cellValue.length === 0;
|
||||
|
||||
return [
|
||||
{
|
||||
label: "Copy",
|
||||
icon: <Copy />,
|
||||
onClick: () => {
|
||||
try {
|
||||
navigator.clipboard.writeText(JSON.stringify(cellValue));
|
||||
enqueueSnackbar("Copied");
|
||||
} catch (error) {
|
||||
enqueueSnackbar(`Failed to copy: ${error}`, { variant: "error" });
|
||||
}
|
||||
},
|
||||
disabled: isEmpty,
|
||||
},
|
||||
{
|
||||
label: "Paste",
|
||||
icon: <Paste />,
|
||||
onClick: async () => {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
const parsed = JSON.parse(text);
|
||||
updateField({
|
||||
path: selectedCell.path,
|
||||
fieldName: selectedCol.fieldName,
|
||||
value: parsed,
|
||||
});
|
||||
} catch (error) {
|
||||
enqueueSnackbar(`Failed to paste: ${error}`, { variant: "error" });
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export default ContextMenuActions;
|
||||
@@ -5,6 +5,7 @@ import withBasicCell from "@src/components/fields/_withTableCell/withBasicCell";
|
||||
import { Json as JsonIcon } from "@src/assets/icons";
|
||||
import BasicCell from "./BasicCell";
|
||||
import withSideDrawerEditor from "@src/components/Table/editors/withSideDrawerEditor";
|
||||
import ContextMenuActions from "./ContextMenuActions";
|
||||
|
||||
const SideDrawerField = lazy(
|
||||
() =>
|
||||
@@ -35,5 +36,6 @@ export const config: IFieldConfig = {
|
||||
},
|
||||
SideDrawerField,
|
||||
settings: Settings,
|
||||
contextMenuActions: ContextMenuActions,
|
||||
};
|
||||
export default config;
|
||||
|
||||
12
src/components/fields/LongText/Filter.tsx
Normal file
12
src/components/fields/LongText/Filter.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { IFilterOperator } from "@src/components/fields/types";
|
||||
|
||||
export const filterOperators: IFilterOperator[] = [
|
||||
{
|
||||
label: "equals",
|
||||
value: "==",
|
||||
},
|
||||
{
|
||||
label: "not equals",
|
||||
value: "!=",
|
||||
},
|
||||
];
|
||||
31
src/components/fields/LongText/Settings.tsx
Normal file
31
src/components/fields/LongText/Settings.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { ISettingsProps } from "@src/components/fields/types";
|
||||
import { TextField } from "@mui/material";
|
||||
|
||||
export default function Settings({ onChange, config }: ISettingsProps) {
|
||||
return (
|
||||
<>
|
||||
<TextField
|
||||
type="number"
|
||||
label="Character limit"
|
||||
id="character-limit"
|
||||
value={config.maxLength}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
if (e.target.value === "0") onChange("maxLength")(null);
|
||||
else onChange("maxLength")(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<TextField
|
||||
type="text"
|
||||
label="Validation regex"
|
||||
id="validation-regex"
|
||||
value={config.validationRegex}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
if (e.target.value === "") onChange("validationRegex")(null);
|
||||
else onChange("validationRegex")(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -5,7 +5,8 @@ import withBasicCell from "@src/components/fields/_withTableCell/withBasicCell";
|
||||
import LongTextIcon from "@mui/icons-material/Notes";
|
||||
import BasicCell from "./BasicCell";
|
||||
import TextEditor from "@src/components/Table/editors/TextEditor";
|
||||
import { filterOperators } from "@src/components/fields/ShortText/Filter";
|
||||
|
||||
import { filterOperators } from "./Filter";
|
||||
import BasicContextMenuActions from "@src/components/fields/_BasicCell/BasicCellContextMenuActions";
|
||||
|
||||
const SideDrawerField = lazy(
|
||||
@@ -15,6 +16,10 @@ const SideDrawerField = lazy(
|
||||
)
|
||||
);
|
||||
|
||||
const Settings = lazy(
|
||||
() => import("./Settings" /* webpackChunkName: "Settings-LongText" */)
|
||||
);
|
||||
|
||||
export const config: IFieldConfig = {
|
||||
type: FieldType.longText,
|
||||
name: "Long Text",
|
||||
@@ -28,6 +33,7 @@ export const config: IFieldConfig = {
|
||||
TableCell: withBasicCell(BasicCell),
|
||||
TableEditor: TextEditor,
|
||||
SideDrawerField,
|
||||
settings: Settings,
|
||||
filter: {
|
||||
operators: filterOperators,
|
||||
},
|
||||
|
||||
@@ -1,48 +1,95 @@
|
||||
import { ISettingsProps } from "@src/components/fields/types";
|
||||
|
||||
import { Slider, InputLabel, TextField, Grid } from "@mui/material";
|
||||
import RatingIcon from "@mui/icons-material/Star";
|
||||
import RatingOutlineIcon from "@mui/icons-material/StarBorder"
|
||||
import { InputLabel, TextField, Grid, FormControlLabel, Checkbox, Stack } from "@mui/material";
|
||||
import ToggleButton from "@mui/material/ToggleButton";
|
||||
import ToggleButtonGroup from "@mui/material/ToggleButtonGroup";
|
||||
import MuiRating from "@mui/material/Rating";
|
||||
import { get } from "lodash-es";
|
||||
|
||||
export default function Settings({ onChange, config }: ISettingsProps) {
|
||||
return (
|
||||
<>
|
||||
<Grid container spacing={2} justifyItems="end" direction={"row"}>
|
||||
<Grid item xs={6}>
|
||||
<TextField
|
||||
label="Maximum number of stars"
|
||||
type={"number"}
|
||||
value={config.max}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
onChange("max")(parseInt(e.target.value));
|
||||
}}
|
||||
inputProps={{ min: 1, max: 20 }}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<InputLabel>Star fraction</InputLabel>
|
||||
<ToggleButtonGroup
|
||||
value={config.precision}
|
||||
exclusive
|
||||
fullWidth
|
||||
onChange={(_, value) => {
|
||||
onChange("precision")(value);
|
||||
}}
|
||||
aria-label="text alignment"
|
||||
>
|
||||
<ToggleButton value={0.25} aria-label="quarter">
|
||||
1/4
|
||||
</ToggleButton>
|
||||
<ToggleButton value={0.5} aria-label="half">
|
||||
1/2
|
||||
</ToggleButton>
|
||||
<ToggleButton value={1} aria-label="whole">
|
||||
1
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Grid>
|
||||
<Grid container spacing={2} justifyItems="end" direction={"row"}>
|
||||
<Grid item xs={6}>
|
||||
<TextField
|
||||
label="Highest possible rating"
|
||||
type={"number"}
|
||||
value={config.max}
|
||||
fullWidth
|
||||
error={false}
|
||||
onChange={(e) => {
|
||||
let input = parseInt(e.target.value) || 0
|
||||
if (input > 20) { input = 20 }
|
||||
onChange("max")(input);
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</>
|
||||
<Grid item xs={6}>
|
||||
<InputLabel>Rating fraction</InputLabel>
|
||||
<ToggleButtonGroup
|
||||
value={config.precision}
|
||||
exclusive
|
||||
fullWidth
|
||||
onChange={(_, value) => {
|
||||
onChange("precision")(value);
|
||||
}}
|
||||
aria-label="text alignment"
|
||||
sx={{ pt: 0.5 }}
|
||||
>
|
||||
<ToggleButton value={0.25} aria-label="quarter">
|
||||
1/4
|
||||
</ToggleButton>
|
||||
<ToggleButton value={0.5} aria-label="half">
|
||||
1/2
|
||||
</ToggleButton>
|
||||
<ToggleButton value={1} aria-label="whole">
|
||||
1
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={config.customIcons?.enabled}
|
||||
onChange={(e) =>
|
||||
onChange("customIcons.enabled")(e.target.checked)
|
||||
}
|
||||
name="customIcons.enabled"
|
||||
/>
|
||||
}
|
||||
label="Customize ratings with emoji"
|
||||
style={{ marginLeft: -11 }}
|
||||
/>
|
||||
</Grid>
|
||||
{config.customIcons?.enabled && (
|
||||
<Grid item xs={6} sm={true}>
|
||||
<Stack direction="row" spacing={1}>
|
||||
<TextField
|
||||
id="customIcons.rating"
|
||||
value={get(config, "customIcons.rating")}
|
||||
onChange={(e) =>
|
||||
onChange("customIcons.rating")(e.target.value)
|
||||
}
|
||||
label="Custom icon preview:"
|
||||
className="labelHorizontal"
|
||||
inputProps={{ style: { width: "2ch" } }}
|
||||
/>
|
||||
|
||||
<MuiRating aria-label="Preview of the rating field with custom icon"
|
||||
name="Preview"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
icon={get(config, "customIcons.rating") || <RatingIcon />}
|
||||
size="small"
|
||||
emptyIcon={get(config, "customIcons.rating") || <RatingOutlineIcon />}
|
||||
max={get(config, "max")}
|
||||
precision={get(config, "precision")}
|
||||
sx={{ pt: 0.5 }}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,9 +3,8 @@ import { ISideDrawerFieldProps } from "@src/components/fields/types";
|
||||
import { Grid } from "@mui/material";
|
||||
import { Rating as MuiRating } from "@mui/material";
|
||||
import "@mui/lab";
|
||||
import StarBorderIcon from "@mui/icons-material/StarBorder";
|
||||
|
||||
import { fieldSx, getFieldId } from "@src/components/SideDrawer/utils";
|
||||
import { getStateIcon, getStateOutline } from "./TableCell";
|
||||
import { fieldSx } from "@src/components/SideDrawer/utils";
|
||||
|
||||
export default function Rating({
|
||||
column,
|
||||
@@ -29,7 +28,9 @@ export default function Rating({
|
||||
onChange(newValue);
|
||||
onSubmit();
|
||||
}}
|
||||
emptyIcon={<StarBorderIcon fontSize="inherit" />}
|
||||
icon={getStateIcon(column.config)}
|
||||
emptyIcon={getStateOutline(column.config)}
|
||||
size="small"
|
||||
max={max}
|
||||
precision={precision}
|
||||
sx={{ ml: -0.5 }}
|
||||
|
||||
@@ -1,7 +1,21 @@
|
||||
import { IHeavyCellProps } from "@src/components/fields/types";
|
||||
|
||||
import MuiRating from "@mui/material/Rating";
|
||||
import StarBorderIcon from "@mui/icons-material/StarBorder";
|
||||
import RatingIcon from "@mui/icons-material/Star";
|
||||
import RatingOutlineIcon from "@mui/icons-material/StarBorder"
|
||||
import { get } from "lodash-es";
|
||||
|
||||
|
||||
export const getStateIcon = (config: any) => {
|
||||
// only use the config to get the custom rating icon if enabled via toggle
|
||||
if (!get(config, "customIcons.enabled")) { return <RatingIcon /> }
|
||||
return get(config, "customIcons.rating") || <RatingIcon />;
|
||||
};
|
||||
|
||||
export const getStateOutline = (config: any) => {
|
||||
if (!get(config, "customIcons.enabled")) { return <RatingOutlineIcon /> }
|
||||
return get(config, "customIcons.rating") || <RatingOutlineIcon />;
|
||||
}
|
||||
|
||||
export default function Rating({
|
||||
row,
|
||||
@@ -28,9 +42,11 @@ export default function Rating({
|
||||
name={`${row.id}-${column.key}`}
|
||||
value={typeof value === "number" ? value : 0}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
icon={getStateIcon(column.config)}
|
||||
size="small"
|
||||
disabled={disabled}
|
||||
onChange={(_, newValue) => onSubmit(newValue)}
|
||||
emptyIcon={<StarBorderIcon />}
|
||||
emptyIcon={getStateOutline(column.config)}
|
||||
max={max}
|
||||
precision={precision}
|
||||
sx={{ mx: -0.25 }}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useRef, useLayoutEffect } from "react";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import { EditorProps } from "react-data-grid";
|
||||
import { get } from "lodash-es";
|
||||
import { useSnackbar } from "notistack";
|
||||
|
||||
import { TextField } from "@mui/material";
|
||||
|
||||
@@ -14,6 +15,7 @@ import { doc, deleteField } from "firebase/firestore";
|
||||
export default function TextEditor({ row, column }: EditorProps<any>) {
|
||||
const [firebaseDb] = useAtom(firebaseDbAtom, projectScope);
|
||||
const updateField = useSetAtom(updateFieldAtom, tableScope);
|
||||
const { enqueueSnackbar } = useSnackbar();
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
@@ -23,11 +25,17 @@ export default function TextEditor({ row, column }: EditorProps<any>) {
|
||||
return () => {
|
||||
const newValue = inputElement?.value;
|
||||
if (newValue !== undefined && newValue !== "") {
|
||||
updateField({
|
||||
path: row._rowy_ref.path,
|
||||
fieldName: column.key,
|
||||
value: doc(firebaseDb, newValue),
|
||||
});
|
||||
try {
|
||||
const refValue = doc(firebaseDb, newValue);
|
||||
|
||||
updateField({
|
||||
path: row._rowy_ref.path,
|
||||
fieldName: column.key,
|
||||
value: refValue,
|
||||
});
|
||||
} catch (e: any) {
|
||||
enqueueSnackbar(`Invalid path: ${e.message}`, { variant: "error" });
|
||||
}
|
||||
} else {
|
||||
updateField({
|
||||
path: row._rowy_ref.path,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useAtom } from "jotai";
|
||||
import { doc } from "firebase/firestore";
|
||||
import { ISideDrawerFieldProps } from "@src/components/fields/types";
|
||||
@@ -19,20 +20,37 @@ export default function Reference({
|
||||
const [projectId] = useAtom(projectIdAtom, projectScope);
|
||||
const [firebaseDb] = useAtom(firebaseDbAtom, projectScope);
|
||||
|
||||
const transformedValue =
|
||||
const [localValue, setLocalValue] = useState(
|
||||
Boolean(value) && "path" in value && typeof value.path === "string"
|
||||
? value.path
|
||||
: "";
|
||||
: ""
|
||||
);
|
||||
const [error, setError] = useState("");
|
||||
|
||||
return (
|
||||
<Stack direction="row">
|
||||
<Stack direction="row" alignItems="flex-start">
|
||||
<TextField
|
||||
variant="filled"
|
||||
fullWidth
|
||||
margin="none"
|
||||
onChange={(e) => onChange(doc(firebaseDb, e.target.value))}
|
||||
onBlur={onSubmit}
|
||||
value={transformedValue}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
doc(firebaseDb, e.target.value);
|
||||
setError("");
|
||||
} catch (e: any) {
|
||||
setError(e.message);
|
||||
}
|
||||
setLocalValue(e.target.value);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!error) {
|
||||
onChange(doc(firebaseDb, localValue));
|
||||
onSubmit();
|
||||
}
|
||||
}}
|
||||
value={localValue}
|
||||
error={Boolean(error)}
|
||||
helperText={error}
|
||||
id={getFieldId(column.key)}
|
||||
label=""
|
||||
hiddenLabel
|
||||
@@ -41,14 +59,14 @@ export default function Reference({
|
||||
|
||||
<IconButton
|
||||
size="small"
|
||||
href={`https://console.firebase.google.com/project/${projectId}/firestore/data/~2F${transformedValue.replace(
|
||||
href={`https://console.firebase.google.com/project/${projectId}/firestore/data/~2F${localValue.replace(
|
||||
/\//g,
|
||||
"~2F"
|
||||
)}`}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
aria-label="Open in Firebase Console"
|
||||
disabled={!transformedValue}
|
||||
disabled={Boolean(error) || !localValue}
|
||||
edge="end"
|
||||
sx={{ ml: 1 }}
|
||||
>
|
||||
|
||||
@@ -15,6 +15,7 @@ const SideDrawerField = lazy(
|
||||
"./SideDrawerField" /* webpackChunkName: "SideDrawerField-ShortText" */
|
||||
)
|
||||
);
|
||||
|
||||
const Settings = lazy(
|
||||
() => import("./Settings" /* webpackChunkName: "Settings-ShortText" */)
|
||||
);
|
||||
|
||||
@@ -23,6 +23,9 @@ export const EXTERNAL_LINKS = {
|
||||
rowyAppHostName: "rowy.app",
|
||||
|
||||
dateFormat: "https://date-fns.org/v2.24.0/docs/format",
|
||||
|
||||
welcomeVideo:
|
||||
"https://www.youtube.com/watch?v=rJWASZW2ivg&list=PLow2dGbF6XclrTSvW3ug1pRxbGwsIgcWJ&index=1",
|
||||
} as const;
|
||||
|
||||
const WIKI_PATHS = {
|
||||
@@ -38,6 +41,9 @@ const WIKI_PATHS = {
|
||||
howToDefaultValues: "/how-to/default-values",
|
||||
howToCustomViews: "/how-to/custom-views",
|
||||
|
||||
faqs: "/category/faqs",
|
||||
faqsAccess: "/faqs/access",
|
||||
|
||||
fieldTypesSupportedFields: "/field-types/supported-fields",
|
||||
fieldTypesDerivative: "/field-types/derivative",
|
||||
fieldTypesConnectTable: "/field-types/connect-table",
|
||||
@@ -55,6 +61,10 @@ const WIKI_PATHS = {
|
||||
extensionsSendgridEmail: "/extensions/sendgrid-email",
|
||||
extensionsTwilioMessage: "/extensions/twilio-message",
|
||||
webhooks: "/webhooks",
|
||||
|
||||
importAirtable: "/import-export-data/import-airtable",
|
||||
importAirtableApiKey: "/import-export-data/import-airtable#api-key",
|
||||
importAirtableTableUrl: "/import-export-data/import-airtable#table-url",
|
||||
};
|
||||
export const WIKI_LINKS = mapValues(
|
||||
WIKI_PATHS,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import { matchSorter } from "match-sorter";
|
||||
import { matchSorter, rankings } from "match-sorter";
|
||||
|
||||
export function useBasicSearch<T>(
|
||||
list: T[],
|
||||
@@ -10,7 +10,9 @@ export function useBasicSearch<T>(
|
||||
const [query, setQuery] = useState("");
|
||||
const handleQuery = useDebouncedCallback(setQuery, debounce);
|
||||
|
||||
const results = query ? matchSorter(list, query, { keys }) : list;
|
||||
const results = query
|
||||
? matchSorter(list, query, { keys, threshold: rankings.ACRONYM })
|
||||
: list;
|
||||
|
||||
return [results, query, handleQuery] as const;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useEffect } from "react";
|
||||
import { useAtom, Atom } from "jotai";
|
||||
import { Scope } from "jotai/core/atom";
|
||||
|
||||
function beforeUnloadHandler(event: BeforeUnloadEvent) {
|
||||
event.preventDefault();
|
||||
@@ -14,7 +13,10 @@ function beforeUnloadHandler(event: BeforeUnloadEvent) {
|
||||
* @param atom - The atom’s value to listen to
|
||||
* @param scope - The atom scope
|
||||
*/
|
||||
export default function useBeforeUnload(atom: Atom<any | null>, scope: Scope) {
|
||||
export default function useBeforeUnload(
|
||||
atom: Atom<any | null>,
|
||||
scope: NonNullable<Parameters<typeof useAtom>[1]>
|
||||
) {
|
||||
const [atomValue] = useAtom(atom, scope);
|
||||
|
||||
const atomValueFalsy = !atomValue;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import useMemoValue from "use-memo-value";
|
||||
import { useAtom, PrimitiveAtom, useSetAtom, SetStateAction } from "jotai";
|
||||
import { Scope } from "jotai/core/atom";
|
||||
import { set } from "lodash-es";
|
||||
import {
|
||||
Firestore,
|
||||
@@ -77,7 +76,7 @@ interface IUseFirestoreCollectionWithAtomOptions<T> {
|
||||
*/
|
||||
export function useFirestoreCollectionWithAtom<T = TableRow>(
|
||||
dataAtom: PrimitiveAtom<T[]>,
|
||||
dataScope: Scope | undefined,
|
||||
dataScope: Parameters<typeof useAtom>[1] | undefined,
|
||||
path: string | undefined,
|
||||
options?: IUseFirestoreCollectionWithAtomOptions<T>
|
||||
) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEffect } from "react";
|
||||
import useMemoValue from "use-memo-value";
|
||||
import { useAtom, PrimitiveAtom, useSetAtom } from "jotai";
|
||||
import { Scope } from "jotai/core/atom";
|
||||
import { set } from "lodash-es";
|
||||
import {
|
||||
Firestore,
|
||||
@@ -45,7 +44,7 @@ interface IUseFirestoreDocWithAtomOptions<T> {
|
||||
*/
|
||||
export function useFirestoreDocWithAtom<T = TableRow>(
|
||||
dataAtom: PrimitiveAtom<T>,
|
||||
dataScope: Scope | undefined,
|
||||
dataScope: Parameters<typeof useAtom>[1] | undefined,
|
||||
path: string | undefined,
|
||||
options?: IUseFirestoreDocWithAtomOptions<T>
|
||||
) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useEffect } from "react";
|
||||
import { useSetAtom } from "jotai";
|
||||
import { PrimitiveAtom, Scope } from "jotai/core/atom";
|
||||
|
||||
/**
|
||||
* A hook that listens to when the target key is pressed
|
||||
@@ -11,8 +10,8 @@ import { PrimitiveAtom, Scope } from "jotai/core/atom";
|
||||
*/
|
||||
export default function useKeyPressWithAtom(
|
||||
targetKey: string,
|
||||
atom: PrimitiveAtom<boolean>,
|
||||
scope: Scope
|
||||
atom: Parameters<typeof useSetAtom>[0],
|
||||
scope: Parameters<typeof useSetAtom>[1]
|
||||
) {
|
||||
const setAtom = useSetAtom(atom, scope);
|
||||
|
||||
|
||||
44
src/layouts/AdminRoute.tsx
Normal file
44
src/layouts/AdminRoute.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { PropsWithChildren } from "react";
|
||||
import { useAtom } from "jotai";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import { Typography, Button } from "@mui/material";
|
||||
import LockIcon from "@mui/icons-material/LockOutlined";
|
||||
import HomeIcon from "@mui/icons-material/HomeOutlined";
|
||||
|
||||
import EmptyState from "@src/components/EmptyState";
|
||||
|
||||
import { projectScope, userRolesAtom } from "@src/atoms/projectScope";
|
||||
import { ROUTES } from "@src/constants/routes";
|
||||
import { TOP_BAR_HEIGHT } from "@src/layouts/Navigation/TopBar";
|
||||
|
||||
/**
|
||||
* Lock pages for admins only
|
||||
*/
|
||||
export default function AdminRoute({ children }: PropsWithChildren<{}>) {
|
||||
const [userRoles] = useAtom(userRolesAtom, projectScope);
|
||||
|
||||
if (!userRoles.includes("ADMIN"))
|
||||
return (
|
||||
<EmptyState
|
||||
role="alert"
|
||||
fullScreen
|
||||
Icon={LockIcon}
|
||||
message="Access denied"
|
||||
description={
|
||||
<>
|
||||
<Typography>
|
||||
You must be an admin of this workspace to access this page.
|
||||
</Typography>
|
||||
|
||||
<Button component={Link} to={ROUTES.home} startIcon={<HomeIcon />}>
|
||||
Home
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
style={{ marginTop: -TOP_BAR_HEIGHT, marginBottom: -TOP_BAR_HEIGHT }}
|
||||
/>
|
||||
);
|
||||
|
||||
return children as JSX.Element;
|
||||
}
|
||||
@@ -3,17 +3,17 @@ import { useEffect } from "react";
|
||||
import {
|
||||
Menu,
|
||||
MenuProps,
|
||||
ListSubheader,
|
||||
MenuItem,
|
||||
ListItemIcon,
|
||||
ListItemSecondaryAction,
|
||||
Divider,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
} from "@mui/material";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
import SocialLogo from "@src/components/SocialLogo";
|
||||
|
||||
import { EXTERNAL_LINKS } from "@src/constants/externalLinks";
|
||||
import { EXTERNAL_LINKS, WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
import { logEvent, analytics } from "analytics";
|
||||
import meta from "@root/package.json";
|
||||
|
||||
export default function HelpMenu({
|
||||
anchorEl,
|
||||
@@ -46,28 +46,9 @@ export default function HelpMenu({
|
||||
id="help-menu"
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||
transformOrigin={{ vertical: "bottom", horizontal: "left" }}
|
||||
sx={{ "& .MuiPaper-root": { mt: 1.5, py: 1 } }}
|
||||
sx={{ "& .MuiPaper-root": { mt: 1.5 } }}
|
||||
PaperProps={{ elevation: 12 }}
|
||||
>
|
||||
<ListSubheader
|
||||
sx={{
|
||||
mb: 0.5,
|
||||
typography: "subtitle1",
|
||||
}}
|
||||
>
|
||||
Get support
|
||||
</ListSubheader>
|
||||
<ListSubheader
|
||||
sx={{
|
||||
mb: 1.5,
|
||||
maxWidth: 260,
|
||||
typography: "body2",
|
||||
color: "text.secondary",
|
||||
}}
|
||||
>
|
||||
Reach out for help and find FAQs on GitHub Discussions
|
||||
</ListSubheader>
|
||||
|
||||
<MenuItem
|
||||
component="a"
|
||||
href={EXTERNAL_LINKS.gitHub + "/discussions"}
|
||||
@@ -75,14 +56,20 @@ export default function HelpMenu({
|
||||
rel="noopener noreferrer"
|
||||
onClick={onClose as any}
|
||||
>
|
||||
<ListItemIcon sx={{ mr: 1 }}>
|
||||
<SocialLogo platform="gitHub" />
|
||||
</ListItemIcon>
|
||||
GitHub Discussions
|
||||
Get support
|
||||
{externalLinkIcon}
|
||||
</MenuItem>
|
||||
|
||||
<Divider variant="middle" />
|
||||
<MenuItem
|
||||
component="a"
|
||||
href={WIKI_LINKS.faqs}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={onClose as any}
|
||||
>
|
||||
FAQs
|
||||
{externalLinkIcon}
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
component="a"
|
||||
@@ -91,12 +78,18 @@ export default function HelpMenu({
|
||||
rel="noopener noreferrer"
|
||||
onClick={onClose as any}
|
||||
>
|
||||
<ListItemIcon sx={{ mr: 1 }}>
|
||||
<SocialLogo platform="gitHub" />
|
||||
</ListItemIcon>
|
||||
Feature request
|
||||
Feature requests
|
||||
{externalLinkIcon}
|
||||
</MenuItem>
|
||||
|
||||
<Divider variant="middle" />
|
||||
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={`Rowy v${meta.version}`}
|
||||
primaryTypographyProps={{ color: "text.disabled" }}
|
||||
/>
|
||||
</ListItem>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
86
src/layouts/Navigation/NavDrawer/LearningMenu.tsx
Normal file
86
src/layouts/Navigation/NavDrawer/LearningMenu.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { useEffect } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
import {
|
||||
Menu,
|
||||
MenuProps,
|
||||
MenuItem,
|
||||
ListItemSecondaryAction,
|
||||
} from "@mui/material";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
import { ChevronRight as ChevronRightIcon } from "@src/assets/icons";
|
||||
|
||||
import { EXTERNAL_LINKS, WIKI_LINKS } from "@src/constants/externalLinks";
|
||||
import { ROUTES } from "@src/constants/routes";
|
||||
import { logEvent, analytics } from "analytics";
|
||||
|
||||
export default function LearningMenu({
|
||||
anchorEl,
|
||||
onClose,
|
||||
}: Pick<MenuProps, "anchorEl" | "onClose">) {
|
||||
const open = Boolean(anchorEl);
|
||||
useEffect(() => {
|
||||
if (open) logEvent(analytics, "open_learning_menu");
|
||||
}, [open]);
|
||||
|
||||
const externalLinkIcon = (
|
||||
<ListItemSecondaryAction
|
||||
sx={{
|
||||
position: "relative",
|
||||
transform: "none",
|
||||
ml: "auto",
|
||||
pl: 2,
|
||||
color: "text.disabled",
|
||||
right: -2,
|
||||
}}
|
||||
>
|
||||
<InlineOpenInNewIcon />
|
||||
</ListItemSecondaryAction>
|
||||
);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
id="learning-menu"
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||
transformOrigin={{ vertical: "bottom", horizontal: "left" }}
|
||||
sx={{ "& .MuiPaper-root": { mt: 1.5 } }}
|
||||
PaperProps={{ elevation: 12 }}
|
||||
>
|
||||
<MenuItem
|
||||
component="a"
|
||||
href={WIKI_LINKS.howTo}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={onClose as any}
|
||||
>
|
||||
How-to guides
|
||||
{externalLinkIcon}
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
component={Link}
|
||||
to={ROUTES.tableTutorial}
|
||||
onClick={onClose as any}
|
||||
>
|
||||
Table tutorial
|
||||
<ListItemSecondaryAction sx={{ color: "text.disabled", height: 20 }}>
|
||||
<ChevronRightIcon />
|
||||
</ListItemSecondaryAction>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
component="a"
|
||||
href={EXTERNAL_LINKS.welcomeVideo}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={onClose as any}
|
||||
>
|
||||
Video tutorials
|
||||
{externalLinkIcon}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
import Logo from "@src/assets/Logo";
|
||||
import NavItem from "./NavItem";
|
||||
import GetStartedProgress from "@src/components/GetStartedChecklist/GetStartedProgress";
|
||||
import LearningMenu from "./LearningMenu";
|
||||
import CommunityMenu from "./CommunityMenu";
|
||||
import HelpMenu from "./HelpMenu";
|
||||
import { INavDrawerContentsProps } from "./NavDrawerContents";
|
||||
@@ -73,6 +74,8 @@ export default function NavDrawer({
|
||||
const [getStartedCompleted, getStartedCompletionCount] =
|
||||
useGetStartedCompletion();
|
||||
|
||||
const [learningMenuAnchorEl, setLearningMenuAnchorEl] =
|
||||
useState<HTMLButtonElement | null>(null);
|
||||
const [communityMenuAnchorEl, setCommunityMenuAnchorEl] =
|
||||
useState<HTMLButtonElement | null>(null);
|
||||
const [helpMenuAnchorEl, setHelpMenuAnchorEl] =
|
||||
@@ -300,13 +303,27 @@ export default function NavDrawer({
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<NavItem href={WIKI_LINKS.howTo}>
|
||||
<NavItem
|
||||
onClick={(e: any) => {
|
||||
setLearningMenuAnchorEl(e.currentTarget);
|
||||
setHover("persist");
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<LearningIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Learning" />
|
||||
{externalLinkIcon}
|
||||
<ListItemSecondaryAction>
|
||||
<ChevronRightIcon />
|
||||
</ListItemSecondaryAction>
|
||||
</NavItem>
|
||||
<LearningMenu
|
||||
anchorEl={learningMenuAnchorEl}
|
||||
onClose={() => {
|
||||
setLearningMenuAnchorEl(null);
|
||||
setHover(false);
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useAtom } from "jotai";
|
||||
import { useAtom, Atom } from "jotai";
|
||||
import { useLocation, Navigate } from "react-router-dom";
|
||||
|
||||
import Loading from "@src/components/Loading";
|
||||
@@ -8,10 +8,16 @@ import { ROUTES } from "@src/constants/routes";
|
||||
|
||||
export interface IRequireAuthProps {
|
||||
children: React.ReactElement;
|
||||
atom?: Atom<any>;
|
||||
scope?: Parameters<typeof useAtom>[1];
|
||||
}
|
||||
|
||||
export default function RequireAuth({ children }: IRequireAuthProps) {
|
||||
const [currentUser] = useAtom(currentUserAtom, projectScope);
|
||||
export default function RequireAuth({
|
||||
children,
|
||||
atom = currentUserAtom,
|
||||
scope = projectScope,
|
||||
}: IRequireAuthProps) {
|
||||
const [currentUser] = useAtom(atom, scope);
|
||||
const location = useLocation();
|
||||
|
||||
if (currentUser === undefined)
|
||||
@@ -25,6 +31,7 @@ export default function RequireAuth({ children }: IRequireAuthProps) {
|
||||
<Navigate
|
||||
to={ROUTES.auth + `?redirect=${encodeURIComponent(redirect)}`}
|
||||
replace
|
||||
state={location.state}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useAtom, Provider } from "jotai";
|
||||
import { DebugAtoms } from "@src/atoms/utils";
|
||||
import { useParams, useOutlet } from "react-router-dom";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import { find } from "lodash-es";
|
||||
import { find, isEmpty } from "lodash-es";
|
||||
|
||||
import ErrorFallback, {
|
||||
ERROR_TABLE_NOT_FOUND,
|
||||
@@ -16,6 +16,7 @@ import TableSkeleton from "@src/components/Table/TableSkeleton";
|
||||
import {
|
||||
projectScope,
|
||||
currentUserAtom,
|
||||
projectSettingsAtom,
|
||||
tablesAtom,
|
||||
} from "@src/atoms/projectScope";
|
||||
import {
|
||||
@@ -32,10 +33,22 @@ export default function ProvidedTablePage() {
|
||||
const { id } = useParams();
|
||||
const outlet = useOutlet();
|
||||
const [currentUser] = useAtom(currentUserAtom, projectScope);
|
||||
const [projectSettings] = useAtom(projectSettingsAtom, projectScope);
|
||||
const [tables] = useAtom(tablesAtom, projectScope);
|
||||
|
||||
const tableSettings = useMemo(() => find(tables, ["id", id]), [tables, id]);
|
||||
if (!tableSettings) throw new Error(ERROR_TABLE_NOT_FOUND + ": " + id);
|
||||
if (!tableSettings) {
|
||||
if (isEmpty(projectSettings)) {
|
||||
return (
|
||||
<>
|
||||
<TableToolbarSkeleton />
|
||||
<TableSkeleton />
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
throw new Error(ERROR_TABLE_NOT_FOUND + ": " + id);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import { find, groupBy } from "lodash-es";
|
||||
import { find, groupBy, sortBy } from "lodash-es";
|
||||
|
||||
import {
|
||||
Container,
|
||||
@@ -44,7 +44,7 @@ import { useScrollToHash } from "@src/hooks/useScrollToHash";
|
||||
|
||||
const SEARCH_KEYS = ["id", "name", "section", "description"];
|
||||
|
||||
export default function HomePage() {
|
||||
export default function TablesPage() {
|
||||
const [userRoles] = useAtom(userRolesAtom, projectScope);
|
||||
const [userSettings] = useAtom(userSettingsAtom, projectScope);
|
||||
const [updateUserSettings] = useAtom(updateUserSettingsAtom, projectScope);
|
||||
@@ -67,7 +67,7 @@ export default function HomePage() {
|
||||
: [];
|
||||
const sections: Record<string, TableSettings[]> = {
|
||||
Favorites: favorites.map((id) => find(results, { id })) as TableSettings[],
|
||||
...groupBy(results, "section"),
|
||||
...groupBy(sortBy(results, ["section", "name"]), "section"),
|
||||
};
|
||||
|
||||
if (!Array.isArray(tables))
|
||||
|
||||
@@ -8,3 +8,7 @@ export const isTargetInsideBox = (target: Element, box: Element) => {
|
||||
|
||||
export const spreadSx = (sx?: SxProps<Theme>) =>
|
||||
Array.isArray(sx) ? sx : sx ? [sx] : [];
|
||||
|
||||
export const copyToClipboard = (text: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
};
|
||||
|
||||
88
yarn.lock
88
yarn.lock
@@ -2845,15 +2845,37 @@
|
||||
"@types/yargs" "^17.0.8"
|
||||
chalk "^4.0.0"
|
||||
|
||||
"@jridgewell/gen-mapping@^0.3.0":
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9"
|
||||
integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==
|
||||
dependencies:
|
||||
"@jridgewell/set-array" "^1.0.1"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.10"
|
||||
"@jridgewell/trace-mapping" "^0.3.9"
|
||||
|
||||
"@jridgewell/resolve-uri@^3.0.3":
|
||||
version "3.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.5.tgz#68eb521368db76d040a6315cdb24bf2483037b9c"
|
||||
integrity sha512-VPeQ7+wH0itvQxnG+lIzWgkysKIr3L9sslimFW55rHMdGu/qCQ5z5h9zq4gI8uBtqkpHhsF4Z/OwExufUCThew==
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
|
||||
integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
|
||||
|
||||
"@jridgewell/set-array@^1.0.1":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
|
||||
integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==
|
||||
|
||||
"@jridgewell/source-map@^0.3.2":
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb"
|
||||
integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==
|
||||
dependencies:
|
||||
"@jridgewell/gen-mapping" "^0.3.0"
|
||||
"@jridgewell/trace-mapping" "^0.3.9"
|
||||
|
||||
"@jridgewell/sourcemap-codec@^1.4.10":
|
||||
version "1.4.11"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz#771a1d8d744eeb71b6adb35808e1a6c7b9b8c8ec"
|
||||
integrity sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg==
|
||||
version "1.4.14"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
|
||||
integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
|
||||
|
||||
"@jridgewell/trace-mapping@^0.3.0":
|
||||
version "0.3.4"
|
||||
@@ -2863,6 +2885,14 @@
|
||||
"@jridgewell/resolve-uri" "^3.0.3"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.10"
|
||||
|
||||
"@jridgewell/trace-mapping@^0.3.9":
|
||||
version "0.3.14"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed"
|
||||
integrity sha512-bJWEfQ9lPTvm3SneWwRFVLzrh6nhjwqw7TUFFBEMzwvg7t7PCDenf2lDwqo4NQXzdpgBXyFgDWnQA+2vkruksQ==
|
||||
dependencies:
|
||||
"@jridgewell/resolve-uri" "^3.0.3"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.10"
|
||||
|
||||
"@leichtgewicht/ip-codec@^2.0.1":
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.3.tgz#0300943770e04231041a51bd39f0439b5c7ab4f0"
|
||||
@@ -4514,9 +4544,9 @@ acorn@^7.0.0, acorn@^7.1.1:
|
||||
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
|
||||
|
||||
acorn@^8.2.4, acorn@^8.4.1, acorn@^8.5.0, acorn@^8.7.0:
|
||||
version "8.7.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf"
|
||||
integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==
|
||||
version "8.7.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30"
|
||||
integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==
|
||||
|
||||
address@^1.0.1, address@^1.1.2:
|
||||
version "1.1.2"
|
||||
@@ -4787,9 +4817,9 @@ astral-regex@^2.0.0:
|
||||
integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
|
||||
|
||||
async@^2.6.2:
|
||||
version "2.6.3"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff"
|
||||
integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==
|
||||
version "2.6.4"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-2.6.4.tgz#706b7ff6084664cd7eae713f6f965433b5504221"
|
||||
integrity sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==
|
||||
dependencies:
|
||||
lodash "^4.17.14"
|
||||
|
||||
@@ -5167,9 +5197,9 @@ buffer-equal@0.0.1:
|
||||
integrity sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs=
|
||||
|
||||
buffer-from@^1.0.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
|
||||
integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
|
||||
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
|
||||
|
||||
buffer@^6.0.3:
|
||||
version "6.0.3"
|
||||
@@ -8715,10 +8745,10 @@ jju@~1.4.0:
|
||||
resolved "https://registry.yarnpkg.com/jju/-/jju-1.4.0.tgz#a3abe2718af241a2b2904f84a625970f389ae32a"
|
||||
integrity sha1-o6vicYryQaKykE+EpiWXDzia4yo=
|
||||
|
||||
jotai@^1.7.2:
|
||||
version "1.7.2"
|
||||
resolved "https://registry.yarnpkg.com/jotai/-/jotai-1.7.2.tgz#80587cf10f51a614c2028688e12d2abc6ac0b00c"
|
||||
integrity sha512-ksvpW1Wu+/HwW1iDYq23PpXLu2df5Vv+eWw70jRAx7IEY4c+qRsORULnqPFurSy/X8LSoPcRhVDJx/cyf8jjMg==
|
||||
jotai@^1.8.4:
|
||||
version "1.8.4"
|
||||
resolved "https://registry.yarnpkg.com/jotai/-/jotai-1.8.4.tgz#e188bff3cc790c758d25646f6f5daf9854c98eef"
|
||||
integrity sha512-bkHDHNxm7bU4+bJL4z96fTlJYN34UDRTu3ghEajJrDepayON9YEaxPrXr7xhLnIRntoFC6eDYYhMNA/ilbj2RQ==
|
||||
|
||||
js-base64@^2.4.3:
|
||||
version "2.6.4"
|
||||
@@ -12491,15 +12521,7 @@ source-map-resolve@^0.6.0:
|
||||
atob "^2.1.2"
|
||||
decode-uri-component "^0.2.0"
|
||||
|
||||
source-map-support@^0.5.6:
|
||||
version "0.5.19"
|
||||
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61"
|
||||
integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==
|
||||
dependencies:
|
||||
buffer-from "^1.0.0"
|
||||
source-map "^0.6.0"
|
||||
|
||||
source-map-support@~0.5.20:
|
||||
source-map-support@^0.5.6, source-map-support@~0.5.20:
|
||||
version "0.5.21"
|
||||
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f"
|
||||
integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==
|
||||
@@ -12517,7 +12539,7 @@ source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7:
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
|
||||
integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
|
||||
|
||||
source-map@^0.7.3, source-map@~0.7.2:
|
||||
source-map@^0.7.3:
|
||||
version "0.7.3"
|
||||
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
|
||||
integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==
|
||||
@@ -13008,13 +13030,13 @@ terser-webpack-plugin@^5.1.3, terser-webpack-plugin@^5.2.5:
|
||||
terser "^5.7.2"
|
||||
|
||||
terser@^5.0.0, terser@^5.10.0, terser@^5.7.2:
|
||||
version "5.12.1"
|
||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.12.1.tgz#4cf2ebed1f5bceef5c83b9f60104ac4a78b49e9c"
|
||||
integrity sha512-NXbs+7nisos5E+yXwAD+y7zrcTkMqb0dEJxIGtSKPdCBzopf7ni4odPul2aechpV7EXNvOudYOX2bb5tln1jbQ==
|
||||
version "5.14.2"
|
||||
resolved "https://registry.yarnpkg.com/terser/-/terser-5.14.2.tgz#9ac9f22b06994d736174f4091aa368db896f1c10"
|
||||
integrity sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA==
|
||||
dependencies:
|
||||
"@jridgewell/source-map" "^0.3.2"
|
||||
acorn "^8.5.0"
|
||||
commander "^2.20.0"
|
||||
source-map "~0.7.2"
|
||||
source-map-support "~0.5.20"
|
||||
|
||||
test-exclude@^6.0.0:
|
||||
|
||||
Reference in New Issue
Block a user