Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions apps/space/components/account/auth-forms/email.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { observer } from "mobx-react";
import { CircleAlert, XCircle } from "lucide-react";
// types
import { Button } from "@plane/propel/button";
import { useTranslation } from "@plane/i18n";
import type { IEmailCheckData } from "@plane/types";
// ui
import { Input, Spinner } from "@plane/ui";
Expand All @@ -25,13 +26,14 @@ type TAuthEmailForm = {

export const AuthEmailForm = observer(function AuthEmailForm(props: TAuthEmailForm) {
const { onSubmit, defaultEmail } = props;
const { t } = useTranslation();
// states
const [isSubmitting, setIsSubmitting] = useState(false);
const [email, setEmail] = useState(defaultEmail);

const emailError = useMemo(
() => (email && !checkEmailValidity(email) ? { email: "Email is invalid" } : undefined),
[email]
() => (email && !checkEmailValidity(email) ? { email: t("localized_ui.space_auth.email_invalid") } : undefined),
[email, t]
);

const handleFormSubmit = async (event: FormEvent<HTMLFormElement>) => {
Expand All @@ -53,7 +55,7 @@ export const AuthEmailForm = observer(function AuthEmailForm(props: TAuthEmailFo
<form onSubmit={handleFormSubmit} className="mt-5 space-y-4">
<div className="space-y-1">
<label className="text-13 font-medium text-tertiary" htmlFor="email">
Email
{t("localized_ui.space_auth.email")}
</label>
<div
className={cn(
Expand All @@ -76,13 +78,12 @@ export const AuthEmailForm = observer(function AuthEmailForm(props: TAuthEmailFo
placeholder="name@company.com"
className={`h-10 w-full border-0 disable-autofill-style placeholder:text-placeholder autofill:bg-danger-subtle focus:bg-none active:bg-transparent`}
autoComplete="off"
autoFocus
ref={inputRef}
/>
Comment on lines 82 to 86
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore initial focus to the auth email input

The email form no longer autofocuses its primary input, so users who open Space auth and immediately type or press Enter must first click into the field. This is a workflow regression from the previous behavior and affects keyboard-first sign-in/sign-up flows across the first auth step; please restore autoFocus (or equivalent mount-time focus logic) on the initial email field.

Useful? React with 👍 / 👎.

{email.length > 0 && (
<button
type="button"
aria-label="Clear email"
aria-label={t("localized_ui.space_auth.clear_email")}
onClick={() => {
setEmail("");
inputRef.current?.focus();
Expand All @@ -101,7 +102,7 @@ export const AuthEmailForm = observer(function AuthEmailForm(props: TAuthEmailFo
)}
</div>
<Button type="submit" variant="primary" className="w-full" size="xl" disabled={isButtonDisabled}>
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
{isSubmitting ? <Spinner height="20px" width="20px" /> : t("localized_ui.space_auth.continue")}
</Button>
</form>
);
Expand Down
40 changes: 21 additions & 19 deletions apps/space/components/account/auth-forms/password.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { observer } from "mobx-react";
import { Eye, EyeOff, XCircle } from "lucide-react";
// plane imports
import { API_BASE_URL, E_PASSWORD_STRENGTH } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { AuthService } from "@plane/services";
import { Input, Spinner, PasswordStrengthIndicator } from "@plane/ui";
Expand Down Expand Up @@ -41,6 +42,7 @@ const authService = new AuthService();

export const AuthPasswordForm = observer(function AuthPasswordForm(props: Props) {
const { email, nextPath, isSMTPConfigured, handleAuthStep, handleEmailClear, mode } = props;
const { t } = useTranslation();
// ref
const formRef = useRef<HTMLFormElement>(null);
// states
Expand Down Expand Up @@ -79,14 +81,11 @@ export const AuthPasswordForm = observer(function AuthPasswordForm(props: Props)

const isButtonDisabled = useMemo(
() =>
!isSubmitting &&
!!passwordFormData.password &&
(mode === EAuthModes.SIGN_UP
? getPasswordStrength(passwordFormData.password) === E_PASSWORD_STRENGTH.STRENGTH_VALID &&
passwordFormData.password === passwordFormData.confirm_password
: true)
? false
: true,
isSubmitting ||
!passwordFormData.password ||
(mode === EAuthModes.SIGN_UP &&
(getPasswordStrength(passwordFormData.password) !== E_PASSWORD_STRENGTH.STRENGTH_VALID ||
passwordFormData.password !== passwordFormData.confirm_password)),
[isSubmitting, mode, passwordFormData.confirm_password, passwordFormData.password]
);

Expand Down Expand Up @@ -123,7 +122,7 @@ export const AuthPasswordForm = observer(function AuthPasswordForm(props: Props)
<input type="hidden" value={nextPath} name="next_path" />
<div className="space-y-1">
<label className="text-13 font-medium text-tertiary" htmlFor="email">
Email
{t("localized_ui.space_auth.email")}
</label>
<div className={`relative flex items-center rounded-md border border-subtle bg-surface-1`}>
<Input
Expand All @@ -147,20 +146,21 @@ export const AuthPasswordForm = observer(function AuthPasswordForm(props: Props)

<div className="space-y-1">
<label className="text-13 font-medium text-tertiary" htmlFor="password">
{mode === EAuthModes.SIGN_IN ? "Password" : "Set a password"}
{mode === EAuthModes.SIGN_IN
? t("localized_ui.space_auth.password")
: t("localized_ui.space_auth.set_password")}
</label>
<div className="relative flex items-center rounded-md bg-surface-1">
<Input
type={showPassword?.password ? "text" : "password"}
name="password"
value={passwordFormData.password}
onChange={(e) => handleFormChange("password", e.target.value)}
placeholder="Enter password"
placeholder={t("localized_ui.space_auth.enter_password")}
className="h-10 w-full border border-subtle !bg-surface-1 pr-12 disable-autofill-style placeholder:text-placeholder"
onFocus={() => setIsPasswordInputFocused(true)}
onBlur={() => setIsPasswordInputFocused(false)}
autoComplete="off"
autoFocus
/>
Comment on lines 156 to 164
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore mount-time focus for password entry

The password step no longer sets initial focus on its primary input after autoFocus was removed, so users who submit the email step and immediately start typing are left with focus on the prior element instead of the password field. This regresses keyboard-first sign-in/sign-up flows and can cause Enter/typing to behave unexpectedly until the user clicks into the input.

Useful? React with 👍 / 👎.

{showPassword?.password ? (
<EyeOff
Expand All @@ -180,15 +180,15 @@ export const AuthPasswordForm = observer(function AuthPasswordForm(props: Props)
{mode === EAuthModes.SIGN_UP && (
<div className="space-y-1">
<label className="text-13 font-medium text-tertiary" htmlFor="confirm_password">
Confirm password
{t("localized_ui.space_auth.confirm_password")}
</label>
<div className="relative flex items-center rounded-md bg-surface-1">
<Input
type={showPassword?.retypePassword ? "text" : "password"}
name="confirm_password"
value={passwordFormData.confirm_password}
onChange={(e) => handleFormChange("confirm_password", e.target.value)}
placeholder="Confirm password"
placeholder={t("localized_ui.space_auth.confirm_password")}
className="h-10 w-full border border-subtle !bg-surface-1 pr-12 disable-autofill-style placeholder:text-placeholder"
onFocus={() => setIsRetryPasswordInputFocused(true)}
onBlur={() => setIsRetryPasswordInputFocused(false)}
Expand All @@ -208,7 +208,9 @@ export const AuthPasswordForm = observer(function AuthPasswordForm(props: Props)
</div>
{!!passwordFormData.confirm_password &&
passwordFormData.password !== passwordFormData.confirm_password &&
renderPasswordMatchError && <span className="text-13 text-danger-primary">Passwords don{"'"}t match</span>}
renderPasswordMatchError && (
<span className="text-13 text-danger-primary">{t("localized_ui.space_auth.passwords_dont_match")}</span>
)}
</div>
)}

Expand All @@ -219,9 +221,9 @@ export const AuthPasswordForm = observer(function AuthPasswordForm(props: Props)
{isSubmitting ? (
<Spinner height="20px" width="20px" />
) : isSMTPConfigured ? (
"Continue"
t("localized_ui.space_auth.continue")
) : (
"Go to workspace"
t("localized_ui.space_auth.go_to_workspace")
)}
</Button>
{isSMTPConfigured && (
Expand All @@ -232,13 +234,13 @@ export const AuthPasswordForm = observer(function AuthPasswordForm(props: Props)
className="w-full"
size="xl"
>
Sign in with unique code
{t("localized_ui.space_auth.sign_in_with_unique_code")}
</Button>
)}
</>
) : (
<Button type="submit" variant="primary" className="w-full" size="xl" disabled={isButtonDisabled}>
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Create account"}
{isSubmitting ? <Spinner height="20px" width="20px" /> : t("localized_ui.space_auth.create_account")}
</Button>
)}
</div>
Expand Down
27 changes: 17 additions & 10 deletions apps/space/components/account/auth-forms/unique-code.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import React, { useEffect, useState } from "react";
import { CircleCheck, XCircle } from "lucide-react";
// plane imports
import { API_BASE_URL } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { Button } from "@plane/propel/button";
import { AuthService } from "@plane/services";
import { Input, Spinner } from "@plane/ui";
Expand Down Expand Up @@ -39,6 +40,7 @@ const defaultValues: TUniqueCodeFormValues = {

export function AuthUniqueCodeForm(props: TAuthUniqueCodeForm) {
const { mode, email, nextPath, handleEmailClear, generateEmailUniqueCode } = props;
const { t } = useTranslation();
// derived values
const defaultResetTimerValue = 5;
// states
Expand All @@ -52,10 +54,10 @@ export function AuthUniqueCodeForm(props: TAuthUniqueCodeForm) {
const handleFormChange = (key: keyof TUniqueCodeFormValues, value: string) =>
setUniqueCodeFormData((prev) => ({ ...prev, [key]: value }));

const generateNewCode = async (email: string) => {
const generateNewCode = async (targetEmail: string) => {
try {
setIsRequestingNewCode(true);
const uniqueCode = await generateEmailUniqueCode(email);
const uniqueCode = await generateEmailUniqueCode(targetEmail);
setResendCodeTimer(defaultResetTimerValue);
handleFormChange("code", uniqueCode?.code || "");
setIsRequestingNewCode(false);
Expand Down Expand Up @@ -87,7 +89,7 @@ export function AuthUniqueCodeForm(props: TAuthUniqueCodeForm) {
<input type="hidden" value={nextPath} name="next_path" />
<div className="space-y-1">
<label className="text-13 font-medium text-tertiary" htmlFor="email">
Email
{t("localized_ui.space_auth.email")}
</label>
<div className={`relative flex items-center rounded-md border border-subtle bg-surface-1`}>
<Input
Expand All @@ -112,7 +114,7 @@ export function AuthUniqueCodeForm(props: TAuthUniqueCodeForm) {

<div className="space-y-1">
<label className="text-13 font-medium text-tertiary" htmlFor="code">
Unique code
{t("localized_ui.space_auth.unique_code")}
</label>
<Input
name="code"
Expand All @@ -121,12 +123,11 @@ export function AuthUniqueCodeForm(props: TAuthUniqueCodeForm) {
placeholder="123456"
className="h-10 w-full border border-subtle !bg-surface-1 pr-12 disable-autofill-style placeholder:text-placeholder"
autoComplete="off"
autoFocus
/>
Comment on lines 123 to 126
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Restore initial focus on unique-code input

The unique-code form also lost its input autofocus in this change, so when users switch to the OTP step they cannot immediately paste or type the code without first clicking the field. That is a direct usability regression for the email-code auth path and slows down the primary verification flow.

Useful? React with 👍 / 👎.

<div className="flex w-full items-center justify-between px-1 pt-1 text-11">
<p className="flex items-center gap-1 font-medium text-success-primary">
<CircleCheck height={12} width={12} />
Paste the code sent to your email
{t("localized_ui.space_auth.paste_code_sent")}
</p>
<button
type="button"
Expand All @@ -139,17 +140,23 @@ export function AuthUniqueCodeForm(props: TAuthUniqueCodeForm) {
disabled={isRequestNewCodeDisabled}
>
{resendTimerCode > 0
? `Resend in ${resendTimerCode}s`
? t("localized_ui.space_auth.resend_in", { seconds: resendTimerCode })
: isRequestingNewCode
? "Requesting new code"
: "Resend"}
? t("localized_ui.space_auth.requesting_new_code")
: t("localized_ui.space_auth.resend")}
</button>
</div>
</div>

<div className="space-y-2.5">
<Button type="submit" variant="primary" className="w-full" size="xl" disabled={isButtonDisabled}>
{isRequestingNewCode ? "Sending code" : isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
{isRequestingNewCode ? (
t("localized_ui.space_auth.sending_code")
) : isSubmitting ? (
<Spinner height="20px" width="20px" />
) : (
t("localized_ui.space_auth.continue")
)}
</Button>
</div>
</form>
Expand Down
59 changes: 40 additions & 19 deletions apps/web/app/(all)/workspace-invitations/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useSearchParams } from "next/navigation";
import useSWR from "swr";
import { Boxes, Share2, Star, User2 } from "lucide-react";
import { CheckIcon, CloseIcon } from "@plane/propel/icons";
import { useTranslation } from "@plane/i18n";
// components
import { LogoSpinner } from "@/components/common/logo-spinner";
import { EmptySpace, EmptySpaceItem } from "@/components/ui/empty-space";
Expand All @@ -28,6 +29,7 @@ import { WorkspaceService } from "@/services/workspace.service";
const workspaceService = new WorkspaceService();

function WorkspaceInvitationPage() {
const { t } = useTranslation();
// router
const router = useAppRouter();
// query params
Expand All @@ -54,10 +56,9 @@ function WorkspaceInvitationPage() {
})
.then(() => {
if (invitationDetail.email === currentUser?.email) {
router.push(`/${invitationDetail.workspace.slug}`);
} else {
router.push("/");
return router.push(`/${invitationDetail.workspace.slug}`);
}
return router.push("/");
})
.catch((err: unknown) => console.error(err));
};
Expand All @@ -70,7 +71,7 @@ function WorkspaceInvitationPage() {
token: token,
})
.then(() => {
router.push("/");
return router.push("/");
})
.catch((err: unknown) => console.error(err));
};
Expand All @@ -81,40 +82,60 @@ function WorkspaceInvitationPage() {
{invitationDetail && !invitationDetail.responded_at ? (
error ? (
<div className="shadow-2xl flex w-full flex-col space-y-4 rounded-sm border border-subtle bg-surface-1 px-4 py-8 text-center md:w-1/3">
<h2 className="text-18 uppercase">INVITATION NOT FOUND</h2>
<h2 className="text-18 uppercase">{t("localized_ui.workspace_invitation.invitation_not_found")}</h2>
</div>
) : (
<EmptySpace
title={`You have been invited to ${invitationDetail.workspace.name}`}
description="Your workspace is where you'll create projects, collaborate on your work items, and organize different streams of work in your Plane account."
title={t("localized_ui.workspace_invitation.invited_to_workspace", {
workspaceName: invitationDetail.workspace.name,
})}
description={t("localized_ui.workspace_invitation.description")}
>
<EmptySpaceItem Icon={CheckIcon} title="Accept" action={handleAccept} />
<EmptySpaceItem Icon={CloseIcon} title="Ignore" action={handleReject} />
<EmptySpaceItem
Icon={CheckIcon}
title={t("localized_ui.workspace_invitation.accept")}
action={handleAccept}
/>
<EmptySpaceItem
Icon={CloseIcon}
title={t("localized_ui.workspace_invitation.ignore")}
action={handleReject}
/>
</EmptySpace>
)
) : error || invitationDetail?.responded_at ? (
invitationDetail?.accepted ? (
<EmptySpace
title={`You are already a member of ${invitationDetail.workspace.name}`}
description="Your workspace is where you'll create projects, collaborate on your work items, and organize different streams of work in your Plane account."
title={t("localized_ui.workspace_invitation.already_member", {
workspaceName: invitationDetail.workspace.name,
})}
description={t("localized_ui.workspace_invitation.description")}
>
<EmptySpaceItem Icon={Boxes} title="Continue to home" href="/" />
<EmptySpaceItem Icon={Boxes} title={t("localized_ui.workspace_invitation.continue_to_home")} href="/" />
</EmptySpace>
) : (
<EmptySpace
title="This invitation link is not active anymore."
description="Your workspace is where you'll create projects, collaborate on your work items, and organize different streams of work in your Plane account."
link={{ text: "Or start from an empty project", href: "/" }}
title={t("localized_ui.workspace_invitation.inactive_title")}
description={t("localized_ui.workspace_invitation.description")}
link={{ text: t("localized_ui.workspace_invitation.empty_project_link"), href: "/" }}
>
{!currentUser ? (
<EmptySpaceItem Icon={User2} title="Sign in to continue" href="/" />
<EmptySpaceItem
Icon={User2}
title={t("localized_ui.workspace_invitation.sign_in_to_continue")}
href="/"
/>
) : (
<EmptySpaceItem Icon={Boxes} title="Continue to home" href="/" />
<EmptySpaceItem Icon={Boxes} title={t("localized_ui.workspace_invitation.continue_to_home")} href="/" />
)}
<EmptySpaceItem Icon={Star} title="Star us on GitHub" href="https://github.com/makeplane" />
<EmptySpaceItem
Icon={Star}
title={t("localized_ui.workspace_invitation.star_on_github")}
href="https://github.com/makeplane"
/>
<EmptySpaceItem
Icon={Share2}
title="Join our community of active creators"
title={t("localized_ui.workspace_invitation.join_community")}
href="https://forum.plane.so"
/>
</EmptySpace>
Expand Down
Loading