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;
|
||||
}
|
||||
@@ -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 workspace’s subdomain.": "To continue, enter your workspace’s 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",
|
||||
|
||||
Reference in New Issue
Block a user