mirror of
https://github.com/rowyio/rowy.git
synced 2025-12-28 16:06:41 +01:00
add table tutorial at /tutorial/table
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
{
|
||||
44
src/components/StepsProgress.tsx
Normal file
44
src/components/StepsProgress.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -137,6 +137,7 @@ export default function EmptyTable() {
|
||||
margin: "0 auto",
|
||||
textAlign: "center",
|
||||
}}
|
||||
id="empty-table"
|
||||
>
|
||||
{contents}
|
||||
</Stack>
|
||||
|
||||
@@ -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",
|
||||
}}
|
||||
/>
|
||||
|
||||
57
src/components/TableTutorial/Steps/Step1Import.tsx
Normal file
57
src/components/TableTutorial/Steps/Step1Import.tsx
Normal 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: "Let’s 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
94
src/components/TableTutorial/Steps/Step2Add.tsx
Normal file
94
src/components/TableTutorial/Steps/Step2Add.tsx
Normal 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: "Let’s add some columns and rows to your table",
|
||||
description:
|
||||
"When you make changes made to your data in Rowy, they’re reflected in your Firestore database in realtime.",
|
||||
StepComponent,
|
||||
completeText: (
|
||||
<Typography variant="body1">
|
||||
<strong>Nicely done!</strong> Rating is just one of Rowy’s 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
17
src/components/TableTutorial/Steps/Step3Invite.tsx
Normal file
17
src/components/TableTutorial/Steps/Step3Invite.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { ITableTutorialStepComponentProps } from ".";
|
||||
|
||||
export const Step3Invite = {
|
||||
id: "invite",
|
||||
title: "Let’s 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:</>;
|
||||
}
|
||||
16
src/components/TableTutorial/Steps/Step4Code.tsx
Normal file
16
src/components/TableTutorial/Steps/Step4Code.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import { ITableTutorialStepComponentProps } from ".";
|
||||
|
||||
export const Step4Code = {
|
||||
id: "code",
|
||||
title: "Let’s learn how to unlock the true powers of Rowy",
|
||||
description: "TODO:",
|
||||
StepComponent,
|
||||
};
|
||||
|
||||
export default Step4Code;
|
||||
|
||||
function StepComponent({ setComplete }: ITableTutorialStepComponentProps) {
|
||||
setComplete(true);
|
||||
|
||||
return <>TODO:</>;
|
||||
}
|
||||
27
src/components/TableTutorial/Steps/index.ts
Normal file
27
src/components/TableTutorial/Steps/index.ts
Normal 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;
|
||||
}
|
||||
115
src/components/TableTutorial/TableSourceTutorial.tsx
Normal file
115
src/components/TableTutorial/TableSourceTutorial.tsx
Normal 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;
|
||||
}
|
||||
180
src/components/TableTutorial/TableTutorial.tsx
Normal file
180
src/components/TableTutorial/TableTutorial.tsx
Normal 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 }}>
|
||||
I’m 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>
|
||||
);
|
||||
}
|
||||
69
src/components/TableTutorial/TutorialCheckbox.tsx
Normal file
69
src/components/TableTutorial/TutorialCheckbox.tsx
Normal 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",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
9
src/components/TableTutorial/data.ts
Normal file
9
src/components/TableTutorial/data.ts
Normal 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 },
|
||||
];
|
||||
2
src/components/TableTutorial/index.ts
Normal file
2
src/components/TableTutorial/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./TableTutorial";
|
||||
export { default } from "./TableTutorial";
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 />
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 it’s 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>
|
||||
|
||||
|
||||
138
src/pages/Table/TableTutorialPage.tsx
Normal file
138
src/pages/Table/TableTutorialPage.tsx
Normal 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 />;
|
||||
}
|
||||
@@ -68,6 +68,7 @@ export default function CheckboxIcon() {
|
||||
},
|
||||
},
|
||||
}}
|
||||
className="checkbox-icon"
|
||||
>
|
||||
<svg viewBox="0 0 18 18">
|
||||
<polyline
|
||||
|
||||
@@ -68,6 +68,7 @@ export default function CheckboxIndeterminateIcon() {
|
||||
},
|
||||
},
|
||||
}}
|
||||
className="checkbox-indeterminate-icon"
|
||||
>
|
||||
<svg viewBox="0 0 18 18">
|
||||
<line
|
||||
|
||||
@@ -61,6 +61,7 @@ export default function RadioIcon() {
|
||||
},
|
||||
},
|
||||
}}
|
||||
className="radio-icon"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
4
src/types/files.d.ts
vendored
4
src/types/files.d.ts
vendored
@@ -10,3 +10,7 @@ declare module "!!raw-loader!*" {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
declare module "*.csv" {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user