mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-29 00:16:39 +01:00
Merge branch 'develop' into feature/rowy-706-table-upgrade
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
import { atom } from "jotai";
|
||||
import { findIndex } from "lodash-es";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
|
||||
import {
|
||||
tableColumnsOrderedAtom,
|
||||
tableColumnsReducer,
|
||||
updateTableSchemaAtom,
|
||||
tableSchemaAtom,
|
||||
} from "./table";
|
||||
import { ColumnConfig } from "@src/types/table";
|
||||
|
||||
@@ -14,6 +16,7 @@ export interface IAddColumnOptions {
|
||||
/** Index to add column at. If undefined, adds to end */
|
||||
index?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set function adds a column to tableSchema, to the end or by index.
|
||||
* Also fixes any issues with column indexes, so they go from 0 to length - 1
|
||||
@@ -52,6 +55,7 @@ export interface IUpdateColumnOptions {
|
||||
/** If passed, reorders the column to the index */
|
||||
index?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set function updates a column in tableSchema
|
||||
* @throws Error if column not found
|
||||
@@ -110,13 +114,50 @@ export const updateColumnAtom = atom(
|
||||
* ```
|
||||
*/
|
||||
export const deleteColumnAtom = atom(null, async (get, _set, key: string) => {
|
||||
const tableSchema = get(tableSchemaAtom);
|
||||
const tableColumnsOrdered = [...get(tableColumnsOrderedAtom)];
|
||||
const updateTableSchema = get(updateTableSchemaAtom);
|
||||
if (!updateTableSchema) throw new Error("Cannot update table schema");
|
||||
|
||||
const updatedColumns = tableColumnsOrdered
|
||||
.filter((c) => c.key !== key)
|
||||
.map((c) => {
|
||||
// remove column from derivatives listener fields
|
||||
if (c.type === FieldType.derivative) {
|
||||
return {
|
||||
...c,
|
||||
config: {
|
||||
...c.config,
|
||||
listenerFields:
|
||||
c.config?.listenerFields?.filter((f) => f !== key) ?? [],
|
||||
},
|
||||
};
|
||||
} else if (c.type === FieldType.action) {
|
||||
return {
|
||||
...c,
|
||||
config: {
|
||||
...c.config,
|
||||
requiredFields:
|
||||
c.config?.requiredFields?.filter((f) => f !== key) ?? [],
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return c;
|
||||
}
|
||||
})
|
||||
.reduce(tableColumnsReducer, {});
|
||||
|
||||
await updateTableSchema({ columns: updatedColumns }, [`columns.${key}`]);
|
||||
const updatedExtensionObjects = tableSchema?.extensionObjects?.map(
|
||||
(extension) => {
|
||||
return {
|
||||
...extension,
|
||||
requiredFields: extension.requiredFields.filter((f) => f !== key),
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
await updateTableSchema(
|
||||
{ columns: updatedColumns, extensionObjects: updatedExtensionObjects },
|
||||
[`columns.${key}`]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Typography,
|
||||
Divider,
|
||||
} from "@mui/material";
|
||||
import FilterIcon from "@mui/icons-material/FilterList";
|
||||
import LockOpenIcon from "@mui/icons-material/LockOpen";
|
||||
@@ -50,12 +51,18 @@ import {
|
||||
columnModalAtom,
|
||||
tableFiltersPopoverAtom,
|
||||
tableNextPageAtom,
|
||||
tableSchemaAtom,
|
||||
} from "@src/atoms/tableScope";
|
||||
import { FieldType } from "@src/constants/fields";
|
||||
import { getFieldProp } from "@src/components/fields";
|
||||
import { analytics, logEvent } from "@src/analytics";
|
||||
import { formatSubTableName, getTableSchemaPath } from "@src/utils/table";
|
||||
import {
|
||||
formatSubTableName,
|
||||
getTableBuildFunctionPathname,
|
||||
getTableSchemaPath,
|
||||
} from "@src/utils/table";
|
||||
import { runRoutes } from "@src/constants/runRoutes";
|
||||
import { useSnackLogContext } from "@src/contexts/SnackLogContext";
|
||||
|
||||
export interface IMenuModalProps {
|
||||
name: string;
|
||||
@@ -91,6 +98,8 @@ export default function ColumnMenu() {
|
||||
tableScope
|
||||
);
|
||||
const [tableNextPage] = useAtom(tableNextPageAtom, tableScope);
|
||||
const [tableSchema] = useAtom(tableSchemaAtom, tableScope);
|
||||
const snackLogContext = useSnackLogContext();
|
||||
|
||||
const [altPress] = useAtom(altPressAtom, projectScope);
|
||||
const { enqueueSnackbar, closeSnackbar } = useSnackbar();
|
||||
@@ -117,8 +126,42 @@ export default function ColumnMenu() {
|
||||
const userDocHiddenFields =
|
||||
userSettings.tables?.[formatSubTableName(tableId)]?.hiddenFields ?? [];
|
||||
|
||||
let referencedColumns: string[] = [];
|
||||
let referencedExtensions: string[] = [];
|
||||
Object.entries(tableSchema?.columns ?? {}).forEach(([key, c], index) => {
|
||||
if (
|
||||
c.config?.listenerFields?.includes(column.key) ||
|
||||
c.config?.requiredFields?.includes(column.key)
|
||||
) {
|
||||
referencedColumns.push(c.name);
|
||||
}
|
||||
});
|
||||
tableSchema?.extensionObjects?.forEach((extension) => {
|
||||
if (extension.requiredFields.includes(column.key)) {
|
||||
referencedExtensions.push(extension.name);
|
||||
}
|
||||
});
|
||||
const requireRebuild =
|
||||
referencedColumns.length || referencedExtensions.length;
|
||||
|
||||
const handleDeleteColumn = () => {
|
||||
deleteColumn(column.key);
|
||||
if (requireRebuild) {
|
||||
snackLogContext.requestSnackLog();
|
||||
rowyRun({
|
||||
route: runRoutes.buildFunction,
|
||||
body: {
|
||||
tablePath: tableSettings.collection,
|
||||
// pathname must match old URL format
|
||||
pathname: getTableBuildFunctionPathname(
|
||||
tableSettings.id,
|
||||
tableSettings.tableType
|
||||
),
|
||||
tableConfigPath: getTableSchemaPath(tableSettings),
|
||||
},
|
||||
});
|
||||
logEvent(analytics, "deployed_extensions");
|
||||
}
|
||||
logEvent(analytics, "delete_column", { type: column.type });
|
||||
handleClose();
|
||||
};
|
||||
@@ -360,8 +403,8 @@ export default function ColumnMenu() {
|
||||
icon: <ColumnRemoveIcon />,
|
||||
onClick: altPress
|
||||
? handleDeleteColumn
|
||||
: () =>
|
||||
confirm({
|
||||
: () => {
|
||||
return confirm({
|
||||
title: "Delete column?",
|
||||
body: (
|
||||
<>
|
||||
@@ -373,12 +416,39 @@ export default function ColumnMenu() {
|
||||
<Typography sx={{ mt: 1 }}>
|
||||
Key: <code style={{ userSelect: "all" }}>{column.key}</code>
|
||||
</Typography>
|
||||
{requireRebuild ? (
|
||||
<>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
{referencedColumns.length ? (
|
||||
<Typography sx={{ mt: 1 }}>
|
||||
This column will be removed as a dependency of the
|
||||
following columns:{" "}
|
||||
<Typography fontWeight="bold" component="span">
|
||||
{referencedColumns.join(", ")}
|
||||
</Typography>
|
||||
</Typography>
|
||||
) : null}
|
||||
{referencedExtensions.length ? (
|
||||
<Typography sx={{ mt: 1 }}>
|
||||
This column will be removed as a dependency from the
|
||||
following Extensions:{" "}
|
||||
<Typography fontWeight="bold" component="span">
|
||||
{referencedExtensions.join(", ")}
|
||||
</Typography>
|
||||
</Typography>
|
||||
) : null}
|
||||
<Typography sx={{ mt: 1, fontWeight: "bold" }}>
|
||||
You need to re-deploy this table’s cloud function.
|
||||
</Typography>
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
),
|
||||
confirm: "Delete",
|
||||
confirm: requireRebuild ? "Delete & re-deploy" : "Delete",
|
||||
confirmColor: "error",
|
||||
handleConfirm: handleDeleteColumn,
|
||||
}),
|
||||
});
|
||||
},
|
||||
color: "error" as "error",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { FallbackProps } from "react-error-boundary";
|
||||
import { useLocation, Link } from "react-router-dom";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import useOffline from "@src/hooks/useOffline";
|
||||
|
||||
import { Typography, Button } from "@mui/material";
|
||||
import ReloadIcon from "@mui/icons-material/Refresh";
|
||||
import InlineOpenInNewIcon from "@src/components/InlineOpenInNewIcon";
|
||||
import OfflineIcon from "@mui/icons-material/CloudOff";
|
||||
import { Tables as TablesIcon } from "@src/assets/icons";
|
||||
|
||||
import EmptyState, { IEmptyStateProps } from "@src/components/EmptyState";
|
||||
import AccessDenied from "@src/components/AccessDenied";
|
||||
|
||||
import { ROUTES } from "@src/constants/routes";
|
||||
import meta from "@root/package.json";
|
||||
|
||||
export const ERROR_TABLE_NOT_FOUND = "Table not found";
|
||||
import { EXTERNAL_LINKS } from "@src/constants/externalLinks";
|
||||
|
||||
export interface IErrorFallbackProps extends FallbackProps, IEmptyStateProps {}
|
||||
|
||||
@@ -43,9 +39,22 @@ export function ErrorFallbackContents({
|
||||
<Button
|
||||
size={props.basic ? "small" : "medium"}
|
||||
href={
|
||||
meta.repository.url.replace(".git", "") +
|
||||
"/issues/new?labels=bug&template=bug_report.md&title=Error: " +
|
||||
error.message.replace("\n", " ")
|
||||
EXTERNAL_LINKS.gitHub +
|
||||
"/discussions/new?" +
|
||||
new URLSearchParams({
|
||||
labels: "bug",
|
||||
category: "support-q-a",
|
||||
title: [
|
||||
"Error",
|
||||
(error as any).code,
|
||||
(error as any).status,
|
||||
error.message,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(": ")
|
||||
.replace(/\n/g, " "),
|
||||
body: "👉 **Please describe the steps that you took that led to this bug.**",
|
||||
}).toString()
|
||||
}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
@@ -57,37 +66,6 @@ export function ErrorFallbackContents({
|
||||
),
|
||||
};
|
||||
|
||||
if (error.message.startsWith(ERROR_TABLE_NOT_FOUND)) {
|
||||
if (isOffline) {
|
||||
renderProps = { Icon: OfflineIcon, message: "You’re offline" };
|
||||
} else {
|
||||
renderProps = {
|
||||
message: ERROR_TABLE_NOT_FOUND,
|
||||
description: (
|
||||
<>
|
||||
<Typography variant="inherit">
|
||||
Make sure you have the right ID
|
||||
</Typography>
|
||||
<code>
|
||||
{error.message.replace(ERROR_TABLE_NOT_FOUND + ": ", "")}
|
||||
</code>
|
||||
<Button
|
||||
size={props.basic ? "small" : "medium"}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
component={Link}
|
||||
to={ROUTES.tables}
|
||||
startIcon={<TablesIcon />}
|
||||
onClick={() => resetErrorBoundary()}
|
||||
>
|
||||
All tables
|
||||
</Button>
|
||||
</>
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (error.message.startsWith("Loading chunk")) {
|
||||
if (isOffline) {
|
||||
renderProps = { Icon: OfflineIcon, message: "You’re offline" };
|
||||
|
||||
@@ -1,134 +1,244 @@
|
||||
import { useMemo } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { format } from "date-fns";
|
||||
import { find } from "lodash-es";
|
||||
import { find, isEqual } from "lodash-es";
|
||||
import MDEditor from "@uiw/react-md-editor";
|
||||
|
||||
import { Box, IconButton, Stack, Typography } from "@mui/material";
|
||||
import {
|
||||
Box,
|
||||
IconButton,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
|
||||
import EditIcon from "@mui/icons-material/EditOutlined";
|
||||
import EditOffIcon from "@mui/icons-material/EditOffOutlined";
|
||||
|
||||
import { tableScope, tableSettingsAtom } from "@src/atoms/tableScope";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import { useAtom } from "jotai";
|
||||
import {
|
||||
projectScope,
|
||||
tablesAtom,
|
||||
tableSettingsDialogAtom,
|
||||
updateTableAtom,
|
||||
userRolesAtom,
|
||||
} from "@src/atoms/projectScope";
|
||||
import { DATE_TIME_FORMAT } from "@src/constants/dates";
|
||||
import SaveState from "@src/components/SideDrawer/SaveState";
|
||||
|
||||
export default function Details() {
|
||||
const [userRoles] = useAtom(userRolesAtom, projectScope);
|
||||
const [tableSettings] = useAtom(tableSettingsAtom, tableScope);
|
||||
const [tables] = useAtom(tablesAtom, projectScope);
|
||||
const openTableSettingsDialog = useSetAtom(
|
||||
tableSettingsDialogAtom,
|
||||
projectScope
|
||||
);
|
||||
const [updateTable] = useAtom(updateTableAtom, projectScope);
|
||||
const theme = useTheme();
|
||||
|
||||
const settings = useMemo(
|
||||
() => find(tables, ["id", tableSettings.id]),
|
||||
[tables, tableSettings.id]
|
||||
);
|
||||
|
||||
const { description, details, _createdBy } = settings ?? {};
|
||||
|
||||
const [editDescription, setEditDescription] = useState(false);
|
||||
const [localDescription, setLocalDescription] = useState(description ?? "");
|
||||
const [localDetails, setLocalDetails] = useState(details ?? "");
|
||||
const [editDetails, setEditDetails] = useState(false);
|
||||
const [mdFullScreen, setMdFullScreen] = useState(false);
|
||||
|
||||
const [saveState, setSaveState] = useState<
|
||||
"" | "unsaved" | "saving" | "saved"
|
||||
>("");
|
||||
|
||||
if (!settings) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const editButton = userRoles.includes("ADMIN") && (
|
||||
<IconButton
|
||||
aria-label="Edit"
|
||||
onClick={() =>
|
||||
openTableSettingsDialog({
|
||||
mode: "update",
|
||||
data: settings,
|
||||
})
|
||||
}
|
||||
disabled={!openTableSettingsDialog || settings.id.includes("/")}
|
||||
>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
);
|
||||
const handleSave = async () => {
|
||||
setSaveState("saving");
|
||||
await updateTable!({
|
||||
...settings,
|
||||
description: localDescription,
|
||||
details: localDetails,
|
||||
});
|
||||
setSaveState("saved");
|
||||
};
|
||||
|
||||
const { description, details, _createdBy } = settings;
|
||||
const isAdmin = userRoles.includes("ADMIN");
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="column"
|
||||
gap={3}
|
||||
sx={{
|
||||
paddingTop: 3,
|
||||
paddingRight: 3,
|
||||
paddingBottom: 5,
|
||||
"& > .MuiGrid-root": {
|
||||
position: "relative",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* Description */}
|
||||
<Stack direction="column" gap={1}>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="flex-end"
|
||||
>
|
||||
<Typography variant="subtitle1" component="h3">
|
||||
Description
|
||||
</Typography>
|
||||
{editButton}
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
paddingTop: 3,
|
||||
paddingRight: 4,
|
||||
position: "fixed",
|
||||
right: 0,
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
<SaveState state={saveState} />
|
||||
</Box>
|
||||
<Stack
|
||||
gap={3}
|
||||
sx={{
|
||||
paddingTop: 3,
|
||||
paddingRight: 3,
|
||||
paddingBottom: 5,
|
||||
}}
|
||||
>
|
||||
{/* Description */}
|
||||
<Stack gap={1}>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="flex-end"
|
||||
>
|
||||
<Typography variant="subtitle1" component="h3">
|
||||
Description
|
||||
</Typography>
|
||||
{isAdmin && (
|
||||
<IconButton
|
||||
aria-label="Edit description"
|
||||
onClick={() => {
|
||||
setEditDescription(!editDescription);
|
||||
}}
|
||||
sx={{ top: 4 }}
|
||||
>
|
||||
{editDescription ? <EditOffIcon /> : <EditIcon />}
|
||||
</IconButton>
|
||||
)}
|
||||
</Stack>
|
||||
{editDescription ? (
|
||||
<TextField
|
||||
sx={{
|
||||
color: "text.secondary",
|
||||
}}
|
||||
autoFocus={true}
|
||||
value={localDescription}
|
||||
onChange={(e) => {
|
||||
setLocalDescription(e.target.value);
|
||||
saveState !== "unsaved" && setSaveState("unsaved");
|
||||
}}
|
||||
onBlur={() =>
|
||||
isEqual(description, localDescription)
|
||||
? setSaveState("")
|
||||
: handleSave()
|
||||
}
|
||||
rows={2}
|
||||
minRows={2}
|
||||
/>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{localDescription ? localDescription : "No description"}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{description ? description : "No description"}
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
{/* Details */}
|
||||
<Stack direction="column" gap={1}>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="flex-end"
|
||||
>
|
||||
<Typography variant="subtitle1" component="h3">
|
||||
Details
|
||||
</Typography>
|
||||
{editButton}
|
||||
</Stack>
|
||||
{!details ? (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
No details
|
||||
</Typography>
|
||||
) : (
|
||||
{/* Details */}
|
||||
<Stack gap={1}>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="flex-end"
|
||||
>
|
||||
<Typography variant="subtitle1" component="h3">
|
||||
Details
|
||||
</Typography>
|
||||
{isAdmin && (
|
||||
<IconButton
|
||||
aria-label="Edit details"
|
||||
onClick={() => {
|
||||
setEditDetails(!editDetails);
|
||||
}}
|
||||
sx={{ top: 4 }}
|
||||
>
|
||||
{editDetails ? <EditOffIcon /> : <EditIcon />}
|
||||
</IconButton>
|
||||
)}
|
||||
</Stack>
|
||||
<Box
|
||||
data-color-mode={theme.palette.mode}
|
||||
sx={{
|
||||
color: "text.secondary",
|
||||
...theme.typography.body2,
|
||||
"& .w-md-editor": {
|
||||
backgroundColor: `${theme.palette.action.input} !important`,
|
||||
},
|
||||
"& .w-md-editor-fullscreen": {
|
||||
backgroundColor: `${theme.palette.background.paper} !important`,
|
||||
},
|
||||
"& .w-md-editor-toolbar": {
|
||||
display: "flex",
|
||||
gap: 1,
|
||||
},
|
||||
"& .w-md-editor-toolbar > ul": {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
"& .w-md-editor-toolbar > ul:first-of-type": {
|
||||
overflowX: "auto",
|
||||
marginRight: theme.spacing(1),
|
||||
},
|
||||
"& :is(h1, h2, h3, h4, h5, h6)": {
|
||||
marginY: `${theme.spacing(1.5)} !important`,
|
||||
borderBottom: "none !important",
|
||||
},
|
||||
"& details summary": {
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MDEditor.Markdown source={details} />
|
||||
{editDetails ? (
|
||||
<MDEditor
|
||||
style={{ margin: 1 }}
|
||||
value={localDetails}
|
||||
preview={mdFullScreen ? "live" : "edit"}
|
||||
commandsFilter={(command) => {
|
||||
if (command.name === "fullscreen") {
|
||||
command.execute = () => setMdFullScreen(!mdFullScreen);
|
||||
}
|
||||
return command;
|
||||
}}
|
||||
textareaProps={{
|
||||
autoFocus: true,
|
||||
onChange: (e) => {
|
||||
setLocalDetails(e.target.value ?? "");
|
||||
saveState !== "unsaved" && setSaveState("unsaved");
|
||||
},
|
||||
onBlur: () =>
|
||||
isEqual(details, localDetails)
|
||||
? setSaveState("")
|
||||
: handleSave(),
|
||||
}}
|
||||
/>
|
||||
) : !localDetails ? (
|
||||
<Typography variant="body2">No details</Typography>
|
||||
) : (
|
||||
<MDEditor.Markdown source={localDetails} />
|
||||
)}
|
||||
</Box>
|
||||
</Stack>
|
||||
{/* Table Audits */}
|
||||
{_createdBy && (
|
||||
<Stack>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
component="div"
|
||||
style={{ whiteSpace: "normal" }}
|
||||
>
|
||||
Created by{" "}
|
||||
<Typography variant="caption" color="text.primary">
|
||||
{_createdBy.displayName}
|
||||
</Typography>{" "}
|
||||
on{" "}
|
||||
<Typography variant="caption" color="text.primary">
|
||||
{format(_createdBy.timestamp.toDate(), DATE_TIME_FORMAT)}
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{/* Table Audits */}
|
||||
{_createdBy && (
|
||||
<Stack>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
component="div"
|
||||
style={{ whiteSpace: "normal" }}
|
||||
>
|
||||
Created by{" "}
|
||||
<Typography variant="caption" color="text.primary">
|
||||
{_createdBy.displayName}
|
||||
</Typography>{" "}
|
||||
on{" "}
|
||||
<Typography variant="caption" color="text.primary">
|
||||
{format(_createdBy.timestamp.toDate(), DATE_TIME_FORMAT)}
|
||||
</Typography>
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -137,7 +137,11 @@ export default function Export({
|
||||
<ColumnSelect
|
||||
value={columns.map((x) => x.key)}
|
||||
onChange={handleChange(setColumns)}
|
||||
filterColumns={(column) => DOWNLOADABLE_COLUMNS.includes(column.type)}
|
||||
filterColumns={(column) =>
|
||||
column.type === FieldType.derivative
|
||||
? DOWNLOADABLE_COLUMNS.includes(column.config?.renderFieldType)
|
||||
: DOWNLOADABLE_COLUMNS.includes(column.type)
|
||||
}
|
||||
label="Columns to export"
|
||||
labelPlural="columns"
|
||||
TextFieldProps={{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
import { isEqual } from "lodash-es";
|
||||
import { isEqual, isUndefined } from "lodash-es";
|
||||
import { ITableModalProps } from "@src/components/TableModals";
|
||||
|
||||
import Modal from "@src/components/Modal";
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
} from "@src/atoms/tableScope";
|
||||
import { useSnackLogContext } from "@src/contexts/SnackLogContext";
|
||||
|
||||
import { emptyExtensionObject, IExtension, ExtensionType } from "./utils";
|
||||
import { runRoutes } from "@src/constants/runRoutes";
|
||||
import { analytics, logEvent } from "@src/analytics";
|
||||
import {
|
||||
@@ -31,6 +30,14 @@ import {
|
||||
getTableBuildFunctionPathname,
|
||||
} from "@src/utils/table";
|
||||
|
||||
import {
|
||||
emptyExtensionObject,
|
||||
IExtension,
|
||||
ExtensionType,
|
||||
IRuntimeOptions,
|
||||
} from "./utils";
|
||||
import RuntimeOptions from "./RuntimeOptions";
|
||||
|
||||
export default function ExtensionsModal({ onClose }: ITableModalProps) {
|
||||
const [currentUser] = useAtom(currentUserAtom, projectScope);
|
||||
const [rowyRun] = useAtom(rowyRunAtom, projectScope);
|
||||
@@ -39,12 +46,25 @@ export default function ExtensionsModal({ onClose }: ITableModalProps) {
|
||||
const [tableSchema] = useAtom(tableSchemaAtom, tableScope);
|
||||
const [updateTableSchema] = useAtom(updateTableSchemaAtom, tableScope);
|
||||
|
||||
const currentExtensionObjects = (tableSchema.extensionObjects ??
|
||||
[]) as IExtension[];
|
||||
const [localExtensionsObjects, setLocalExtensionsObjects] = useState(
|
||||
currentExtensionObjects
|
||||
tableSchema.extensionObjects ?? []
|
||||
);
|
||||
|
||||
const [localRuntimeOptions, setLocalRuntimeOptions] = useState(
|
||||
tableSchema.runtimeOptions ?? {}
|
||||
);
|
||||
|
||||
const errors = {
|
||||
runtimeOptions: {
|
||||
timeoutSeconds:
|
||||
!isUndefined(localRuntimeOptions.timeoutSeconds) &&
|
||||
!(
|
||||
localRuntimeOptions.timeoutSeconds! > 0 &&
|
||||
localRuntimeOptions.timeoutSeconds! <= 540
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
const [openMigrationGuide, setOpenMigrationGuide] = useState(false);
|
||||
useEffect(() => {
|
||||
if (tableSchema.sparks) setOpenMigrationGuide(true);
|
||||
@@ -57,7 +77,9 @@ export default function ExtensionsModal({ onClose }: ITableModalProps) {
|
||||
} | null>(null);
|
||||
|
||||
const snackLogContext = useSnackLogContext();
|
||||
const edited = !isEqual(currentExtensionObjects, localExtensionsObjects);
|
||||
const edited =
|
||||
!isEqual(tableSchema.extensionObjects ?? [], localExtensionsObjects) ||
|
||||
!isEqual(tableSchema.runtimeOptions ?? {}, localRuntimeOptions);
|
||||
|
||||
const handleClose = (
|
||||
_setOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||
@@ -70,7 +92,8 @@ export default function ExtensionsModal({ onClose }: ITableModalProps) {
|
||||
cancel: "Keep",
|
||||
handleConfirm: () => {
|
||||
_setOpen(false);
|
||||
setLocalExtensionsObjects(currentExtensionObjects);
|
||||
setLocalExtensionsObjects(tableSchema.extensionObjects ?? []);
|
||||
setLocalRuntimeOptions(tableSchema.runtimeOptions ?? {});
|
||||
onClose();
|
||||
},
|
||||
});
|
||||
@@ -79,15 +102,18 @@ export default function ExtensionsModal({ onClose }: ITableModalProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveExtensions = async (callback?: Function) => {
|
||||
const handleSave = async (callback?: Function) => {
|
||||
if (updateTableSchema)
|
||||
await updateTableSchema({ extensionObjects: localExtensionsObjects });
|
||||
await updateTableSchema({
|
||||
extensionObjects: localExtensionsObjects,
|
||||
runtimeOptions: localRuntimeOptions,
|
||||
});
|
||||
if (callback) callback();
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleSaveDeploy = async () => {
|
||||
handleSaveExtensions(() => {
|
||||
handleSave(() => {
|
||||
try {
|
||||
snackLogContext.requestSnackLog();
|
||||
rowyRun({
|
||||
@@ -132,6 +158,13 @@ export default function ExtensionsModal({ onClose }: ITableModalProps) {
|
||||
setExtensionModal(null);
|
||||
};
|
||||
|
||||
const handleUpdateRuntimeOptions = (update: IRuntimeOptions) => {
|
||||
setLocalRuntimeOptions((runtimeOptions) => ({
|
||||
...runtimeOptions,
|
||||
...update,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleUpdateActive = (index: number, active: boolean) => {
|
||||
setLocalExtensionsObjects(
|
||||
localExtensionsObjects.map((extensionObject, i) => {
|
||||
@@ -217,24 +250,31 @@ export default function ExtensionsModal({ onClose }: ITableModalProps) {
|
||||
/>
|
||||
}
|
||||
children={
|
||||
<ExtensionList
|
||||
extensions={localExtensionsObjects}
|
||||
handleUpdateActive={handleUpdateActive}
|
||||
handleEdit={handleEdit}
|
||||
handleDuplicate={handleDuplicate}
|
||||
handleDelete={handleDelete}
|
||||
/>
|
||||
<>
|
||||
<ExtensionList
|
||||
extensions={localExtensionsObjects}
|
||||
handleUpdateActive={handleUpdateActive}
|
||||
handleEdit={handleEdit}
|
||||
handleDuplicate={handleDuplicate}
|
||||
handleDelete={handleDelete}
|
||||
/>
|
||||
<RuntimeOptions
|
||||
runtimeOptions={localRuntimeOptions}
|
||||
handleUpdate={handleUpdateRuntimeOptions}
|
||||
errors={errors.runtimeOptions}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
actions={{
|
||||
primary: {
|
||||
children: "Save & Deploy",
|
||||
onClick: handleSaveDeploy,
|
||||
disabled: !edited,
|
||||
disabled: !edited || errors.runtimeOptions.timeoutSeconds,
|
||||
},
|
||||
secondary: {
|
||||
children: "Save",
|
||||
onClick: () => handleSaveExtensions(),
|
||||
disabled: !edited,
|
||||
onClick: () => handleSave(),
|
||||
disabled: !edited || errors.runtimeOptions.timeoutSeconds,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
117
src/components/TableModals/ExtensionsModal/RuntimeOptions.tsx
Normal file
117
src/components/TableModals/ExtensionsModal/RuntimeOptions.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
import { useState } from "react";
|
||||
import { useAtom, useSetAtom } from "jotai";
|
||||
|
||||
import {
|
||||
Accordion,
|
||||
AccordionDetails,
|
||||
AccordionSummary,
|
||||
Button,
|
||||
Grid,
|
||||
InputAdornment,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import { ChevronDown } from "@src/assets/icons";
|
||||
import MultiSelect from "@rowy/multiselect";
|
||||
|
||||
import {
|
||||
compatibleRowyRunVersionAtom,
|
||||
projectScope,
|
||||
rowyRunModalAtom,
|
||||
} from "@src/atoms/projectScope";
|
||||
|
||||
import { IRuntimeOptions } from "./utils";
|
||||
|
||||
export default function RuntimeOptions({
|
||||
runtimeOptions,
|
||||
handleUpdate,
|
||||
errors,
|
||||
}: {
|
||||
runtimeOptions: IRuntimeOptions;
|
||||
handleUpdate: (runtimeOptions: IRuntimeOptions) => void;
|
||||
errors: { timeoutSeconds: boolean };
|
||||
}) {
|
||||
const [compatibleRowyRunVersion] = useAtom(
|
||||
compatibleRowyRunVersionAtom,
|
||||
projectScope
|
||||
);
|
||||
const openRowyRunModal = useSetAtom(rowyRunModalAtom, projectScope);
|
||||
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const isCompatibleRowyRun = compatibleRowyRunVersion({ minVersion: "1.6.4" });
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
sx={{
|
||||
padding: 0,
|
||||
boxShadow: "none",
|
||||
backgroundImage: "inherit",
|
||||
backgroundColor: "inherit",
|
||||
}}
|
||||
expanded={isCompatibleRowyRun && expanded}
|
||||
>
|
||||
<AccordionSummary
|
||||
sx={{ padding: 0 }}
|
||||
expandIcon={
|
||||
isCompatibleRowyRun ? (
|
||||
<ChevronDown />
|
||||
) : (
|
||||
<Button>Update Rowy Run</Button>
|
||||
)
|
||||
}
|
||||
onClick={() =>
|
||||
isCompatibleRowyRun
|
||||
? setExpanded(!expanded)
|
||||
: openRowyRunModal({
|
||||
version: "1.6.4",
|
||||
feature: "Runtime options",
|
||||
})
|
||||
}
|
||||
>
|
||||
<Typography variant="subtitle1">Runtime options</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails sx={{ padding: 0 }}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={6}>
|
||||
<MultiSelect
|
||||
label="Memory Allocated"
|
||||
value={runtimeOptions.memory ?? "256MB"}
|
||||
onChange={(value) => handleUpdate({ memory: value ?? "256MB" })}
|
||||
multiple={false}
|
||||
options={["128MB", "256MB", "512MB", "1GB", "2GB", "4GB", "8GB"]}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={6}>
|
||||
<TextField
|
||||
value={runtimeOptions.timeoutSeconds ?? 60}
|
||||
label="Timeout"
|
||||
fullWidth
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<InputAdornment position="end">seconds</InputAdornment>
|
||||
),
|
||||
}}
|
||||
onChange={(e) =>
|
||||
!isNaN(Number(e.target.value)) &&
|
||||
handleUpdate({
|
||||
timeoutSeconds: Number(e.target.value),
|
||||
})
|
||||
}
|
||||
inputProps={{
|
||||
inputMode: "numeric",
|
||||
}}
|
||||
error={errors.timeoutSeconds}
|
||||
helperText={
|
||||
errors.timeoutSeconds
|
||||
? "Timeout must be an integer between 1 and 540"
|
||||
: "The maximum timeout that can be specified is 9 mins (540 seconds)"
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
@@ -52,6 +52,12 @@ export interface IExtension {
|
||||
trackedFields?: string[];
|
||||
}
|
||||
|
||||
// https://firebase.google.com/docs/functions/manage-functions#set_runtime_options
|
||||
export interface IRuntimeOptions {
|
||||
memory?: "128MB" | "256MB" | "512MB" | "1GB" | "2GB" | "4GB" | "8GB";
|
||||
timeoutSeconds?: number;
|
||||
}
|
||||
|
||||
export const triggerTypes: ExtensionTrigger[] = ["create", "update", "delete"];
|
||||
|
||||
const extensionBodyTemplate = {
|
||||
|
||||
@@ -14,19 +14,40 @@ export default function TableDetails({ ...props }) {
|
||||
{props.label ?? ""}
|
||||
</InputLabel>
|
||||
<Box
|
||||
data-color-mode={theme.palette.mode}
|
||||
sx={{
|
||||
color: "text.secondary",
|
||||
...theme.typography.body2,
|
||||
"& .w-md-editor": {
|
||||
backgroundColor: `${theme.palette.action.input} !important`,
|
||||
},
|
||||
"& .w-md-editor-fullscreen": {
|
||||
backgroundColor: `${theme.palette.background.paper} !important`,
|
||||
},
|
||||
"& .w-md-editor-toolbar": {
|
||||
display: "flex",
|
||||
gap: 1,
|
||||
},
|
||||
"& .w-md-editor-toolbar > ul": {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
"& .w-md-editor-toolbar > ul:first-of-type": {
|
||||
overflowX: "auto",
|
||||
marginRight: theme.spacing(1),
|
||||
},
|
||||
"& :is(h1, h2, h3, h4, h5, h6)": {
|
||||
marginY: `${theme.spacing(1.5)} !important`,
|
||||
borderBottom: "none !important",
|
||||
},
|
||||
"& details summary": {
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<MDEditor
|
||||
style={{ margin: 1 }}
|
||||
preview="live"
|
||||
toolbarHeight={52}
|
||||
height={150}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
|
||||
@@ -4,27 +4,38 @@ import { Tooltip, Button, ButtonProps } from "@mui/material";
|
||||
export interface ITableToolbarButtonProps extends Partial<ButtonProps> {
|
||||
title: string;
|
||||
icon: React.ReactNode;
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
export const TableToolbarButton = forwardRef(function TableToolbarButton_(
|
||||
{ title, icon, ...props }: ITableToolbarButtonProps,
|
||||
{ title, icon, tooltip, ...props }: ITableToolbarButtonProps,
|
||||
ref: React.Ref<HTMLButtonElement>
|
||||
) {
|
||||
// https://mui.com/material-ui/react-tooltip/#accessibility
|
||||
const tooltipIsDescription = Boolean(tooltip);
|
||||
|
||||
const button = (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="small"
|
||||
style={{ minWidth: 40, height: 32, padding: 0 }}
|
||||
{...props}
|
||||
{...(tooltipIsDescription
|
||||
? {
|
||||
"aria-label": title, // Actual button label
|
||||
title: tooltip, // Tooltip text, used to describe button e.g. why it’s disabled
|
||||
}
|
||||
: {})}
|
||||
ref={ref}
|
||||
>
|
||||
{icon}
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<Tooltip title={title}>
|
||||
<span>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
size="small"
|
||||
style={{ minWidth: 40, height: 32, padding: 0 }}
|
||||
aria-label={title}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
{icon}
|
||||
</Button>
|
||||
</span>
|
||||
<Tooltip title={tooltip || title} describeChild={tooltipIsDescription}>
|
||||
{props.disabled ? <span title="">{button}</span> : button}
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,15 @@ import { Stack } from "@mui/material";
|
||||
|
||||
import ActionFab from "./ActionFab";
|
||||
import { sanitiseCallableName, isUrl } from "./utils";
|
||||
import { get } from "lodash-es";
|
||||
|
||||
export const getActionName = (column: any) => {
|
||||
const config = get(column, "config");
|
||||
if (!get(config, "customName.enabled")) {
|
||||
return get(column, "name");
|
||||
}
|
||||
return get(config, "customName.actionName") || get(column, "name");
|
||||
};
|
||||
|
||||
export default function Action({
|
||||
column,
|
||||
@@ -28,7 +37,7 @@ export default function Action({
|
||||
) : hasRan ? (
|
||||
value.status
|
||||
) : (
|
||||
sanitiseCallableName(column.key)
|
||||
sanitiseCallableName(getActionName(column))
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -559,20 +559,46 @@ const Settings = ({ config, onChange, fieldName }: ISettingsProps) => {
|
||||
title: "Customization",
|
||||
content: (
|
||||
<>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={config.customIcons?.enabled}
|
||||
<Stack>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={config.customName?.enabled}
|
||||
onChange={(e) =>
|
||||
onChange("customName.enabled")(e.target.checked)
|
||||
}
|
||||
name="customName.enabled"
|
||||
/>
|
||||
}
|
||||
label="Customize label for action"
|
||||
style={{ marginLeft: -11 }}
|
||||
/>
|
||||
{config.customName?.enabled && (
|
||||
<TextField
|
||||
id="customName.actionName"
|
||||
value={get(config, "customName.actionName")}
|
||||
onChange={(e) =>
|
||||
onChange("customIcons.enabled")(e.target.checked)
|
||||
onChange("customName.actionName")(e.target.value)
|
||||
}
|
||||
name="customIcons.enabled"
|
||||
/>
|
||||
}
|
||||
label="Customize button icons with emoji"
|
||||
style={{ marginLeft: -11 }}
|
||||
/>
|
||||
|
||||
label="Action name:"
|
||||
className="labelHorizontal"
|
||||
inputProps={{ style: { width: "10ch" } }}
|
||||
></TextField>
|
||||
)}
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={config.customIcons?.enabled}
|
||||
onChange={(e) =>
|
||||
onChange("customIcons.enabled")(e.target.checked)
|
||||
}
|
||||
name="customIcons.enabled"
|
||||
/>
|
||||
}
|
||||
label="Customize button icons with emoji"
|
||||
style={{ marginLeft: -11 }}
|
||||
/>
|
||||
</Stack>
|
||||
{config.customIcons?.enabled && (
|
||||
<Grid container spacing={2} sx={{ mt: { xs: 0, sm: -1 } }}>
|
||||
<Grid item xs={12} sm={true}>
|
||||
|
||||
@@ -10,6 +10,7 @@ import ActionFab from "./ActionFab";
|
||||
import { tableScope, tableRowsAtom } from "@src/atoms/tableScope";
|
||||
import { fieldSx, getFieldId } from "@src/components/SideDrawer/utils";
|
||||
import { sanitiseCallableName, isUrl } from "./utils";
|
||||
import { getActionName } from "./TableCell";
|
||||
|
||||
export default function Action({
|
||||
column,
|
||||
@@ -58,7 +59,7 @@ export default function Action({
|
||||
) : hasRan ? (
|
||||
value.status
|
||||
) : (
|
||||
sanitiseCallableName(column.key)
|
||||
sanitiseCallableName(getActionName(column))
|
||||
)}
|
||||
</Box>
|
||||
|
||||
|
||||
21
src/components/fields/Color/filters.ts
Normal file
21
src/components/fields/Color/filters.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { IFilterOperator } from "@src/components/fields/types";
|
||||
|
||||
export const filterOperators: IFilterOperator[] = [
|
||||
{
|
||||
label: "is",
|
||||
secondaryLabel: "==",
|
||||
value: "color-equal",
|
||||
},
|
||||
{
|
||||
label: "is not",
|
||||
secondaryLabel: "!=",
|
||||
value: "color-not-equal",
|
||||
},
|
||||
];
|
||||
|
||||
export const valueFormatter = (value: any) => {
|
||||
if (value && value.hex) {
|
||||
return value.hex.toString();
|
||||
}
|
||||
return "";
|
||||
};
|
||||
@@ -5,6 +5,7 @@ import { toColor } from "react-color-palette";
|
||||
|
||||
import ColorIcon from "@mui/icons-material/Colorize";
|
||||
import DisplayCell from "./DisplayCell";
|
||||
import { filterOperators, valueFormatter } from "./filters";
|
||||
|
||||
const EditorCell = lazy(
|
||||
() => import("./EditorCell" /* webpackChunkName: "EditorCell-Color" */)
|
||||
@@ -28,6 +29,10 @@ export const config: IFieldConfig = {
|
||||
disablePadding: true,
|
||||
}),
|
||||
SideDrawerField,
|
||||
filter: {
|
||||
operators: filterOperators,
|
||||
valueFormatter,
|
||||
},
|
||||
csvImportParser: (value: string) => {
|
||||
try {
|
||||
const obj = JSON.parse(value);
|
||||
|
||||
@@ -40,13 +40,14 @@ export default function Json({
|
||||
const [editor, setEditor] = useAtom(jsonEditorAtom, projectScope);
|
||||
const [codeValid, setCodeValid] = useState(true);
|
||||
|
||||
const sanitizedValue =
|
||||
const baseValue =
|
||||
value !== undefined && isValidJson(value)
|
||||
? value
|
||||
: column.config?.isArray
|
||||
? []
|
||||
: {};
|
||||
const formattedJson = stringify(sanitizedValue, { space: 2 });
|
||||
const formattedJson = stringify(baseValue, { space: 2 });
|
||||
const sanitizedValue = JSON.parse(formattedJson);
|
||||
|
||||
if (disabled)
|
||||
return (
|
||||
|
||||
@@ -2,24 +2,7 @@ import React, { forwardRef } from "react";
|
||||
import { IDisplayCellProps } from "@src/components/fields/types";
|
||||
|
||||
import MuiRating, { RatingProps as MuiRatingProps } from "@mui/material/Rating";
|
||||
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 />;
|
||||
};
|
||||
import Icon from "./Icon";
|
||||
|
||||
export const Rating = forwardRef(function Rating(
|
||||
{
|
||||
@@ -55,14 +38,15 @@ export const Rating = forwardRef(function Rating(
|
||||
if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(e.key))
|
||||
e.stopPropagation();
|
||||
}}
|
||||
icon={getStateIcon(column.config)}
|
||||
icon={<Icon config={column.config} isEmpty={false} />}
|
||||
emptyIcon={<Icon config={column.config} isEmpty={true} />}
|
||||
size="small"
|
||||
readOnly={disabled}
|
||||
emptyIcon={getStateOutline(column.config)}
|
||||
max={max}
|
||||
precision={precision}
|
||||
sx={{ mx: -0.25 }}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default Rating;
|
||||
|
||||
31
src/components/fields/Rating/Icon.tsx
Normal file
31
src/components/fields/Rating/Icon.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import RatingIcon from "@mui/icons-material/Star";
|
||||
import RatingOutlineIcon from "@mui/icons-material/StarBorder";
|
||||
import { get } from "lodash-es";
|
||||
|
||||
export interface IIconProps {
|
||||
config: any;
|
||||
isEmpty: boolean;
|
||||
}
|
||||
|
||||
export default function Icon({ config, isEmpty }: IIconProps) {
|
||||
if (isEmpty) {
|
||||
return getStateOutline(config);
|
||||
} else {
|
||||
return getStateIcon(config);
|
||||
}
|
||||
}
|
||||
|
||||
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 />;
|
||||
}
|
||||
console.log(get(config, "customIcons.rating"));
|
||||
return get(config, "customIcons.rating") || <RatingIcon />;
|
||||
};
|
||||
const getStateOutline = (config: any) => {
|
||||
if (!get(config, "customIcons.enabled")) {
|
||||
return <RatingOutlineIcon />;
|
||||
}
|
||||
return get(config, "customIcons.rating") || <RatingOutlineIcon />;
|
||||
};
|
||||
@@ -1,11 +1,17 @@
|
||||
import { ISettingsProps } from "@src/components/fields/types";
|
||||
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 {
|
||||
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";
|
||||
import Icon from "./Icon";
|
||||
|
||||
export default function Settings({ onChange, config }: ISettingsProps) {
|
||||
return (
|
||||
@@ -18,10 +24,13 @@ export default function Settings({ onChange, config }: ISettingsProps) {
|
||||
fullWidth
|
||||
error={false}
|
||||
onChange={(e) => {
|
||||
let input = parseInt(e.target.value) || 0
|
||||
if (input > 20) { input = 20 }
|
||||
let input = parseInt(e.target.value) || 0;
|
||||
if (input > 20) {
|
||||
input = 20;
|
||||
}
|
||||
onChange("max")(input);
|
||||
}}
|
||||
inputProps={{ min: 1, max: 20 }}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
@@ -68,28 +77,26 @@ export default function Settings({ onChange, config }: ISettingsProps) {
|
||||
<TextField
|
||||
id="customIcons.rating"
|
||||
value={get(config, "customIcons.rating")}
|
||||
onChange={(e) =>
|
||||
onChange("customIcons.rating")(e.target.value)
|
||||
}
|
||||
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"
|
||||
<MuiRating
|
||||
aria-label="Preview of the rating field with custom icon"
|
||||
name="Preview"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
icon={get(config, "customIcons.rating") || <RatingIcon />}
|
||||
icon={<Icon config={config} isEmpty={false} />}
|
||||
size="small"
|
||||
emptyIcon={get(config, "customIcons.rating") || <RatingOutlineIcon />}
|
||||
emptyIcon={<Icon config={config} isEmpty={true} />}
|
||||
max={get(config, "max")}
|
||||
precision={get(config, "precision")}
|
||||
sx={{ pt: 0.5 }}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +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 { getStateIcon, getStateOutline } from "./DisplayCell";
|
||||
import { fieldSx } from "@src/components/SideDrawer/utils";
|
||||
import Icon from "./Icon";
|
||||
|
||||
export default function Rating({
|
||||
column,
|
||||
@@ -27,8 +27,8 @@ export default function Rating({
|
||||
onChange(newValue);
|
||||
onSubmit();
|
||||
}}
|
||||
icon={getStateIcon(column.config)}
|
||||
emptyIcon={getStateOutline(column.config)}
|
||||
icon={<Icon config={column.config} isEmpty={false} />}
|
||||
emptyIcon={<Icon config={column.config} isEmpty={true} />}
|
||||
size="small"
|
||||
max={max}
|
||||
precision={precision}
|
||||
|
||||
@@ -15,8 +15,8 @@ export const EXTERNAL_LINKS = {
|
||||
twitter: "https://twitter.com/rowyio",
|
||||
productHunt: "https://www.producthunt.com/products/rowy-2",
|
||||
|
||||
rowyRun: meta.repository.url.replace(".git", "Run"),
|
||||
rowyRunGitHub: meta.repository.url.replace(".git", "Run"),
|
||||
rowyRun: meta.repository.url.replace("rowy.git", "backend"),
|
||||
rowyRunGitHub: meta.repository.url.replace("rowy.git", "backend"),
|
||||
// prettier-ignore
|
||||
rowyRunDeploy: `https://deploy.cloud.run/?git_repo=${meta.repository.url.replace(".git", "Run")}.git`,
|
||||
|
||||
|
||||
@@ -376,12 +376,20 @@ export const tableFiltersToFirestoreFilters = (filters: TableFilter[]) => {
|
||||
} else if (filter.operator === "id-equal") {
|
||||
firestoreFilters.push(where(documentId(), "==", filter.value));
|
||||
continue;
|
||||
} else if (filter.operator === "color-equal") {
|
||||
firestoreFilters.push(
|
||||
where(filter.key.concat(".hex"), "==", filter.value.hex.toString())
|
||||
);
|
||||
continue;
|
||||
} else if (filter.operator === "color-not-equal") {
|
||||
firestoreFilters.push(
|
||||
where(filter.key.concat(".hex"), "!=", filter.value.hex.toString())
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
firestoreFilters.push(
|
||||
where(filter.key, filter.operator as WhereFilterOp, filter.value)
|
||||
);
|
||||
}
|
||||
|
||||
return firestoreFilters;
|
||||
};
|
||||
|
||||
@@ -8,11 +8,11 @@ import reportWebVitals from "./reportWebVitals";
|
||||
const container = document.getElementById("root")!;
|
||||
const root = createRoot(container);
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<Providers>
|
||||
<App />
|
||||
</Providers>
|
||||
</StrictMode>
|
||||
// <StrictMode>
|
||||
<Providers>
|
||||
<App />
|
||||
</Providers>
|
||||
// </StrictMode>
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
import { lazy, Suspense } from "react";
|
||||
import { useAtom, Provider } from "jotai";
|
||||
import { DebugAtoms } from "@src/atoms/utils";
|
||||
import { useParams, useOutlet } from "react-router-dom";
|
||||
import { useParams, useOutlet, Link } from "react-router-dom";
|
||||
import { ErrorBoundary } from "react-error-boundary";
|
||||
import { find, isEmpty } from "lodash-es";
|
||||
import useOffline from "@src/hooks/useOffline";
|
||||
|
||||
import ErrorFallback, {
|
||||
ERROR_TABLE_NOT_FOUND,
|
||||
} from "@src/components/ErrorFallback";
|
||||
import { Typography, Button } from "@mui/material";
|
||||
|
||||
import ErrorFallback from "@src/components/ErrorFallback";
|
||||
import TableSourceFirestore from "@src/sources/TableSourceFirestore";
|
||||
import TableToolbarSkeleton from "@src/components/TableToolbar/TableToolbarSkeleton";
|
||||
import TableSkeleton from "@src/components/Table/TableSkeleton";
|
||||
import EmptyState from "@src/components/EmptyState";
|
||||
import OfflineIcon from "@mui/icons-material/CloudOff";
|
||||
import { Tables as TablesIcon } from "@src/assets/icons";
|
||||
|
||||
import {
|
||||
projectScope,
|
||||
@@ -25,6 +29,7 @@ import {
|
||||
tableSettingsAtom,
|
||||
} from "@src/atoms/tableScope";
|
||||
import { SyncAtomValue } from "@src/atoms/utils";
|
||||
import { ROUTES } from "@src/constants/routes";
|
||||
import useDocumentTitle from "@src/hooks/useDocumentTitle";
|
||||
|
||||
// prettier-ignore
|
||||
@@ -41,6 +46,7 @@ export default function ProvidedTablePage() {
|
||||
const [currentUser] = useAtom(currentUserAtom, projectScope);
|
||||
const [projectSettings] = useAtom(projectSettingsAtom, projectScope);
|
||||
const [tables] = useAtom(tablesAtom, projectScope);
|
||||
const isOffline = useOffline();
|
||||
|
||||
const tableSettings = find(tables, ["id", id]);
|
||||
useDocumentTitle(projectId, tableSettings ? tableSettings.name : "Not found");
|
||||
@@ -54,7 +60,41 @@ export default function ProvidedTablePage() {
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
throw new Error(ERROR_TABLE_NOT_FOUND + ": " + id);
|
||||
if (isOffline) {
|
||||
return (
|
||||
<EmptyState
|
||||
role="alert"
|
||||
fullScreen
|
||||
Icon={OfflineIcon}
|
||||
message="You’re offline"
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<EmptyState
|
||||
role="alert"
|
||||
fullScreen
|
||||
message="Table not found"
|
||||
description={
|
||||
<>
|
||||
<Typography variant="inherit">
|
||||
Make sure you have the right ID
|
||||
</Typography>
|
||||
<code>{id}</code>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
component={Link}
|
||||
to={ROUTES.tables}
|
||||
startIcon={<TablesIcon />}
|
||||
>
|
||||
All tables
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1381,11 +1381,12 @@ export const components = (theme: Theme): ThemeOptions => {
|
||||
|
||||
MuiRating: {
|
||||
styleOverrides: {
|
||||
iconFilled: { color: theme.palette.text.secondary },
|
||||
icon: {
|
||||
// https://github.com/mui/material-ui/issues/32557
|
||||
"& .MuiSvgIcon-root": { pointerEvents: "auto" },
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
iconEmpty: { opacity: 0.38 },
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
16
src/types/table.d.ts
vendored
16
src/types/table.d.ts
vendored
@@ -4,7 +4,10 @@ import type {
|
||||
DocumentData,
|
||||
DocumentReference,
|
||||
} from "firebase/firestore";
|
||||
import { IExtension } from "@src/components/TableModals/ExtensionsModal/utils";
|
||||
import {
|
||||
IExtension,
|
||||
IRuntimeOptions,
|
||||
} from "@src/components/TableModals/ExtensionsModal/utils";
|
||||
import { IWebhook } from "@src/components/TableModals/WebhooksModal/utils";
|
||||
|
||||
/**
|
||||
@@ -104,6 +107,7 @@ export type TableSchema = {
|
||||
extensionObjects?: IExtension[];
|
||||
compiledExtension?: string;
|
||||
webhooks?: IWebhook[];
|
||||
runtimeOptions?: IRuntimeOptions;
|
||||
|
||||
/** @deprecated Migrate to Extensions */
|
||||
sparks?: string;
|
||||
@@ -146,7 +150,11 @@ export type ColumnConfig = {
|
||||
/** Regex used in CellValidation */
|
||||
validationRegex: string;
|
||||
/** FieldType to render for Derivative fields */
|
||||
renderFieldType: FieldType;
|
||||
renderFieldType?: FieldType;
|
||||
/** Used in Derivative fields */
|
||||
listenerFields?: string[];
|
||||
/** Used in Derivative and Action fields */
|
||||
requiredFields?: string[];
|
||||
/** For sub-table fields */
|
||||
parentLabel: string[];
|
||||
|
||||
@@ -167,7 +175,9 @@ export type TableFilter = {
|
||||
| "date-before-equal"
|
||||
| "date-after-equal"
|
||||
| "time-minute-equal"
|
||||
| "id-equal";
|
||||
| "id-equal"
|
||||
| "color-equal"
|
||||
| "color-not-equal";
|
||||
value: any;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user