[WEB-5459] feat(codemods): add function declaration transformer with tests (#8137)

- Add jscodeshift-based codemod to convert arrow function components to function declarations
- Support React.FC, observer-wrapped, and forwardRef components
- Include comprehensive test suite covering edge cases
- Add npm script to run transformer across codebase
- Target only .tsx files in source directories, excluding node_modules and declaration files

* [WEB-5459] chore: updates after running codemod

---------

Co-authored-by: sriramveeraghanta <veeraghanta.sriram@gmail.com>
This commit is contained in:
Aaron 2025-11-20 19:09:40 +07:00 committed by GitHub
parent 90866fb925
commit 83fdebf64d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
1771 changed files with 17003 additions and 13856 deletions

View file

@ -1,5 +1,4 @@
"use client";
import React from "react";
// Minimal shim so code using next/image compiles under React Router + Vite
@ -9,6 +8,8 @@ type NextImageProps = React.ImgHTMLAttributes<HTMLImageElement> & {
src: string;
};
const Image: React.FC<NextImageProps> = ({ src, alt = "", ...rest }) => <img src={src} alt={alt} {...rest} />;
function Image({ src, alt = "", ...rest }: NextImageProps) {
return <img src={src} alt={alt} {...rest} />;
}
export default Image;

View file

@ -1,5 +1,4 @@
"use client";
import React from "react";
import { Link as RRLink } from "react-router";
import { ensureTrailingSlash } from "./helper";
@ -12,13 +11,8 @@ type NextLinkProps = React.ComponentProps<"a"> & {
shallow?: boolean; // next.js prop, ignored
};
const Link: React.FC<NextLinkProps> = ({
href,
replace,
prefetch: _prefetch,
scroll: _scroll,
shallow: _shallow,
...rest
}) => <RRLink to={ensureTrailingSlash(href)} replace={replace} {...rest} />;
function Link({ href, replace, prefetch: _prefetch, scroll: _scroll, shallow: _shallow, ...rest }: NextLinkProps) {
return <RRLink to={ensureTrailingSlash(href)} replace={replace} {...rest} />;
}
export default Link;

View file

@ -1,9 +1,8 @@
"use client";
// ui
import { Button } from "@plane/propel/button";
const ErrorPage = () => {
function ErrorPage() {
const handleRetry = () => {
window.location.reload();
};
@ -42,6 +41,6 @@ const ErrorPage = () => {
</div>
</div>
);
};
}
export default ErrorPage;

View file

@ -10,7 +10,7 @@ import { usePublish } from "@/hooks/store/publish";
import { useLabel } from "@/hooks/store/use-label";
import { useStates } from "@/hooks/store/use-state";
const IssuesPage = observer(() => {
const IssuesPage = observer(function IssuesPage() {
// params
const params = useParams<{ anchor: string }>();
const { anchor } = params;

View file

@ -1,22 +1,23 @@
"use client";
// assets
import SomethingWentWrongImage from "@/app/assets/something-went-wrong.svg?url";
const NotFound = () => (
<div className="h-screen w-screen grid place-items-center">
<div className="text-center">
<div className="mx-auto size-32 md:size-52 grid place-items-center rounded-full bg-custom-background-80">
<div className="size-16 md:size-32 grid place-items-center">
<img src={SomethingWentWrongImage} alt="Something went wrong" width={128} height={128} />
function NotFound() {
return (
<div className="h-screen w-screen grid place-items-center">
<div className="text-center">
<div className="mx-auto size-32 md:size-52 grid place-items-center rounded-full bg-custom-background-80">
<div className="size-16 md:size-32 grid place-items-center">
<img src={SomethingWentWrongImage} alt="Something went wrong" width={128} height={128} />
</div>
</div>
<h1 className="mt-8 md:mt-12 text-xl md:text-3xl font-semibold">That didn{"'"}t work</h1>
<p className="mt-2 md:mt-4 text-sm md:text-base">
Check the URL you are entering in the browser{"'"}s address bar and try again.
</p>
</div>
<h1 className="mt-8 md:mt-12 text-xl md:text-3xl font-semibold">That didn{"'"}t work</h1>
<p className="mt-2 md:mt-4 text-sm md:text-base">
Check the URL you are entering in the browser{"'"}s address bar and try again.
</p>
</div>
</div>
);
);
}
export default NotFound;

View file

@ -11,7 +11,7 @@ import { AuthView } from "@/components/views";
// hooks
import { useUser } from "@/hooks/store/use-user";
const HomePage = observer(() => {
const HomePage = observer(function HomePage() {
const { data: currentUser, isAuthenticated, isInitializing } = useUser();
const searchParams = useSearchParams();
const router = useRouter();

View file

@ -1,4 +1,6 @@
// plane editor
import type { TCallbackMentionComponentProps } from "@plane/editor";
export const EditorAdditionalMentionsRoot: React.FC<TCallbackMentionComponentProps> = () => null;
export function EditorAdditionalMentionsRoot(_props: TCallbackMentionComponentProps) {
return null;
}

View file

@ -6,4 +6,6 @@ type Props = {
publishSettings: PublishStore;
};
export const ViewLayoutsRoot = (_props: Props) => <PageNotFound />;
export function ViewLayoutsRoot(_props: Props) {
return <PageNotFound />;
}

View file

@ -5,4 +5,6 @@ type Props = {
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export const ViewNavbarRoot = (props: Props) => <></>;
export function ViewNavbarRoot(props: Props) {
return <></>;
}

View file

@ -10,7 +10,7 @@ type TAuthBanner = {
handleBannerData?: (bannerData: TAuthErrorInfo | undefined) => void;
};
export const AuthBanner: React.FC<TAuthBanner> = (props) => {
export function AuthBanner(props: TAuthBanner) {
const { bannerData, handleBannerData } = props;
if (!bannerData) return <></>;
@ -28,4 +28,4 @@ export const AuthBanner: React.FC<TAuthBanner> = (props) => {
</div>
</div>
);
};
}

View file

@ -27,7 +27,7 @@ const Titles: TAuthHeaderDetails = {
},
};
export const AuthHeader: React.FC<TAuthHeader> = (props) => {
export function AuthHeader(props: TAuthHeader) {
const { authMode } = props;
const getHeaderSubHeader = (mode: EAuthModes | null): TAuthHeaderContent => {
@ -51,4 +51,4 @@ export const AuthHeader: React.FC<TAuthHeader> = (props) => {
</div>
</>
);
};
}

View file

@ -32,7 +32,7 @@ import { AuthUniqueCodeForm } from "./unique-code";
const authService = new SitesAuthService();
export const AuthRoot: React.FC = observer(() => {
export const AuthRoot = observer(function AuthRoot() {
// router params
const searchParams = useSearchParams();
const emailParam = searchParams.get("email") || undefined;

View file

@ -19,7 +19,7 @@ type TAuthEmailForm = {
onSubmit: (data: IEmailCheckData) => Promise<void>;
};
export const AuthEmailForm: React.FC<TAuthEmailForm> = observer((props) => {
export const AuthEmailForm = observer(function AuthEmailForm(props: TAuthEmailForm) {
const { onSubmit, defaultEmail } = props;
// states
const [isSubmitting, setIsSubmitting] = useState(false);

View file

@ -35,7 +35,7 @@ const defaultValues: TPasswordFormValues = {
const authService = new AuthService();
export const AuthPasswordForm: React.FC<Props> = observer((props: Props) => {
export const AuthPasswordForm = observer(function AuthPasswordForm(props: Props) {
const { email, nextPath, isSMTPConfigured, handleAuthStep, handleEmailClear, mode } = props;
// ref
const formRef = useRef<HTMLFormElement>(null);

View file

@ -33,7 +33,7 @@ const defaultValues: TUniqueCodeFormValues = {
code: "",
};
export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
export function AuthUniqueCodeForm(props: TAuthUniqueCodeForm) {
const { mode, email, nextPath, handleEmailClear, generateEmailUniqueCode } = props;
// derived values
const defaultResetTimerValue = 5;
@ -150,4 +150,4 @@ export const AuthUniqueCodeForm: React.FC<TAuthUniqueCodeForm> = (props) => {
</div>
</form>
);
};
}

View file

@ -6,7 +6,7 @@ type Props = {
isSignUp?: boolean;
};
export const TermsAndConditions: React.FC<Props> = (props) => {
export function TermsAndConditions(props: Props) {
const { isSignUp = false } = props;
return (
<span className="flex items-center justify-center py-6">
@ -23,4 +23,4 @@ export const TermsAndConditions: React.FC<Props> = (props) => {
</p>
</span>
);
};
}

View file

@ -10,7 +10,7 @@ import { UserAvatar } from "@/components/issues/navbar/user-avatar";
// hooks
import { useUser } from "@/hooks/store/use-user";
export const UserLoggedIn = observer(() => {
export const UserLoggedIn = observer(function UserLoggedIn() {
// store hooks
const { data: user } = useUser();

View file

@ -5,7 +5,7 @@ import { useTheme } from "next-themes";
import LogoSpinnerDark from "@/app/assets/images/logo-spinner-dark.gif?url";
import LogoSpinnerLight from "@/app/assets/images/logo-spinner-light.gif?url";
export const LogoSpinner = () => {
export function LogoSpinner() {
const { resolvedTheme } = useTheme();
const logoSrc = resolvedTheme === "dark" ? LogoSpinnerLight : LogoSpinnerDark;
@ -15,4 +15,4 @@ export const LogoSpinner = () => {
<img src={logoSrc} alt="logo" className="h-6 w-auto sm:h-11" />
</div>
);
};
}

View file

@ -8,7 +8,7 @@ type TPoweredBy = {
disabled?: boolean;
};
export const PoweredBy: React.FC<TPoweredBy> = (props) => {
export function PoweredBy(props: TPoweredBy) {
// props
const { disabled = false } = props;
@ -27,4 +27,4 @@ export const PoweredBy: React.FC<TPoweredBy> = (props) => {
</div>
</a>
);
};
}

View file

@ -8,7 +8,7 @@ type Props = {
logo: TLogoProps;
};
export const ProjectLogo: React.FC<Props> = (props) => {
export function ProjectLogo(props: Props) {
const { className, logo } = props;
if (logo.in_use === "icon" && logo.icon)
@ -31,4 +31,4 @@ export const ProjectLogo: React.FC<Props> = (props) => {
);
return <span />;
};
}

View file

@ -5,7 +5,7 @@ import { EditorAdditionalMentionsRoot } from "@/plane-web/components/editor";
// local components
import { EditorUserMention } from "./user";
export const EditorMentionsRoot: React.FC<TCallbackMentionComponentProps> = (props) => {
export function EditorMentionsRoot(props: TCallbackMentionComponentProps) {
const { entity_identifier, entity_name } = props;
switch (entity_name) {
@ -14,4 +14,4 @@ export const EditorMentionsRoot: React.FC<TCallbackMentionComponentProps> = (pro
default:
return <EditorAdditionalMentionsRoot {...props} />;
}
};
}

View file

@ -9,7 +9,7 @@ type Props = {
id: string;
};
export const EditorUserMention: React.FC<Props> = observer((props) => {
export const EditorUserMention = observer(function EditorUserMention(props: Props) {
const { id } = props;
// store hooks
const { data: currentUser } = useUser();

View file

@ -32,7 +32,10 @@ type LiteTextEditorWrapperProps = MakeOptional<
}
);
export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapperProps>((props, ref) => {
export const LiteTextEditor = React.forwardRef(function LiteTextEditor(
props: LiteTextEditorWrapperProps,
ref: React.ForwardedRef<EditorRefApi>
) {
const {
anchor,
containerClassName,

View file

@ -29,7 +29,10 @@ type RichTextEditorWrapperProps = MakeOptional<
}
);
export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProps>((props, ref) => {
export const RichTextEditor = forwardRef(function RichTextEditor(
props: RichTextEditorWrapperProps,
ref: React.ForwardedRef<EditorRefApi>
) {
const {
anchor,
containerClassName,

View file

@ -19,7 +19,7 @@ type Props = {
const toolbarItems = TOOLBAR_ITEMS.lite;
export const IssueCommentToolbar: React.FC<Props> = (props) => {
export function IssueCommentToolbar(props: Props) {
const { executeCommand, handleSubmit, isCommentEmpty, editorRef, isSubmitting, showSubmitButton } = props;
// states
const [activeStates, setActiveStates] = useState<Record<string, boolean>>({});
@ -113,4 +113,4 @@ export const IssueCommentToolbar: React.FC<Props> = (props) => {
</div>
</div>
);
};
}

View file

@ -6,7 +6,7 @@ import { Button } from "@plane/propel/button";
import InstanceFailureDarkImage from "@/app/assets/instance/instance-failure-dark.svg?url";
import InstanceFailureImage from "@/app/assets/instance/instance-failure.svg?url";
export const InstanceFailureView: React.FC = () => {
export function InstanceFailureView() {
const { resolvedTheme } = useTheme();
const instanceImage = resolvedTheme === "dark" ? InstanceFailureDarkImage : InstanceFailureImage;
@ -34,4 +34,4 @@ export const InstanceFailureView: React.FC = () => {
</div>
</div>
);
};
}

View file

@ -17,7 +17,7 @@ type Props = {
export const replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, " ");
export const AppliedFiltersList: React.FC<Props> = observer((props) => {
export const AppliedFiltersList = observer(function AppliedFiltersList(props: Props) {
const { appliedFilters = {}, handleRemoveAllFilters, handleRemoveFilter } = props;
const { t } = useTranslation();

View file

@ -10,7 +10,7 @@ type Props = {
values: string[];
};
export const AppliedLabelsFilters: React.FC<Props> = (props) => {
export function AppliedLabelsFilters(props: Props) {
const { handleRemove, labels, values } = props;
return (
@ -41,4 +41,4 @@ export const AppliedLabelsFilters: React.FC<Props> = (props) => {
})}
</>
);
};
}

View file

@ -8,7 +8,7 @@ type Props = {
values: TIssuePriorities[];
};
export const AppliedPriorityFilters: React.FC<Props> = (props) => {
export function AppliedPriorityFilters(props: Props) {
const { handleRemove, values } = props;
return (
@ -30,4 +30,4 @@ export const AppliedPriorityFilters: React.FC<Props> = (props) => {
))}
</>
);
};
}

View file

@ -15,7 +15,7 @@ type TIssueAppliedFilters = {
anchor: string;
};
export const IssueAppliedFilters: React.FC<TIssueAppliedFilters> = observer((props) => {
export const IssueAppliedFilters = observer(function IssueAppliedFilters(props: TIssueAppliedFilters) {
const { anchor } = props;
// router
const router = useRouter();

View file

@ -12,7 +12,7 @@ type Props = {
values: string[];
};
export const AppliedStateFilters: React.FC<Props> = observer((props) => {
export const AppliedStateFilters = observer(function AppliedStateFilters(props: Props) {
const { handleRemove, values } = props;
const { sortedStates: states } = useStates();

View file

@ -13,7 +13,7 @@ type Props = {
placement?: Placement;
};
export const FiltersDropdown: React.FC<Props> = (props) => {
export function FiltersDropdown(props: Props) {
const { children, title = "Dropdown", placement } = props;
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
@ -62,4 +62,4 @@ export const FiltersDropdown: React.FC<Props> = (props) => {
}}
</Popover>
);
};
}

View file

@ -9,15 +9,17 @@ interface IFilterHeader {
handleIsPreviewEnabled: () => void;
}
export const FilterHeader = ({ title, isPreviewEnabled, handleIsPreviewEnabled }: IFilterHeader) => (
<div className="sticky top-0 flex items-center justify-between gap-2 bg-custom-background-100">
<div className="flex-grow truncate text-xs font-medium text-custom-text-300">{title}</div>
<button
type="button"
className="grid h-5 w-5 flex-shrink-0 place-items-center rounded hover:bg-custom-background-80"
onClick={handleIsPreviewEnabled}
>
{isPreviewEnabled ? <ChevronUpIcon height={14} width={14} /> : <ChevronDownIcon height={14} width={14} />}
</button>
</div>
);
export function FilterHeader({ title, isPreviewEnabled, handleIsPreviewEnabled }: IFilterHeader) {
return (
<div className="sticky top-0 flex items-center justify-between gap-2 bg-custom-background-100">
<div className="flex-grow truncate text-xs font-medium text-custom-text-300">{title}</div>
<button
type="button"
className="grid h-5 w-5 flex-shrink-0 place-items-center rounded hover:bg-custom-background-80"
onClick={handleIsPreviewEnabled}
>
{isPreviewEnabled ? <ChevronUpIcon height={14} width={14} /> : <ChevronDownIcon height={14} width={14} />}
</button>
</div>
);
}

View file

@ -12,7 +12,7 @@ type Props = {
multiple?: boolean;
};
export const FilterOption: React.FC<Props> = (props) => {
export function FilterOption(props: Props) {
const { icon, isChecked, multiple = true, onClick, title } = props;
return (
@ -34,4 +34,4 @@ export const FilterOption: React.FC<Props> = (props) => {
</div>
</button>
);
};
}

View file

@ -1,5 +1,4 @@
"use client";
import React, { useState } from "react";
// plane imports
import { Loader } from "@plane/ui";
@ -9,9 +8,9 @@ import type { IIssueLabel } from "@/types/issue";
import { FilterHeader } from "./helpers/filter-header";
import { FilterOption } from "./helpers/filter-option";
const LabelIcons = ({ color }: { color: string }) => (
<span className="h-2.5 w-2.5 rounded-full" style={{ backgroundColor: color }} />
);
function LabelIcons({ color }: { color: string }) {
return <span className="h-2.5 w-2.5 rounded-full" style={{ backgroundColor: color }} />;
}
type Props = {
appliedFilters: string[] | null;
@ -20,7 +19,7 @@ type Props = {
searchQuery: string;
};
export const FilterLabels: React.FC<Props> = (props) => {
export function FilterLabels(props: Props) {
const { appliedFilters, handleUpdate, labels, searchQuery } = props;
const [itemsToRender, setItemsToRender] = useState(5);
@ -82,4 +81,4 @@ export const FilterLabels: React.FC<Props> = (props) => {
)}
</>
);
};
}

View file

@ -16,7 +16,7 @@ type Props = {
searchQuery: string;
};
export const FilterPriority: React.FC<Props> = observer((props) => {
export const FilterPriority = observer(function FilterPriority(props: Props) {
const { appliedFilters, handleUpdate, searchQuery } = props;
// hooks

View file

@ -20,7 +20,7 @@ type IssueFiltersDropdownProps = {
anchor: string;
};
export const IssueFiltersDropdown: React.FC<IssueFiltersDropdownProps> = observer((props) => {
export const IssueFiltersDropdown = observer(function IssueFiltersDropdown(props: IssueFiltersDropdownProps) {
const { anchor } = props;
// router
const router = useRouter();

View file

@ -16,7 +16,7 @@ type Props = {
layoutDisplayFiltersOptions: TIssueFilterKeys[];
};
export const FilterSelection: React.FC<Props> = observer((props) => {
export const FilterSelection = observer(function FilterSelection(props: Props) {
const { filters, handleFilters, layoutDisplayFiltersOptions } = props;
const [filtersSearchQuery, setFiltersSearchQuery] = useState("");

View file

@ -18,7 +18,7 @@ type Props = {
searchQuery: string;
};
export const FilterState: React.FC<Props> = observer((props) => {
export const FilterState = observer(function FilterState(props: Props) {
const { appliedFilters, handleUpdate, searchQuery } = props;
const { sortedStates: states } = useStates();

View file

@ -1,20 +1,22 @@
// assets
import SomethingWentWrongImage from "@/app/assets/something-went-wrong.svg?url";
export const SomethingWentWrongError = () => (
<div className="grid min-h-screen w-full place-items-center p-6">
<div className="text-center">
<div className="mx-auto grid h-52 w-52 place-items-center rounded-full bg-custom-background-80">
<div className="grid h-32 w-32 place-items-center">
<img
src={SomethingWentWrongImage}
alt="Oops! Something went wrong"
className="w-full h-full object-contain"
/>
export function SomethingWentWrongError() {
return (
<div className="grid min-h-screen w-full place-items-center p-6">
<div className="text-center">
<div className="mx-auto grid h-52 w-52 place-items-center rounded-full bg-custom-background-80">
<div className="grid h-32 w-32 place-items-center">
<img
src={SomethingWentWrongImage}
alt="Oops! Something went wrong"
className="w-full h-full object-contain"
/>
</div>
</div>
<h1 className="mt-12 text-3xl font-semibold">Oops! Something went wrong.</h1>
<p className="mt-4 text-custom-text-300">The public board does not exist. Please check the URL.</p>
</div>
<h1 className="mt-12 text-3xl font-semibold">Oops! Something went wrong.</h1>
<p className="mt-4 text-custom-text-300">The public board does not exist. Please check the URL.</p>
</div>
</div>
);
);
}

View file

@ -14,7 +14,7 @@ interface Props {
getIssueLoader: (groupId?: string | undefined, subGroupId?: string | undefined) => TLoader;
}
export const IssueLayoutHOC = observer((props: Props) => {
export const IssueLayoutHOC = observer(function IssueLayoutHOC(props: Props) {
const { getIssueLoader, getGroupIssueCount } = props;
const issueCount = getGroupIssueCount(undefined, undefined, false);

View file

@ -15,7 +15,7 @@ import { KanBan } from "./default";
type Props = {
anchor: string;
};
export const IssueKanbanLayoutRoot: React.FC<Props> = observer((props: Props) => {
export const IssueKanbanLayoutRoot = observer(function IssueKanbanLayoutRoot(props: Props) {
const { anchor } = props;
// store hooks
const { groupedIssueIds, getIssueLoader, fetchNextPublicIssues, getGroupIssueCount, getPaginationData } = useIssue();

View file

@ -11,7 +11,7 @@ import { usePublish } from "@/hooks/store/publish";
type Props = {
issueId: string;
};
export const BlockReactions = observer((props: Props) => {
export const BlockReactions = observer(function BlockReactions(props: Props) {
const { issueId } = props;
const { anchor } = useParams();
const { canVote, canReact } = usePublish(anchor.toString());

View file

@ -36,7 +36,7 @@ interface IssueDetailsBlockProps {
displayProperties: IIssueDisplayProperties | undefined;
}
const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((props) => {
const KanbanIssueDetailsBlock = observer(function KanbanIssueDetailsBlock(props: IssueDetailsBlockProps) {
const { issue, displayProperties } = props;
const { anchor } = useParams();
// hooks
@ -67,7 +67,7 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((prop
);
});
export const KanbanIssueBlock: React.FC<IssueBlockProps> = observer((props) => {
export const KanbanIssueBlock = observer(function KanbanIssueBlock(props: IssueBlockProps) {
const { issueId, groupId, subGroupId, displayProperties } = props;
const searchParams = useSearchParams();
// query params

View file

@ -13,7 +13,7 @@ interface IssueBlocksListProps {
scrollableContainerRef?: MutableRefObject<HTMLDivElement | null>;
}
export const KanbanIssueBlocksList: React.FC<IssueBlocksListProps> = observer((props) => {
export const KanbanIssueBlocksList = observer(function KanbanIssueBlocksList(props: IssueBlocksListProps) {
const { subGroupId, groupId, issueIds, displayProperties, scrollableContainerRef } = props;
return (

View file

@ -42,7 +42,7 @@ export interface IKanBan {
showEmptyGroup?: boolean;
}
export const KanBan: React.FC<IKanBan> = observer((props) => {
export const KanBan = observer(function KanBan(props: IKanBan) {
const {
groupedIssueIds,
displayProperties,

View file

@ -12,7 +12,7 @@ interface IHeaderGroupByCard {
count: number;
}
export const HeaderGroupByCard: React.FC<IHeaderGroupByCard> = observer((props) => {
export const HeaderGroupByCard = observer(function HeaderGroupByCard(props: IHeaderGroupByCard) {
const { icon, title, count } = props;
return (

View file

@ -11,7 +11,7 @@ interface IHeaderSubGroupByCard {
toggleExpanded: () => void;
}
export const HeaderSubGroupByCard: React.FC<IHeaderSubGroupByCard> = observer((props) => {
export const HeaderSubGroupByCard = observer(function HeaderSubGroupByCard(props: IHeaderSubGroupByCard) {
const { icon, title, count, isExpanded, toggleExpanded } = props;
return (
<div

View file

@ -36,12 +36,15 @@ interface IKanbanGroup {
}
// Loader components
const KanbanIssueBlockLoader = forwardRef<HTMLSpanElement>((props, ref) => (
<span ref={ref} className="block h-28 m-1.5 animate-pulse bg-custom-background-80 rounded" />
));
const KanbanIssueBlockLoader = forwardRef(function KanbanIssueBlockLoader(
props: Record<string, unknown>,
ref: React.ForwardedRef<HTMLSpanElement>
) {
return <span ref={ref} className="block h-28 m-1.5 animate-pulse bg-custom-background-80 rounded" />;
});
KanbanIssueBlockLoader.displayName = "KanbanIssueBlockLoader";
export const KanbanGroup = observer((props: IKanbanGroup) => {
export const KanbanGroup = observer(function KanbanGroup(props: IKanbanGroup) {
const {
groupId,
subGroupId,

View file

@ -43,7 +43,7 @@ export interface IKanBanSwimLanes {
orderBy: TIssueOrderByOptions | undefined;
}
export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
export const KanBanSwimLanes = observer(function KanBanSwimLanes(props: IKanBanSwimLanes) {
const {
groupedIssueIds,
displayProperties,
@ -125,8 +125,14 @@ const visibilitySubGroupByGroupCount = (subGroupIssueCount: number, showEmptyGro
return subGroupHeaderVisibility;
};
const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = observer(
({ subGroupBy, groupBy, groupList, showEmptyGroup, getGroupIssueCount }) => (
const SubGroupSwimlaneHeader = observer(function SubGroupSwimlaneHeader({
subGroupBy,
groupBy,
groupList,
showEmptyGroup,
getGroupIssueCount,
}: ISubGroupSwimlaneHeader) {
return (
<div className="relative flex h-max min-h-full w-full items-center gap-2">
{groupList &&
groupList.length > 0 &&
@ -143,8 +149,8 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = observer(
);
})}
</div>
)
);
);
});
interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
groupedIssueIds: TGroupedIssues | TSubGroupedIssues;
@ -162,7 +168,7 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
loadMoreIssues: (groupId?: string, subGroupId?: string) => void;
}
const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
const SubGroupSwimlane = observer(function SubGroupSwimlane(props: ISubGroupSwimlane) {
const {
groupedIssueIds,
subGroupBy,
@ -219,7 +225,7 @@ interface ISubGroup {
loadMoreIssues: (groupId?: string, subGroupId?: string) => void;
}
const SubGroup: React.FC<ISubGroup> = observer((props) => {
const SubGroup = observer(function SubGroup(props: ISubGroup) {
const {
groupedIssueIds,
subGroupBy,

View file

@ -13,7 +13,7 @@ type Props = {
anchor: string;
};
export const IssuesListLayoutRoot = observer((props: Props) => {
export const IssuesListLayoutRoot = observer(function IssuesListLayoutRoot(props: Props) {
const { anchor } = props;
// store hooks
const {

View file

@ -24,7 +24,7 @@ interface IssueBlockProps {
displayProperties: IIssueDisplayProperties | undefined;
}
export const IssueBlock = observer((props: IssueBlockProps) => {
export const IssueBlock = observer(function IssueBlock(props: IssueBlockProps) {
const { anchor } = useParams();
const { issueId, displayProperties } = props;
const searchParams = useSearchParams();

View file

@ -10,7 +10,7 @@ interface Props {
containerRef: MutableRefObject<HTMLDivElement | null>;
}
export const IssueBlocksList: React.FC<Props> = (props) => {
export function IssueBlocksList(props: Props) {
const { issueIds = [], groupId, displayProperties } = props;
return (
@ -22,4 +22,4 @@ export const IssueBlocksList: React.FC<Props> = (props) => {
))}
</div>
);
};
}

View file

@ -35,7 +35,7 @@ export interface IList {
getIssueLoader: (groupId?: string | undefined, subGroupId?: string | undefined) => TLoader;
}
export const List: React.FC<IList> = observer((props) => {
export const List = observer(function List(props: IList) {
const {
groupedIssueIds,
groupBy,

View file

@ -11,7 +11,7 @@ interface IHeaderGroupByCard {
toggleListGroup: (id: string) => void;
}
export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => {
export const HeaderGroupByCard = observer(function HeaderGroupByCard(props: IHeaderGroupByCard) {
const { groupID, icon, title, count, toggleListGroup } = props;
return (

View file

@ -38,24 +38,29 @@ interface Props {
}
// List loader component
const ListLoaderItemRow = forwardRef<HTMLDivElement>((props, ref) => (
<div ref={ref} className="flex items-center justify-between h-11 p-3 border-b border-custom-border-200">
<div className="flex items-center gap-3">
<span className="h-5 w-10 bg-custom-background-80 rounded animate-pulse" />
<span className={`h-5 w-52 bg-custom-background-80 rounded animate-pulse`} />
const ListLoaderItemRow = forwardRef(function ListLoaderItemRow(
props: Record<string, unknown>,
ref: React.ForwardedRef<HTMLDivElement>
) {
return (
<div ref={ref} className="flex items-center justify-between h-11 p-3 border-b border-custom-border-200">
<div className="flex items-center gap-3">
<span className="h-5 w-10 bg-custom-background-80 rounded animate-pulse" />
<span className={`h-5 w-52 bg-custom-background-80 rounded animate-pulse`} />
</div>
<div className="flex items-center gap-2">
{[...Array(6)].map((_, index) => (
<Fragment key={index}>
<span key={index} className="h-5 w-5 bg-custom-background-80 rounded animate-pulse" />
</Fragment>
))}
</div>
</div>
<div className="flex items-center gap-2">
{[...Array(6)].map((_, index) => (
<Fragment key={index}>
<span key={index} className="h-5 w-5 bg-custom-background-80 rounded animate-pulse" />
</Fragment>
))}
</div>
</div>
));
);
});
ListLoaderItemRow.displayName = "ListLoaderItemRow";
export const ListGroup = observer((props: Props) => {
export const ListGroup = observer(function ListGroup(props: Props) {
const {
groupIssueIds = [],
group,

View file

@ -27,7 +27,7 @@ export interface IIssueProperties {
className: string;
}
export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
export const IssueProperties = observer(function IssueProperties(props: IIssueProperties) {
const { issue, displayProperties, className } = props;
if (!displayProperties || !issue.project_id) return null;

View file

@ -14,7 +14,7 @@ type Props = {
shouldShowBorder?: boolean;
};
export const IssueBlockCycle = observer(({ cycleId, shouldShowBorder = true }: Props) => {
export const IssueBlockCycle = observer(function IssueBlockCycle({ cycleId, shouldShowBorder = true }: Props) {
const { getCycleById } = useCycle();
const cycle = getCycleById(cycleId);

View file

@ -17,7 +17,7 @@ type Props = {
shouldShowBorder?: boolean;
};
export const IssueBlockDate = observer((props: Props) => {
export const IssueBlockDate = observer(function IssueBlockDate(props: Props) {
const { due_date, stateId, shouldHighLight = true, shouldShowBorder = true } = props;
const { getStateById } = useStates();

View file

@ -12,7 +12,7 @@ type Props = {
shouldShowLabel?: boolean;
};
export const IssueBlockLabels = observer(({ labelIds, shouldShowLabel = false }: Props) => {
export const IssueBlockLabels = observer(function IssueBlockLabels({ labelIds, shouldShowLabel = false }: Props) {
const { getLabelsByIds } = useLabel();
const labels = getLabelsByIds(labelIds);

View file

@ -24,7 +24,7 @@ type AvatarProps = {
icon?: LucideIcon;
};
export const ButtonAvatars: React.FC<AvatarProps> = observer((props: AvatarProps) => {
export const ButtonAvatars = observer(function ButtonAvatars(props: AvatarProps) {
const { showTooltip, members, icon: Icon } = props;
if (Array.isArray(members)) {
@ -56,7 +56,7 @@ export const ButtonAvatars: React.FC<AvatarProps> = observer((props: AvatarProps
);
});
export const IssueBlockMembers = observer(({ memberIds, shouldShowBorder = true }: Props) => {
export const IssueBlockMembers = observer(function IssueBlockMembers({ memberIds, shouldShowBorder = true }: Props) {
const { getMembersByIds } = useMember();
const members = getMembersByIds(memberIds);

View file

@ -14,7 +14,7 @@ type Props = {
shouldShowBorder?: boolean;
};
export const IssueBlockModules = observer(({ moduleIds, shouldShowBorder = true }: Props) => {
export const IssueBlockModules = observer(function IssueBlockModules({ moduleIds, shouldShowBorder = true }: Props) {
const { getModulesByIds } = useModule();
const modules = getModulesByIds(moduleIds ?? []);

View file

@ -9,13 +9,13 @@ import type { TIssuePriorities } from "@plane/types";
// constants
import { cn, getIssuePriorityFilters } from "@plane/utils";
export const IssueBlockPriority = ({
export function IssueBlockPriority({
priority,
shouldShowName = false,
}: {
priority: TIssuePriorities | null;
shouldShowName?: boolean;
}) => {
}) {
// hooks
const { t } = useTranslation();
const priority_detail = priority != null ? getIssuePriorityFilters(priority) : null;
@ -65,4 +65,4 @@ export const IssueBlockPriority = ({
</div>
</Tooltip>
);
};
}

View file

@ -13,7 +13,7 @@ type Props = {
stateId: string | undefined;
shouldShowBorder?: boolean;
};
export const IssueBlockState = observer(({ stateId, shouldShowBorder = true }: Props) => {
export const IssueBlockState = observer(function IssueBlockState({ stateId, shouldShowBorder = true }: Props) {
const { getStateById } = useStates();
const state = getStateById(stateId);

View file

@ -22,7 +22,7 @@ type Props = {
publishSettings: PublishStore;
};
export const IssuesLayoutsRoot: React.FC<Props> = observer((props) => {
export const IssuesLayoutsRoot = observer(function IssuesLayoutsRoot(props: Props) {
const { peekId, publishSettings } = props;
// store hooks
const { getIssueFilters } = useIssueFilter();

View file

@ -9,18 +9,21 @@ interface IWithDisplayPropertiesHOC {
children: React.ReactNode;
}
export const WithDisplayPropertiesHOC = observer(
({ displayProperties, shouldRenderProperty, displayPropertyKey, children }: IWithDisplayPropertiesHOC) => {
let shouldDisplayPropertyFromFilters = false;
if (Array.isArray(displayPropertyKey))
shouldDisplayPropertyFromFilters = displayPropertyKey.every((key) => !!displayProperties[key]);
else shouldDisplayPropertyFromFilters = !!displayProperties[displayPropertyKey];
export const WithDisplayPropertiesHOC = observer(function WithDisplayPropertiesHOC({
displayProperties,
shouldRenderProperty,
displayPropertyKey,
children,
}: IWithDisplayPropertiesHOC) {
let shouldDisplayPropertyFromFilters = false;
if (Array.isArray(displayPropertyKey))
shouldDisplayPropertyFromFilters = displayPropertyKey.every((key) => !!displayProperties[key]);
else shouldDisplayPropertyFromFilters = !!displayProperties[displayPropertyKey];
const renderProperty =
shouldDisplayPropertyFromFilters && (shouldRenderProperty ? shouldRenderProperty(displayProperties) : true);
const renderProperty =
shouldDisplayPropertyFromFilters && (shouldRenderProperty ? shouldRenderProperty(displayProperties) : true);
if (!renderProperty) return null;
if (!renderProperty) return null;
return <>{children}</>;
}
);
return <>{children}</>;
});

View file

@ -24,7 +24,7 @@ export type NavbarControlsProps = {
publishSettings: PublishStore;
};
export const NavbarControls: React.FC<NavbarControlsProps> = observer((props) => {
export const NavbarControls = observer(function NavbarControls(props: NavbarControlsProps) {
// props
const { publishSettings } = props;
// router

View file

@ -2,11 +2,11 @@ import type { TIssueLayout } from "@plane/constants";
import { ListLayoutIcon, BoardLayoutIcon } from "@plane/propel/icons";
import type { ISvgIcons } from "@plane/propel/icons";
export const IssueLayoutIcon = ({
export function IssueLayoutIcon({
layout,
size,
...props
}: { layout: TIssueLayout; size?: number } & Omit<ISvgIcons, "width" | "height">) => {
}: { layout: TIssueLayout; size?: number } & Omit<ISvgIcons, "width" | "height">) {
const iconProps = {
...props,
...(size && { width: size, height: size }),
@ -20,4 +20,4 @@ export const IssueLayoutIcon = ({
default:
return null;
}
};
}

View file

@ -19,7 +19,7 @@ type Props = {
anchor: string;
};
export const IssuesLayoutSelection: React.FC<Props> = observer((props) => {
export const IssuesLayoutSelection = observer(function IssuesLayoutSelection(props: Props) {
const { anchor } = props;
// hooks
const { t } = useTranslation();

View file

@ -13,7 +13,7 @@ type Props = {
publishSettings: PublishStore;
};
export const IssuesNavbarRoot: React.FC<Props> = observer((props) => {
export const IssuesNavbarRoot = observer(function IssuesNavbarRoot(props: Props) {
const { publishSettings } = props;
// hooks
const { project_details } = publishSettings;

View file

@ -7,7 +7,7 @@ import { useTheme } from "next-themes";
// mobx react lite
export const NavbarTheme = observer(() => {
export const NavbarTheme = observer(function NavbarTheme() {
const [appTheme, setAppTheme] = useState("light");
const { setTheme, theme } = useTheme();

View file

@ -20,7 +20,7 @@ import { useUser } from "@/hooks/store/use-user";
const authService = new AuthService();
export const UserAvatar: React.FC = observer(() => {
export const UserAvatar = observer(function UserAvatar() {
const pathName = usePathname();
const searchParams = useSearchParams();
// query params

View file

@ -26,7 +26,7 @@ type Props = {
disabled?: boolean;
};
export const AddComment: React.FC<Props> = observer((props) => {
export const AddComment = observer(function AddComment(props: Props) {
const { anchor } = props;
// states
const [uploadedAssetIds, setUploadAssetIds] = useState<string[]>([]);

View file

@ -24,7 +24,7 @@ type Props = {
comment: TIssuePublicComment;
};
export const CommentCard: React.FC<Props> = observer((props) => {
export const CommentCard = observer(function CommentCard(props: Props) {
const { anchor, comment } = props;
// store hooks
const { peekId, deleteIssueComment, updateIssueComment, uploadCommentAsset } = useIssueDetails();
@ -157,7 +157,6 @@ export const CommentCard: React.FC<Props> = observer((props) => {
</div>
</div>
</div>
{!isInIframe && currentUser?.id === comment?.actor_detail?.id && (
<Menu as="div" className="relative w-min text-left">
<Menu.Button

View file

@ -20,7 +20,7 @@ type Props = {
commentId: string;
};
export const CommentReactions: React.FC<Props> = observer((props) => {
export const CommentReactions = observer(function CommentReactions(props: Props) {
const { anchor, commentId } = props;
// state
const [isPickerOpen, setIsPickerOpen] = useState(false);

View file

@ -17,7 +17,7 @@ type Props = {
issueDetails: IIssue | undefined;
};
export const FullScreenPeekView: React.FC<Props> = observer((props) => {
export const FullScreenPeekView = observer(function FullScreenPeekView(props: Props) {
const { anchor, handleClose, issueDetails } = props;
return (

View file

@ -38,7 +38,7 @@ const PEEK_MODES: {
},
];
export const PeekOverviewHeader: React.FC<Props> = observer((props) => {
export const PeekOverviewHeader = observer(function PeekOverviewHeader(props: Props) {
const { handleClose } = props;
const { peekMode, setPeekMode } = useIssueDetails();

View file

@ -23,7 +23,7 @@ type Props = {
issueDetails: IIssue;
};
export const PeekOverviewIssueActivity: React.FC<Props> = observer((props) => {
export const PeekOverviewIssueActivity = observer(function PeekOverviewIssueActivity(props: Props) {
const { anchor } = props;
// router
const pathname = usePathname();

View file

@ -12,7 +12,7 @@ type Props = {
issueDetails: IIssue;
};
export const PeekOverviewIssueDetails: React.FC<Props> = observer((props) => {
export const PeekOverviewIssueDetails = observer(function PeekOverviewIssueDetails(props: Props) {
const { anchor, issueDetails } = props;
// store hooks
const { project_details, workspace: workspaceID } = usePublish(anchor);

View file

@ -24,7 +24,10 @@ type Props = {
mode?: IPeekMode;
};
export const PeekOverviewIssueProperties: React.FC<Props> = observer(({ issueDetails, mode }) => {
export const PeekOverviewIssueProperties = observer(function PeekOverviewIssueProperties({
issueDetails,
mode,
}: Props) {
// hooks
const { t } = useTranslation();
const { getStateById } = useStates();

View file

@ -10,7 +10,7 @@ type Props = {
anchor: string;
};
export const IssueReactions: React.FC<Props> = observer((props) => {
export const IssueReactions = observer(function IssueReactions(props: Props) {
const { anchor } = props;
// store hooks
const { canVote, canReact } = usePublish(anchor);

View file

@ -16,7 +16,7 @@ type TIssuePeekOverview = {
handlePeekClose?: () => void;
};
export const IssuePeekOverview: React.FC<TIssuePeekOverview> = observer((props) => {
export const IssuePeekOverview = observer(function IssuePeekOverview(props: TIssuePeekOverview) {
const { anchor, peekId, handlePeekClose } = props;
const router = useRouter();
const searchParams = useSearchParams();

View file

@ -19,7 +19,7 @@ type Props = {
issueDetails: IIssue | undefined;
};
export const SidePeekView: React.FC<Props> = observer((props) => {
export const SidePeekView = observer(function SidePeekView(props: Props) {
const { anchor, handleClose, issueDetails } = props;
// store hooks
const { canComment } = usePublish(anchor);

View file

@ -19,7 +19,7 @@ type IssueEmojiReactionsProps = {
issueIdFromProps?: string;
};
export const IssueEmojiReactions: React.FC<IssueEmojiReactionsProps> = observer((props) => {
export const IssueEmojiReactions = observer(function IssueEmojiReactions(props: IssueEmojiReactionsProps) {
const { anchor, issueIdFromProps } = props;
// state
const [isPickerOpen, setIsPickerOpen] = useState(false);

View file

@ -19,7 +19,7 @@ type TIssueVotes = {
size?: "md" | "sm";
};
export const IssueVotes: React.FC<TIssueVotes> = observer((props) => {
export const IssueVotes = observer(function IssueVotes(props: TIssueVotes) {
const { anchor, issueIdFromProps, size = "md" } = props;
// states
const [isSubmitting, setIsSubmitting] = useState(false);

View file

@ -5,6 +5,6 @@ type Props = {
className?: string;
};
export const Icon: React.FC<Props> = ({ iconName, className = "" }) => (
<span className={`material-symbols-rounded text-sm font-light leading-5 ${className}`}>{iconName}</span>
);
export function Icon({ iconName, className = "" }: Props) {
return <span className={`material-symbols-rounded text-sm font-light leading-5 ${className}`}>{iconName}</span>;
}

View file

@ -3,21 +3,23 @@
// images
import Image404 from "@/app/assets/404.svg?url";
export const PageNotFound = () => (
<div className={`h-screen w-full overflow-hidden bg-custom-background-100`}>
<div className="grid h-full place-items-center p-4">
<div className="space-y-8 text-center">
<div className="relative mx-auto h-60 w-60 lg:h-80 lg:w-80">
<img src={Image404} alt="404- Page not found" className="w-full h-full object-contain" />
</div>
<div className="space-y-2">
<h3 className="text-lg font-semibold">Oops! Something went wrong.</h3>
<p className="text-sm text-custom-text-200">
Sorry, the page you are looking for cannot be found. It may have been removed, had its name changed, or is
temporarily unavailable.
</p>
export function PageNotFound() {
return (
<div className={`h-screen w-full overflow-hidden bg-custom-background-100`}>
<div className="grid h-full place-items-center p-4">
<div className="space-y-8 text-center">
<div className="relative mx-auto h-60 w-60 lg:h-80 lg:w-80">
<img src={Image404} alt="404- Page not found" className="w-full h-full object-contain" />
</div>
<div className="space-y-2">
<h3 className="text-lg font-semibold">Oops! Something went wrong.</h3>
<p className="text-sm text-custom-text-200">
Sorry, the page you are looking for cannot be found. It may have been removed, had its name changed, or is
temporarily unavailable.
</p>
</div>
</div>
</div>
</div>
</div>
);
);
}

View file

@ -6,10 +6,12 @@ import { PoweredBy } from "@/components/common/powered-by";
// local imports
import { AuthHeader } from "./header";
export const AuthView = () => (
<div className="relative z-10 flex flex-col items-center w-screen h-screen overflow-hidden overflow-y-auto pt-6 pb-10 px-8">
<AuthHeader />
<AuthRoot />
<PoweredBy />
</div>
);
export function AuthView() {
return (
<div className="relative z-10 flex flex-col items-center w-screen h-screen overflow-hidden overflow-y-auto pt-6 pb-10 px-8">
<AuthHeader />
<AuthRoot />
<PoweredBy />
</div>
);
}

View file

@ -4,10 +4,12 @@ import React from "react";
import Link from "next/link";
import { PlaneLockup } from "@plane/propel/icons";
export const AuthHeader = () => (
<div className="flex items-center justify-between gap-6 w-full flex-shrink-0 sticky top-0">
<Link href="/">
<PlaneLockup height={20} width={95} className="text-custom-text-100" />
</Link>
</div>
);
export function AuthHeader() {
return (
<div className="flex items-center justify-between gap-6 w-full flex-shrink-0 sticky top-0">
<Link href="/">
<PlaneLockup height={20} width={95} className="text-custom-text-100" />
</Link>
</div>
);
}

View file

@ -17,7 +17,7 @@ import { InstanceFailureView } from "@/components/instance/instance-failure-view
import { useInstance } from "@/hooks/store/use-instance";
import { useUser } from "@/hooks/store/use-user";
export const InstanceProvider = observer(({ children }: { children: React.ReactNode }) => {
export const InstanceProvider = observer(function InstanceProvider({ children }: { children: React.ReactNode }) {
const { fetchInstanceInfo, instance, error } = useInstance();
const { fetchCurrentUser } = useUser();
const { resolvedTheme } = useTheme();

View file

@ -23,7 +23,7 @@ export type StoreProviderProps = {
initialState?: any;
};
export const StoreProvider = ({ children, initialState = undefined }: StoreProviderProps) => {
export function StoreProvider({ children, initialState = undefined }: StoreProviderProps) {
const store = initializeStore();
// If your page has Next.js data fetching methods that use a Mobx store, it will
// get hydrated here, check `pages/ssg.js` and `pages/ssr.js` for more details
@ -32,4 +32,4 @@ export const StoreProvider = ({ children, initialState = undefined }: StoreProvi
}
return <StoreContext.Provider value={store}>{children}</StoreContext.Provider>;
};
}

View file

@ -5,7 +5,7 @@ import { useTheme } from "next-themes";
import { Toast } from "@plane/propel/toast";
import { resolveGeneralTheme } from "@plane/utils";
export const ToastProvider = ({ children }: { children: React.ReactNode }) => {
export function ToastProvider({ children }: { children: React.ReactNode }) {
// themes
const { resolvedTheme } = useTheme();
@ -15,4 +15,4 @@ export const ToastProvider = ({ children }: { children: React.ReactNode }) => {
{children}
</>
);
};
}