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 * as React from "react";
import styled from "styled-components"; import styled from "styled-components";
import { stringToColor } from "@shared/utils/color"; import { stringToColor } from "@shared/utils/color";
import RootStore from "~/stores/RootStore"; import RootStore from "~/stores/RootStore";
import { LoginDialog } from "~/scenes/Login/components/LoginDialog";
import TeamNew from "~/scenes/TeamNew"; import TeamNew from "~/scenes/TeamNew";
import TeamLogo from "~/components/TeamLogo"; import TeamLogo from "~/components/TeamLogo";
import { createAction } from "~/actions"; import { createAction } from "~/actions";
import { ActionContext } from "~/types"; import { ActionContext } from "~/types";
import Desktop from "~/utils/Desktop";
import { TeamSection } from "../sections"; import { TeamSection } from "../sections";
export const createTeamsList = ({ stores }: { stores: RootStore }) => 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)` const StyledTeamLogo = styled(TeamLogo)`
border-radius: 2px; border-radius: 2px;
border: 0; 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 ContextMenu from "~/components/ContextMenu";
import Template from "~/components/ContextMenu/Template"; import Template from "~/components/ContextMenu/Template";
import { navigateToSettings, logout } from "~/actions/definitions/navigation"; 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 useActionContext from "~/hooks/useActionContext";
import usePrevious from "~/hooks/usePrevious"; import usePrevious from "~/hooks/usePrevious";
import useStores from "~/hooks/useStores"; import useStores from "~/hooks/useStores";
@@ -39,6 +43,7 @@ const OrganizationMenu: React.FC = ({ children }: Props) => {
() => [ () => [
...createTeamsList(context), ...createTeamsList(context),
createTeam, createTeam,
desktopLoginTeam,
separator(), separator(),
navigateToSettings, navigateToSettings,
logout, logout,

View File

@@ -7,7 +7,7 @@ import InputLarge from "~/components/InputLarge";
import PluginIcon from "~/components/PluginIcon"; import PluginIcon from "~/components/PluginIcon";
import { client } from "~/utils/ApiClient"; import { client } from "~/utils/ApiClient";
import Desktop from "~/utils/Desktop"; import Desktop from "~/utils/Desktop";
import { getRedirectUrl } from "../getRedirectUrl"; import { getRedirectUrl } from "../urls";
type Props = { type Props = {
id: string; 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 AuthenticationProvider from "./components/AuthenticationProvider";
import BackButton from "./components/BackButton"; import BackButton from "./components/BackButton";
import Notices from "./components/Notices"; import Notices from "./components/Notices";
import { getRedirectUrl } from "./getRedirectUrl"; import { getRedirectUrl, navigateToSubdomain } from "./urls";
type Props = { type Props = {
children?: (config?: Config) => React.ReactNode; children?: (config?: Config) => React.ReactNode;
@@ -66,17 +66,7 @@ function Login({ children }: Props) {
const handleGoSubdomain = React.useCallback(async (event) => { const handleGoSubdomain = React.useCallback(async (event) => {
event.preventDefault(); event.preventDefault();
const data = Object.fromEntries(new FormData(event.target)); const data = Object.fromEntries(new FormData(event.target));
const normalizedSubdomain = data.subdomain await navigateToSubdomain(data.subdomain.toString());
.toString()
.toLowerCase()
.trim()
.replace(/^https?:\/\//, "");
const host = `https://${normalizedSubdomain}.getoutline.com`;
await Desktop.bridge.addCustomHost(host);
setTimeout(() => {
window.location.href = host;
}, 500);
}, []); }, []);
React.useEffect(() => { React.useEffect(() => {
@@ -186,6 +176,8 @@ function Login({ children }: Props) {
name="subdomain" name="subdomain"
style={{ textAlign: "right" }} style={{ textAlign: "right" }}
placeholder={t("subdomain")} placeholder={t("subdomain")}
pattern="^[a-z\d-]+$"
required
> >
<Domain>.getoutline.com</Domain> <Domain>.getoutline.com</Domain>
</Input> </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. * 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 * We pass the host into the auth URL so that the server can redirect on error
* and keep the user on the same page. * and keep the user on the same page.
*
* @param authUrl The URL to redirect to after authentication
*/ */
export function getRedirectUrl(authUrl: string) { export function getRedirectUrl(authUrl: string) {
const { custom, teamSubdomain, host } = parseDomain(window.location.origin); const { custom, teamSubdomain, host } = parseDomain(window.location.origin);
@@ -23,3 +25,18 @@ export function getRedirectUrl(authUrl: string) {
return url.toString(); 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", "Select a workspace": "Select a workspace",
"New workspace": "New workspace", "New workspace": "New workspace",
"Create a workspace": "Create a workspace", "Create a workspace": "Create a workspace",
"Login to workspace": "Login to workspace",
"Invite people": "Invite people", "Invite people": "Invite people",
"Invite to workspace": "Invite to workspace", "Invite to workspace": "Invite to workspace",
"Promote to {{ role }}": "Promote to {{ role }}", "Promote to {{ role }}": "Promote to {{ role }}",
@@ -710,6 +711,9 @@
"Continue with Email": "Continue with Email", "Continue with Email": "Continue with Email",
"Continue with {{ authProviderName }}": "Continue with {{ authProviderName }}", "Continue with {{ authProviderName }}": "Continue with {{ authProviderName }}",
"Back to home": "Back to home", "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.", "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.", "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.", "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.", "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", "Choose workspace": "Choose workspace",
"This login method requires choosing your workspace to continue": "This login method requires choosing your workspace to continue", "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", "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.", "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", "Back to login": "Back to login",