Merge branch 'develop' of https://github.com/rowyio/rowy into multi-file-upload

This commit is contained in:
Han Tuerker
2022-11-12 00:46:22 +03:00
16 changed files with 254 additions and 84 deletions

View File

@@ -8,7 +8,7 @@ on:
env:
REACT_APP_FIREBASE_PROJECT_ID: rowyio
REACT_APP_FIREBASE_PROJECT_WEB_API_KEY:
"${{ secrets.FIREBASE_WEB_API_KEY_ROWYIO }}"
"${{ secrets.FIREBASE_WEB_API_KEY_TRYROWY }}"
CI: ""
jobs:
build_and_preview:
@@ -27,6 +27,6 @@ jobs:
with:
repoToken: "${{ secrets.GITHUB_TOKEN }}"
firebaseServiceAccount:
"${{ secrets.FIREBASE_SERVICE_ACCOUNT_ROWYIO }}"
"${{ secrets.FIREBASE_SERVICE_ACCOUNT_TRYROWY }}"
expires: 14d
projectId: rowyio

View File

@@ -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
@@ -112,13 +116,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}`]
);
});

View File

@@ -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 tables cloud function.
</Typography>
</>
) : null}
</>
),
confirm: "Delete",
confirm: requireRebuild ? "Delete & re-deploy" : "Delete",
confirmColor: "error",
handleConfirm: handleDeleteColumn,
}),
});
},
color: "error" as "error",
},
];

View File

@@ -1,22 +1,18 @@
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 { EXTERNAL_LINKS } from "@src/constants/externalLinks";
export const ERROR_TABLE_NOT_FOUND = "Table not found";
export interface IErrorFallbackProps extends FallbackProps, IEmptyStateProps {}
export function ErrorFallbackContents({
@@ -57,7 +53,7 @@ export function ErrorFallbackContents({
.filter(Boolean)
.join(": ")
.replace(/\n/g, " "),
body: "👉 **Please describe how to reproduce this bug here.**",
body: "👉 **Please describe the steps that you took that led to this bug.**",
}).toString()
}
target="_blank"
@@ -70,37 +66,6 @@ export function ErrorFallbackContents({
),
};
if (error.message.startsWith(ERROR_TABLE_NOT_FOUND)) {
if (isOffline) {
renderProps = { Icon: OfflineIcon, message: "Youre 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: "Youre offline" };

View File

@@ -111,7 +111,7 @@ export default function MenuContents({ onClose }: IMenuContentsProps) {
disabled:
selectedColumn.editable === false ||
!row ||
cellValue ||
cellValue === undefined ||
getFieldProp("group", selectedColumn.type) === "Auditing",
onClick: altPress
? handleClearValue

View File

@@ -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";
@@ -56,11 +56,12 @@ export default function ExtensionsModal({ onClose }: ITableModalProps) {
const errors = {
runtimeOptions: {
timeoutSeconds: !(
!!localRuntimeOptions.timeoutSeconds &&
localRuntimeOptions.timeoutSeconds > 0 &&
localRuntimeOptions.timeoutSeconds <= 540
),
timeoutSeconds:
!isUndefined(localRuntimeOptions.timeoutSeconds) &&
!(
localRuntimeOptions.timeoutSeconds! > 0 &&
localRuntimeOptions.timeoutSeconds! <= 540
),
},
};

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

View File

@@ -7,6 +7,7 @@ import ColorIcon from "@mui/icons-material/Colorize";
import BasicCell from "@src/components/fields/_BasicCell/BasicCellNull";
import InlineCell from "./InlineCell";
import NullEditor from "@src/components/Table/editors/NullEditor";
import { filterOperators, valueFormatter } from "./filters";
const PopoverCell = lazy(
() => import("./PopoverCell" /* webpackChunkName: "PopoverCell-Color" */)
@@ -31,6 +32,10 @@ export const config: IFieldConfig = {
}),
TableEditor: NullEditor as any,
SideDrawerField,
filter: {
operators: filterOperators,
valueFormatter
},
csvImportParser: (value: string) => {
try {
const obj = JSON.parse(value);

View File

@@ -0,0 +1,28 @@
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 />;
}

View File

@@ -1,11 +1,11 @@
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 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 (
@@ -80,9 +80,9 @@ export default function Settings({ onChange, config }: ISettingsProps) {
<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 }}

View File

@@ -3,8 +3,9 @@ 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 "./TableCell";
import { fieldSx } from "@src/components/SideDrawer/utils";
import Icon from "./Icon"
export default function Rating({
column,
@@ -28,8 +29,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}

View File

@@ -1,22 +1,9 @@
import { IHeavyCellProps } from "@src/components/fields/types";
import MuiRating from "@mui/material/Rating";
import RatingIcon from "@mui/icons-material/Star";
import RatingOutlineIcon from "@mui/icons-material/StarBorder"
import { get } from "lodash-es";
import Icon from "./Icon"
export const getStateIcon = (config: any) => {
// only use the config to get the custom rating icon if enabled via toggle
if (!get(config, "customIcons.enabled")) { return <RatingIcon /> }
return get(config, "customIcons.rating") || <RatingIcon />;
};
export const getStateOutline = (config: any) => {
if (!get(config, "customIcons.enabled")) { return <RatingOutlineIcon /> }
return get(config, "customIcons.rating") || <RatingOutlineIcon />;
}
export default function Rating({
row,
column,
@@ -42,11 +29,11 @@ export default function Rating({
name={`${row.id}-${column.key}`}
value={typeof value === "number" ? value : 0}
onClick={(e) => e.stopPropagation()}
icon={getStateIcon(column.config)}
icon={<Icon config={column.config} isEmpty={false} />}
size="small"
disabled={disabled}
onChange={(_, newValue) => onSubmit(newValue)}
emptyIcon={getStateOutline(column.config)}
emptyIcon={<Icon config={column.config} isEmpty={true} />}
max={max}
precision={precision}
sx={{ mx: -0.25 }}

View File

@@ -376,12 +376,16 @@ 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;
};

View File

@@ -1,17 +1,21 @@
import { 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 TablePage from "./TablePage";
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,
@@ -26,6 +30,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";
/**
@@ -39,6 +44,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");
@@ -52,7 +58,41 @@ export default function ProvidedTablePage() {
</>
);
} else {
throw new Error(ERROR_TABLE_NOT_FOUND + ": " + id);
if (isOffline) {
return (
<EmptyState
role="alert"
fullScreen
Icon={OfflineIcon}
message="Youre 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>
</>
}
/>
);
}
}
}

View File

@@ -1375,11 +1375,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 },
},
},

View File

@@ -149,6 +149,10 @@ export type ColumnConfig = {
};
/** FieldType to render for Derivative fields */
renderFieldType?: FieldType;
/** Used in Derivative fields */
listenerFields?: string[];
/** Used in Derivative and Action fields */
requiredFields?: string[];
/** For sub-table fields */
parentLabel?: string[];
@@ -169,7 +173,9 @@ export type TableFilter = {
| "date-before-equal"
| "date-after-equal"
| "time-minute-equal"
| "id-equal";
| "id-equal"
| "color-equal"
| "color-not-equal";
value: any;
};