Add dialog to allow easy login to multiple workspaces in desktop app
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
import { PlusIcon } from "outline-icons";
|
||||
import { ArrowIcon, PlusIcon } from "outline-icons";
|
||||
import * as React from "react";
|
||||
import styled from "styled-components";
|
||||
import { stringToColor } from "@shared/utils/color";
|
||||
import RootStore from "~/stores/RootStore";
|
||||
import { LoginDialog } from "~/scenes/Login/components/LoginDialog";
|
||||
import TeamNew from "~/scenes/TeamNew";
|
||||
import TeamLogo from "~/components/TeamLogo";
|
||||
import { createAction } from "~/actions";
|
||||
import { ActionContext } from "~/types";
|
||||
import Desktop from "~/utils/Desktop";
|
||||
import { TeamSection } from "../sections";
|
||||
|
||||
export const createTeamsList = ({ stores }: { stores: RootStore }) =>
|
||||
@@ -66,9 +68,27 @@ export const createTeam = createAction({
|
||||
},
|
||||
});
|
||||
|
||||
export const desktopLoginTeam = createAction({
|
||||
name: ({ t }) => t("Login to workspace"),
|
||||
analyticsName: "Login to workspace",
|
||||
keywords: "change switch workspace organization team",
|
||||
section: TeamSection,
|
||||
icon: <ArrowIcon />,
|
||||
visible: () => Desktop.isElectron(),
|
||||
perform: ({ t, event, stores }) => {
|
||||
event?.preventDefault();
|
||||
event?.stopPropagation();
|
||||
|
||||
stores.dialogs.openModal({
|
||||
title: t("Login to workspace"),
|
||||
content: <LoginDialog />,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const StyledTeamLogo = styled(TeamLogo)`
|
||||
border-radius: 2px;
|
||||
border: 0;
|
||||
`;
|
||||
|
||||
export const rootTeamActions = [switchTeam, createTeam];
|
||||
export const rootTeamActions = [switchTeam, createTeam, desktopLoginTeam];
|
||||
|
||||
@@ -5,7 +5,11 @@ import { MenuButton, useMenuState } from "reakit/Menu";
|
||||
import ContextMenu from "~/components/ContextMenu";
|
||||
import Template from "~/components/ContextMenu/Template";
|
||||
import { navigateToSettings, logout } from "~/actions/definitions/navigation";
|
||||
import { createTeam, createTeamsList } from "~/actions/definitions/teams";
|
||||
import {
|
||||
createTeam,
|
||||
createTeamsList,
|
||||
desktopLoginTeam,
|
||||
} from "~/actions/definitions/teams";
|
||||
import useActionContext from "~/hooks/useActionContext";
|
||||
import usePrevious from "~/hooks/usePrevious";
|
||||
import useStores from "~/hooks/useStores";
|
||||
@@ -39,6 +43,7 @@ const OrganizationMenu: React.FC = ({ children }: Props) => {
|
||||
() => [
|
||||
...createTeamsList(context),
|
||||
createTeam,
|
||||
desktopLoginTeam,
|
||||
separator(),
|
||||
navigateToSettings,
|
||||
logout,
|
||||
|
||||
@@ -7,7 +7,7 @@ import InputLarge from "~/components/InputLarge";
|
||||
import PluginIcon from "~/components/PluginIcon";
|
||||
import { client } from "~/utils/ApiClient";
|
||||
import Desktop from "~/utils/Desktop";
|
||||
import { getRedirectUrl } from "../getRedirectUrl";
|
||||
import { getRedirectUrl } from "../urls";
|
||||
|
||||
type Props = {
|
||||
id: string;
|
||||
|
||||
56
app/scenes/Login/components/LoginDialog.tsx
Normal file
56
app/scenes/Login/components/LoginDialog.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import * as React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import styled from "styled-components";
|
||||
import { s } from "@shared/styles";
|
||||
import ButtonLarge from "~/components/ButtonLarge";
|
||||
import Input from "~/components/Input";
|
||||
import Text from "~/components/Text";
|
||||
import { navigateToSubdomain } from "../urls";
|
||||
|
||||
type FormData = {
|
||||
subdomain: string;
|
||||
};
|
||||
|
||||
export function LoginDialog() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleSubmit = async (data: FormData) => {
|
||||
try {
|
||||
await navigateToSubdomain(data.subdomain);
|
||||
} catch {
|
||||
toast.error(t("The workspace could not be found"));
|
||||
}
|
||||
};
|
||||
|
||||
const { register, handleSubmit: formHandleSubmit } = useForm<FormData>({
|
||||
mode: "all",
|
||||
defaultValues: {
|
||||
subdomain: "",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={formHandleSubmit(handleSubmit)}>
|
||||
<Text as="p">{t("To continue, enter your workspace’s subdomain.")}</Text>
|
||||
<Input
|
||||
autoFocus
|
||||
maxLength={255}
|
||||
autoComplete="off"
|
||||
placeholder={t("subdomain")}
|
||||
{...register("subdomain", { required: true, pattern: /^[a-z\d-]+$/ })}
|
||||
>
|
||||
<Domain>.getoutline.com</Domain>
|
||||
</Input>
|
||||
<ButtonLarge type="submit" fullwidth>
|
||||
{t("Continue")}
|
||||
</ButtonLarge>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
const Domain = styled.div`
|
||||
color: ${s("textSecondary")};
|
||||
padding: 0 8px 0 0;
|
||||
`;
|
||||
@@ -33,7 +33,7 @@ import { detectLanguage } from "~/utils/language";
|
||||
import AuthenticationProvider from "./components/AuthenticationProvider";
|
||||
import BackButton from "./components/BackButton";
|
||||
import Notices from "./components/Notices";
|
||||
import { getRedirectUrl } from "./getRedirectUrl";
|
||||
import { getRedirectUrl, navigateToSubdomain } from "./urls";
|
||||
|
||||
type Props = {
|
||||
children?: (config?: Config) => React.ReactNode;
|
||||
@@ -66,17 +66,7 @@ function Login({ children }: Props) {
|
||||
const handleGoSubdomain = React.useCallback(async (event) => {
|
||||
event.preventDefault();
|
||||
const data = Object.fromEntries(new FormData(event.target));
|
||||
const normalizedSubdomain = data.subdomain
|
||||
.toString()
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/^https?:\/\//, "");
|
||||
const host = `https://${normalizedSubdomain}.getoutline.com`;
|
||||
await Desktop.bridge.addCustomHost(host);
|
||||
|
||||
setTimeout(() => {
|
||||
window.location.href = host;
|
||||
}, 500);
|
||||
await navigateToSubdomain(data.subdomain.toString());
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -186,6 +176,8 @@ function Login({ children }: Props) {
|
||||
name="subdomain"
|
||||
style={{ textAlign: "right" }}
|
||||
placeholder={t("subdomain")}
|
||||
pattern="^[a-z\d-]+$"
|
||||
required
|
||||
>
|
||||
<Domain>.getoutline.com</Domain>
|
||||
</Input>
|
||||
|
||||
@@ -8,6 +8,8 @@ import Desktop from "~/utils/Desktop";
|
||||
* apex (env.URL) for authentication so that the state cookie can be set and read.
|
||||
* We pass the host into the auth URL so that the server can redirect on error
|
||||
* and keep the user on the same page.
|
||||
*
|
||||
* @param authUrl The URL to redirect to after authentication
|
||||
*/
|
||||
export function getRedirectUrl(authUrl: string) {
|
||||
const { custom, teamSubdomain, host } = parseDomain(window.location.origin);
|
||||
@@ -23,3 +25,18 @@ export function getRedirectUrl(authUrl: string) {
|
||||
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to a subdomain, adding it to the custom hosts list on desktop first.
|
||||
*
|
||||
* @param subdomain The subdomain to navigate to
|
||||
*/
|
||||
export async function navigateToSubdomain(subdomain: string) {
|
||||
const normalizedSubdomain = subdomain
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/^https?:\/\//, "");
|
||||
const host = `https://${normalizedSubdomain}.getoutline.com`;
|
||||
await Desktop.bridge.addCustomHost(host);
|
||||
window.location.href = host;
|
||||
}
|
||||
Reference in New Issue
Block a user