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 * 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];
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
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 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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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 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.",
|
"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",
|
||||||
|
|||||||
Reference in New Issue
Block a user