diff --git a/app/actions/definitions/teams.tsx b/app/actions/definitions/teams.tsx
index a4662b489..a9847551d 100644
--- a/app/actions/definitions/teams.tsx
+++ b/app/actions/definitions/teams.tsx
@@ -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: ,
+ visible: () => Desktop.isElectron(),
+ perform: ({ t, event, stores }) => {
+ event?.preventDefault();
+ event?.stopPropagation();
+
+ stores.dialogs.openModal({
+ title: t("Login to workspace"),
+ content: ,
+ });
+ },
+});
+
const StyledTeamLogo = styled(TeamLogo)`
border-radius: 2px;
border: 0;
`;
-export const rootTeamActions = [switchTeam, createTeam];
+export const rootTeamActions = [switchTeam, createTeam, desktopLoginTeam];
diff --git a/app/menus/OrganizationMenu.tsx b/app/menus/OrganizationMenu.tsx
index d20e67807..b487003cc 100644
--- a/app/menus/OrganizationMenu.tsx
+++ b/app/menus/OrganizationMenu.tsx
@@ -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,
diff --git a/app/scenes/Login/components/AuthenticationProvider.tsx b/app/scenes/Login/components/AuthenticationProvider.tsx
index 5d564453d..0eede84bd 100644
--- a/app/scenes/Login/components/AuthenticationProvider.tsx
+++ b/app/scenes/Login/components/AuthenticationProvider.tsx
@@ -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;
diff --git a/app/scenes/Login/components/LoginDialog.tsx b/app/scenes/Login/components/LoginDialog.tsx
new file mode 100644
index 000000000..c8a5b0a7f
--- /dev/null
+++ b/app/scenes/Login/components/LoginDialog.tsx
@@ -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({
+ mode: "all",
+ defaultValues: {
+ subdomain: "",
+ },
+ });
+
+ return (
+
+ );
+}
+
+const Domain = styled.div`
+ color: ${s("textSecondary")};
+ padding: 0 8px 0 0;
+`;
diff --git a/app/scenes/Login/index.tsx b/app/scenes/Login/index.tsx
index c01ad845a..2f486c827 100644
--- a/app/scenes/Login/index.tsx
+++ b/app/scenes/Login/index.tsx
@@ -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
>
.getoutline.com
diff --git a/app/scenes/Login/getRedirectUrl.ts b/app/scenes/Login/urls.ts
similarity index 61%
rename from app/scenes/Login/getRedirectUrl.ts
rename to app/scenes/Login/urls.ts
index e5039a956..c4d7e33e4 100644
--- a/app/scenes/Login/getRedirectUrl.ts
+++ b/app/scenes/Login/urls.ts
@@ -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;
+}
diff --git a/shared/i18n/locales/en_US/translation.json b/shared/i18n/locales/en_US/translation.json
index a5a169ccd..fd7140fd3 100644
--- a/shared/i18n/locales/en_US/translation.json
+++ b/shared/i18n/locales/en_US/translation.json
@@ -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 {{ emailLinkSentTo }} if an account exists.": "A magic sign-in link has been sent to the email {{ emailLinkSentTo }} if an account exists.",
"Back to login": "Back to login",