Add dialog to allow easy login to multiple workspaces in desktop app

This commit is contained in:
Tom Moor
2024-04-13 10:35:47 -04:00
parent 90ed6a5366
commit 689886797c
7 changed files with 110 additions and 17 deletions

View File

@@ -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];

View File

@@ -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,

View File

@@ -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;

View 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 workspaces 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;
`;

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -102,6 +102,7 @@
"Select a workspace": "Select a workspace",
"New workspace": "New workspace",
"Create a workspace": "Create a workspace",
"Login to workspace": "Login to workspace",
"Invite people": "Invite people",
"Invite to workspace": "Invite to workspace",
"Promote to {{ role }}": "Promote to {{ role }}",
@@ -710,6 +711,9 @@
"Continue with Email": "Continue with Email",
"Continue with {{ authProviderName }}": "Continue with {{ authProviderName }}",
"Back to home": "Back to home",
"The workspace could not be found": "The workspace could not be found",
"To continue, enter your workspaces subdomain.": "To continue, enter your workspaces subdomain.",
"subdomain": "subdomain",
"The domain associated with your email address has not been allowed for this workspace.": "The domain associated with your email address has not been allowed for this workspace.",
"Unable to sign-in. Please navigate to your workspace's custom URL, then try to sign-in again.<1></1>If you were invited to a workspace, you will find a link to it in the invite email.": "Unable to sign-in. Please navigate to your workspace's custom URL, then try to sign-in again.<1></1>If you were invited to a workspace, you will find a link to it in the invite email.",
"Sorry, a new account cannot be created with a personal Gmail address.<1></1>Please use a Google Workspaces account instead.": "Sorry, a new account cannot be created with a personal Gmail address.<1></1>Please use a Google Workspaces account instead.",
@@ -735,7 +739,6 @@
"Your custom domain is successfully pointing at Outline. To complete the setup process please contact support.": "Your custom domain is successfully pointing at Outline. To complete the setup process please contact support.",
"Choose workspace": "Choose workspace",
"This login method requires choosing your workspace to continue": "This login method requires choosing your workspace to continue",
"subdomain": "subdomain",
"Check your email": "Check your email",
"A magic sign-in link has been sent to the email <em>{{ emailLinkSentTo }}</em> if an account exists.": "A magic sign-in link has been sent to the email <em>{{ emailLinkSentTo }}</em> if an account exists.",
"Back to login": "Back to login",