[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:
parent
90866fb925
commit
83fdebf64d
1771 changed files with 17003 additions and 13856 deletions
|
|
@ -17,7 +17,7 @@ type IInstanceAIForm = {
|
|||
|
||||
type AIFormValues = Record<TInstanceAIConfigurationKeys, string>;
|
||||
|
||||
export const InstanceAIForm: React.FC<IInstanceAIForm> = (props) => {
|
||||
export function InstanceAIForm(props: IInstanceAIForm) {
|
||||
const { config } = props;
|
||||
// store
|
||||
const { updateInstanceConfigurations } = useInstance();
|
||||
|
|
@ -133,4 +133,4 @@ export const InstanceAIForm: React.FC<IInstanceAIForm> = (props) => {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { useInstance } from "@/hooks/store";
|
|||
import type { Route } from "./+types/page";
|
||||
import { InstanceAIForm } from "./form";
|
||||
|
||||
const InstanceAIPage = observer<React.FC<Route.ComponentProps>>(() => {
|
||||
const InstanceAIPage = observer(function InstanceAIPage(_props: Route.ComponentProps) {
|
||||
// store
|
||||
const { fetchInstanceConfigurations, formattedConfig } = useInstance();
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ type Props = {
|
|||
|
||||
type GiteaConfigFormValues = Record<TInstanceGiteaAuthenticationConfigurationKeys, string>;
|
||||
|
||||
export const InstanceGiteaConfigForm: FC<Props> = (props) => {
|
||||
export function InstanceGiteaConfigForm(props: Props) {
|
||||
const { config } = props;
|
||||
// states
|
||||
const [isDiscardChangesModalOpen, setIsDiscardChangesModalOpen] = useState(false);
|
||||
|
|
@ -207,4 +207,4 @@ export const InstanceGiteaConfigForm: FC<Props> = (props) => {
|
|||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { useInstance } from "@/hooks/store";
|
|||
import type { Route } from "./+types/page";
|
||||
import { InstanceGiteaConfigForm } from "./form";
|
||||
|
||||
const InstanceGiteaAuthenticationPage = observer(() => {
|
||||
const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthenticationPage() {
|
||||
// store
|
||||
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
|
||||
// state
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ type Props = {
|
|||
|
||||
type GithubConfigFormValues = Record<TInstanceGithubAuthenticationConfigurationKeys, string>;
|
||||
|
||||
export const InstanceGithubConfigForm: React.FC<Props> = (props) => {
|
||||
export function InstanceGithubConfigForm(props: Props) {
|
||||
const { config } = props;
|
||||
// states
|
||||
const [isDiscardChangesModalOpen, setIsDiscardChangesModalOpen] = useState(false);
|
||||
|
|
@ -245,4 +245,4 @@ export const InstanceGithubConfigForm: React.FC<Props> = (props) => {
|
|||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ import { useInstance } from "@/hooks/store";
|
|||
import type { Route } from "./+types/page";
|
||||
import { InstanceGithubConfigForm } from "./form";
|
||||
|
||||
const InstanceGithubAuthenticationPage = observer<React.FC<Route.ComponentProps>>(() => {
|
||||
const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthenticationPage(
|
||||
_props: Route.ComponentProps
|
||||
) {
|
||||
// store
|
||||
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
|
||||
// state
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ type Props = {
|
|||
|
||||
type GitlabConfigFormValues = Record<TInstanceGitlabAuthenticationConfigurationKeys, string>;
|
||||
|
||||
export const InstanceGitlabConfigForm: React.FC<Props> = (props) => {
|
||||
export function InstanceGitlabConfigForm(props: Props) {
|
||||
const { config } = props;
|
||||
// states
|
||||
const [isDiscardChangesModalOpen, setIsDiscardChangesModalOpen] = useState(false);
|
||||
|
|
@ -208,4 +208,4 @@ export const InstanceGitlabConfigForm: React.FC<Props> = (props) => {
|
|||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ import { useInstance } from "@/hooks/store";
|
|||
import type { Route } from "./+types/page";
|
||||
import { InstanceGitlabConfigForm } from "./form";
|
||||
|
||||
const InstanceGitlabAuthenticationPage = observer<React.FC<Route.ComponentProps>>(() => {
|
||||
const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthenticationPage(
|
||||
_props: Route.ComponentProps
|
||||
) {
|
||||
// store
|
||||
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
|
||||
// state
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ type Props = {
|
|||
|
||||
type GoogleConfigFormValues = Record<TInstanceGoogleAuthenticationConfigurationKeys, string>;
|
||||
|
||||
export const InstanceGoogleConfigForm: React.FC<Props> = (props) => {
|
||||
export function InstanceGoogleConfigForm(props: Props) {
|
||||
const { config } = props;
|
||||
// states
|
||||
const [isDiscardChangesModalOpen, setIsDiscardChangesModalOpen] = useState(false);
|
||||
|
|
@ -232,4 +232,4 @@ export const InstanceGoogleConfigForm: React.FC<Props> = (props) => {
|
|||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,9 @@ import { useInstance } from "@/hooks/store";
|
|||
import type { Route } from "./+types/page";
|
||||
import { InstanceGoogleConfigForm } from "./form";
|
||||
|
||||
const InstanceGoogleAuthenticationPage = observer<React.FC<Route.ComponentProps>>(() => {
|
||||
const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthenticationPage(
|
||||
_props: Route.ComponentProps
|
||||
) {
|
||||
// store
|
||||
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
|
||||
// state
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { useInstance } from "@/hooks/store";
|
|||
import { AuthenticationModes } from "@/plane-admin/components/authentication";
|
||||
import type { Route } from "./+types/page";
|
||||
|
||||
const InstanceAuthenticationPage = observer<React.FC<Route.ComponentProps>>(() => {
|
||||
const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(_props: Route.ComponentProps) {
|
||||
// store
|
||||
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ const EMAIL_SECURITY_OPTIONS: { [key in TEmailSecurityKeys]: string } = {
|
|||
NONE: "No email security",
|
||||
};
|
||||
|
||||
export const InstanceEmailForm: React.FC<IInstanceEmailForm> = (props) => {
|
||||
export function InstanceEmailForm(props: IInstanceEmailForm) {
|
||||
const { config } = props;
|
||||
// states
|
||||
const [isSendTestEmailModalOpen, setIsSendTestEmailModalOpen] = useState(false);
|
||||
|
|
@ -224,4 +224,4 @@ export const InstanceEmailForm: React.FC<IInstanceEmailForm> = (props) => {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { useInstance } from "@/hooks/store";
|
|||
import type { Route } from "./+types/page";
|
||||
import { InstanceEmailForm } from "./email-config-form";
|
||||
|
||||
const InstanceEmailPage = observer<React.FC<Route.ComponentProps>>(() => {
|
||||
const InstanceEmailPage = observer(function InstanceEmailPage(_props: Route.ComponentProps) {
|
||||
// store
|
||||
const { fetchInstanceConfigurations, formattedConfig, disableEmail } = useInstance();
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ enum ESendEmailSteps {
|
|||
|
||||
const instanceService = new InstanceService();
|
||||
|
||||
export const SendTestEmailModal: React.FC<Props> = (props) => {
|
||||
export function SendTestEmailModal(props: Props) {
|
||||
const { isOpen, handleClose } = props;
|
||||
|
||||
// state
|
||||
|
|
@ -133,4 +133,4 @@ export const SendTestEmailModal: React.FC<Props> = (props) => {
|
|||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export interface IGeneralConfigurationForm {
|
|||
instanceAdmins: IInstanceAdmin[];
|
||||
}
|
||||
|
||||
export const GeneralConfigurationForm: React.FC<IGeneralConfigurationForm> = observer((props) => {
|
||||
export const GeneralConfigurationForm = observer(function GeneralConfigurationForm(props: IGeneralConfigurationForm) {
|
||||
const { instance, instanceAdmins } = props;
|
||||
// hooks
|
||||
const { instanceConfigurations, updateInstanceInfo, updateInstanceConfigurations } = useInstance();
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ type TIntercomConfig = {
|
|||
isTelemetryEnabled: boolean;
|
||||
};
|
||||
|
||||
export const IntercomConfig: React.FC<TIntercomConfig> = observer((props) => {
|
||||
export const IntercomConfig = observer(function IntercomConfig(props: TIntercomConfig) {
|
||||
const { isTelemetryEnabled } = props;
|
||||
// hooks
|
||||
const { instanceConfigurations, updateInstanceConfigurations, fetchInstanceConfigurations } = useInstance();
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
|
|||
// hooks
|
||||
import { useTheme } from "@/hooks/store";
|
||||
|
||||
export const HamburgerToggle = observer(() => {
|
||||
export const HamburgerToggle = observer(function HamburgerToggle() {
|
||||
const { isSidebarCollapsed, toggleSidebar } = useTheme();
|
||||
return (
|
||||
<div
|
||||
|
|
@ -22,7 +22,7 @@ export const HamburgerToggle = observer(() => {
|
|||
);
|
||||
});
|
||||
|
||||
export const AdminHeader = observer(() => {
|
||||
export const AdminHeader = observer(function AdminHeader() {
|
||||
const pathName = usePathname();
|
||||
|
||||
const getHeaderTitle = (pathName: string) => {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ type IInstanceImageConfigForm = {
|
|||
|
||||
type ImageConfigFormValues = Record<TInstanceImageConfigurationKeys, string>;
|
||||
|
||||
export const InstanceImageConfigForm: React.FC<IInstanceImageConfigForm> = (props) => {
|
||||
export function InstanceImageConfigForm(props: IInstanceImageConfigForm) {
|
||||
const { config } = props;
|
||||
// store hooks
|
||||
const { updateInstanceConfigurations } = useInstance();
|
||||
|
|
@ -77,4 +77,4 @@ export const InstanceImageConfigForm: React.FC<IInstanceImageConfigForm> = (prop
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { useInstance } from "@/hooks/store";
|
|||
import type { Route } from "./+types/page";
|
||||
import { InstanceImageConfigForm } from "./form";
|
||||
|
||||
const InstanceImagePage = observer<React.FC<Route.ComponentProps>>(() => {
|
||||
const InstanceImagePage = observer(function InstanceImagePage(_props: Route.ComponentProps) {
|
||||
// store
|
||||
const { formattedConfig, fetchInstanceConfigurations } = useInstance();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
|
@ -14,7 +13,7 @@ import type { Route } from "./+types/layout";
|
|||
import { AdminHeader } from "./header";
|
||||
import { AdminSidebar } from "./sidebar";
|
||||
|
||||
const AdminLayout: React.FC<Route.ComponentProps> = () => {
|
||||
function AdminLayout(_props: Route.ComponentProps) {
|
||||
// router
|
||||
const { replace } = useRouter();
|
||||
// store hooks
|
||||
|
|
@ -48,6 +47,6 @@ const AdminLayout: React.FC<Route.ComponentProps> = () => {
|
|||
}
|
||||
|
||||
return <></>;
|
||||
};
|
||||
}
|
||||
|
||||
export default observer(AdminLayout);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { useTheme, useUser } from "@/hooks/store";
|
|||
// service initialization
|
||||
const authService = new AuthService();
|
||||
|
||||
export const AdminSidebarDropdown = observer(() => {
|
||||
export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
|
||||
// store hooks
|
||||
const { isSidebarCollapsed } = useTheme();
|
||||
const { currentUser, signOut } = useUser();
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ const helpOptions = [
|
|||
},
|
||||
];
|
||||
|
||||
export const AdminSidebarHelpSection: React.FC = observer(() => {
|
||||
export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection() {
|
||||
// states
|
||||
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
|
||||
// store
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ const INSTANCE_ADMIN_LINKS = [
|
|||
},
|
||||
];
|
||||
|
||||
export const AdminSidebarMenu = observer(() => {
|
||||
export const AdminSidebarMenu = observer(function AdminSidebarMenu() {
|
||||
// store hooks
|
||||
const { isSidebarCollapsed, toggleSidebar } = useTheme();
|
||||
// router
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { AdminSidebarDropdown } from "./sidebar-dropdown";
|
|||
import { AdminSidebarHelpSection } from "./sidebar-help-section";
|
||||
import { AdminSidebarMenu } from "./sidebar-menu";
|
||||
|
||||
export const AdminSidebar = observer(() => {
|
||||
export const AdminSidebar = observer(function AdminSidebar() {
|
||||
// store
|
||||
const { isSidebarCollapsed, toggleSidebar } = useTheme();
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import { useWorkspace } from "@/hooks/store";
|
|||
|
||||
const instanceWorkspaceService = new InstanceWorkspaceService();
|
||||
|
||||
export const WorkspaceCreateForm = () => {
|
||||
export function WorkspaceCreateForm() {
|
||||
// router
|
||||
const router = useRouter();
|
||||
// states
|
||||
|
|
@ -209,4 +209,4 @@ export const WorkspaceCreateForm = () => {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,19 +5,21 @@ import { observer } from "mobx-react";
|
|||
import type { Route } from "./+types/page";
|
||||
import { WorkspaceCreateForm } from "./form";
|
||||
|
||||
const WorkspaceCreatePage = observer<React.FC<Route.ComponentProps>>(() => (
|
||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="text-xl font-medium text-custom-text-100">Create a new workspace on this instance.</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
You will need to invite users from Workspace Settings after you create this workspace.
|
||||
const WorkspaceCreatePage = observer(function WorkspaceCreatePage(_props: Route.ComponentProps) {
|
||||
return (
|
||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
||||
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
||||
<div className="text-xl font-medium text-custom-text-100">Create a new workspace on this instance.</div>
|
||||
<div className="text-sm font-normal text-custom-text-300">
|
||||
You will need to invite users from Workspace Settings after you create this workspace.
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
||||
<WorkspaceCreateForm />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
||||
<WorkspaceCreateForm />
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
);
|
||||
});
|
||||
|
||||
export const meta: Route.MetaFunction = () => [{ title: "Create Workspace - God Mode" }];
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import { WorkspaceListItem } from "@/components/workspace/list-item";
|
|||
import { useInstance, useWorkspace } from "@/hooks/store";
|
||||
import type { Route } from "./+types/page";
|
||||
|
||||
const WorkspaceManagementPage = observer<React.FC<Route.ComponentProps>>(() => {
|
||||
const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props: Route.ComponentProps) {
|
||||
// states
|
||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||
// store
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ type TAuthBanner = {
|
|||
handleBannerData?: (bannerData: TAdminAuthErrorInfo | undefined) => void;
|
||||
};
|
||||
|
||||
export const AuthBanner: React.FC<TAuthBanner> = (props) => {
|
||||
export function AuthBanner(props: TAuthBanner) {
|
||||
const { bannerData, handleBannerData } = props;
|
||||
|
||||
if (!bannerData) return <></>;
|
||||
|
|
@ -27,4 +27,4 @@ export const AuthBanner: React.FC<TAuthBanner> = (props) => {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
"use client";
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||
|
|
@ -11,7 +10,7 @@ import { useInstance } from "@/hooks/store";
|
|||
import type { Route } from "./+types/page";
|
||||
import { InstanceSignInForm } from "./sign-in-form";
|
||||
|
||||
const HomePage = () => {
|
||||
function HomePage() {
|
||||
// store hooks
|
||||
const { instance, error } = useInstance();
|
||||
|
||||
|
|
@ -36,7 +35,7 @@ const HomePage = () => {
|
|||
|
||||
// if instance is fetched and setup is done, show sign in form
|
||||
return <InstanceSignInForm />;
|
||||
};
|
||||
}
|
||||
|
||||
export default observer(HomePage);
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ const defaultFromData: TFormData = {
|
|||
password: "",
|
||||
};
|
||||
|
||||
export const InstanceSignInForm: React.FC = () => {
|
||||
export function InstanceSignInForm() {
|
||||
// search params
|
||||
const searchParams = useSearchParams();
|
||||
const emailParam = searchParams.get("email") || undefined;
|
||||
|
|
@ -192,4 +192,4 @@ export const InstanceSignInForm: React.FC = () => {
|
|||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import useSWR from "swr";
|
|||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
|
||||
export const InstanceProvider = observer<React.FC<React.PropsWithChildren>>((props) => {
|
||||
export const InstanceProvider = observer(function InstanceProvider(props: React.PropsWithChildren) {
|
||||
const { children } = props;
|
||||
// store hooks
|
||||
const { fetchInstanceInfo } = useInstance();
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export type StoreProviderProps = {
|
|||
initialState?: any;
|
||||
};
|
||||
|
||||
export const StoreProvider = ({ children, initialState = {} }: StoreProviderProps) => {
|
||||
export function StoreProvider({ children, initialState = {} }: StoreProviderProps) {
|
||||
const store = initializeStore(initialState);
|
||||
return <StoreContext.Provider value={store}>{children}</StoreContext.Provider>;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useTheme } from "next-themes";
|
|||
import { Toast } from "@plane/propel/toast";
|
||||
import { resolveGeneralTheme } from "@plane/utils";
|
||||
|
||||
export const ToastWithTheme = () => {
|
||||
export function ToastWithTheme() {
|
||||
const { resolvedTheme } = useTheme();
|
||||
return <Toast theme={resolveGeneralTheme(resolvedTheme)} />;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import useSWR from "swr";
|
|||
// hooks
|
||||
import { useInstance, useTheme, useUser } from "@/hooks/store";
|
||||
|
||||
export const UserProvider = observer<React.FC<React.PropsWithChildren>>(({ children }) => {
|
||||
export const UserProvider = observer(function UserProvider({ children }: React.PropsWithChildren) {
|
||||
// hooks
|
||||
const { isSidebarCollapsed, toggleSidebar } = useTheme();
|
||||
const { currentUser, fetchCurrentUser } = useUser();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -5,30 +5,32 @@ import { Button } from "@plane/propel/button";
|
|||
// images
|
||||
import Image404 from "@/app/assets/images/404.svg?url";
|
||||
|
||||
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="h-full w-full object-contain" />
|
||||
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="h-full w-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>
|
||||
<Link to="/general/">
|
||||
<span className="flex justify-center py-4">
|
||||
<Button variant="neutral-primary" size="md">
|
||||
Go to general settings
|
||||
</Button>
|
||||
</span>
|
||||
</Link>
|
||||
</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>
|
||||
<Link to="/general/">
|
||||
<span className="flex justify-center py-4">
|
||||
<Button variant="neutral-primary" size="md">
|
||||
Go to general settings
|
||||
</Button>
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
export default PageNotFound;
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ export const getAuthenticationModes: (props: TGetBaseAuthenticationModeProps) =>
|
|||
},
|
||||
];
|
||||
|
||||
export const AuthenticationModes = observer<React.FC<TAuthenticationModeProps>>((props) => {
|
||||
export const AuthenticationModes = observer(function AuthenticationModes(props: TAuthenticationModeProps) {
|
||||
const { disabled, updateConfig } = props;
|
||||
// next-themes
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
|
|
|||
|
|
@ -7,9 +7,15 @@ import { SquareArrowOutUpRight } from "lucide-react";
|
|||
import { getButtonStyling } from "@plane/propel/button";
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
export const UpgradeButton: React.FC = () => (
|
||||
<a href="https://plane.so/pricing?mode=self-hosted" target="_blank" className={cn(getButtonStyling("primary", "sm"))}>
|
||||
Upgrade
|
||||
<SquareArrowOutUpRight className="h-3.5 w-3.5 p-0.5" />
|
||||
</a>
|
||||
);
|
||||
export function UpgradeButton() {
|
||||
return (
|
||||
<a
|
||||
href="https://plane.so/pricing?mode=self-hosted"
|
||||
target="_blank"
|
||||
className={cn(getButtonStyling("primary", "sm"))}
|
||||
>
|
||||
Upgrade
|
||||
<SquareArrowOutUpRight className="h-3.5 w-3.5 p-0.5" />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ type Props = {
|
|||
unavailable?: boolean;
|
||||
};
|
||||
|
||||
export const AuthenticationMethodCard: React.FC<Props> = (props) => {
|
||||
export function AuthenticationMethodCard(props: Props) {
|
||||
const { name, description, icon, config, disabled = false, withBorder = true, unavailable = false } = props;
|
||||
|
||||
return (
|
||||
|
|
@ -52,4 +52,4 @@ export const AuthenticationMethodCard: React.FC<Props> = (props) => {
|
|||
<div className={`shrink-0 ${disabled && "opacity-70"}`}>{config}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ type Props = {
|
|||
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
|
||||
};
|
||||
|
||||
export const EmailCodesConfiguration: React.FC<Props> = observer((props) => {
|
||||
export const EmailCodesConfiguration = observer(function EmailCodesConfiguration(props: Props) {
|
||||
const { disabled, updateConfig } = props;
|
||||
// store
|
||||
const { formattedConfig } = useInstance();
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ type Props = {
|
|||
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
|
||||
};
|
||||
|
||||
export const GiteaConfiguration: React.FC<Props> = observer((props) => {
|
||||
export const GiteaConfiguration = observer(function GiteaConfiguration(props: Props) {
|
||||
const { disabled, updateConfig } = props;
|
||||
// store
|
||||
const { formattedConfig } = useInstance();
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ type Props = {
|
|||
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
|
||||
};
|
||||
|
||||
export const GithubConfiguration: React.FC<Props> = observer((props) => {
|
||||
export const GithubConfiguration = observer(function GithubConfiguration(props: Props) {
|
||||
const { disabled, updateConfig } = props;
|
||||
// store
|
||||
const { formattedConfig } = useInstance();
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ type Props = {
|
|||
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
|
||||
};
|
||||
|
||||
export const GitlabConfiguration: React.FC<Props> = observer((props) => {
|
||||
export const GitlabConfiguration = observer(function GitlabConfiguration(props: Props) {
|
||||
const { disabled, updateConfig } = props;
|
||||
// store
|
||||
const { formattedConfig } = useInstance();
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ type Props = {
|
|||
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
|
||||
};
|
||||
|
||||
export const GoogleConfiguration: React.FC<Props> = observer((props) => {
|
||||
export const GoogleConfiguration = observer(function GoogleConfiguration(props: Props) {
|
||||
const { disabled, updateConfig } = props;
|
||||
// store
|
||||
const { formattedConfig } = useInstance();
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ type Props = {
|
|||
updateConfig: (key: TInstanceAuthenticationMethodKeys, value: string) => void;
|
||||
};
|
||||
|
||||
export const PasswordLoginConfiguration: React.FC<Props> = observer((props) => {
|
||||
export const PasswordLoginConfiguration = observer(function PasswordLoginConfiguration(props: Props) {
|
||||
const { disabled, updateConfig } = props;
|
||||
// store
|
||||
const { formattedConfig } = useInstance();
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ type TBanner = {
|
|||
message: string;
|
||||
};
|
||||
|
||||
export const Banner: FC<TBanner> = (props) => {
|
||||
export function Banner(props: TBanner) {
|
||||
const { type, message } = props;
|
||||
|
||||
return (
|
||||
|
|
@ -29,4 +29,4 @@ export const Banner: FC<TBanner> = (props) => {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ type Props = {
|
|||
icon?: React.ReactNode | undefined;
|
||||
};
|
||||
|
||||
export const BreadcrumbLink: React.FC<Props> = (props) => {
|
||||
export function BreadcrumbLink(props: Props) {
|
||||
const { href, label, icon } = props;
|
||||
return (
|
||||
<Tooltip tooltipContent={label} position="bottom">
|
||||
|
|
@ -35,4 +35,4 @@ export const BreadcrumbLink: React.FC<Props> = (props) => {
|
|||
</li>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,16 +6,18 @@ type TProps = {
|
|||
darkerShade?: boolean;
|
||||
};
|
||||
|
||||
export const CodeBlock = ({ children, className, darkerShade }: TProps) => (
|
||||
<span
|
||||
className={cn(
|
||||
"px-0.5 text-xs text-custom-text-300 bg-custom-background-90 font-semibold rounded-md border border-custom-border-100",
|
||||
{
|
||||
"text-custom-text-200 bg-custom-background-80 border-custom-border-200": darkerShade,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
export function CodeBlock({ children, className, darkerShade }: TProps) {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"px-0.5 text-xs text-custom-text-300 bg-custom-background-90 font-semibold rounded-md border border-custom-border-100",
|
||||
{
|
||||
"text-custom-text-200 bg-custom-background-80 border-custom-border-200": darkerShade,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ type Props = {
|
|||
onDiscardHref: string;
|
||||
};
|
||||
|
||||
export const ConfirmDiscardModal: React.FC<Props> = (props) => {
|
||||
export function ConfirmDiscardModal(props: Props) {
|
||||
const { isOpen, handleClose, onDiscardHref } = props;
|
||||
|
||||
return (
|
||||
|
|
@ -71,4 +71,4 @@ export const ConfirmDiscardModal: React.FC<Props> = (props) => {
|
|||
</Dialog>
|
||||
</Transition.Root>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export type TControllerInputFormField = {
|
|||
required: boolean;
|
||||
};
|
||||
|
||||
export const ControllerInput: React.FC<Props> = (props) => {
|
||||
export function ControllerInput(props: Props) {
|
||||
const { name, control, type, label, description, placeholder, error, required } = props;
|
||||
// states
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
|
@ -81,4 +81,4 @@ export const ControllerInput: React.FC<Props> = (props) => {
|
|||
{description && <p className="pt-0.5 text-xs text-custom-text-300">{description}</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export type TCopyField = {
|
|||
description: string | React.ReactNode;
|
||||
};
|
||||
|
||||
export const CopyField: React.FC<Props> = (props) => {
|
||||
export function CopyField(props: Props) {
|
||||
const { label, url, description } = props;
|
||||
|
||||
return (
|
||||
|
|
@ -43,4 +43,4 @@ export const CopyField: React.FC<Props> = (props) => {
|
|||
<div className="text-xs text-custom-text-300">{description}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,32 +16,27 @@ type Props = {
|
|||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const EmptyState: React.FC<Props> = ({
|
||||
title,
|
||||
description,
|
||||
image,
|
||||
primaryButton,
|
||||
secondaryButton,
|
||||
disabled = false,
|
||||
}) => (
|
||||
<div className={`flex h-full w-full items-center justify-center`}>
|
||||
<div className="flex w-full flex-col items-center text-center">
|
||||
{image && <img src={image} className="w-52 sm:w-60" alt={primaryButton?.text || "button image"} />}
|
||||
<h6 className="mb-3 mt-6 text-xl font-semibold sm:mt-8">{title}</h6>
|
||||
{description && <p className="mb-7 px-5 text-custom-text-300 sm:mb-8">{description}</p>}
|
||||
<div className="flex items-center gap-4">
|
||||
{primaryButton && (
|
||||
<Button
|
||||
variant="primary"
|
||||
prependIcon={primaryButton.icon}
|
||||
onClick={primaryButton.onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{primaryButton.text}
|
||||
</Button>
|
||||
)}
|
||||
{secondaryButton}
|
||||
export function EmptyState({ title, description, image, primaryButton, secondaryButton, disabled = false }: Props) {
|
||||
return (
|
||||
<div className={`flex h-full w-full items-center justify-center`}>
|
||||
<div className="flex w-full flex-col items-center text-center">
|
||||
{image && <img src={image} className="w-52 sm:w-60" alt={primaryButton?.text || "button image"} />}
|
||||
<h6 className="mb-3 mt-6 text-xl font-semibold sm:mt-8">{title}</h6>
|
||||
{description && <p className="mb-7 px-5 text-custom-text-300 sm:mb-8">{description}</p>}
|
||||
<div className="flex items-center gap-4">
|
||||
{primaryButton && (
|
||||
<Button
|
||||
variant="primary"
|
||||
prependIcon={primaryButton.icon}
|
||||
onClick={primaryButton.onClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
{primaryButton.text}
|
||||
</Button>
|
||||
)}
|
||||
{secondaryButton}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,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;
|
||||
|
|
@ -12,4 +12,4 @@ export const LogoSpinner = () => {
|
|||
<img src={logoSrc} alt="logo" className="h-6 w-auto sm:h-11" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ type TPageHeader = {
|
|||
description?: string;
|
||||
};
|
||||
|
||||
export const PageHeader: React.FC<TPageHeader> = (props) => {
|
||||
export function PageHeader(props: TPageHeader) {
|
||||
const { title = "God Mode - Plane", description = "Plane god mode" } = props;
|
||||
|
||||
return (
|
||||
|
|
@ -14,4 +14,4 @@ export const PageHeader: React.FC<TPageHeader> = (props) => {
|
|||
<meta name="description" content={description} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { AuthHeader } from "@/app/(all)/(home)/auth-header";
|
|||
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 = observer(() => {
|
||||
export const InstanceFailureView = observer(function InstanceFailureView() {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
const instanceImage = resolvedTheme === "dark" ? InstanceFailureDarkImage : InstanceFailureImage;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
"use client";
|
||||
|
||||
export const FormHeader = ({ heading, subHeading }: { heading: string; subHeading: string }) => (
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-2xl font-semibold text-custom-text-100 leading-7">{heading}</span>
|
||||
<span className="text-lg font-semibold text-custom-text-400 leading-7">{subHeading}</span>
|
||||
</div>
|
||||
);
|
||||
export function FormHeader({ heading, subHeading }: { heading: string; subHeading: string }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-2xl font-semibold text-custom-text-100 leading-7">{heading}</span>
|
||||
<span className="text-lg font-semibold text-custom-text-400 leading-7">{subHeading}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,24 +5,26 @@ import { Button } from "@plane/propel/button";
|
|||
// assets
|
||||
import PlaneTakeOffImage from "@/app/assets/images/plane-takeoff.png?url";
|
||||
|
||||
export const InstanceNotReady: React.FC = () => (
|
||||
<div className="h-full w-full relative container px-5 mx-auto flex justify-center items-center">
|
||||
<div className="w-auto max-w-2xl relative space-y-8 py-10">
|
||||
<div className="relative flex flex-col justify-center items-center space-y-4">
|
||||
<h1 className="text-3xl font-bold pb-3">Welcome aboard Plane!</h1>
|
||||
<img src={PlaneTakeOffImage} alt="Plane Logo" />
|
||||
<p className="font-medium text-base text-custom-text-400">
|
||||
Get started by setting up your instance and workspace
|
||||
</p>
|
||||
</div>
|
||||
export function InstanceNotReady() {
|
||||
return (
|
||||
<div className="h-full w-full relative container px-5 mx-auto flex justify-center items-center">
|
||||
<div className="w-auto max-w-2xl relative space-y-8 py-10">
|
||||
<div className="relative flex flex-col justify-center items-center space-y-4">
|
||||
<h1 className="text-3xl font-bold pb-3">Welcome aboard Plane!</h1>
|
||||
<img src={PlaneTakeOffImage} alt="Plane Logo" />
|
||||
<p className="font-medium text-base text-custom-text-400">
|
||||
Get started by setting up your instance and workspace
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Link href={"/setup/?auth_enabled=0"}>
|
||||
<Button size="lg" className="w-full">
|
||||
Get started
|
||||
</Button>
|
||||
</Link>
|
||||
<div>
|
||||
<Link href={"/setup/?auth_enabled=0"}>
|
||||
<Button size="lg" className="w-full">
|
||||
Get started
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,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 InstanceLoading = () => {
|
||||
export function InstanceLoading() {
|
||||
const { resolvedTheme } = useTheme();
|
||||
|
||||
const logoSrc = resolvedTheme === "dark" ? LogoSpinnerLight : LogoSpinnerDark;
|
||||
|
|
@ -13,4 +13,4 @@ export const InstanceLoading = () => {
|
|||
<img src={logoSrc} alt="logo" className="h-6 w-auto sm:h-11" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ const defaultFromData: TFormData = {
|
|||
is_telemetry_enabled: true,
|
||||
};
|
||||
|
||||
export const InstanceSetupForm: React.FC = (props) => {
|
||||
export function InstanceSetupForm(props) {
|
||||
const {} = props;
|
||||
// search params
|
||||
const searchParams = useSearchParams();
|
||||
|
|
@ -351,4 +351,4 @@ export const InstanceSetupForm: React.FC = (props) => {
|
|||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import TakeoffIconLight from "@/app/assets/logos/takeoff-icon-light.svg?url";
|
|||
import { useTheme } from "@/hooks/store";
|
||||
// icons
|
||||
|
||||
export const NewUserPopup = observer(() => {
|
||||
export const NewUserPopup = observer(function NewUserPopup() {
|
||||
// hooks
|
||||
const { isNewUserPopup, toggleNewUserPopup } = useTheme();
|
||||
// theme
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ type TWorkspaceListItemProps = {
|
|||
workspaceId: string;
|
||||
};
|
||||
|
||||
export const WorkspaceListItem = observer(({ workspaceId }: TWorkspaceListItemProps) => {
|
||||
export const WorkspaceListItem = observer(function WorkspaceListItem({ workspaceId }: TWorkspaceListItemProps) {
|
||||
// store hooks
|
||||
const { getWorkspaceById } = useWorkspace();
|
||||
// derived values
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue