Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
30 changes: 24 additions & 6 deletions apps/admin/core/components/instance/setup-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { API_BASE_URL, E_PASSWORD_STRENGTH } from "@plane/constants";
import { Button } from "@plane/propel/button";
import { AuthService } from "@plane/services";
import { Checkbox, Input, PasswordStrengthIndicator, Spinner } from "@plane/ui";
import { getPasswordStrength } from "@plane/utils";
import { getPasswordStrength, validatePersonName, validateCompanyName } from "@plane/utils";
// components
import { AuthHeader } from "@/app/(all)/(home)/auth-header";
import { Banner } from "@/components/common/banner";
Expand Down Expand Up @@ -167,9 +167,15 @@ export function InstanceSetupForm() {
inputSize="md"
placeholder="Wilber"
value={formData.first_name}
onChange={(e) => handleFormChange("first_name", e.target.value)}
autoComplete="on"
onChange={(e) => {
const validation = validatePersonName(e.target.value);
if (validation === true || e.target.value === "") {
handleFormChange("first_name", e.target.value);
}
}}
autoComplete="off"
autoFocus
maxLength={50}
/>
</div>
<div className="w-full space-y-1">
Expand All @@ -184,8 +190,14 @@ export function InstanceSetupForm() {
inputSize="md"
placeholder="Wright"
value={formData.last_name}
onChange={(e) => handleFormChange("last_name", e.target.value)}
autoComplete="on"
onChange={(e) => {
const validation = validatePersonName(e.target.value);
if (validation === true || e.target.value === "") {
handleFormChange("last_name", e.target.value);
}
}}
autoComplete="off"
maxLength={50}
/>
</div>
</div>
Expand Down Expand Up @@ -223,7 +235,13 @@ export function InstanceSetupForm() {
inputSize="md"
placeholder="Company name"
value={formData.company_name}
onChange={(e) => handleFormChange("company_name", e.target.value)}
onChange={(e) => {
const validation = validateCompanyName(e.target.value, false);
if (validation === true || e.target.value === "") {
handleFormChange("company_name", e.target.value);
}
}}
maxLength={80}
Comment on lines +238 to +244
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Company name required parameter inconsistent with UI.

The UI marks company name as required (Line 228 shows the asterisk), but validateCompanyName is called with required: false. If the field is truly required, pass true to enforce validation:

-                  const validation = validateCompanyName(e.target.value, false);
+                  const validation = validateCompanyName(e.target.value, true);
🤖 Prompt for AI Agents
In @apps/admin/core/components/instance/setup-form.tsx around lines 238 - 244,
The company name field is marked required in the UI but validateCompanyName is
being called with required: false; update the onChange handler so
validateCompanyName is called with required: true (instead of false) so the
required validation is enforced before calling handleFormChange for
"company_name"; ensure the rest of the handler behavior remains the same and
that maxLength/empty-string allowance logic still matches your intended UX.

/>
</div>

Expand Down
7 changes: 4 additions & 3 deletions apps/web/core/components/onboarding/create-workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IUser, IWorkspace, TOnboardingSteps } from "@plane/types";
// ui
import { CustomSelect, Input, Spinner } from "@plane/ui";
import { validateWorkspaceName, validateSlug } from "@plane/utils";
// hooks
import { useWorkspace } from "@/hooks/store/use-workspace";
import { useUserProfile, useUserSettings } from "@/hooks/store/user";
Expand Down Expand Up @@ -132,8 +133,7 @@ export const CreateWorkspace = observer(function CreateWorkspace(props: Props) {
name="name"
rules={{
required: t("common.errors.required"),
validate: (value) =>
/^[\w\s-]*$/.test(value) || t("workspace_creation.errors.validation.name_alphanumeric"),
validate: (value) => validateWorkspaceName(value, true),
maxLength: {
value: 80,
message: t("workspace_creation.errors.validation.name_length"),
Expand Down Expand Up @@ -194,7 +194,8 @@ export const CreateWorkspace = observer(function CreateWorkspace(props: Props) {
type="text"
value={value.toLocaleLowerCase().trim().replace(/ /g, "-")}
onChange={(e) => {
if (/^[a-zA-Z0-9_-]+$/.test(e.target.value)) setInvalidSlug(false);
const validation = validateSlug(e.target.value);
if (validation === true) setInvalidSlug(false);
else setInvalidSlug(true);
onChange(e.target.value.toLowerCase());
Comment on lines 196 to 200
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Same slug validation issue as in steps/workspace/create.tsx.

The slug validation runs on the raw input (e.target.value) while the displayed value is transformed (Line 195). Validate the transformed value for consistency:

                  onChange={(e) => {
-                    const validation = validateSlug(e.target.value);
+                    const transformedSlug = e.target.value.toLowerCase().trim().replace(/ /g, "-");
+                    const validation = validateSlug(transformedSlug);
                    if (validation === true) setInvalidSlug(false);
                    else setInvalidSlug(true);
                    onChange(e.target.value.toLowerCase());
                  }}
🤖 Prompt for AI Agents
In @apps/web/core/components/onboarding/create-workspace.tsx around lines 196 -
200, The slug validation is using the raw input value instead of the
transformed/displayed slug; update the onChange handler in the create-workspace
component so it computes the transformedSlug (e.g., const transformed =
e.target.value.toLowerCase()) and run validateSlug(transformed) before calling
setInvalidSlug and onChange; specifically adjust the handler around
validateSlug, setInvalidSlug, and onChange to operate on the transformed value
rather than e.target.value.

}}
Expand Down
12 changes: 7 additions & 5 deletions apps/web/core/components/onboarding/profile-setup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type { IUser, TUserProfile, TOnboardingSteps } from "@plane/types";
// ui
import { Input, PasswordStrengthIndicator, Spinner } from "@plane/ui";
// components
import { cn, getFileURL, getPasswordStrength } from "@plane/utils";
import { cn, getFileURL, getPasswordStrength, validatePersonName } from "@plane/utils";
import { UserImageUploadModal } from "@/components/core/modals/user-image-upload-modal";
// hooks
import { useUser, useUserProfile } from "@/hooks/store/user";
Expand Down Expand Up @@ -297,9 +297,10 @@ export const ProfileSetup = observer(function ProfileSetup(props: Props) {
name="first_name"
rules={{
required: "First name is required",
validate: validatePersonName,
maxLength: {
value: 24,
message: "First name must be within 24 characters.",
value: 50,
message: "First name must be within 50 characters.",
},
}}
render={({ field: { value, onChange, ref } }) => (
Expand Down Expand Up @@ -334,9 +335,10 @@ export const ProfileSetup = observer(function ProfileSetup(props: Props) {
name="last_name"
rules={{
required: "Last name is required",
validate: validatePersonName,
maxLength: {
value: 24,
message: "Last name must be within 24 characters.",
value: 50,
message: "Last name must be within 50 characters.",
},
}}
render={({ field: { value, onChange, ref } }) => (
Expand Down
7 changes: 4 additions & 3 deletions apps/web/core/components/onboarding/steps/profile/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IUser } from "@plane/types";
import { EOnboardingSteps } from "@plane/types";
import { cn, getFileURL, getPasswordStrength } from "@plane/utils";
import { cn, getFileURL, getPasswordStrength, validatePersonName } from "@plane/utils";
// components
import { UserImageUploadModal } from "@/components/core/modals/user-image-upload-modal";
// hooks
Expand Down Expand Up @@ -202,9 +202,10 @@ export const ProfileSetupStep = observer(function ProfileSetupStep({ handleStepC
name="first_name"
rules={{
required: "Name is required",
validate: validatePersonName,
maxLength: {
value: 24,
message: "Name must be within 24 characters.",
value: 50,
message: "Name must be within 50 characters.",
},
}}
render={({ field: { value, onChange, ref } }) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IUser, IWorkspace } from "@plane/types";
import { Spinner } from "@plane/ui";
import { cn } from "@plane/utils";
import { cn, validateWorkspaceName, validateSlug } from "@plane/utils";
// hooks
import { useWorkspace } from "@/hooks/store/use-workspace";
import { useUserProfile, useUserSettings } from "@/hooks/store/user";
Expand Down Expand Up @@ -139,8 +139,7 @@ export const WorkspaceCreateStep = observer(function WorkspaceCreateStep({
name="name"
rules={{
required: t("common.errors.required"),
validate: (value) =>
/^[\w\s-]*$/.test(value) || t("workspace_creation.errors.validation.name_alphanumeric"),
validate: (value) => validateWorkspaceName(value, true),
maxLength: {
value: 80,
message: t("workspace_creation.errors.validation.name_length"),
Expand Down Expand Up @@ -213,7 +212,8 @@ export const WorkspaceCreateStep = observer(function WorkspaceCreateStep({
type="text"
value={value.toLocaleLowerCase().trim().replace(/ /g, "-")}
onChange={(e) => {
if (/^[a-zA-Z0-9_-]+$/.test(e.target.value)) setInvalidSlug(false);
const validation = validateSlug(e.target.value);
if (validation === true) setInvalidSlug(false);
else setInvalidSlug(true);
onChange(e.target.value.toLowerCase());
Comment on lines 214 to 218
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Slug validation runs on raw input before transformation.

The displayed slug value applies transformations (lowercase, trim, replace spaces with hyphens on Line 213), but validateSlug on Line 215 validates e.target.value directly. This means typing a space will fail validation even though it would be transformed to a hyphen in the display.

Consider validating the transformed value instead:

                  onChange={(e) => {
-                    const validation = validateSlug(e.target.value);
+                    const transformedSlug = e.target.value.toLowerCase().trim().replace(/ /g, "-");
+                    const validation = validateSlug(transformedSlug);
                    if (validation === true) setInvalidSlug(false);
                    else setInvalidSlug(true);
                    onChange(e.target.value.toLowerCase());
                  }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onChange={(e) => {
if (/^[a-zA-Z0-9_-]+$/.test(e.target.value)) setInvalidSlug(false);
const validation = validateSlug(e.target.value);
if (validation === true) setInvalidSlug(false);
else setInvalidSlug(true);
onChange(e.target.value.toLowerCase());
onChange={(e) => {
const transformedSlug = e.target.value.toLowerCase().trim().replace(/ /g, "-");
const validation = validateSlug(transformedSlug);
if (validation === true) setInvalidSlug(false);
else setInvalidSlug(true);
onChange(e.target.value.toLowerCase());
}}
🤖 Prompt for AI Agents
In @apps/web/core/components/onboarding/steps/workspace/create.tsx around lines
214 - 218, The handler is validating the raw input instead of the
displayed/transformed slug; compute the transformed slug first (same logic used
for display: trim, toLowerCase, replace spaces with hyphens), then call
validateSlug on that transformed value, setInvalidSlug based on that result, and
pass the transformed slug into onChange instead of e.target.value so validation
matches the shown value (refer to validateSlug, setInvalidSlug, and the onChange
handler).

}}
Expand Down
22 changes: 11 additions & 11 deletions apps/web/core/components/profile/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { handleCoverImageChange } from "@/helpers/cover-image.helper";
// hooks
import { useInstance } from "@/hooks/store/use-instance";
import { useUser, useUserProfile } from "@/hooks/store/user";
// utils
import { validatePersonName, validateDisplayName } from "@plane/utils";

type TUserProfileForm = {
avatar_url: string;
Expand Down Expand Up @@ -255,6 +257,7 @@ export const ProfileForm = observer(function ProfileForm(props: TProfileFormProp
name="first_name"
rules={{
required: "Please enter first name",
validate: validatePersonName,
}}
render={({ field: { value, onChange, ref } }) => (
<Input
Expand All @@ -267,7 +270,7 @@ export const ProfileForm = observer(function ProfileForm(props: TProfileFormProp
hasError={Boolean(errors.first_name)}
placeholder="Enter your first name"
className={`w-full rounded-md ${errors.first_name ? "border-danger-strong" : ""}`}
maxLength={24}
maxLength={50}
autoComplete="on"
/>
)}
Expand All @@ -279,6 +282,9 @@ export const ProfileForm = observer(function ProfileForm(props: TProfileFormProp
<Controller
control={control}
name="last_name"
rules={{
validate: validatePersonName,
}}
Comment on lines +285 to +287
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validation mismatch: last_name appears optional but validation requires it.

The last_name field label (line 281) has no required asterisk, suggesting it's optional. However, validatePersonName returns "Name is required" for empty values, effectively making last_name mandatory and blocking form submission when empty.

Either mark the field as required in the UI, or use a validator that permits empty values:

🔧 Option 1: Create an optional person name validator

In validation.ts, add an optional variant:

export const validateOptionalPersonName = (name: string): boolean | string => {
  if (!name || name.trim() === "") {
    return true; // Allow empty
  }
  // Rest of validation...
  if (name.length > 50) {
    return "Name must be 50 characters or less";
  }
  if (!PERSON_NAME_REGEX.test(name)) {
    return "Names can only contain letters, spaces, hyphens, and apostrophes";
  }
  return true;
};

Then use it here:

 rules={{
-  validate: validatePersonName,
+  validate: validateOptionalPersonName,
 }}
🔧 Option 2: Mark last_name as required in UI
-<h4 className="text-13 font-medium text-secondary">{t("last_name")}</h4>
+<h4 className="text-13 font-medium text-secondary">
+  {t("last_name")}&nbsp;
+  <span className="text-danger-primary">*</span>
+</h4>
🤖 Prompt for AI Agents
In @apps/web/core/components/profile/form.tsx around lines 285 - 287, The
last_name field label is presented as optional but its rules use
validatePersonName which rejects empty values; either make the field truly
optional by adding a validator that permits empty input (e.g., create
validateOptionalPersonName in validation.ts that returns true for
empty/whitespace and otherwise enforces the same length/regex checks as
validatePersonName, then swap validatePersonName for validateOptionalPersonName
in the rules) or make the field required in the UI (update the label to indicate
required and add a required rule alongside validatePersonName). Ensure you
reference and update validatePersonName/validateOptionalPersonName and the
last_name field's rules/label accordingly.

render={({ field: { value, onChange, ref } }) => (
<Input
id="last_name"
Expand All @@ -290,11 +296,12 @@ export const ProfileForm = observer(function ProfileForm(props: TProfileFormProp
hasError={Boolean(errors.last_name)}
placeholder="Enter your last name"
className="w-full rounded-md"
maxLength={24}
maxLength={50}
autoComplete="on"
/>
)}
/>
{errors.last_name && <span className="text-11 text-danger-primary">{errors.last_name.message}</span>}
</div>
<div className="flex flex-col gap-1">
<h4 className="text-13 font-medium text-secondary">
Expand All @@ -306,14 +313,7 @@ export const ProfileForm = observer(function ProfileForm(props: TProfileFormProp
name="display_name"
rules={{
required: "Display name is required.",
validate: (value) => {
if (value.trim().length < 1) return "Display name can't be empty.";
if (value.split(" ").length > 1) return "Display name can't have two consecutive spaces.";
if (value.replace(/\s/g, "").length < 1) return "Display name must be at least 1 character long.";
if (value.replace(/\s/g, "").length > 20)
return "Display name must be less than 20 characters long.";
return true;
},
validate: validateDisplayName,
}}
render={({ field: { value, onChange, ref } }) => (
<Input
Expand All @@ -326,7 +326,7 @@ export const ProfileForm = observer(function ProfileForm(props: TProfileFormProp
hasError={Boolean(errors?.display_name)}
placeholder="Enter your display name"
className={`w-full ${errors?.display_name ? "border-danger-strong" : ""}`}
maxLength={24}
maxLength={50}
/>
)}
/>
Expand Down
7 changes: 4 additions & 3 deletions apps/web/core/components/workspace/create-workspace-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import type { IWorkspace } from "@plane/types";
// ui
import { CustomSelect, Input } from "@plane/ui";
import { validateWorkspaceName, validateSlug } from "@plane/utils";
// hooks
import { useWorkspace } from "@/hooks/store/use-workspace";
import { useAppRouter } from "@/hooks/use-app-router";
Expand Down Expand Up @@ -120,8 +121,7 @@ export const CreateWorkspaceForm = observer(function CreateWorkspaceForm(props:
name="name"
rules={{
required: t("common.errors.required"),
validate: (value) =>
/^[\w\s-]*$/.test(value) || t("workspace_creation.errors.validation.name_alphanumeric"),
validate: (value) => validateWorkspaceName(value, true),
maxLength: {
value: 80,
message: t("workspace_creation.errors.validation.name_length"),
Expand Down Expand Up @@ -172,7 +172,8 @@ export const CreateWorkspaceForm = observer(function CreateWorkspaceForm(props:
type="text"
value={value.toLocaleLowerCase().trim().replace(/ /g, "-")}
onChange={(e) => {
if (/^[a-zA-Z0-9_-]+$/.test(e.target.value)) setInvalidSlug(false);
const validation = validateSlug(e.target.value);
if (validation === true) setInvalidSlug(false);
else setInvalidSlug(true);
onChange(e.target.value.toLowerCase());
}}
Expand Down
1 change: 1 addition & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export * from "./tab-indices";
export * from "./theme";
export { resolveGeneralTheme } from "./theme-legacy";
export * from "./url";
export * from "./validation";
export * from "./work-item-filters";
export * from "./work-item";
export * from "./workspace";
Loading
Loading