add table tutorial at /tutorial/table

This commit is contained in:
Sidney Alcantara
2022-07-14 13:36:17 +10:00
parent 133a9d5dfa
commit 16c45e935d
29 changed files with 877 additions and 49 deletions

View File

@@ -44,6 +44,8 @@ const TablesPage = lazy(() => import("@src/pages/TablesPage" /* webpackChunkName
const ProvidedTablePage = lazy(() => import("@src/pages/Table/ProvidedTablePage" /* webpackChunkName: "ProvidedTablePage" */));
// prettier-ignore
const ProvidedSubTablePage = lazy(() => import("@src/pages/Table/ProvidedSubTablePage" /* webpackChunkName: "ProvidedSubTablePage" */));
// prettier-ignore
const TableTutorialPage = lazy(() => import("@src/pages/Table/TableTutorialPage" /* webpackChunkName: "TableTutorialPage" */));
// prettier-ignore
const FunctionPage = lazy(() => import("@src/pages/FunctionPage" /* webpackChunkName: "FunctionPage" */));
@@ -125,6 +127,11 @@ export default function App() {
<Route path=":id" element={<TableGroupRedirectPage />} />
</Route>
<Route
path={ROUTES.tableTutorial}
element={<TableTutorialPage />}
/>
<Route path={ROUTES.function}>
<Route
index

View File

@@ -100,6 +100,9 @@ export { FormatListChecks as Checklist };
import { FileTableBoxOutline } from "mdi-material-ui";
export { FileTableBoxOutline as Project };
import { TableColumn } from "mdi-material-ui";
export { TableColumn };
export * from "./AddRow";
export * from "./AddRowTop";
export * from "./ChevronDown";

View File

@@ -1,4 +1,5 @@
import { use100vh } from "react-div-100vh";
import clsx from "clsx";
import {
Grid,
@@ -40,7 +41,13 @@ export default function EmptyState({
if (basic)
return (
<Grid container alignItems="center" spacing={1} {...props}>
<Grid
container
alignItems="center"
spacing={1}
{...props}
className={clsx("empty-state", "empty-state--basic", props.className)}
>
<Grid item>
<Icon style={{ display: "block" }} />
</Grid>
@@ -66,6 +73,11 @@ export default function EmptyState({
textAlign: "center",
...props.style,
}}
className={clsx(
"empty-state",
"empty-state--full-screen",
props.className
)}
>
<Grid
item

View File

@@ -9,7 +9,7 @@ import { Project as ProjectIcon } from "@src/assets/icons";
import Modal, { IModalProps } from "@src/components/Modal";
import SteppedAccordion from "@src/components/SteppedAccordion";
import Progress from "./Progress";
import StepsProgress from "@src/components/StepsProgress";
import {
globalScope,
@@ -73,14 +73,18 @@ export default function GetStartedChecklist({
},
]}
>
<Progress sx={{ mb: 2 }} />
<StepsProgress value={1} steps={5} sx={{ mb: 2 }} />
<SteppedAccordion
steps={[
{
id: "workspace",
title: "Create a workspace",
labelButtonProps: { icon: <CheckedIcon color="success" /> },
labelButtonProps: {
icon: (
<CheckedIcon color="success" sx={{ color: "success.light" }} />
),
},
content: null,
},
{

View File

@@ -0,0 +1,44 @@
import { Box, BoxProps, Typography } from "@mui/material";
import { spreadSx } from "@src/utils/ui";
export interface IStepsProgressProps extends Partial<BoxProps> {
steps: number;
value: number;
}
export default function StepsProgress({
steps,
value,
sx,
...props
}: IStepsProgressProps) {
return (
<Box
{...props}
sx={[
{ display: "flex", alignItems: "center", gap: 0.5 },
...spreadSx(sx),
]}
>
<Typography
className="steps-progress__label"
sx={{ flex: 3, fontVariantNumeric: "tabular-nums" }}
>
{Math.min(Math.max(value, 0), steps)}/{steps}
</Typography>
{new Array(steps).fill(undefined).map((_, i) => (
<Box
key={i + 1}
sx={{
flex: 1,
borderRadius: 1,
height: 8,
bgcolor: i + 1 <= value ? "success.light" : "divider",
transition: (theme) => theme.transitions.create("background-color"),
}}
/>
))}
</Box>
);
}

View File

@@ -1,7 +1,8 @@
import { useAtom } from "jotai";
import MultiSelect, { MultiSelectProps } from "@rowy/multiselect";
import { Stack, StackProps, Typography } from "@mui/material";
import { Stack, StackProps, Typography, Chip } from "@mui/material";
import { TableColumn as TableColumnIcon } from "@src/assets/icons";
import { globalScope, altPressAtom } from "@src/atoms/globalScope";
import { tableScope, tableColumnsOrderedAtom } from "@src/atoms/tableScope";
@@ -53,7 +54,6 @@ export default function ColumnSelect({
TextFieldProps={{
...props.TextFieldProps,
SelectProps: {
...props.TextFieldProps?.SelectProps,
renderValue: () => {
if (Array.isArray(props.value) && props.value.length > 1)
return `${props.value.length} columns`;
@@ -72,6 +72,7 @@ export default function ColumnSelect({
value
);
},
...props.TextFieldProps?.SelectProps,
},
}}
/>
@@ -92,6 +93,8 @@ export function ColumnItem({
}: IColumnItemProps) {
const [altPress] = useAtom(altPressAtom, globalScope);
const isNew = option.index === undefined && !option.type;
return (
<Stack
direction="row"
@@ -100,10 +103,18 @@ export function ColumnItem({
{...props}
sx={[{ color: "text.secondary", width: "100%" }, ...spreadSx(props.sx)]}
>
{getFieldProp("icon", option.type)}
{getFieldProp("icon", option.type) ?? (
<TableColumnIcon color="disabled" />
)}
<Typography color="text.primary" style={{ flexGrow: 1 }}>
{altPress ? <code>{option.value}</code> : option.label}
</Typography>
{isNew && (
<Chip label="New" color="primary" size="small" variant="outlined" />
)}
{altPress ? (
<Typography
color="text.disabled"

View File

@@ -137,6 +137,7 @@ export default function EmptyTable() {
margin: "0 auto",
textAlign: "center",
}}
id="empty-table"
>
{contents}
</Stack>

View File

@@ -10,13 +10,16 @@ import {
FormControlLabel,
Checkbox,
Chip,
Stack,
Box,
} from "@mui/material";
import ArrowIcon from "@mui/icons-material/ArrowForward";
import { TableColumn as TableColumnIcon } from "@src/assets/icons";
import { IStepProps } from ".";
import FadeList from "@src/components/TableModals/ScrollableList";
import Column, { COLUMN_HEADER_HEIGHT } from "@src/components/Table/Column";
import MultiSelect from "@rowy/multiselect";
import ColumnSelect from "@src/components/Table/ColumnSelect";
import {
tableScope,
@@ -24,6 +27,7 @@ import {
tableColumnsOrderedAtom,
} from "@src/atoms/tableScope";
import { FieldType } from "@src/constants/fields";
import { getFieldProp } from "@src/components/fields";
import { suggestType } from "@src/components/TableModals/ImportExistingWizard/utils";
export default function Step1Columns({
@@ -215,9 +219,8 @@ export default function Step1Columns({
<Grid item xs>
{selected && (
<MultiSelect
<ColumnSelect
multiple={false}
options={tableColumns}
value={columnKey}
onChange={handleChange(field) as any}
TextFieldProps={{
@@ -227,21 +230,34 @@ export default function Step1Columns({
if (!columnKey) return "Select or add column";
else
return (
<>
<Stack
direction="row"
gap={1}
alignItems="center"
>
<Box sx={{ width: 24, height: 24 }}>
{!isNewColumn ? (
getFieldProp("icon", matchingColumn?.type)
) : (
<TableColumnIcon color="disabled" />
)}
</Box>
{matchingColumn?.name}
{isNewColumn && (
<Chip
label="New"
color="primary"
size="small"
sx={{
marginLeft: (theme) =>
theme.spacing(1) + " !important",
backgroundColor: "action.focus",
variant="outlined"
style={{
marginLeft: "auto",
pointerEvents: "none",
height: 24,
fontWeight: "normal",
}}
/>
)}
</>
</Stack>
);
},
sx: [
@@ -272,14 +288,14 @@ export default function Step1Columns({
!columnKey && { color: "text.disabled" },
],
},
sx: { "& .MuiInputLabel-root": { display: "none" } },
}}
clearable={false}
displayEmpty
labelPlural="columns"
freeText
AddButtonProps={{ children: "Add new column…" }}
AddButtonProps={{ children: "Create column…" }}
AddDialogProps={{
title: "Add new column",
title: "Create column",
textFieldLabel: "Column name",
}}
/>

View File

@@ -0,0 +1,57 @@
import { useState, useEffect } from "react";
import { useAtom } from "jotai";
import { ITableTutorialStepComponentProps } from ".";
import { Typography } from "@mui/material";
import TutorialCheckbox from "@src/components/TableTutorial/TutorialCheckbox";
import { tableScope, tableRowsAtom } from "@src/atoms/tableScope";
export const Step1Import = {
id: "import",
title: "Lets create a simple product pricing table",
description:
"Rowy connects to your database and displays it in a spreadsheet UI, making it easy to manage your data.",
StepComponent,
completeText: (
<Typography variant="body1">
<strong>Great work!</strong> Save time by importing data to tables. You
can also export your data to CSV, TSV, and JSON.
</Typography>
),
};
export default Step1Import;
function StepComponent({ setComplete }: ITableTutorialStepComponentProps) {
const [checked, setChecked] = useState([false]);
if (checked.every(Boolean)) setComplete(true);
else setComplete(false);
const handleChange =
(index: number) => (event: React.ChangeEvent<HTMLInputElement>) =>
setChecked((c) => {
const cloned = [...c];
cloned.splice(index, 1, event.target.checked);
return cloned;
});
const [tableRows] = useAtom(tableRowsAtom, tableScope);
useEffect(() => {
if (tableRows.length >= 5)
handleChange(0)({ target: { checked: true } } as any);
}, [tableRows]);
return (
<>
<ol>
<li>
<TutorialCheckbox
label="Begin by clicking “Import CSV” to import our sample dataset"
checked={checked[0]}
onChange={handleChange(0)}
/>
</li>
</ol>
</>
);
}

View File

@@ -0,0 +1,94 @@
import { useState, useEffect } from "react";
import { useAtom } from "jotai";
import { isEmpty } from "lodash-es";
import { ITableTutorialStepComponentProps } from ".";
import { Typography } from "@mui/material";
import TutorialCheckbox from "@src/components/TableTutorial/TutorialCheckbox";
import {
tableScope,
tableColumnsOrderedAtom,
tableRowsAtom,
} from "@src/atoms/tableScope";
import { FieldType } from "@src/constants/fields";
export const Step2Add = {
id: "add",
title: "Lets add some columns and rows to your table",
description:
"When you make changes made to your data in Rowy, theyre reflected in your Firestore database in realtime.",
StepComponent,
completeText: (
<Typography variant="body1">
<strong>Nicely done!</strong> Rating is just one of Rowys 30+ field
types. You can explore the others when making your own tables.
</Typography>
),
};
export default Step2Add;
function StepComponent({ setComplete }: ITableTutorialStepComponentProps) {
const [checked, setChecked] = useState([false, false, false]);
if (checked.every(Boolean)) setComplete(true);
else setComplete(false);
const handleChange =
(index: number) => (event: React.ChangeEvent<HTMLInputElement>) =>
setChecked((c) => {
const cloned = [...c];
cloned.splice(index, 1, event.target.checked);
return cloned;
});
const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope);
useEffect(() => {
if (
tableColumnsOrdered.some(
(c) =>
c.type === FieldType.rating && c.name.toLowerCase().includes("rating")
)
)
handleChange(0)({ target: { checked: true } } as any);
}, [tableColumnsOrdered]);
const [tableRows] = useAtom(tableRowsAtom, tableScope);
useEffect(() => {
if (tableRows.length >= 6) {
handleChange(1)({ target: { checked: true } } as any);
const { _rowy_ref, ...firstRow } = tableRows[0];
if (!isEmpty(firstRow)) {
handleChange(2)({ target: { checked: true } } as any);
}
}
}, [tableRows]);
return (
<>
<ol>
<li>
<TutorialCheckbox
label="Add a column named “Rating”, with the field type “Rating”"
checked={checked[0]}
onChange={handleChange(0)}
/>
</li>
<li>
<TutorialCheckbox
label="Add a row"
checked={checked[1]}
onChange={handleChange(1)}
/>
</li>
<li>
<TutorialCheckbox
label="Enter some data in the new row"
checked={checked[2]}
onChange={handleChange(2)}
/>
</li>
</ol>
</>
);
}

View File

@@ -0,0 +1,17 @@
import { ITableTutorialStepComponentProps } from ".";
export const Step3Invite = {
id: "invite",
title: "Lets create a simple product pricing table",
description:
"Rowy allows you to invite your team members with granular, role-based access controls.",
StepComponent,
};
export default Step3Invite;
function StepComponent({ setComplete }: ITableTutorialStepComponentProps) {
setComplete(true);
return <>TODO:</>;
}

View File

@@ -0,0 +1,16 @@
import { ITableTutorialStepComponentProps } from ".";
export const Step4Code = {
id: "code",
title: "Lets learn how to unlock the true powers of Rowy",
description: "TODO:",
StepComponent,
};
export default Step4Code;
function StepComponent({ setComplete }: ITableTutorialStepComponentProps) {
setComplete(true);
return <>TODO:</>;
}

View File

@@ -0,0 +1,27 @@
import Step1Import from "./Step1Import";
import Step2Add from "./Step2Add";
import Step3Invite from "./Step3Invite";
import Step4Code from "./Step4Code";
export const TUTORIAL_STEPS = [
Step1Import,
Step2Add,
Step3Invite,
Step4Code,
] as Array<ITableTutorialStepProps>;
export interface ITableTutorialStepProps {
onNext: () => void;
isFinal: boolean;
id: string;
title: React.ReactNode;
description: React.ReactNode;
StepComponent: React.ComponentType<ITableTutorialStepComponentProps>;
completeText?: React.ReactNode;
}
export interface ITableTutorialStepComponentProps {
complete: boolean;
setComplete: (complete: boolean) => void;
}

View File

@@ -0,0 +1,115 @@
import { useCallback } from "react";
import { useSetAtom } from "jotai";
import { useAtomCallback } from "jotai/utils";
import { cloneDeep, unset, findIndex, sortBy } from "lodash-es";
import {
tableScope,
tableSchemaAtom,
updateTableSchemaAtom,
tableRowsDbAtom,
_updateRowDbAtom,
_deleteRowDbAtom,
_bulkWriteDbAtom,
addRowAtom,
} from "@src/atoms/tableScope";
import { TableSchema, TableRow, BulkWriteFunction } from "@src/types/table";
import { updateRowData } from "@src/utils/table";
import { TABLE_SCHEMAS } from "@src/config/dbPaths";
export const TUTORIAL_COLLECTION = "tutorial";
export const TUTORIAL_TABLE_SETTINGS = {
id: TUTORIAL_COLLECTION,
name: "Tutorial",
collection: TUTORIAL_COLLECTION,
roles: ["ADMIN"],
section: "",
tableType: "primaryCollection",
audit: false,
};
export const TUTORIAL_TABLE_SCHEMA = {
_rowy_ref: {
path: TABLE_SCHEMAS + "/" + TUTORIAL_COLLECTION,
id: TUTORIAL_COLLECTION,
},
};
export function TableSourceTutorial() {
const setTableSchema = useSetAtom(tableSchemaAtom, tableScope);
const setUpdateTableSchema = useSetAtom(updateTableSchemaAtom, tableScope);
setUpdateTableSchema(
() => async (update: Partial<TableSchema>, deleteFields?: string[]) => {
setTableSchema((current) => {
const withFieldsDeleted = cloneDeep(current);
if (Array.isArray(deleteFields)) {
for (const field of deleteFields) {
unset(withFieldsDeleted, field);
}
}
return updateRowData(withFieldsDeleted || {}, update);
});
}
);
const setRowsDb = useSetAtom(tableRowsDbAtom, tableScope);
const readRowsDb = useAtomCallback(
useCallback((get) => get(tableRowsDbAtom), []),
tableScope
);
const setUpdateRowDb = useSetAtom(_updateRowDbAtom, tableScope);
setUpdateRowDb(() => (path: string, update: Partial<TableRow>) => {
setRowsDb((_rows) => {
const rows = [..._rows];
const index = findIndex(rows, ["_rowy_ref.path", path]);
// Append if not found and sort by ID
if (index === -1) {
return sortBy(
[
...rows,
{ ...update, _rowy_ref: { id: path.split("/").pop()!, path } },
],
["_rowy_ref.id"]
);
}
rows[index] = updateRowData(rows[index], update);
return rows;
});
return Promise.resolve();
});
const setDeleteRowDb = useSetAtom(_deleteRowDbAtom, tableScope);
setDeleteRowDb(() => async (path: string) => {
const _rows = await readRowsDb();
const rows = [..._rows];
const index = findIndex(rows, ["_rowy_ref.path", path]);
if (index > -1) {
rows.splice(index, 1);
setRowsDb(rows);
}
return Promise.resolve();
});
const setBulkWriteDb = useSetAtom(_bulkWriteDbAtom, tableScope);
const addRow = useSetAtom(addRowAtom, tableScope);
// WARNING: Only supports bulk add row for import CSV
setBulkWriteDb(
() =>
(
operations: Parameters<BulkWriteFunction>[0],
_onBatchCommit: Parameters<BulkWriteFunction>[1]
) =>
addRow({
row: operations.map((operation) => ({
...(operation as any).data,
_rowy_ref: { id: operation.path, path: operation.path },
})),
setId: "decrement",
})
);
return null;
}

View File

@@ -0,0 +1,180 @@
import { useState, Fragment } from "react";
import { useSetAtom } from "jotai";
import { useNavigate } from "react-router-dom";
import {
Slide,
Drawer,
Typography,
IconButton,
Box,
Stack,
Button,
} from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
import ArrowForwardIcon from "@mui/icons-material/ArrowForward";
import StepsProgress from "@src/components/StepsProgress";
import { TUTORIAL_STEPS } from "./Steps";
import { globalScope, confirmDialogAtom } from "@src/atoms/globalScope";
import { ROUTES } from "@src/constants/routes";
import { NAV_DRAWER_COLLAPSED_WIDTH } from "@src/layouts/Navigation/NavDrawer";
export default function TableTutorial() {
const confirm = useSetAtom(confirmDialogAtom, globalScope);
const navigate = useNavigate();
const [completed, setCompleted] = useState(
new Array(TUTORIAL_STEPS.length).fill(false)
);
const [currentStep, setCurrentStep] = useState(0);
const handleComplete = (value: boolean) =>
setCompleted((c) => {
if (c[currentStep] === value) return c;
const newCompleted = [...c];
newCompleted[currentStep] = value;
return newCompleted;
});
const handleNext = () =>
setCurrentStep((c) => Math.min(c + 1, TUTORIAL_STEPS.length - 1));
const stepProps = TUTORIAL_STEPS[currentStep];
const StepComponent = stepProps.StepComponent;
const isFinal = currentStep === TUTORIAL_STEPS.length - 1;
return (
<Slide in direction="up">
<Drawer
variant="permanent"
anchor="bottom"
sx={{
position: "fixed",
top: "auto",
bottom: `env(safe-area-inset-bottom)`,
left: {
xs: `env(safe-area-inset-left)`,
md: NAV_DRAWER_COLLAPSED_WIDTH + 2,
},
right: `env(safe-area-inset-right)`,
height: "min(50vh, 440px)",
"& .MuiPaper-root": {
position: "static",
height: "100%",
borderRadius: 2,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
border: "none",
px: { xs: 2, sm: 3, md: 6 },
py: { xs: 3, md: 5 },
pb: (theme) =>
`max(env(safe-area-inset-bottom), ${theme.spacing(5)})`,
overflow: "auto",
display: "flex",
flexDirection: "column",
gap: 2,
},
"& ol": { m: 0, p: 0, listStyle: "none" },
"& p": { maxWidth: "100ch" },
}}
PaperProps={{ elevation: 2 }}
>
<Typography variant="overline" sx={{ mb: -2 }}>
Get started tutorial
</Typography>
<IconButton
aria-label="Close tutorial"
size="medium"
sx={{
marginLeft: "auto",
width: 40,
position: "absolute",
top: 12,
right: 12,
}}
onClick={() =>
confirm({
title: "Close tutorial?",
body: "Your progress will be lost",
handleConfirm: () => navigate(ROUTES.tables),
confirm: "Close tutorial",
cancel: "Continue tutorial",
confirmColor: "error",
buttonLayout: "vertical",
})
}
>
<CloseIcon />
</IconButton>
<Fragment key={TUTORIAL_STEPS[currentStep].id}>
<header>
<Typography variant="h5" component="h1" gutterBottom>
{stepProps.title}
</Typography>
<Typography variant="body1">{stepProps.description}</Typography>
</header>
<div style={{ flexGrow: 1 }}>
<StepComponent
complete={completed[currentStep]}
setComplete={handleComplete}
/>
</div>
{stepProps.completeText && (
<Box
sx={{
visibility: completed[currentStep] ? "visible" : "hidden",
opacity: completed[currentStep] ? 1 : 0,
transition: (theme) => theme.transitions.create("opacity"),
}}
>
{stepProps.completeText}
</Box>
)}
</Fragment>
<Stack
direction="row"
justifyContent="flex-end"
alignItems="center"
gap={1}
>
<StepsProgress
steps={TUTORIAL_STEPS.length}
value={completed.filter(Boolean).length}
style={{
flexGrow: 0,
flexShrink: 1,
flexBasis: 200,
marginRight: "auto",
}}
/>
<Button variant="text" style={{ flexShrink: 0 }}>
Im stuck
</Button>
<Button
variant="contained"
color="primary"
style={{ flexShrink: 0 }}
disabled={!completed[currentStep]}
onClick={handleNext}
endIcon={isFinal ? <ArrowForwardIcon /> : undefined}
>
{isFinal ? "Finish" : "Next"}
</Button>
</Stack>
</Drawer>
</Slide>
);
}

View File

@@ -0,0 +1,69 @@
import {
FormControlLabel,
FormControlLabelProps,
Checkbox,
CheckboxProps,
} from "@mui/material";
export interface ITutorialCheckboxProps {
label: FormControlLabelProps["label"];
checked: CheckboxProps["checked"];
onChange: CheckboxProps["onChange"];
}
export default function TutorialCheckbox({
checked,
onChange,
label,
}: ITutorialCheckboxProps) {
return (
<FormControlLabel
label={label}
sx={{
ml: -6 / 8,
mr: 0,
"& .MuiFormControlLabel-label": {
mt: ((36 - 20) / 2 + 1) / 8,
// typography: "body1",
},
}}
control={
<Checkbox
checked={checked}
onChange={onChange}
color="success"
sx={{
color: "success.light",
p: 3 / 8,
mr: 0.5,
"& .checkbox-icon": {
color: "success.light",
borderRadius: "50%",
borderWidth: 2,
width: 24,
height: 24,
placeItems: "center",
"& svg": {
position: "relative",
top: 1,
left: 1,
},
"& .tick": {
stroke: "currentColor",
},
},
'& .checkbox-icon, &.Mui-checked .checkbox-icon, &[aria-selected="true"] .checkbox-icon':
{
backgroundColor: "transparent",
},
}}
/>
}
/>
);
}

View File

@@ -0,0 +1,9 @@
export const columns = ["Name", "Price"];
export const rows = [
{ Name: "Wine - Vouvray Cuvee Domaine", Price: 22.73 },
{ Name: "Hold Up Tool Storage Rack", Price: 55.52 },
{ Name: "Squid - Tubes / Tenticles 10/20", Price: 91.34 },
{ Name: "Squeeze Bottle", Price: 3.11 },
{ Name: "Flour - Semolina", Price: 97.01 },
];

View File

@@ -0,0 +1,2 @@
export * from "./TableTutorial";
export { default } from "./TableTutorial";

View File

@@ -1,6 +1,6 @@
import Logo from "@src/assets/Logo";
import BreadcrumbsTableRoot from "@src/components/Table/BreadcrumbsTableRoot";
import { FadeProps } from "@mui/material";
import { FadeProps, Typography } from "@mui/material";
export enum ROUTES {
home = "/",
@@ -38,6 +38,9 @@ export enum ROUTES {
members = "/members",
debugSettings = "/settings/debug",
tutorial = "/tutorial",
tableTutorial = "/tutorial/table",
test = "/test",
themeTest = "/test/theme",
rowyRunTest = "/test/rowyRunTest",
@@ -65,6 +68,18 @@ export const ROUTE_TITLES = {
[ROUTES.members]: "Members",
[ROUTES.debugSettings]: "Debug",
[ROUTES.tutorial]: "Tutorial",
[ROUTES.tableTutorial]: {
title: "Tutorial",
titleComponent: (_o, _i) => (
<Typography component="h1" variant="h6">
Tutorial
</Typography>
),
titleTransitionProps: { style: { transformOrigin: "0 50%" } },
leftAligned: true,
},
[ROUTES.test]: "Test",
[ROUTES.themeTest]: "Theme Test",
[ROUTES.rowyRunTest]: "Rowy Run Test",

View File

@@ -1,23 +0,0 @@
import { Box, BoxProps, Typography } from "@mui/material";
import { spreadSx } from "@src/utils/ui";
export default function Progress({ sx }: Partial<BoxProps>) {
return (
<Box
sx={[
{ display: "flex", alignItems: "center", gap: 0.5 },
...spreadSx(sx),
]}
>
<Typography style={{ flex: 3 }}>1/5</Typography>
<Box
sx={{ flex: 1, borderRadius: 1, height: 8, bgcolor: "success.light" }}
/>
<Box sx={{ flex: 1, borderRadius: 1, height: 8, bgcolor: "divider" }} />
<Box sx={{ flex: 1, borderRadius: 1, height: 8, bgcolor: "divider" }} />
<Box sx={{ flex: 1, borderRadius: 1, height: 8, bgcolor: "divider" }} />
<Box sx={{ flex: 1, borderRadius: 1, height: 8, bgcolor: "divider" }} />
</Box>
);
}

View File

@@ -33,7 +33,7 @@ import Logo from "@src/assets/Logo";
import NavItem from "./NavItem";
import SettingsNav from "./SettingsNav";
import NavTableSection from "./NavTableSection";
import Progress from "./GetStartedChecklist/Progress";
import StepsProgress from "@src/components/StepsProgress";
import CommunityMenu from "./CommunityMenu";
import HelpMenu from "./HelpMenu";
@@ -324,7 +324,9 @@ export default function NavDrawer({
</ListItemIcon>
<ListItemText
primary="Get started"
secondary={<Progress sx={{ mr: 3 }} />}
secondary={
<StepsProgress value={1} steps={5} sx={{ mr: 3 }} />
}
/>
<ListItemSecondaryAction>
<ChevronRightIcon />

View File

@@ -11,7 +11,7 @@ import ErrorFallback, {
IErrorFallbackProps,
} from "@src/components/ErrorFallback";
import Loading from "@src/components/Loading";
import GetStartedChecklist from "./GetStartedChecklist";
import GetStartedChecklist from "@src/components/GetStartedChecklist";
import {
globalScope,

View File

@@ -34,13 +34,18 @@ const BuildLogsSnack = lazy(() => import("@src/components/TableModals/CloudLogsM
export interface ITablePageProps {
/** Disable modals on this table when a sub-table is open and its listening to URL state */
disableModals?: boolean;
/** Disable side drawer */
disableSideDrawer?: boolean;
}
/**
* TablePage renders all the UI for the table.
* Must be wrapped by either `ProvidedTablePage` or `ProvidedSubTablePage`.
*/
export default function TablePage({ disableModals }: ITablePageProps) {
export default function TablePage({
disableModals,
disableSideDrawer,
}: ITablePageProps) {
const [tableSchema] = useAtom(tableSchemaAtom, tableScope);
const snackLogContext = useSnackLogContext();
@@ -63,7 +68,7 @@ export default function TablePage({ disableModals }: ITablePageProps) {
return (
<Suspense fallback={null}>
<Fade in style={{ transitionDelay: "500ms" }}>
<div>
<div className="empty-table-container">
<EmptyTable />
<Suspense fallback={null}>
@@ -94,7 +99,7 @@ export default function TablePage({ disableModals }: ITablePageProps) {
<ErrorBoundary FallbackComponent={InlineErrorFallback}>
<Suspense fallback={null}>
<SideDrawer dataGridRef={dataGridRef} />
{!disableSideDrawer && <SideDrawer dataGridRef={dataGridRef} />}
</Suspense>
</ErrorBoundary>

View File

@@ -0,0 +1,138 @@
import { Suspense } from "react";
import { useAtom, useSetAtom, Provider } from "jotai";
import { DebugAtoms } from "@src/atoms/utils";
import { ErrorBoundary } from "react-error-boundary";
import { Box, Typography, Button } from "@mui/material";
import { Import as ImportIcon } from "@src/assets/icons";
import ErrorFallback from "@src/components/ErrorFallback";
import TablePage from "./TablePage";
import TableToolbarSkeleton from "@src/components/TableToolbar/TableToolbarSkeleton";
import TableSkeleton from "@src/components/Table/TableSkeleton";
import TableTutorial from "@src/components/TableTutorial";
import EmptyState from "@src/components/EmptyState";
import TableModals from "@src/components/TableModals";
import { globalScope, currentUserAtom } from "@src/atoms/globalScope";
import {
tableScope,
tableIdAtom,
tableSettingsAtom,
tableSchemaAtom,
tableColumnsOrderedAtom,
tableRowsAtom,
tableModalAtom,
importCsvAtom,
} from "@src/atoms/tableScope";
import {
TUTORIAL_COLLECTION,
TUTORIAL_TABLE_SETTINGS,
TUTORIAL_TABLE_SCHEMA,
TableSourceTutorial,
} from "@src/components/TableTutorial/TableSourceTutorial";
import { TOP_BAR_HEIGHT } from "@src/layouts/Navigation/TopBar";
import * as csvData from "@src/components/TableTutorial/data";
/**
* Wraps `TablePage` with the data for a top-level table.
*/
export default function TableTutorialPage() {
const [currentUser] = useAtom(currentUserAtom, globalScope);
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense
fallback={
<>
<TableToolbarSkeleton />
<TableSkeleton />
</>
}
>
<Provider
key={tableScope.description + "/" + TUTORIAL_COLLECTION}
scope={tableScope}
initialValues={[
[currentUserAtom, currentUser],
[tableIdAtom, TUTORIAL_COLLECTION],
[tableSettingsAtom, TUTORIAL_TABLE_SETTINGS],
[tableSchemaAtom, TUTORIAL_TABLE_SCHEMA],
]}
>
<DebugAtoms scope={tableScope} />
<TableSourceTutorial />
<Suspense
fallback={
<>
<TableToolbarSkeleton />
<TableSkeleton />
</>
}
>
<Box
component="main"
sx={{
".empty-state--full-screen, #empty-table": {
height: `calc(100vh - ${TOP_BAR_HEIGHT}px - min(50vh, 440px)) !important`,
},
".table-container > .rdg": {
paddingBottom:
"max(env(safe-area-inset-bottom), min(50vh, 440px))",
width: "100%",
".rdg-row, .rdg-header-row": {
marginRight: `env(safe-area-inset-right)`,
},
},
}}
>
<Content />
<TableTutorial />
</Box>
</Suspense>
</Provider>
</Suspense>
</ErrorBoundary>
);
}
function Content() {
const [tableColumnsOrdered] = useAtom(tableColumnsOrderedAtom, tableScope);
const [tableRows] = useAtom(tableRowsAtom, tableScope);
const openTableModal = useSetAtom(tableModalAtom, tableScope);
const setImportCsv = useSetAtom(importCsvAtom, tableScope);
if (tableColumnsOrdered.length === 0 || tableRows.length === 0) {
return (
<EmptyState
Icon={(() => null) as any}
message="Get started"
description={
<>
<Typography>There is no data in this table.</Typography>
<Typography>You can import our sample CSV file:</Typography>
<Button
variant="contained"
color="primary"
startIcon={<ImportIcon />}
onClick={() => {
setImportCsv({ importType: "csv", csvData });
openTableModal("importCsv");
}}
>
Import CSV
</Button>
<TableModals />
</>
}
/>
);
}
return <TablePage disableSideDrawer />;
}

View File

@@ -68,6 +68,7 @@ export default function CheckboxIcon() {
},
},
}}
className="checkbox-icon"
>
<svg viewBox="0 0 18 18">
<polyline

View File

@@ -68,6 +68,7 @@ export default function CheckboxIndeterminateIcon() {
},
},
}}
className="checkbox-indeterminate-icon"
>
<svg viewBox="0 0 18 18">
<line

View File

@@ -61,6 +61,7 @@ export default function RadioIcon() {
},
},
}}
className="radio-icon"
/>
);
}

View File

@@ -10,3 +10,7 @@ declare module "!!raw-loader!*" {
const content: string;
export default content;
}
declare module "*.csv" {
const content: string;
export default content;
}