jira/apps/web/core/components/issues/title-input.tsx
Atul Tameshwari 8803088086
refactor: migrate web TextArea to @makeplane/propel TextArea (#9747)
* chore: update @makeplane/propel version in pnpm-lock.yaml to 0.3.0

* refactor: replace TextArea usage with TextAreaGroup in forms

Updated various forms across the application to utilize TextAreaGroup for better structure and error handling. This change enhances the consistency of the description fields in forms, ensuring they are wrapped in a Field component for validation and styling. The updates were made in the CreateApiTokenForm, CycleForm, ModuleForm, ProjectDetailsForm, and others.

* refactor: enhance TextArea components across forms with auto-resize and maxRows

Updated various forms to improve the TextArea components by adding auto-resize functionality and setting a maximum number of rows. This change enhances user experience by allowing better visibility and management of text input across the CreateApiTokenForm, CycleForm, ModuleForm, ProjectDetailsForm, and others.

* refactor: remove unnecessary containerClassName from TextArea components

Removed the redundant containerClassName="-ml-3" from TextArea components in InboxIssueMainContent, IssueMainContent, and PeekOverviewIssueDetails to streamline the code and improve consistency across the application.

* refactor: remove maxRows from TextArea components in Cycle and Module analytics sidebars

Updated the TextArea components in CycleSidebarDetails and ModuleAnalyticsSidebar to remove the maxRows property, allowing for unrestricted text input. This change enhances the user experience by providing more flexibility in text entry.

* refactor: remove className prop from IssueTitleInput component

Eliminated the optional className prop from the IssueTitleInputProps interface to streamline the component's API and improve consistency across the application.

* refactor: remove PageEditorTitle component to streamline editor functionality

Deleted the PageEditorTitle component from the editor, simplifying the codebase and enhancing maintainability. This change aligns with ongoing efforts to improve component structure and reduce redundancy in the application.

* fix: ensure description field in StateForm defaults to an empty string

Updated the description field in the StateForm component to default to an empty string when no value is provided. This change improves the handling of form data and ensures consistent behavior in the input field.

* refactor: simplify IssueTitleInput component by removing TextAreaGroup

Updated the IssueTitleInput component to eliminate the TextAreaGroup wrapper, replacing it with a div for improved structure. Additionally, adjusted the TextArea properties to set the surface to "inline" and changed the rows to 1, enhancing the input's appearance and functionality.

* refactor: streamline description rendering in ModuleAnalyticsSidebar

Removed redundant wrapping of the description TextArea in the ModuleAnalyticsSidebar component, simplifying the code while maintaining functionality. This change enhances readability and consistency in the component's structure.
2026-09-07 18:22:19 +05:30

185 lines
5.9 KiB
TypeScript

/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useState, useEffect, useCallback, useRef } from "react";
import { observer } from "mobx-react";
import { Field } from "@makeplane/propel/components/field";
import { TextArea } from "@makeplane/propel/components/text-area";
import { useTranslation } from "@plane/i18n";
import type { TNameDescriptionLoader } from "@plane/types";
// types
import { cn } from "@plane/utils";
import useDebounce from "@/hooks/use-debounce";
import type { TIssueOperations } from "./issue-detail";
// hooks
export type IssueTitleInputProps = {
disabled?: boolean;
value: string | undefined | null;
workspaceSlug: string;
isSubmitting: TNameDescriptionLoader;
setIsSubmitting: (value: TNameDescriptionLoader) => void;
issueOperations: TIssueOperations;
projectId: string;
issueId: string;
containerClassName?: string;
};
export const IssueTitleInput = observer(function IssueTitleInput(props: IssueTitleInputProps) {
const {
disabled,
value,
workspaceSlug,
isSubmitting,
setIsSubmitting,
issueId,
issueOperations,
projectId,
containerClassName,
} = props;
const { t } = useTranslation();
// states
const [title, setTitle] = useState(value || "");
const [isLengthVisible, setIsLengthVisible] = useState(false);
// ref to track if there are unsaved changes
const hasUnsavedChanges = useRef(false);
// ref to store current title value for cleanup function
const currentTitleRef = useRef(title);
// hooks
const debouncedValue = useDebounce(title, 1500);
useEffect(() => {
if (value) {
setTitle(value);
currentTitleRef.current = value;
// Reset unsaved changes flag when value is set from props
hasUnsavedChanges.current = false;
}
}, [value]);
useEffect(() => {
const textarea = document.querySelector("#title-input");
if (debouncedValue && debouncedValue !== value) {
if (debouncedValue.trim().length > 0) {
issueOperations.update(workspaceSlug, projectId, issueId, { name: debouncedValue }).finally(() => {
setIsSubmitting("saved");
hasUnsavedChanges.current = false;
if (textarea && !textarea.matches(":focus")) {
const trimmedTitle = debouncedValue.trim();
if (trimmedTitle !== title) setTitle(trimmedTitle);
}
});
} else {
setTitle(value || "");
setIsSubmitting("saved");
hasUnsavedChanges.current = false;
}
}
// DO NOT Add more dependencies here. It will cause multiple requests to be sent.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [debouncedValue]);
useEffect(() => {
const handleBlur = () => {
const trimmedTitle = title.trim();
if (trimmedTitle !== title && isSubmitting !== "submitting") {
if (trimmedTitle.length > 0) {
setTitle(trimmedTitle);
setIsSubmitting("submitting");
hasUnsavedChanges.current = true;
} else {
setTitle(value || "");
setIsSubmitting("saved");
hasUnsavedChanges.current = false;
}
}
};
const textarea = document.querySelector("#title-input"); // You might need to change this selector according to your TextArea component
if (textarea) {
textarea.addEventListener("blur", handleBlur);
}
return () => {
if (textarea) {
textarea.removeEventListener("blur", handleBlur);
}
};
}, [title, isSubmitting, setIsSubmitting]);
// Save on unmount if there are unsaved changes
useEffect(
() => () => {
if (hasUnsavedChanges.current && currentTitleRef.current.trim().length > 0) {
issueOperations
.update(workspaceSlug, projectId, issueId, { name: currentTitleRef.current.trim() })
.catch((error) => {
console.error("Failed to save title on unmount:", error);
})
.finally(() => {
setIsSubmitting("saved");
hasUnsavedChanges.current = false;
});
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
const handleTitleChange = useCallback(
(e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
setIsSubmitting("submitting");
const titleFromEvent = e.target.value;
setTitle(titleFromEvent);
currentTitleRef.current = titleFromEvent;
hasUnsavedChanges.current = true;
},
[setIsSubmitting]
);
if (disabled) return <div className="text-20 font-medium whitespace-pre-line">{title}</div>;
return (
<div className="flex flex-col gap-1.5">
<div className={cn("relative", containerClassName)}>
<Field name="title" invalid={title?.length === 0}>
<div className="flex w-full">
<TextArea
size="2xl"
surface="inline"
autoResize
rows={1}
id="title-input"
disabled={disabled}
value={title}
onChange={handleTitleChange}
maxLength={255}
placeholder={t("issue.title.label")}
onFocus={() => setIsLengthVisible(true)}
onBlur={() => setIsLengthVisible(false)}
/>
</div>
</Field>
<div
className={cn(
"pointer-events-none absolute right-1 bottom-1 z-2 rounded-sm bg-surface-1 p-0.5 text-11 text-secondary opacity-0 transition-opacity",
{
"opacity-100": isLengthVisible,
}
)}
>
<span className={`${title.length === 0 || title.length > 255 ? "text-danger-primary" : ""}`}>
{title.length}
</span>
/255
</div>
</div>
{title?.length === 0 && (
<span className="text-13 font-medium text-danger-primary">{t("form.title.required")}</span>
)}
</div>
);
});