Merge branch 'rc' of https://github.com/rowyio/rowy into rc

This commit is contained in:
shamsmosowi
2022-05-31 21:56:07 +08:00
44 changed files with 1217 additions and 177 deletions

View File

@@ -23,6 +23,9 @@ A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Rowy Run version**
If applicable, share the Rowy Run version from your project settings.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]

View File

@@ -1,6 +1,6 @@
{
"name": "rowy",
"version": "2.4.0",
"version": "2.5.0",
"homepage": "https://rowy.io",
"repository": {
"type": "git",
@@ -15,10 +15,10 @@
"@hookform/resolvers": "^2.8.5",
"@mdi/js": "^6.5.95",
"@monaco-editor/react": "^4.3.1",
"@mui/icons-material": "^5.4.4",
"@mui/lab": "^5.0.0-alpha.71",
"@mui/material": "^5.4.4",
"@mui/styles": "^5.4.4",
"@mui/icons-material": "^5.5.1",
"@mui/lab": "^5.0.0-alpha.73",
"@mui/material": "^5.5.1",
"@mui/styles": "^5.5.1",
"@rowy/form-builder": "^0.5.3",
"@rowy/multiselect": "^0.2.3",
"@tinymce/tinymce-react": "^3.12.6",

View File

@@ -38,7 +38,7 @@ export default function CodeEditor({
extraLibs,
diagnosticsOptions,
onUnmount,
defaultLanguage = "javascript",
...props
}: ICodeEditorProps) {
const theme = useTheme();
@@ -77,7 +77,7 @@ export default function CodeEditor({
style={fullScreen ? { height: "100%" } : {}}
>
<Editor
defaultLanguage="javascript"
defaultLanguage={defaultLanguage}
value={initialEditorValue}
loading={<CircularProgressOptical size={20} sx={{ m: 2 }} />}
className="editor"

View File

@@ -48,7 +48,10 @@ interface Rowy {
/**
* Get an existing secret from the secret manager.
*/
get: (name: SecretNames, version?: string) => Promise<string | undefined>;
get: (
name: SecretNames,
version?: string
) => Promise<string | any | undefined>;
};
/**
* Gives access to the Cloud Storage.

View File

@@ -21,6 +21,7 @@ export default function Confirmation({
confirm,
confirmationCommand,
handleConfirm,
handleCancel,
confirmColor,
open,
handleClose,
@@ -60,7 +61,14 @@ export default function Confirmation({
<DialogActions>
{!hideCancel && (
<Button onClick={handleClose}>{cancel ?? "Cancel"}</Button>
<Button
onClick={() => {
if (handleCancel) handleCancel();
handleClose();
}}
>
{cancel ?? "Cancel"}
</Button>
)}
<Button
onClick={() => {

View File

@@ -8,6 +8,7 @@ export type confirmationProps =
confirm?: string | JSX.Element;
confirmationCommand?: string;
handleConfirm: () => void;
handleCancel?: () => void;
open?: Boolean;
confirmColor?: string;
}

View File

@@ -0,0 +1,110 @@
import { ReactNode, useState } from "react";
import {
Dialog,
DialogProps,
Slide,
IconButton,
Container,
} from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
import ScrollableDialogContent, {
IScrollableDialogContentProps,
} from "./ScrollableDialogContent";
export interface IFullScreenModalProps
extends Partial<Omit<DialogProps, "title">> {
onClose: (setOpen: React.Dispatch<React.SetStateAction<boolean>>) => void;
disableBackdropClick?: boolean;
disableEscapeKeyDown?: boolean;
"aria-labelledby": DialogProps["aria-labelledby"];
header?: ReactNode;
children?: ReactNode;
footer?: ReactNode;
hideCloseButton?: boolean;
ScrollableDialogContentProps?: Partial<IScrollableDialogContentProps>;
}
export default function FullScreenModal({
onClose,
disableBackdropClick,
disableEscapeKeyDown,
header,
children,
footer,
hideCloseButton,
ScrollableDialogContentProps,
...props
}: IFullScreenModalProps) {
const [open, setOpen] = useState(true);
const handleClose: NonNullable<DialogProps["onClose"]> = (_, reason) => {
if (
(disableBackdropClick && reason === "backdropClick") ||
(disableEscapeKeyDown && reason === "escapeKeyDown")
) {
setEmphasizeCloseButton(true);
return;
}
setOpen(false);
setEmphasizeCloseButton(false);
setTimeout(() => onClose(setOpen), 300);
};
const [emphasizeCloseButton, setEmphasizeCloseButton] = useState(false);
return (
<Dialog
fullScreen
open={open}
onClose={handleClose}
TransitionComponent={Slide}
TransitionProps={{ direction: "up" } as any}
{...props}
>
<Container
sx={{
display: "flex",
flexDirection: "column",
height: "100%",
pt: { xs: "var(--dialog-spacing)", xl: 6 },
}}
>
{!hideCloseButton && (
<IconButton
onClick={handleClose as any}
aria-label="Close"
sx={{
position: "absolute",
top: (theme) => theme.spacing(1),
right: (theme) => theme.spacing(1),
bgcolor: emphasizeCloseButton ? "error.main" : undefined,
color: emphasizeCloseButton ? "error.contrastText" : undefined,
"&:hover": emphasizeCloseButton
? { bgcolor: "error.dark" }
: undefined,
}}
className="dialog-close"
>
<CloseIcon />
</IconButton>
)}
{header}
<ScrollableDialogContent
style={{ padding: 0 }}
{...ScrollableDialogContentProps}
>
{children}
</ScrollableDialogContent>
{footer}
</Container>
</Dialog>
);
}

View File

@@ -9,7 +9,9 @@ const components = {
a: (props) => <Link color="inherit" {...props} />,
p: Typography,
// eslint-disable-next-line jsx-a11y/alt-text
img: (props) => <img style={{ maxWidth: "100%" }} {...props} />,
img: (props) => (
<img style={{ maxWidth: "100%", borderRadius: 4 }} {...props} />
),
};
const restrictionPresets = {
@@ -31,7 +33,7 @@ export default function RenderedMarkdown({
unwrapDisallowed
linkTarget="_blank"
remarkPlugins={remarkPlugins}
components={components}
components={{ ...components, ...props.components }}
/>
);
}

View File

@@ -19,7 +19,7 @@ import { WIKI_LINKS } from "@src/constants/externalLinks";
import { useProjectContext } from "@src/contexts/ProjectContext";
export default function RowyRunModal() {
const { userClaims } = useAppContext();
const { userRoles } = useAppContext();
const { settings } = useProjectContext();
const [state, setState] = useAtom(rowyRunModalAtom);
@@ -79,12 +79,12 @@ export default function RowyRunModal() {
size="large"
onClick={handleClose}
style={{ display: "flex" }}
disabled={!userClaims?.roles.includes("ADMIN")}
disabled={!userRoles.includes("ADMIN")}
>
Set up Rowy Run
</Button>
{!userClaims?.roles.includes("ADMIN") && (
{!userRoles.includes("ADMIN") && (
<Typography
variant="body2"
textAlign="center"

View File

@@ -38,13 +38,10 @@ export default function SideDrawer() {
const handleNavigate = (direction: "up" | "down") => () => {
if (!tableState?.rows) return;
let row = cell!.row;
if (direction === "up" && row > 0) row -= 1;
if (direction === "down" && row < tableState.rows.length - 1) row += 1;
setCell!((cell) => ({ column: cell!.column, row }));
const idx = tableState?.columns[cell!.column]?.index;
dataGridRef?.current?.selectCell({ rowIdx: row, idx }, false);
};
@@ -68,13 +65,6 @@ export default function SideDrawer() {
useEffect(() => {
if (cell && tableState?.rows[cell.row]) {
window.history.pushState(
"",
`${tableState?.config.id}`,
`${window.location.pathname}?rowRef=${encodeURIComponent(
tableState?.rows[cell.row].ref.path
)}`
);
if (urlDocState.doc) {
urlDocState.unsubscribe();
dispatchUrlDoc({ path: "", doc: null });

View File

@@ -4,6 +4,7 @@ import { getFieldProp } from "@src/components/fields";
import MenuContents from "./MenuContent";
import DuplicateIcon from "@src/assets/icons/CopyCells";
import DeleteIcon from "@mui/icons-material/DeleteOutlined";
import LinkIcon from "@mui/icons-material/Link";
import { useProjectContext } from "@src/contexts/ProjectContext";
import { useContextMenuAtom } from "@src/atoms/ContextMenu";
@@ -47,8 +48,18 @@ export default function ContextMenu() {
}
const row = tableState?.rows[selectedCell!.rowIndex];
if (userRoles.includes("ADMIN") && row) {
if (row) {
const rowActions = [
{
label: "Copy link to row",
icon: <LinkIcon />,
onClick: () => {
const rowRef = encodeURIComponent(row.ref.path);
navigator.clipboard.writeText(
window.location.href + `?rowRef=${rowRef}`
);
},
},
{
label: "Duplicate row",
icon: <DuplicateIcon />,

View File

@@ -1,5 +1,6 @@
import { useState } from "react";
import { useHistory } from "react-router-dom";
import { useSnackbar } from "notistack";
import { IconButton, Menu, MenuItem, DialogContentText } from "@mui/material";
import DeleteIcon from "@mui/icons-material/DeleteOutlined";
@@ -27,14 +28,21 @@ export default function DeleteMenu({ clearDialog, data }: IDeleteMenuProps) {
const handleClose = () => setAnchorEl(null);
const history = useHistory();
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
const handleResetStructure = async () => {
const snack = enqueueSnackbar("Resetting columns…", { persist: true });
const schemaDocRef = db.doc(`${TABLE_SCHEMAS}/${data!.id}`);
await schemaDocRef.update({ columns: {} });
clearDialog();
closeSnackbar(snack);
};
const handleDelete = async () => {
const snack = enqueueSnackbar("Deleting table…", { persist: true });
const tablesDocRef = db.doc(SETTINGS);
const tableData = (await tablesDocRef.get()).data();
const updatedTables = tableData?.tables.filter(
@@ -49,8 +57,10 @@ export default function DeleteMenu({ clearDialog, data }: IDeleteMenuProps) {
)
.doc(data?.id)
.delete();
await analytics.logEvent("delete_table");
clearDialog();
closeSnackbar(snack);
history.push(routes.home);
};

View File

@@ -1,33 +1,22 @@
import { useEffect } from "react";
import { useWatch } from "react-hook-form";
import _camelCase from "lodash/camelCase";
import {
ShortTextComponent,
IShortTextComponentProps,
} from "@rowy/form-builder";
import { TextField, TextFieldProps } from "@mui/material";
import { IFieldComponentProps, FieldAssistiveText } from "@rowy/form-builder";
export interface ICamelCaseIdProps
extends IFieldComponentProps,
Omit<
TextFieldProps,
"variant" | "name" | "label" | "onBlur" | "onChange" | "value" | "ref"
> {
export interface ITableIdProps extends IShortTextComponentProps {
watchedField?: string;
}
export default function CamelCaseId({
field: { onChange, onBlur, value, ref },
export default function TableId({ watchedField, ...props }: ITableIdProps) {
const {
field: { onChange },
useFormMethods: { control },
disabled,
} = props;
name,
useFormMethods: { control },
errorMessage,
assistiveText,
disabled,
watchedField,
...props
}: ICamelCaseIdProps) {
const watchedValue = useWatch({ control, name: watchedField } as any);
useEffect(() => {
if (!disabled && typeof watchedValue === "string" && !!watchedValue)
@@ -35,37 +24,9 @@ export default function CamelCaseId({
}, [watchedValue, disabled]);
return (
<TextField
onChange={onChange}
onBlur={onBlur}
value={value}
fullWidth
error={!!errorMessage}
helperText={
(errorMessage || assistiveText) && (
<>
{errorMessage}
<FieldAssistiveText style={{ margin: 0 }} disabled={!!disabled}>
{assistiveText}
</FieldAssistiveText>
</>
)
}
FormHelperTextProps={{ component: "div" } as any}
name={name}
id={`field-${name}`}
sx={{ "& .MuiInputBase-input": { fontFamily: "mono" } }}
<ShortTextComponent
{...props}
disabled={disabled}
inputProps={{
required: false,
// https://github.com/react-hook-form/react-hook-form/issues/4485
disabled: false,
readOnly: disabled,
style: disabled ? { cursor: "default" } : undefined,
}}
inputRef={ref}
sx={{ "& .MuiInputBase-input": { fontFamily: "mono" } }}
/>
);
}

View File

@@ -0,0 +1,27 @@
import { useEffect } from "react";
import { useWatch } from "react-hook-form";
import _startCase from "lodash/startCase";
import {
ShortTextComponent,
IShortTextComponentProps,
} from "@rowy/form-builder";
export interface ITableNameProps extends IShortTextComponentProps {
watchedField?: string;
}
export default function TableName({ watchedField, ...props }: ITableNameProps) {
const {
field: { onChange },
useFormMethods: { control },
disabled,
} = props;
const watchedValue = useWatch({ control, name: watchedField } as any);
useEffect(() => {
if (!disabled && typeof watchedValue === "string" && !!watchedValue)
onChange(_startCase(watchedValue));
}, [watchedValue, disabled]);
return <ShortTextComponent {...props} />;
}

View File

@@ -194,10 +194,11 @@ export const tableSettings = (
// Step 2: Display
{
step: "display",
type: FieldType.shortText,
type: "tableName",
name: "name",
label: "Table name",
required: true,
watchedField: "collection",
assistiveText: "User-facing name for this table",
autoFocus: true,
gridCols: { xs: 12, sm: 6 },

View File

@@ -2,12 +2,14 @@ import useSWR from "swr";
import _find from "lodash/find";
import _sortBy from "lodash/sortBy";
import _get from "lodash/get";
import _isEmpty from "lodash/isEmpty";
import { useSnackbar } from "notistack";
import { DialogContentText, Stack, Typography } from "@mui/material";
import { FormDialog, FormFields } from "@rowy/form-builder";
import { tableSettings } from "./form";
import TableName from "./TableName";
import TableId from "./TableId";
import SuggestedRules from "./SuggestedRules";
import SteppedAccordion from "@src/components/SteppedAccordion";
@@ -25,12 +27,31 @@ import {
TABLE_GROUP_SCHEMAS,
TABLE_SCHEMAS,
} from "@src/config/dbPaths";
import { Controller } from "react-hook-form";
export enum TableSettingsDialogModes {
create,
update,
}
export interface ICreateTableDialogProps {
const customComponents = {
tableName: {
component: TableName,
defaultValue: "",
validation: [["string"]],
},
tableId: {
component: TableId,
defaultValue: "",
validation: [["string"]],
},
suggestedRules: {
component: SuggestedRules,
defaultValue: "",
validation: [["string"]],
},
};
export interface ITableSettingsProps {
mode: TableSettingsDialogModes | null;
clearDialog: () => void;
data: Table | null;
@@ -40,7 +61,7 @@ export default function TableSettings({
mode,
clearDialog,
data,
}: ICreateTableDialogProps) {
}: ITableSettingsProps) {
const { settingsActions, roles, tables, rowyRun } = useProjectContext();
const sectionNames = Array.from(
new Set((tables ?? []).map((t) => t.section))
@@ -49,12 +70,17 @@ export default function TableSettings({
const router = useRouter();
const { requestConfirmation } = useConfirmation();
const snackLogContext = useSnackLogContext();
const { enqueueSnackbar } = useSnackbar();
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
const { data: collections } = useSWR(
"firebaseCollections",
() => rowyRun?.({ route: runRoutes.listCollections }),
{ revalidateIfStale: false, dedupingInterval: 60_000 }
{
revalidateOnMount: true,
revalidateOnFocus: false,
revalidateOnReconnect: false,
dedupingInterval: 60_000 * 60,
}
);
const open = mode !== null;
@@ -64,11 +90,12 @@ export default function TableSettings({
const handleSubmit = async (v) => {
const { _suggestedRules, ...values } = v;
const data = { ...values };
if (values.schemaSource)
data.schemaSource = _find(tables, { id: values.schemaSource });
const hasExtensions = Boolean(_get(data, "_schema.extensionObjects"));
const hasWebhooks = Boolean(_get(data, "_schema.webhooks"));
const hasExtensions = !_isEmpty(_get(data, "_schema.extensionObjects"));
const hasWebhooks = !_isEmpty(_get(data, "_schema.webhooks"));
const deployExtensionsWebhooks = (onComplete?: () => void) => {
if (rowyRun && (hasExtensions || hasWebhooks)) {
requestConfirmation({
@@ -124,6 +151,31 @@ export default function TableSettings({
if (onComplete) onComplete();
},
handleCancel: async () => {
let _schema: Record<string, any> = {};
if (hasExtensions) {
_schema.extensionObjects = _get(
data,
"_schema.extensionObjects"
)!.map((x) => ({
...x,
active: false,
}));
}
if (hasWebhooks) {
_schema.webhooks = _get(data, "_schema.webhooks")!.map((x) => ({
...x,
active: false,
}));
}
await settingsActions?.updateTable({
id: data.id,
tableType: data.tableType,
_schema,
});
if (onComplete) onComplete();
},
});
} else {
if (onComplete) onComplete();
@@ -135,7 +187,11 @@ export default function TableSettings({
deployExtensionsWebhooks();
clearDialog();
analytics.logEvent("update_table", { type: values.tableType });
enqueueSnackbar("Updated table");
} else {
const creatingSnackbar = enqueueSnackbar("Creating table…", {
persist: true,
});
await settingsActions?.createTable(data);
await analytics.logEvent("create_table", { type: values.tableType });
deployExtensionsWebhooks(() => {
@@ -149,6 +205,7 @@ export default function TableSettings({
router.history.push(values.id);
}
clearDialog();
closeSnackbar(creatingSnackbar);
});
}
};
@@ -168,18 +225,6 @@ export default function TableSettings({
),
Array.isArray(collections) ? collections.filter((x) => x !== CONFIG) : null
);
const customComponents = {
tableId: {
component: TableId,
defaultValue: "",
validation: [["string"]],
},
suggestedRules: {
component: SuggestedRules,
defaultValue: "",
validation: [["string"]],
},
};
return (
<FormDialog
@@ -203,6 +248,13 @@ export default function TableSettings({
return (
<>
<Controller
control={formFieldsProps.control}
name="_schema"
defaultValue={{}}
render={() => <></>}
/>
<Stack
direction="row"
spacing={1}

View File

@@ -14,11 +14,7 @@ import { useConfirmation } from "@src/components/ConfirmationDialog";
import { useActionParams } from "./FormDialog/Context";
import { runRoutes } from "@src/constants/runRoutes";
const replacer = (data: any) => (m: string, key: string) => {
const objKey = key.split(":")[0];
const defaultValue = key.split(":")[1] || "";
return _get(data, objKey, defaultValue);
};
import { replacer } from "@src/utils/fns";
const getStateIcon = (actionState, config) => {
switch (actionState) {

View File

@@ -76,7 +76,7 @@ const Settings = ({ config, onChange }) => {
const scriptExtraLibs = [
[
"declare class ActionParams {",
"declare interface actionParams {",
" /**",
" * actionParams are provided by dialog popup form",
" */",

View File

@@ -13,7 +13,7 @@ type ActionContext = {
storage: firebasestorage.Storage;
db: FirebaseFirestore.Firestore;
auth: firebaseauth.BaseAuth;
actionParams: ActionParams;
actionParams: actionParams;
user: ActionUser;
};

View File

@@ -0,0 +1,38 @@
import MultiSelect from "@rowy/multiselect";
const languages = [
"javascript",
"typescript",
"json",
"html",
"css",
"scss",
"shell",
"yaml",
"xml",
"ruby",
"python",
"php",
"markdown",
"rust",
"csharp",
"cpp",
"c",
"java",
"go",
"plaintext",
];
export default function Settings({ config, onChange }) {
return (
<MultiSelect
searchable
multiple={false}
options={languages}
value={config.language ?? "javascript"}
onChange={(value) => {
onChange("language")(value);
}}
label="Language"
/>
);
}

View File

@@ -13,7 +13,12 @@ export default function Code({
control={control}
name={column.key}
render={({ field: { onChange, value } }) => (
<CodeEditor disabled={disabled} value={value} onChange={onChange} />
<CodeEditor
defaultLanguage={column.config?.language}
disabled={disabled}
value={value}
onChange={onChange}
/>
)}
/>
);

View File

@@ -6,6 +6,10 @@ import CodeIcon from "@mui/icons-material/Code";
import BasicCell from "./BasicCell";
import withSideDrawerEditor from "@src/components/Table/editors/withSideDrawerEditor";
const Settings = lazy(
() => import("./Settings" /* webpackChunkName: "Settings-ConnectService" */)
);
const SideDrawerField = lazy(
() =>
import("./SideDrawerField" /* webpackChunkName: "SideDrawerField-Code" */)
@@ -23,5 +27,6 @@ export const config: IFieldConfig = {
TableCell: withBasicCell(BasicCell),
TableEditor: withSideDrawerEditor(BasicCell),
SideDrawerField,
settings: Settings,
};
export default config;

View File

@@ -1,16 +1,23 @@
import { IPopoverCellProps } from "../types";
import { ColorPicker, toColor } from "react-color-palette";
import { useDebouncedCallback } from "use-debounce";
import "react-color-palette/lib/css/styles.css";
import { useEffect, useState } from "react";
export default function Color({ value, onSubmit }: IPopoverCellProps) {
const handleChangeComplete = (color: any) => onSubmit(color);
const [localValue, setLocalValue] = useState(value);
const [handleChangeComplete] = useDebouncedCallback((color) => {
onSubmit(color);
}, 400);
useEffect(() => {
handleChangeComplete(localValue);
}, [localValue]);
return (
<ColorPicker
width={240}
height={180}
color={value?.hex ? value : toColor("hex", "#fff")}
onChange={handleChangeComplete}
color={localValue?.hex ? localValue : toColor("hex", "#fff")}
onChange={setLocalValue}
alpha
/>
);

View File

@@ -0,0 +1,57 @@
import { forwardRef } from "react";
import { IPopoverInlineCellProps } from "../types";
import { ButtonBase, Grid, Chip } from "@mui/material";
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
import ChipList from "@src/components/Table/formatters/ChipList";
import { get } from "lodash";
import { getLabel } from "./utils";
export const ConnectService = forwardRef(function ConnectService(
{ value, showPopoverCell, disabled, column }: IPopoverInlineCellProps,
ref: React.Ref<any>
) {
const config = column.config ?? {};
const displayKey = config.titleKey ?? config.primaryKey;
return (
<ButtonBase
onClick={() => showPopoverCell(true)}
ref={ref}
disabled={disabled}
className="cell-collapse-padding"
sx={{
height: "100%",
font: "inherit",
color: "inherit !important",
letterSpacing: "inherit",
textAlign: "inherit",
justifyContent: "flex-start",
}}
>
<ChipList>
{Array.isArray(value) &&
value.map((item) => (
<Grid item key={get(item, config.id)}>
<Chip label={getLabel(config, item)} size="small" />
</Grid>
))}
</ChipList>
{!disabled && (
<ArrowDropDownIcon
className="row-hover-iconButton"
sx={{
flexShrink: 0,
mr: 0.5,
borderRadius: 1,
p: (32 - 24) / 2 / 8,
boxSizing: "content-box",
}}
/>
)}
</ButtonBase>
);
});
export default ConnectService;

View File

@@ -0,0 +1,35 @@
import { IPopoverCellProps } from "../types";
import ConnectServiceSelect from "./Select";
export default function ConnectService({
value,
onSubmit,
column,
parentRef,
showPopoverCell,
disabled,
docRef,
}: IPopoverCellProps) {
return (
<ConnectServiceSelect
value={value}
onChange={onSubmit}
column={column}
disabled={disabled}
docRef={docRef}
TextFieldProps={{
style: { display: "none" },
SelectProps: {
open: true,
MenuProps: {
anchorEl: parentRef,
anchorOrigin: { vertical: "bottom", horizontal: "left" },
transformOrigin: { vertical: "top", horizontal: "left" },
},
onClose: () => showPopoverCell(false),
},
}}
/>
);
}

View File

@@ -0,0 +1,213 @@
import React, { useEffect, useState } from "react";
import clsx from "clsx";
import { useDebouncedCallback } from "use-debounce";
import _get from "lodash/get";
import {
Button,
Checkbox,
Divider,
Grid,
InputAdornment,
List,
ListItemIcon,
ListItemText,
MenuItem,
TextField,
Typography,
Radio,
} from "@mui/material";
import SearchIcon from "@mui/icons-material/Search";
import { IConnectorSelectProps } from ".";
import useStyles from "./styles";
import Loading from "@src/components/Loading";
import { useProjectContext } from "@src/contexts/ProjectContext";
import { replacer } from "@src/utils/fns";
import { getLabel } from "../utils";
import { useSnackbar } from "notistack";
export interface IPopupContentsProps
extends Omit<IConnectorSelectProps, "className" | "TextFieldProps"> {}
// TODO: Implement infinite scroll here
export default function PopupContents({
value = [],
onChange,
column,
docRef,
}: IPopupContentsProps) {
const { rowyRun, tableState } = useProjectContext();
const { enqueueSnackbar } = useSnackbar();
// const url = config.url ;
const { config } = column;
const elementId = config.elementId;
const multiple = Boolean(config.multiple);
const classes = useStyles();
// Webservice search query
const [query, setQuery] = useState("");
// Webservice response
const [response, setResponse] = useState<any | null>(null);
const [hits, setHits] = useState<any[]>([]);
useEffect(() => {
console.log(response);
if (response?.success === false) {
enqueueSnackbar(response.message, { variant: "error" });
} else if (Array.isArray(response?.hits)) {
setHits(response.hits);
} else {
setHits([]);
//enqueueSnackbar("response is not any array", { variant: "error" });
}
}, [response]);
const [search] = useDebouncedCallback(
async (query: string) => {
const resp = await rowyRun!({
route: { method: "POST", path: "/connector" },
body: {
columnKey: column.key,
query: query,
schemaDocPath: tableState?.config.tableConfig.path,
rowDocPath: docRef.path,
},
});
setResponse(resp);
},
1000,
{ leading: true }
);
useEffect(() => {
search(query);
}, [query]);
if (!response) return <Loading />;
const select = (hit: any) => () => {
if (multiple) onChange([...value, hit]);
else onChange([hit]);
};
const deselect = (hit: any) => () => {
if (multiple)
onChange(value.filter((v) => v[elementId] !== hit[elementId]));
else onChange([]);
};
const selectedValues = value?.map((item) => _get(item, elementId));
const clearSelection = () => onChange([]);
return (
<Grid container direction="column" className={classes.grid}>
<Grid item className={classes.searchRow}>
<TextField
value={query}
onChange={(e) => setQuery(e.target.value)}
fullWidth
variant="filled"
margin="dense"
label="Search items"
className={classes.noMargins}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<SearchIcon />
</InputAdornment>
),
}}
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
/>
</Grid>
<Grid item xs className={classes.listRow}>
<List className={classes.list}>
{hits.map((hit) => {
const isSelected = selectedValues.some((v) => v === hit[elementId]);
console.log({
isSelected,
selectedValues,
elementId,
});
return (
<React.Fragment key={_get(hit, elementId)}>
<MenuItem
dense
onClick={isSelected ? deselect(hit) : select(hit)}
disabled={
!isSelected && multiple && value.length >= config.max
}
>
<ListItemIcon className={classes.checkboxContainer}>
{multiple ? (
<Checkbox
edge="start"
checked={isSelected}
tabIndex={-1}
color="secondary"
className={classes.checkbox}
disableRipple
inputProps={{
"aria-labelledby": `label-${_get(hit, elementId)}`,
}}
/>
) : (
<Radio
edge="start"
checked={isSelected}
tabIndex={-1}
color="secondary"
className={classes.checkbox}
disableRipple
inputProps={{
"aria-labelledby": `label-${_get(hit, elementId)}`,
}}
/>
)}
</ListItemIcon>
<ListItemText
id={`label-${_get(hit, elementId)}`}
primary={getLabel(config, hit)}
/>
</MenuItem>
<Divider className={classes.divider} />
</React.Fragment>
);
})}
</List>
</Grid>
{multiple && (
<Grid item className={clsx(classes.footerRow, classes.selectedRow)}>
<Grid
container
direction="row"
justifyContent="space-between"
alignItems="center"
>
<Typography
variant="button"
color="textSecondary"
className={classes.selectedNum}
>
{value?.length} of {hits?.length}
</Typography>
<Button
disabled={!value || value.length === 0}
onClick={clearSelection}
color="primary"
className={classes.selectAllButton}
>
Clear selection
</Button>
</Grid>
</Grid>
)}
</Grid>
);
}

View File

@@ -0,0 +1,72 @@
import { Suspense } from "react";
import clsx from "clsx";
import { TextField, TextFieldProps } from "@mui/material";
import useStyles from "./styles";
import Loading from "@src/components/Loading";
import ErrorBoundary from "@src/components/ErrorBoundary";
import PopupContents from "./PopupContents";
export type ServiceValue = {
value: string;
[prop: string]: any;
};
export interface IConnectorSelectProps {
value: ServiceValue[];
onChange: (value: ServiceValue[]) => void;
column: any;
editable?: boolean;
/** Optional style overrides for root MUI `TextField` component */
className?: string;
/** Override any props of the root MUI `TextField` component */
TextFieldProps?: Partial<TextFieldProps>;
docRef: firebase.default.firestore.DocumentReference;
disabled?: boolean;
}
export default function ConnectorSelect({
value = [],
className,
TextFieldProps = {},
disabled,
...props
}: IConnectorSelectProps) {
const classes = useStyles();
const sanitisedValue = Array.isArray(value) ? value : [];
return (
<TextField
label=""
hiddenLabel
variant={"filled" as any}
select
value={sanitisedValue}
className={clsx(classes.root, className)}
{...TextFieldProps}
SelectProps={{
renderValue: (value) => `${(value as any[]).length} selected`,
displayEmpty: true,
classes: { select: classes.selectRoot },
...TextFieldProps.SelectProps,
// Must have this set to prevent MUI transforming `value`
// prop for this component to a comma-separated string
MenuProps: {
classes: { paper: classes.paper, list: classes.menuChild },
MenuListProps: { disablePadding: true },
anchorOrigin: { vertical: "bottom", horizontal: "center" },
transformOrigin: { vertical: "top", horizontal: "center" },
...TextFieldProps.SelectProps?.MenuProps,
},
}}
disabled={disabled}
>
<ErrorBoundary>
<Suspense fallback={<Loading />}>
<PopupContents value={sanitisedValue} {...props} />
</Suspense>
</ErrorBoundary>
</TextField>
);
}

View File

@@ -0,0 +1,83 @@
import { makeStyles, createStyles } from "@mui/styles";
export const useStyles = makeStyles((theme) =>
createStyles({
root: { minWidth: 200 },
selectRoot: { paddingRight: theme.spacing(4) },
paper: { overflow: "hidden", maxHeight: "calc(100% - 48px)" },
menuChild: {
padding: `0 ${theme.spacing(2)}`,
minWidth: 340,
// Need to set fixed height here so popup is positioned correctly
height: 340,
},
grid: { outline: 0 },
noMargins: { margin: 0 },
searchRow: { marginTop: theme.spacing(2) },
listRow: {
background: `${theme.palette.background.paper} no-repeat`,
position: "relative",
margin: theme.spacing(0, -2),
maxWidth: `calc(100% + ${theme.spacing(4)})`,
"&::before, &::after": {
content: '""',
position: "absolute",
top: 0,
left: 0,
right: 0,
zIndex: 9,
display: "block",
height: 16,
background: `linear-gradient(to bottom, ${theme.palette.background.paper}, rgba(255, 255, 255, 0))`,
},
"&::after": {
top: "auto",
bottom: 0,
background: `linear-gradient(to top, ${theme.palette.background.paper}, rgba(255, 255, 255, 0))`,
},
},
list: () => {
let maxHeightDeductions = 0;
maxHeightDeductions -= 64; // search box
maxHeightDeductions -= 48; // multiple
maxHeightDeductions += 8; // footer padding
return {
padding: theme.spacing(2, 0),
overflowY: "auto" as "auto",
// height: `calc(340px - ${-maxHeightDeductions}px)`,
height: 340 + maxHeightDeductions,
};
},
checkboxContainer: { minWidth: theme.spacing(36 / 8) },
checkbox: {
padding: theme.spacing(6 / 8, 9 / 8),
"&:hover": { background: "transparent" },
},
divider: { margin: theme.spacing(0, 2, 0, 6.5) },
footerRow: { marginBottom: theme.spacing(2) },
selectedRow: {
"$listRow + &": { marginTop: -theme.spacing(1) },
"$footerRow + &": { marginTop: -theme.spacing(2) },
marginBottom: 0,
"& > div": { height: 48 },
},
selectAllButton: { marginRight: -theme.spacing(1) },
selectedNum: { fontFeatureSettings: '"tnum"' },
})
);
export default useStyles;

View File

@@ -0,0 +1,164 @@
import { lazy, Suspense, useState } from "react";
import _get from "lodash/get";
import stringify from "json-stable-stringify-without-jsonify";
import {
Stepper,
Step,
StepButton,
StepContent,
Stack,
Grid,
Switch,
TextField,
FormControl,
FormLabel,
FormControlLabel,
RadioGroup,
Radio,
Typography,
InputLabel,
Link,
Checkbox,
FormHelperText,
Fab,
} from "@mui/material";
import SteppedAccordion from "@src/components/SteppedAccordion";
import FieldSkeleton from "@src/components/SideDrawer/Form/FieldSkeleton";
import CodeEditorHelper from "@src/components/CodeEditor/CodeEditorHelper";
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
/* eslint-disable import/no-webpack-loader-syntax */
import connectorDefs from "!!raw-loader!./connector.d.ts";
import { useProjectContext } from "@src/contexts/ProjectContext";
import { WIKI_LINKS } from "@src/constants/externalLinks";
import { useAppContext } from "@src/contexts/AppContext";
import { baseFunction } from "./utils";
//import typeDefs from "!!raw-loader!./types.d.ts";
const CodeEditor = lazy(
() =>
import("@src/components/CodeEditor" /* webpackChunkName: "CodeEditor" */)
);
// external service requirement
// Web service URL : url
// Results key path :resultsKey
// Primary Key : primaryKey
// Title Key : titleKey
// rowy managed service
// function that takes query & user then returns a list of objects with an array of objects
// Primary Key : primaryKey
// label Key : labelKey
// single select or multiselect
const diagnosticsOptions = {
noSemanticValidation: false,
noSyntaxValidation: false,
noSuggestionDiagnostics: true,
};
export default function Settings({ config, onChange }) {
const { projectId } = useAppContext();
return (
<>
<div>
<Typography variant="subtitle2">Connector Function</Typography>
<Suspense fallback={<FieldSkeleton height={200} />}>
<CodeEditor
minHeight={200}
value={config.connectorFn ?? baseFunction}
onChange={onChange("connectorFn")}
diagnosticsOptions={diagnosticsOptions}
extraLibs={[connectorDefs]}
/>
</Suspense>
<CodeEditorHelper
docLink={WIKI_LINKS.fieldTypesConnector + "#examples"}
additionalVariables={[]}
/>
</div>
<FormControl>
{/* <InputLabel variant="filled">Primary Key</InputLabel> */}
<TextField
id="elementId"
label="ID"
placeholder="id"
name="id"
value={config.elementId}
fullWidth
onChange={(e) => onChange("elementId")(e.target.value)}
helperText={
<>
The key that will be used to uniquely identify the selected
option.{" "}
<Link
href={WIKI_LINKS.fieldTypesConnector + "#api"}
target="_blank"
rel="noopener noreferrer"
>
Learn more
<InlineOpenInNewIcon />
</Link>
</>
}
/>
</FormControl>
<FormControl>
{/* <InputLabel variant="filled">Title Key</InputLabel> */}
<TextField
id="labelFormatter"
label="Label Formatter"
placeholder="{{id}} - {{name}}"
name="labelFormatter"
value={config.labelFormatter}
fullWidth
onChange={(e) => onChange("labelFormatter")(e.target.value)}
helperText={
<>
The field key or template that will be used to display the
selected option.{" "}
<Link
href={WIKI_LINKS.fieldTypesConnector + "#api"}
target="_blank"
rel="noopener noreferrer"
>
Learn more
<InlineOpenInNewIcon />
</Link>
</>
}
/>
</FormControl>
<FormControl>
<Grid container>
<InputLabel variant="filled">
Allow for multiple item selection
</InputLabel>
<Switch
checked={config.multiple}
onChange={(e) => onChange("multiple")(e.target.checked)}
/>
</Grid>
</FormControl>
<FormControl>
{config.multiple && (
<>
<TextField
type="number"
id="max"
label="Maximum"
placeholder="Maximum"
name="max"
value={config.max}
fullWidth
onChange={(e) => onChange("max")(e.target.value)}
helperText="The maximum number of items that can be selected, or 0 for no limit."
/>
</>
)}
</FormControl>
</>
);
}

View File

@@ -0,0 +1,74 @@
import { Controller } from "react-hook-form";
import { ISideDrawerFieldProps } from "../types";
import { get } from "lodash";
import { useTheme, Grid, Chip } from "@mui/material";
import ConnectServiceSelect from "./Select";
import { getLabel } from "./utils";
export default function ConnectService({
column,
control,
disabled,
docRef,
}: ISideDrawerFieldProps) {
const theme = useTheme();
const config = column.config ?? {};
const displayKey = config.titleKey ?? config.primaryKey;
return (
<Controller
control={control}
name={column.key}
render={({ field: { onChange, onBlur, value } }) => {
const handleDelete = (id: any) => () => {
// if (multiple)
onChange(value.filter((v) => get(v, config.elementId) !== id));
// else form.setFieldValue(field.name, []);
};
return (
<>
{!disabled && (
<ConnectServiceSelect
column={column}
value={value}
onChange={onChange}
docRef={docRef}
TextFieldProps={{
label: "",
hiddenLabel: true,
fullWidth: true,
onBlur,
SelectProps: {
renderValue: () => `${value?.length ?? 0} selected`,
},
}}
/>
)}
{Array.isArray(value) && (
<Grid container spacing={0.5} style={{ marginTop: 2 }}>
{value.map((item) => {
const key = get(item, config.elementId);
console.log(key, item);
return (
<Grid item key={key}>
<Chip
component="li"
label={getLabel(config, item)}
onDelete={disabled ? undefined : handleDelete(key)}
/>
</Grid>
);
})}
</Grid>
)}
</>
);
}}
/>
);
}

View File

@@ -0,0 +1,22 @@
type ConnectorUser = {
timestamp: Date;
displayName: string;
email: string;
uid: string;
emailVerified: boolean;
photoURL: string;
roles: string[];
};
type ConnectorContext = {
row: Row;
ref: FirebaseFirestore.DocumentReference;
storage: firebasestorage.Storage;
db: FirebaseFirestore.Firestore;
auth: firebaseauth.BaseAuth;
query: string;
user: ConnectorUser;
};
type ConnectorResult = any[];
type Connector = (
context: ConnectorContext
) => Promise<ConnectorResult> | ActionResult;

View File

@@ -0,0 +1,42 @@
import { lazy } from "react";
import { IFieldConfig, FieldType } from "@src/components/fields/types";
import withPopoverCell from "../_withTableCell/withPopoverCell";
import ConnectorIcon from "@mui/icons-material/Cable";
import BasicCell from "../_BasicCell/BasicCellNull";
import InlineCell from "./InlineCell";
import NullEditor from "@src/components/Table/editors/NullEditor";
const PopoverCell = lazy(
() =>
import("./PopoverCell" /* webpackChunkName: "PopoverCell-ConnectService" */)
);
const SideDrawerField = lazy(
() =>
import(
"./SideDrawerField" /* webpackChunkName: "SideDrawerField-ConnectService" */
)
);
const Settings = lazy(
() => import("./Settings" /* webpackChunkName: "Settings-ConnectService" */)
);
export const config: IFieldConfig = {
type: FieldType.connector,
name: "Connector",
group: "Connection",
dataType: "any",
initialValue: "",
initializable: true,
icon: <ConnectorIcon />,
description:
"Connects to any table or API to fetch a list of results based on a text query or row data.",
TableCell: withPopoverCell(BasicCell, InlineCell, PopoverCell, {
anchorOrigin: { horizontal: "left", vertical: "bottom" },
transparent: true,
}),
TableEditor: NullEditor as any,
SideDrawerField,
requireConfiguration: true,
settings: Settings,
};
export default config;

View File

@@ -0,0 +1,5 @@
type ConnectService = (request: {
query: string;
row: any;
user: any;
}) => Promise<any[]>;

View File

@@ -0,0 +1,21 @@
import { replacer } from "@src/utils/fns";
import _get from "lodash/get";
export const sanitiseValue = (value: any) => {
if (value === undefined || value === null || value === "") return [];
else return value as string[];
};
export const baseFunction = `const connectorFn: Connector = async ({query, row, user}) => {
// TODO: Implement your service function here
return [];
};`;
export const getLabel = (config, row) => {
if (!config.labelFormatter) {
return `⚠️ needs configuration`;
} else if (config.labelFormatter.includes("{{")) {
return config.labelFormatter.replace(/\{\{(.*?)\}\}/g, replacer(row));
} else {
return _get(row, config.labelFormatter);
}
};

View File

@@ -30,6 +30,7 @@ const useStyles = makeStyles((theme) =>
root: {
padding: theme.spacing(0, 0.5, 0, 1),
outline: "none",
height: "100%",
},
dragActive: {
backgroundColor: alpha(

View File

@@ -39,6 +39,7 @@ import UpdatedAt from "./UpdatedAt";
import User from "./User";
import Id from "./Id";
import Status from "./Status";
import Connector from "./Connector";
import { TableColumn } from "../Table";
// Export field configs in order for FieldsDropdown
@@ -68,6 +69,7 @@ export const FIELDS: IFieldConfig[] = [
Image_,
File_,
// CONNECTION
Connector,
SubTable,
ConnectTable,
ConnectService,

View File

@@ -36,6 +36,7 @@ const WIKI_PATHS = {
fieldTypesSupportedFields: "/field-types/supported-fields",
fieldTypesDerivative: "/field-types/derivative",
fieldTypesConnectTable: "/field-types/connect-table",
fieldTypesConnector: "/field-types/connector",
fieldTypesConnectService: "/field-types/connect-service",
fieldTypesAction: "/field-types/action",
fieldTypesAdd: "/field-types/add",

View File

@@ -26,6 +26,7 @@ export enum FieldType {
file = "FILE",
// CONNECTION
subTable = "SUB_TABLE",
connector = "CONNECTOR",
connectTable = "DOCUMENT_SELECT",
connectService = "SERVICE_SELECT",
// CODE

View File

@@ -81,15 +81,18 @@ export const AppProvider: React.FC = ({ children }) => {
};
useEffect(() => {
auth.onAuthStateChanged((auth) => {
setCurrentUser(auth);
if (auth)
if (auth) {
auth.getIdTokenResult(true).then((results) => {
setCurrentUser(auth);
setAuthToken(results.token);
setUserRoles(
Array.isArray(results.claims.roles) ? results.claims.roles : []
);
setUserClaims(results.claims);
});
} else {
setCurrentUser(auth);
}
});
}, []);

View File

@@ -6,9 +6,6 @@ import _find from "lodash/find";
import firebase from "firebase/app";
import { compare } from "compare-versions";
import { Button } from "@mui/material";
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
import useTable, { TableActions, TableState } from "@src/hooks/useTable";
import useSettings from "@src/hooks/useSettings";
import { useAppContext } from "./AppContext";
@@ -123,6 +120,7 @@ export const ProjectContextProvider: React.FC = ({ children }) => {
const { enqueueSnackbar } = useSnackbar();
const { tableState, tableActions } = useTable();
const [tables, setTables] = useState<IProjectContext["tables"]>();
const [settings, settingsActions] = useSettings();
const table = _find(tables, (table) => table.id === tableState.config.id);
@@ -166,7 +164,12 @@ export const ProjectContextProvider: React.FC = ({ children }) => {
() =>
Array.isArray(tables)
? Array.from(
new Set(tables.reduce((a, c) => [...a, ...c.roles], ["ADMIN"]))
new Set(
tables.reduce(
(a, c) => [...a, ...c.roles],
["ADMIN", "EDITOR", "VIEWER"]
)
)
)
: [],
[tables]
@@ -373,8 +376,8 @@ export const ProjectContextProvider: React.FC = ({ children }) => {
const { service, ...rest } = args;
const authToken = await getAuthToken();
const serviceUrl = service
? settings.doc.services[service]
: settings.doc.rowyRunUrl;
? settings.doc?.services[service]
: settings.doc?.rowyRunUrl;
if (serviceUrl) {
return rowyRun({

View File

@@ -310,14 +310,19 @@ const useTableData = () => {
let ref = db.collection(path).doc();
if (typeof id === "string") ref = db.collection(path).doc(id);
else if (id?.type === "smaller")
ref = db
.collection(path)
.doc(
decrementId(
rows.find((r) => !r._rowy_outOfOrder)?.id ?? "zzzzzzzzzzzzzzzzzzzz"
)
);
else if (id?.type === "smaller") {
let prevId =
rows.find((r) => !r._rowy_outOfOrder)?.id ?? "zzzzzzzzzzzzzzzzzzzz";
if (
tableState.orderBy?.length !== 0 ||
tableState.filters?.length !== 0
) {
const query = await db.collection(tableState.path).limit(1).get();
prevId = query.empty ? "zzzzzzzzzzzzzzzzzzzz" : query.docs[0].id;
}
ref = db.collection(path).doc(decrementId(prevId));
}
const newId = ref.id;
const missingRequiredFields = requiredFields

View File

@@ -78,10 +78,10 @@ export default function HomePage() {
data: null,
});
const handleCreateTable = () =>
const handleCreateTable = (data?: null | (Table & { tableType: string })) =>
setSettingsDialogState({
mode: TableSettingsDialogModes.create,
data: null,
data: data || null,
});
const [settingsDocState] = useDoc(
@@ -107,7 +107,7 @@ export default function HomePage() {
<Fab
color="secondary"
aria-label="Create table"
onClick={handleCreateTable}
onClick={() => handleCreateTable()}
sx={{
zIndex: "speedDial",
position: "fixed",

View File

@@ -195,3 +195,9 @@ export const isTargetInsideBox = (target, box) => {
const boxRect = box.getBoundingClientRect();
return targetRect.y < boxRect.y + boxRect.height;
};
export const replacer = (data: any) => (m: string, key: string) => {
const objKey = key.split(":")[0];
const defaultValue = key.split(":")[1] || "";
return _get(data, objKey, defaultValue);
};

100
yarn.lock
View File

@@ -2429,38 +2429,38 @@
"@monaco-editor/loader" "^1.2.0"
prop-types "^15.7.2"
"@mui/base@5.0.0-alpha.70":
version "5.0.0-alpha.70"
resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-alpha.70.tgz#e280ee3b69d86034f2cff445161747940129d576"
integrity sha512-8UZWhz1JYuQnPkAbC37cl4aL1JyNWZ08wDXlp57W7fYQp5xFpBP/7p56AcWg2qG9CNJP0IlFg2Wp4md1v2l4iA==
"@mui/base@5.0.0-alpha.72":
version "5.0.0-alpha.72"
resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-alpha.72.tgz#551d64402ee5065cf81fd1388a3e7ab8c426fe3e"
integrity sha512-WCAooa9eqbsC68LhyKtDBRumH4hV1eRZ0A3SDKFHSwYG9fCOdsFv/H1dIYRJM0rwD45bMnuDiG3Qmx7YsTiptw==
dependencies:
"@babel/runtime" "^7.17.2"
"@emotion/is-prop-valid" "^1.1.2"
"@mui/utils" "^5.4.4"
"@popperjs/core" "^2.4.4"
"@popperjs/core" "^2.11.3"
clsx "^1.1.1"
prop-types "^15.7.2"
react-is "^17.0.2"
"@mui/icons-material@^5.4.4":
version "5.4.4"
resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.4.4.tgz#0dc7b4e68cbbdfc675f09f0763be1100aad910af"
integrity sha512-7zoRpjO8vsd+bPvXq6rtXu0V8Saj70X09dtTQogZmxQKabrYW3g7+Yym7SCRA7IYVF3ysz2AvdQrGD1P/sGepg==
"@mui/icons-material@^5.5.1":
version "5.5.1"
resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.5.1.tgz#848a57972617411370775980cbc6990588d4aafb"
integrity sha512-40f68p5+Yhq3dCn3QYHqQt5RETPyR3AkDw+fma8PtcjqvZ+d+jF84kFmT6NqwA3he7TlwluEtkyAmPzUE4uPdA==
dependencies:
"@babel/runtime" "^7.17.2"
"@mui/lab@^5.0.0-alpha.71":
version "5.0.0-alpha.71"
resolved "https://registry.yarnpkg.com/@mui/lab/-/lab-5.0.0-alpha.71.tgz#6be1e648b63316dd628c0179b418adfd4c0a5c22"
integrity sha512-ScGfSsiYa2XZl+TYEgDFoCv1DoXoWNQwyJBbDlapacEw10wGmY6sgMkCjsPhpuabgC5FVOaV5k30OxG7cZKXJQ==
"@mui/lab@^5.0.0-alpha.73":
version "5.0.0-alpha.73"
resolved "https://registry.yarnpkg.com/@mui/lab/-/lab-5.0.0-alpha.73.tgz#f1b81682be155d87d492f64202ada8b7fe83be08"
integrity sha512-10Uj0Atc7gBTXKX4VV38P6RdqTQrJZxcl3HeEcytIO1S3NAGfc7gZ3Hdpnhtj5U8kcRJZZPH9LtrBbMZzxU/1A==
dependencies:
"@babel/runtime" "^7.17.2"
"@date-io/date-fns" "^2.13.1"
"@date-io/dayjs" "^2.13.1"
"@date-io/luxon" "^2.13.1"
"@date-io/moment" "^2.13.1"
"@mui/base" "5.0.0-alpha.70"
"@mui/system" "^5.4.4"
"@mui/base" "5.0.0-alpha.72"
"@mui/system" "^5.5.1"
"@mui/utils" "^5.4.4"
clsx "^1.1.1"
prop-types "^15.7.2"
@@ -2468,19 +2468,19 @@
react-transition-group "^4.4.2"
rifm "^0.12.1"
"@mui/material@^5.4.4":
version "5.4.4"
resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.4.4.tgz#2652a07085bf107da590007286336640f605055e"
integrity sha512-VDJC7GzO1HTFqfMe2zwvaW/sRhABBJXFkKEv5gO3uXx7x9fdwJHQr4udU7NWZCUdOcx9Y0h3BsAILLefYq+WPw==
"@mui/material@^5.5.1":
version "5.5.1"
resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.5.1.tgz#9ca89a8b32afd59c843a5bc0332b0786cf9bf1d0"
integrity sha512-bJSYgymgSZ7btPTNnWFrr2EmGoVQc4A/0WLfP/ESY2dxnhnbFDwt7twiOKmJp3u84YXriEDt5v9EZQLf7A+y0Q==
dependencies:
"@babel/runtime" "^7.17.2"
"@mui/base" "5.0.0-alpha.70"
"@mui/system" "^5.4.4"
"@mui/types" "^7.1.2"
"@mui/base" "5.0.0-alpha.72"
"@mui/system" "^5.5.1"
"@mui/types" "^7.1.3"
"@mui/utils" "^5.4.4"
"@types/react-transition-group" "^4.4.4"
clsx "^1.1.1"
csstype "^3.0.10"
csstype "^3.0.11"
hoist-non-react-statics "^3.3.2"
prop-types "^15.7.2"
react-is "^17.0.2"
@@ -2504,18 +2504,18 @@
"@emotion/cache" "^11.7.1"
prop-types "^15.7.2"
"@mui/styles@^5.4.4":
version "5.4.4"
resolved "https://registry.yarnpkg.com/@mui/styles/-/styles-5.4.4.tgz#e6a18f3528a494d782fd80e7feb01bd75a3570dd"
integrity sha512-w+9VIC1+JiPfF7osomX1j+aX7yyNNw8BnMYo6niK+zbwIxSYX/wcq4Jh7rlt6FSiaKL4Qi1uf7MPlNAhIxXq3g==
"@mui/styles@^5.5.1":
version "5.5.1"
resolved "https://registry.yarnpkg.com/@mui/styles/-/styles-5.5.1.tgz#cfd2b6dbdb4b2cb0e989568bb9cc45f5d7346d2a"
integrity sha512-mxwfjwTwPE+r7/U4Nn/QKPzJ2cIqmRuK3xu44Us613D5jqPeB/ftOsAy0OpCYAwpkUxmQIrRQiilQ8zE+f4rBQ==
dependencies:
"@babel/runtime" "^7.17.2"
"@emotion/hash" "^0.8.0"
"@mui/private-theming" "^5.4.4"
"@mui/types" "^7.1.2"
"@mui/types" "^7.1.3"
"@mui/utils" "^5.4.4"
clsx "^1.1.1"
csstype "^3.0.10"
csstype "^3.0.11"
hoist-non-react-statics "^3.3.2"
jss "^10.8.2"
jss-plugin-camel-case "^10.8.2"
@@ -2527,24 +2527,24 @@
jss-plugin-vendor-prefixer "^10.8.2"
prop-types "^15.7.2"
"@mui/system@^5.4.4":
version "5.4.4"
resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.4.4.tgz#cd5d3d35c75594abd88708e715b5e39a8874ff51"
integrity sha512-Zjbztq2o/VRuRRCWjG44juRrPKYLQMqtQpMHmMttGu5BnvK6PAPW3WOY0r1JCAwLhbd8Kug9nyhGQYKETjo+tQ==
"@mui/system@^5.5.1":
version "5.5.1"
resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.5.1.tgz#1f2b2a8c5542db6176e3b5a8ed12aea602cdeb81"
integrity sha512-2hynI4hN8304hOCT8sc4knJviwUUYJ7XK3mXwQ0nagVGOPnWSOad/nYADm7K0vdlCeUXLIbDbe7oNN3Kaiu2kA==
dependencies:
"@babel/runtime" "^7.17.2"
"@mui/private-theming" "^5.4.4"
"@mui/styled-engine" "^5.4.4"
"@mui/types" "^7.1.2"
"@mui/types" "^7.1.3"
"@mui/utils" "^5.4.4"
clsx "^1.1.1"
csstype "^3.0.10"
csstype "^3.0.11"
prop-types "^15.7.2"
"@mui/types@^7.1.2":
version "7.1.2"
resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.1.2.tgz#4f3678ae77a7a3efab73b6e040469cc6df2144ac"
integrity sha512-SD7O1nVzqG+ckQpFjDhXPZjRceB8HQFHEvdLLrPhlJy4lLbwEBbxK74Tj4t6Jgk0fTvLJisuwOutrtYe9P/xBQ==
"@mui/types@^7.1.3":
version "7.1.3"
resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.1.3.tgz#d7636f3046110bcccc63e6acfd100e2ad9ca712a"
integrity sha512-DDF0UhMBo4Uezlk+6QxrlDbchF79XG6Zs0zIewlR4c0Dt6GKVFfUtzPtHCH1tTbcSlq/L2bGEdiaoHBJ9Y1gSA==
"@mui/utils@^5.4.4":
version "5.4.4"
@@ -2620,10 +2620,10 @@
schema-utils "^2.6.5"
source-map "^0.7.3"
"@popperjs/core@^2.4.4":
version "2.10.2"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.2.tgz#0798c03351f0dea1a5a4cabddf26a55a7cbee590"
integrity sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==
"@popperjs/core@^2.11.3":
version "2.11.4"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.4.tgz#d8c7b8db9226d2d7664553a0741ad7d0397ee503"
integrity sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg==
"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
version "1.1.2"
@@ -6252,10 +6252,10 @@ cssstyle@^2.3.0:
dependencies:
cssom "~0.3.6"
csstype@^3.0.10:
version "3.0.10"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.10.tgz#2ad3a7bed70f35b965707c092e5f30b327c290e5"
integrity sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==
csstype@^3.0.11:
version "3.0.11"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.11.tgz#d66700c5eacfac1940deb4e3ee5642792d85cd33"
integrity sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==
csstype@^3.0.2:
version "3.0.9"
@@ -11864,9 +11864,9 @@ minimatch@3.0.4, minimatch@^3.0.4:
brace-expansion "^1.1.7"
minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
version "1.2.6"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
minipass-collect@^1.0.2:
version "1.0.2"