Wizard: add a first-run wizard to help create new accounts.
parent
2a43a9e94e
commit
7ba75ae3c1
@ -0,0 +1,162 @@
|
||||
// SPDX-FileCopyrightText: © 2017 EteSync Authors
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import * as React from "react";
|
||||
import * as Etebase from "etebase";
|
||||
import { Redirect, useLocation } from "react-router";
|
||||
import Button from "@material-ui/core/Button";
|
||||
import Alert from "@material-ui/lab/Alert";
|
||||
|
||||
import { routeResolver } from "./App";
|
||||
|
||||
import Container from "./widgets/Container";
|
||||
import LoadingIndicator from "./widgets/LoadingIndicator";
|
||||
import Wizard, { WizardNavigationBar, PagePropsType } from "./widgets/Wizard";
|
||||
|
||||
import { SyncManager } from "./sync/SyncManager";
|
||||
|
||||
import { store } from "./store";
|
||||
import { useCredentials } from "./credentials";
|
||||
|
||||
import wizardWelcome from "./images/wizard-welcome.svg";
|
||||
import wizardCreate from "./images/wizard-create.svg";
|
||||
|
||||
interface LocationState {
|
||||
from: {
|
||||
pathname: string;
|
||||
};
|
||||
}
|
||||
|
||||
const wizardPages = [
|
||||
(props: PagePropsType) => (
|
||||
<>
|
||||
<div style={{ display: "flex", flexDirection: "column", flex: 1, justifyContent: "center", alignItems: "center" }}>
|
||||
<h2 style={{ textAlign: "center" }}>Welcome to EteSync!</h2>
|
||||
<p style={{ textAlign: "center" }}>
|
||||
Please follow these few quick steps to help you get started.
|
||||
</p>
|
||||
<img src={wizardWelcome} style={{ maxWidth: "30em", marginTop: "2em" }} />
|
||||
</div>
|
||||
<WizardNavigationBar {...props} />
|
||||
</>
|
||||
),
|
||||
(props: PagePropsType) => (
|
||||
<SetupCollectionsPage {...props} />
|
||||
),
|
||||
];
|
||||
|
||||
function SetupCollectionsPage(props: PagePropsType) {
|
||||
const etebase = useCredentials()!;
|
||||
const [error, setError] = React.useState<Error>();
|
||||
const [loading, setLoading] = React.useState(false);
|
||||
async function onNext() {
|
||||
setLoading(true);
|
||||
try {
|
||||
const colMgr = etebase.getCollectionManager();
|
||||
const types = [
|
||||
["etebase.vcard", "My Contacts"],
|
||||
["etebase.vevent", "My Calendar"],
|
||||
["etebase.vtodo", "My Tasks"],
|
||||
];
|
||||
for (const [type, name] of types) {
|
||||
const meta: Etebase.CollectionMetadata = {
|
||||
type,
|
||||
name,
|
||||
};
|
||||
const collection = await colMgr.create(meta, "");
|
||||
await colMgr.upload(collection);
|
||||
}
|
||||
|
||||
props.next?.();
|
||||
} catch (e) {
|
||||
setError(e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const next = (loading) ? undefined : onNext;
|
||||
return (
|
||||
<>
|
||||
<div style={{ display: "flex", flexDirection: "column", flex: 1, justifyContent: "center", alignItems: "center" }}>
|
||||
<h2 style={{ textAlign: "center" }}>Setup Collections</h2>
|
||||
<p style={{ textAlign: "center", maxWidth: "50em" }}>
|
||||
In order to start using EteSync you need to create collections to store your data. Clicking <i>Finish</i> below will create a default calendar, address book and a task list for you.
|
||||
</p>
|
||||
{(loading) ? (
|
||||
<LoadingIndicator style={{ display: "block", margin: "40px auto" }} />
|
||||
) : (error) ? (
|
||||
<Alert severity="error">{error.message}</Alert>
|
||||
) : (
|
||||
<img src={wizardCreate} style={{ maxWidth: "30em", marginTop: "2em" }} />
|
||||
)}
|
||||
</div>
|
||||
<WizardNavigationBar {...props} next={next} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function WizardPage() {
|
||||
const [tryCount, setTryCount] = React.useState(0);
|
||||
const [ranWizard, setRanWizard] = React.useState(false);
|
||||
const [syncError, setSyncError] = React.useState<Error>();
|
||||
const etebase = useCredentials();
|
||||
const location = useLocation();
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
|
||||
React.useEffect(() => {
|
||||
setLoading(true);
|
||||
setSyncError(undefined);
|
||||
(async () => {
|
||||
const syncManager = SyncManager.getManager(etebase!);
|
||||
const sync = syncManager.sync(true);
|
||||
try {
|
||||
await sync;
|
||||
|
||||
const cachedCollection = store.getState().cache.collections;
|
||||
// XXX new account - though should change test to see if there are any PIM types
|
||||
if (cachedCollection.size > 0) {
|
||||
setRanWizard(true);
|
||||
}
|
||||
} catch (e) {
|
||||
setSyncError(e);
|
||||
}
|
||||
setLoading(false);
|
||||
})();
|
||||
}, [tryCount]);
|
||||
|
||||
if (syncError) {
|
||||
return (
|
||||
<Container>
|
||||
<div style={{ display: "flex", flexDirection: "column", flex: 1, justifyContent: "center", alignItems: "center" }}>
|
||||
<h2 style={{ textAlign: "center" }}>Error!</h2>
|
||||
<p style={{ textAlign: "center" }}>
|
||||
{syncError?.message}
|
||||
</p>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => setTryCount(tryCount + 1)}
|
||||
>
|
||||
Retry
|
||||
</Button>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (<LoadingIndicator style={{ display: "block", margin: "40px auto" }} />);
|
||||
}
|
||||
|
||||
if (!ranWizard) {
|
||||
return (
|
||||
<Wizard pages={wizardPages} onFinish={() => setRanWizard(true)} style={{ display: "flex", flexDirection: "column", flex: 1 }} />
|
||||
);
|
||||
}
|
||||
|
||||
const { from } = location.state as LocationState || { from: { pathname: routeResolver.getRoute("home") } };
|
||||
return (
|
||||
<Redirect to={from.pathname} />
|
||||
);
|
||||
}
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 49 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 40 KiB |
@ -0,0 +1,63 @@
|
||||
// SPDX-FileCopyrightText: © 2019 EteSync Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import * as React from "react";
|
||||
import Button from "@material-ui/core/Button";
|
||||
|
||||
import Container from "./Container";
|
||||
|
||||
export interface PagePropsType {
|
||||
prev?: () => void;
|
||||
next?: () => void;
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
export function WizardNavigationBar(props: PagePropsType) {
|
||||
const first = props.currentPage === 0;
|
||||
const last = props.currentPage === props.totalPages - 1;
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "row", justifyContent: "space-between", marginTop: "auto" }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={first}
|
||||
onClick={props.prev}
|
||||
>
|
||||
Prev
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={!props.next}
|
||||
onClick={props.next}
|
||||
>
|
||||
{(last) ? "Finish" : "Next"}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface PropsType extends React.HTMLProps<HTMLDivElement> {
|
||||
pages: ((props: PagePropsType) => React.ReactNode)[];
|
||||
onFinish: () => void;
|
||||
}
|
||||
|
||||
export default function Wizard(inProps: PropsType) {
|
||||
const [currentPage, setCurrentPage] = React.useState(0);
|
||||
const { pages, onFinish, ...props } = inProps;
|
||||
|
||||
const Content = pages[currentPage];
|
||||
|
||||
const first = currentPage === 0;
|
||||
const last = currentPage === pages.length - 1;
|
||||
const prev = !first ? () => setCurrentPage(currentPage - 1) : undefined;
|
||||
const next = !last ? () => setCurrentPage(currentPage + 1) : onFinish;
|
||||
|
||||
return (
|
||||
<Container {...props}>
|
||||
{Content({ prev, next, currentPage, totalPages: pages.length })}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
Loading…
Reference in New Issue