Allow selection of the account.

This was surprisingly complex because I tried to use react-router and
found no easily and reasonable way to do it, so instead I hooked the
location change event myself and plumbed it through. This required me to
switch to a class-style component rather than a functional one, which
required translating a bunch of coding styles through React.

Overall I'm happy, it works pretty well and simply.
This commit is contained in:
Eli Ribble 2024-08-27 15:45:33 -07:00
parent 1d7d96de4a
commit c5afd9f895
2 changed files with 105 additions and 55 deletions

View File

@ -5,19 +5,20 @@ import { IAccount } from "jmap-client-ts/lib/types";
type AccountIdMap = { [accountId: string]: IAccount }; type AccountIdMap = { [accountId: string]: IAccount };
type AccountListProps = { type AccountListProps = {
account: IAccount | null;
accounts: AccountIdMap; accounts: AccountIdMap;
}; };
const AccountList: React.FC<AccountListProps> = ({ accounts }) => { const AccountList: React.FC<AccountListProps> = ({ account, accounts }) => {
return ( return (
<Dropdown> <Dropdown>
<Dropdown.Toggle variant="success" id="dropdown-basic"> <Dropdown.Toggle variant="success" id="dropdown-basic">
Dropdown Button {account ? account.name : "select account"}
</Dropdown.Toggle> </Dropdown.Toggle>
<Dropdown.Menu> <Dropdown.Menu>
{Object.keys(accounts).map((key: keyof AccountIdMap) => ( {Object.keys(accounts).map((key: keyof AccountIdMap) => (
<Dropdown.Item href={"#/" + key}>{accounts[key].name}</Dropdown.Item> <Dropdown.Item href={"#" + key}>{accounts[key].name}</Dropdown.Item>
))} ))}
</Dropdown.Menu> </Dropdown.Menu>
</Dropdown> </Dropdown>

View File

@ -2,98 +2,147 @@ import "./App.css";
import "bootstrap/dist/css/bootstrap.min.css"; import "bootstrap/dist/css/bootstrap.min.css";
import * as base64 from "base-64"; import * as base64 from "base-64";
import { Client } from "jmap-client-ts"; import { Client } from "jmap-client-ts";
import { ISession } from "jmap-client-ts/lib/types"; import { IAccount, ISession } from "jmap-client-ts/lib/types";
import { FetchTransport } from "jmap-client-ts/lib/utils/fetch-transport"; import { FetchTransport } from "jmap-client-ts/lib/utils/fetch-transport";
import AccountList from "./AccountList"; import AccountList from "./AccountList";
import AuthModal from "./AuthModal"; import AuthModal from "./AuthModal";
import React, { useEffect, useState } from "react"; import React from "react";
interface IAuth { interface IAuth {
email: string; email: string;
password: string; password: string;
} }
interface IAppState { interface ILocation {
auth: IAuth; accountId: string;
client: Client | null;
session: ISession | null;
} }
const App = () => { type AppState = {
const [state, setInternalState] = useState<IAppState>({ auth: IAuth;
auth: { email: "", password: "" }, location: ILocation;
client: null, session: ISession | null;
session: null, };
});
// When the user provides credentials type AppProps = {};
const onLogin = (email: string, password: string) => {
// Store the provided credentials for now class App extends React.Component<AppProps, AppState> {
state.auth.email = email; account(): IAccount | null {
state.auth.password = password; if (!(this.state.session && this.state.session.accounts)) return null;
state.client = null; return this.state.session.accounts[this.state.location.accountId];
setInternalState(state); }
localStorage.setItem("auth", JSON.stringify(state.auth)); client: Client | null = null;
doLogin(state.auth); state: AppState = {
auth: { email: "", password: "" },
location: { accountId: "" },
session: null,
}; };
// Make the request to get system metadata // Make the request to get system metadata
const doLogin = (auth: IAuth) => { doLogin(auth: IAuth) {
const domain = auth.email.split("@")[1]; const domain = auth.email.split("@")[1];
const well_known_url = "https://" + domain + "/.well-known/jmap"; const well_known_url = "https://" + domain + "/.well-known/jmap";
const basic_auth = const basic_auth =
"Basic " + base64.encode(auth.email + ":" + auth.password); "Basic " + base64.encode(auth.email + ":" + auth.password);
state.client = new Client({ this.client = new Client({
accessToken: "fake token", accessToken: "fake token",
httpHeaders: { Authorization: basic_auth }, httpHeaders: { Authorization: basic_auth },
sessionUrl: well_known_url, sessionUrl: well_known_url,
transport: new FetchTransport(fetch.bind(window)), transport: new FetchTransport(fetch.bind(window)),
}); });
state.client this.client
.fetchSession() .fetchSession()
.then(() => { .then(() => {
console.log("Session recieved"); console.log("Session received");
if (state.client) {
state.session = state.client.getSession(); // For the type checker
setInternalState({ if (!this.client) return;
...state,
session: state.client.getSession(), const session = this.client.getSession();
}); this.setState({
} ...this.state,
session: session,
});
}) })
.catch((error) => console.error(error)); .catch((error) => console.error(error));
return; return;
}; }
const loadAuth = () => { onHashChange() {
console.log(window.location.hash);
const hash = window.location.hash.substring(1);
this.setState({
...this.state,
location: { accountId: hash },
});
}
// When the user provides credentials
onLogin(email: string, password: string) {
// Store the provided credentials for now
this.setState({
...this.state,
auth: {
email: email,
password: password,
},
});
localStorage.setItem("auth", JSON.stringify(this.state.auth));
this.doLogin({ email, password });
}
loadAuth() {
const data = localStorage.getItem("auth"); const data = localStorage.getItem("auth");
if (!data) return; if (!data) return;
const auth = JSON.parse(data); const auth = JSON.parse(data);
state.auth = auth; this.setState({
if (state.client == null) { ...this.state,
console.log("NULL STATE.client"); auth: auth,
doLogin(state.auth); });
if (this.client == null) {
this.doLogin(auth);
return; return;
} }
}; }
useEffect(() => { componentDidMount() {
loadAuth(); window.addEventListener(
}, []); "hashchange",
() => {
this.onHashChange();
},
false,
);
this.loadAuth();
this.onHashChange();
}
return ( componentWillUnmount() {
<div className="App"> window.removeEventListener(
{state && state.auth ? ( "hashchange",
<AccountList accounts={state.session ? state.session.accounts : {}} /> () => {
) : ( this.onHashChange();
<AuthModal onLogin={onLogin}></AuthModal> },
)} false,
</div> );
); }
};
render() {
return (
<div className="App">
{this.state && this.state.auth ? (
<AccountList
account={this.account()}
accounts={this.state.session ? this.state.session.accounts : {}}
/>
) : (
<AuthModal onLogin={this.onLogin}></AuthModal>
)}
</div>
);
}
}
export default App; export default App;