Merge branch 'develop' into rc

This commit is contained in:
shamsmosowi
2022-10-03 08:01:12 +02:00
61 changed files with 1036 additions and 272 deletions

View File

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

View File

@@ -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.
[![Run on Google Cloud](https://deploy.cloud.run/button.svg)](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.
[![Run on Google Cloud](https://deploy.cloud.run/button.svg)](https://deploy.rowy.app/)
[![Run on Google Cloud](https://deploy.cloud.run/button.svg)](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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ import { Box } from "@mui/material";
export interface ICircularProgressTimedProps
extends ICircularProgressOpticalProps {
/** Duration in seconds */
duration: number;
complete: boolean;
}

View File

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

View File

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

View File

@@ -329,6 +329,7 @@ export default function ColumnMenu() {
</>
),
handleConfirm: handleEvaluateAll,
confirm: "Evaluate",
}),
},
];

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,14 @@
import { memo } from "react";
/**
* Used for global Modals that can have customizable text
* so that the default text doesnt appear as the modal closes.
*/
const MemoizedText = memo(
function MemoizedTextComponent({ text }: { text: React.ReactNode }) {
return <>{text}</>;
},
() => true
);
export default MemoizedText;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 {",
" /**",

View File

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

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

View File

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

View File

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

View File

@@ -300,7 +300,7 @@ export const tableSettings = (
type: FieldType.checkbox,
name: "audit",
label: "Enable auditing for this table",
defaultValue: true,
defaultValue: false,
},
{
step: "auditing",

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -0,0 +1,12 @@
import { IFilterOperator } from "@src/components/fields/types";
export const filterOperators: IFilterOperator[] = [
{
label: "equals",
value: "==",
},
{
label: "not equals",
value: "!=",
},
];

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,6 +15,7 @@ const SideDrawerField = lazy(
"./SideDrawerField" /* webpackChunkName: "SideDrawerField-ShortText" */
)
);
const Settings = lazy(
() => import("./Settings" /* webpackChunkName: "Settings-ShortText" */)
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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