diff --git a/src/MigrateV2.tsx b/src/MigrateV2.tsx index ac77f1f..759e14b 100644 --- a/src/MigrateV2.tsx +++ b/src/MigrateV2.tsx @@ -13,7 +13,6 @@ import ContactsIcon from "@material-ui/icons/Contacts"; import CalendarTodayIcon from "@material-ui/icons/CalendarToday"; import FormatListBulletedIcon from "@material-ui/icons/FormatListBulleted"; -import Container from "./widgets/Container"; import { useSelector } from "react-redux"; import { StoreState, CredentialsData, UserInfoData } from "./store"; import AppBarOverride from "./widgets/AppBarOverride"; @@ -25,6 +24,8 @@ import Alert from "@material-ui/lab/Alert"; import { arrayToChunkIterator } from "./helpers"; import { ContactType, EventType, TaskType, PimType } from "./pim-types"; import PasswordField from "./widgets/PasswordField"; +import Wizard, { PagePropsType, WizardNavigationBar } from "./widgets/Wizard"; +import ExternalLink from "./widgets/ExternalLink"; interface PropsType { etesync: CredentialsData; @@ -40,7 +41,55 @@ interface FormErrors { } export default function MigrateV2(props: PropsType) { - const [wantedJournals, setWantedJournals] = React.useState>(ImmutableMap({})); + const [etebase, setEtebase] = React.useState(); + + const wizardPages = [ + (props: PagePropsType) => ( + <> +
+

Etebase 2.0 Migration tool

+

+ This tool will help you migrate your data to EteSync 2.0. + The migration doesn't delete any data. It only copies your data over to the new EteSync 2.0 server. This means that there is no risk of data-loss in the migration. +

+
+ + + ), + (pageProps: PagePropsType) => ( + + ), + (pageProps: PagePropsType) => ( + + ), + (_props: PagePropsType) => ( + <> +
+

Migration finished successfully!

+

+ You should now log into your apps using your EteSync 2.0 credentials, and logout from your EteSync 1.0 accounts. +

+

+ The EteSync 2.0 web client is located at: https://pim.etesync.com +

+
+ + ), + ]; + + return ( + <> + + 1} style={{ display: "flex", flexDirection: "column", flex: 1 }} /> + + ); +} + +interface OurPagePropsType extends PropsType, PagePropsType { + etebase?: Etebase.Account; +} + +export function WizardAccountPage(props: OurPagePropsType & { setEtebase: (etebase: Etebase.Account) => void }) { const [username, setUsername] = React.useState(""); const [password, setPassword] = React.useState(""); const [server, setServer] = React.useState(""); @@ -49,9 +98,191 @@ export default function MigrateV2(props: PropsType) { const [loading, setLoading] = React.useState(false); const [progress, setProgress] = React.useState(""); const [errors, setErrors] = React.useState({}); + const email = props.etesync.credentials.email; + + const styles = { + form: { + }, + forgotPassword: { + paddingTop: 20, + }, + alertInfo: { + marginTop: 20, + maxWidth: "50em", + }, + textField: { + width: "20em", + marginTop: 20, + }, + submit: { + marginTop: 40, + marginBottom: 40, + }, + }; + + function handleInputChange(func: (value: string) => void) { + return (event: React.ChangeEvent) => { + func(event.target.value); + }; + } + + async function onSubmit() { + setLoading(true); + setProgress(""); + try { + const errors: FormErrors = {}; + const fieldRequired = "This field is required!"; + if (!username) { + errors.errorEmail = fieldRequired; + } + if (!password) { + errors.errorPassword = fieldRequired; + } + if (showAdvanced && !server.startsWith("http")) { + errors.errorServer = "Server URI must start with http/https://"; + } + + if (Object.keys(errors).length) { + setErrors(errors); + return; + } else { + setErrors({}); + } + + const serverUrl = (showAdvanced) ? server : undefined; + + let etebase: Etebase.Account; + if (hasAccount) { + setProgress("Logging into EteSync 2.0 account"); + etebase = await Etebase.Account.login(username, password, serverUrl); + } else { + setProgress("Creating an EteSync 2.0 account"); + const user: Etebase.User = { + username, + email, + }; + etebase = await Etebase.Account.signup(user, password, serverUrl); + } + props.setEtebase(etebase); + props.next?.(); + setProgress("Done"); + } catch (e) { + if (e instanceof Etebase.UnauthorizedError) { + errors.errorGeneral = "Wrong username or password"; + } else { + errors.errorGeneral = e.toString(); + } + setErrors(errors); + } finally { + setLoading(false); + } + } + + let advancedSettings = null; + if (showAdvanced) { + advancedSettings = ( + + +
+
+ ); + } + + if (props.etebase) { + return ( + <> +

EteSync 2.0 credentials

+

Already logged in. Click "Next" to continue.

+ + ); + } + + return ( + <> +

EteSync 2.0 credentials

+ setHasAccount(!hasAccount)} />} + label="I already have an EteSync 2.0 account" + /> +
+ +
+ + {!hasAccount && ( + + Please make sure you remember your password, as it can't be recovered if lost! + + )} + + setShowAdvanced(!showAdvanced)} + /> + } + label="Advanced settings" + /> + + {advancedSettings} + {errors.errorGeneral && ( + {errors.errorGeneral} + )} + {progress && ( + {progress} + )} +
+ +
+ + ); +} + +export function WizardMigrationPage(props: OurPagePropsType) { + const [wantedJournals, setWantedJournals] = React.useState>(ImmutableMap({})); + const [loading, setLoading] = React.useState(false); + const [done, setDone] = React.useState(false); + const [progress, setProgress] = React.useState(""); + const [errors, setErrors] = React.useState({}); const journals = useSelector((state: StoreState) => state.cache.journals!); const journalEntries = useSelector((state: StoreState) => state.cache.entries); - const email = props.etesync.credentials.email; const derived = props.etesync.encryptionKey; const decryptedJournals = React.useMemo(() => { @@ -122,50 +353,12 @@ export default function MigrateV2(props: PropsType) { }, }; - function handleInputChange(func: (value: string) => void) { - return (event: React.ChangeEvent) => { - func(event.target.value); - }; - } - async function onSubmit() { setLoading(true); setProgress(""); try { - const errors: FormErrors = {}; - const fieldRequired = "This field is required!"; - if (!username) { - errors.errorEmail = fieldRequired; - } - if (!password) { - errors.errorPassword = fieldRequired; - } - if (showAdvanced && !server.startsWith("http")) { - errors.errorServer = "Server URI must start with http/https://"; - } - - if (Object.keys(errors).length) { - setErrors(errors); - return; - } else { - setErrors({}); - } - - const serverUrl = (showAdvanced) ? server : undefined; let malformed = 0; - - let etebase: Etebase.Account; - if (hasAccount) { - setProgress("Logging into EteSync 2.0 account"); - etebase = await Etebase.Account.login(username, password, serverUrl); - } else { - setProgress("Logging into EteSync 2.0 account"); - const user: Etebase.User = { - username, - email, - }; - etebase = await Etebase.Account.signup(user, password, serverUrl); - } + const etebase = props.etebase!; const colMgr = etebase.getCollectionManager(); const { etesync, userInfo } = props; @@ -274,50 +467,19 @@ export default function MigrateV2(props: PropsType) { } else { setProgress("Done"); } + setDone(true); } catch (e) { - if (e instanceof Etebase.UnauthorizedError) { - errors.errorGeneral = "Wrong username or password"; - } else { - errors.errorGeneral = e.toString(); - } + errors.errorGeneral = e.toString(); setErrors(errors); } finally { setLoading(false); } } - let advancedSettings = null; - if (showAdvanced) { - advancedSettings = ( - - -
-
- ); - } - return ( - - -

- This tool will help you migrate your data to EteSync 2.0. -

-

- The migration doesn't delete any data. It only copies your data over to the new EteSync 2.0 server. This means that there is no risk of data-loss in the migration. -

-

- Please select the collections you would like to migrate, and then enter your EteSync 2.0 credentials and click migrate. -

+ <> +

Choose collections to migrate

-

EteSync 2.0 credentials

- setHasAccount(!hasAccount)} />} - label="I already have an EteSync 2.0 account" - /> -
- -
- - {!hasAccount && ( - - Please make sure you remember your password, as it can't be recovered if lost! - - )} - - setShowAdvanced(!showAdvanced)} - /> - } - label="Advanced settings" - /> - - {advancedSettings} {errors.errorGeneral && ( {errors.errorGeneral} )} @@ -389,18 +507,28 @@ export default function MigrateV2(props: PropsType) { {progress} )}
- + {(done) ? ( + + ) : ( + + )}
-
+ ); } diff --git a/src/widgets/Wizard.tsx b/src/widgets/Wizard.tsx new file mode 100644 index 0000000..39af413 --- /dev/null +++ b/src/widgets/Wizard.tsx @@ -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 ( +
+ + +
+ ); +} + +interface PropsType extends React.HTMLProps { + 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 ( + + {Content({ prev, next, currentPage, totalPages: pages.length })} + + ); +} +