[WEB-5042] feat: sites vite migration (#7965)
4
.gitignore
vendored
|
|
@ -103,6 +103,10 @@ storybook-static
|
||||||
|
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
|
|
||||||
|
build/
|
||||||
|
.react-router/
|
||||||
|
AGENTS.md
|
||||||
|
|
||||||
build/
|
build/
|
||||||
.react-router/
|
.react-router/
|
||||||
AGENTS.md
|
AGENTS.md
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
import type { FC } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { Lightbulb } from "lucide-react";
|
import { Lightbulb } from "lucide-react";
|
||||||
import { Button } from "@plane/propel/button";
|
import { Button } from "@plane/propel/button";
|
||||||
|
|
@ -17,7 +17,7 @@ type IInstanceAIForm = {
|
||||||
|
|
||||||
type AIFormValues = Record<TInstanceAIConfigurationKeys, string>;
|
type AIFormValues = Record<TInstanceAIConfigurationKeys, string>;
|
||||||
|
|
||||||
export const InstanceAIForm: FC<IInstanceAIForm> = (props) => {
|
export const InstanceAIForm: React.FC<IInstanceAIForm> = (props) => {
|
||||||
const { config } = props;
|
const { config } = props;
|
||||||
// store
|
// store
|
||||||
const { updateInstanceConfigurations } = useInstance();
|
const { updateInstanceConfigurations } = useInstance();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { FC } from "react";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { isEmpty } from "lodash-es";
|
import { isEmpty } from "lodash-es";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
@ -29,7 +28,7 @@ type Props = {
|
||||||
|
|
||||||
type GithubConfigFormValues = Record<TInstanceGithubAuthenticationConfigurationKeys, string>;
|
type GithubConfigFormValues = Record<TInstanceGithubAuthenticationConfigurationKeys, string>;
|
||||||
|
|
||||||
export const InstanceGithubConfigForm: FC<Props> = (props) => {
|
export const InstanceGithubConfigForm: React.FC<Props> = (props) => {
|
||||||
const { config } = props;
|
const { config } = props;
|
||||||
// states
|
// states
|
||||||
const [isDiscardChangesModalOpen, setIsDiscardChangesModalOpen] = useState(false);
|
const [isDiscardChangesModalOpen, setIsDiscardChangesModalOpen] = useState(false);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import type { FC } from "react";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { isEmpty } from "lodash-es";
|
import { isEmpty } from "lodash-es";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
@ -25,7 +24,7 @@ type Props = {
|
||||||
|
|
||||||
type GitlabConfigFormValues = Record<TInstanceGitlabAuthenticationConfigurationKeys, string>;
|
type GitlabConfigFormValues = Record<TInstanceGitlabAuthenticationConfigurationKeys, string>;
|
||||||
|
|
||||||
export const InstanceGitlabConfigForm: FC<Props> = (props) => {
|
export const InstanceGitlabConfigForm: React.FC<Props> = (props) => {
|
||||||
const { config } = props;
|
const { config } = props;
|
||||||
// states
|
// states
|
||||||
const [isDiscardChangesModalOpen, setIsDiscardChangesModalOpen] = useState(false);
|
const [isDiscardChangesModalOpen, setIsDiscardChangesModalOpen] = useState(false);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
import type { FC } from "react";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { isEmpty } from "lodash-es";
|
import { isEmpty } from "lodash-es";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
@ -27,7 +27,7 @@ type Props = {
|
||||||
|
|
||||||
type GoogleConfigFormValues = Record<TInstanceGoogleAuthenticationConfigurationKeys, string>;
|
type GoogleConfigFormValues = Record<TInstanceGoogleAuthenticationConfigurationKeys, string>;
|
||||||
|
|
||||||
export const InstanceGoogleConfigForm: FC<Props> = (props) => {
|
export const InstanceGoogleConfigForm: React.FC<Props> = (props) => {
|
||||||
const { config } = props;
|
const { config } = props;
|
||||||
// states
|
// states
|
||||||
const [isDiscardChangesModalOpen, setIsDiscardChangesModalOpen] = useState(false);
|
const [isDiscardChangesModalOpen, setIsDiscardChangesModalOpen] = useState(false);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { FC } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import React, { useMemo, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
// types
|
// types
|
||||||
import { Button } from "@plane/propel/button";
|
import { Button } from "@plane/propel/button";
|
||||||
|
|
@ -31,7 +30,7 @@ const EMAIL_SECURITY_OPTIONS: { [key in TEmailSecurityKeys]: string } = {
|
||||||
NONE: "No email security",
|
NONE: "No email security",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
|
export const InstanceEmailForm: React.FC<IInstanceEmailForm> = (props) => {
|
||||||
const { config } = props;
|
const { config } = props;
|
||||||
// states
|
// states
|
||||||
const [isSendTestEmailModalOpen, setIsSendTestEmailModalOpen] = useState(false);
|
const [isSendTestEmailModalOpen, setIsSendTestEmailModalOpen] = useState(false);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import type { FC } from "react";
|
import { useEffect, useState, Fragment } from "react";
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// plane imports
|
// plane imports
|
||||||
import { Button } from "@plane/propel/button";
|
import { Button } from "@plane/propel/button";
|
||||||
|
|
@ -20,7 +19,7 @@ enum ESendEmailSteps {
|
||||||
|
|
||||||
const instanceService = new InstanceService();
|
const instanceService = new InstanceService();
|
||||||
|
|
||||||
export const SendTestEmailModal: FC<Props> = (props) => {
|
export const SendTestEmailModal: React.FC<Props> = (props) => {
|
||||||
const { isOpen, handleClose } = props;
|
const { isOpen, handleClose } = props;
|
||||||
|
|
||||||
// state
|
// state
|
||||||
|
|
@ -62,10 +61,10 @@ export const SendTestEmailModal: FC<Props> = (props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Transition.Root show={isOpen} as={React.Fragment}>
|
<Transition.Root show={isOpen} as={Fragment}>
|
||||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={React.Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
enterFrom="opacity-0"
|
enterFrom="opacity-0"
|
||||||
enterTo="opacity-100"
|
enterTo="opacity-100"
|
||||||
|
|
@ -78,7 +77,7 @@ export const SendTestEmailModal: FC<Props> = (props) => {
|
||||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||||
<div className="my-10 flex justify-center p-4 text-center sm:p-0 md:my-20">
|
<div className="my-10 flex justify-center p-4 text-center sm:p-0 md:my-20">
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={React.Fragment}
|
as={Fragment}
|
||||||
enter="ease-out duration-300"
|
enter="ease-out duration-300"
|
||||||
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
"use client";
|
"use client";
|
||||||
import type { FC } from "react";
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { Telescope } from "lucide-react";
|
import { Telescope } from "lucide-react";
|
||||||
|
|
@ -20,7 +19,7 @@ export interface IGeneralConfigurationForm {
|
||||||
instanceAdmins: IInstanceAdmin[];
|
instanceAdmins: IInstanceAdmin[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GeneralConfigurationForm: FC<IGeneralConfigurationForm> = observer((props) => {
|
export const GeneralConfigurationForm: React.FC<IGeneralConfigurationForm> = observer((props) => {
|
||||||
const { instance, instanceAdmins } = props;
|
const { instance, instanceAdmins } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const { instanceConfigurations, updateInstanceInfo, updateInstanceConfigurations } = useInstance();
|
const { instanceConfigurations, updateInstanceInfo, updateInstanceConfigurations } = useInstance();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { FC } from "react";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
@ -14,7 +13,7 @@ type TIntercomConfig = {
|
||||||
isTelemetryEnabled: boolean;
|
isTelemetryEnabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IntercomConfig: FC<TIntercomConfig> = observer((props) => {
|
export const IntercomConfig: React.FC<TIntercomConfig> = observer((props) => {
|
||||||
const { isTelemetryEnabled } = props;
|
const { isTelemetryEnabled } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const { instanceConfigurations, updateInstanceConfigurations, fetchInstanceConfigurations } = useInstance();
|
const { instanceConfigurations, updateInstanceConfigurations, fetchInstanceConfigurations } = useInstance();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { FC } from "react";
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { Menu, Settings } from "lucide-react";
|
import { Menu, Settings } from "lucide-react";
|
||||||
|
|
@ -11,7 +10,7 @@ import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
|
||||||
// hooks
|
// hooks
|
||||||
import { useTheme } from "@/hooks/store";
|
import { useTheme } from "@/hooks/store";
|
||||||
|
|
||||||
export const HamburgerToggle: FC = observer(() => {
|
export const HamburgerToggle = observer(() => {
|
||||||
const { isSidebarCollapsed, toggleSidebar } = useTheme();
|
const { isSidebarCollapsed, toggleSidebar } = useTheme();
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -23,7 +22,7 @@ export const HamburgerToggle: FC = observer(() => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const AdminHeader: FC = observer(() => {
|
export const AdminHeader = observer(() => {
|
||||||
const pathName = usePathname();
|
const pathName = usePathname();
|
||||||
|
|
||||||
const getHeaderTitle = (pathName: string) => {
|
const getHeaderTitle = (pathName: string) => {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
"use client";
|
"use client";
|
||||||
import type { FC } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { Button } from "@plane/propel/button";
|
import { Button } from "@plane/propel/button";
|
||||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||||
|
|
@ -15,7 +14,7 @@ type IInstanceImageConfigForm = {
|
||||||
|
|
||||||
type ImageConfigFormValues = Record<TInstanceImageConfigurationKeys, string>;
|
type ImageConfigFormValues = Record<TInstanceImageConfigurationKeys, string>;
|
||||||
|
|
||||||
export const InstanceImageConfigForm: FC<IInstanceImageConfigForm> = (props) => {
|
export const InstanceImageConfigForm: React.FC<IInstanceImageConfigForm> = (props) => {
|
||||||
const { config } = props;
|
const { config } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { updateInstanceConfigurations } = useInstance();
|
const { updateInstanceConfigurations } = useInstance();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { FC } from "react";
|
|
||||||
import { useState, useRef } from "react";
|
import { useState, useRef } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
@ -35,7 +34,7 @@ const helpOptions = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const AdminSidebarHelpSection: FC = observer(() => {
|
export const AdminSidebarHelpSection: React.FC = observer(() => {
|
||||||
// states
|
// states
|
||||||
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
|
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
|
||||||
// store
|
// store
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { FC } from "react";
|
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// plane helpers
|
// plane helpers
|
||||||
|
|
@ -12,7 +11,7 @@ import { AdminSidebarDropdown } from "./sidebar-dropdown";
|
||||||
import { AdminSidebarHelpSection } from "./sidebar-help-section";
|
import { AdminSidebarHelpSection } from "./sidebar-help-section";
|
||||||
import { AdminSidebarMenu } from "./sidebar-menu";
|
import { AdminSidebarMenu } from "./sidebar-menu";
|
||||||
|
|
||||||
export const AdminSidebar: FC = observer(() => {
|
export const AdminSidebar = observer(() => {
|
||||||
// store
|
// store
|
||||||
const { isSidebarCollapsed, toggleSidebar } = useTheme();
|
const { isSidebarCollapsed, toggleSidebar } = useTheme();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import type { FC } from "react";
|
|
||||||
import { Info } from "lucide-react";
|
import { Info } from "lucide-react";
|
||||||
// plane constants
|
// plane constants
|
||||||
import type { TAdminAuthErrorInfo } from "@plane/constants";
|
import type { TAdminAuthErrorInfo } from "@plane/constants";
|
||||||
|
|
@ -10,7 +9,7 @@ type TAuthBanner = {
|
||||||
handleBannerData?: (bannerData: TAdminAuthErrorInfo | undefined) => void;
|
handleBannerData?: (bannerData: TAdminAuthErrorInfo | undefined) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AuthBanner: FC<TAuthBanner> = (props) => {
|
export const AuthBanner: React.FC<TAuthBanner> = (props) => {
|
||||||
const { bannerData, handleBannerData } = props;
|
const { bannerData, handleBannerData } = props;
|
||||||
|
|
||||||
if (!bannerData) return <></>;
|
if (!bannerData) return <></>;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { FC } from "react";
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { Eye, EyeOff } from "lucide-react";
|
import { Eye, EyeOff } from "lucide-react";
|
||||||
|
|
@ -46,7 +45,7 @@ const defaultFromData: TFormData = {
|
||||||
password: "",
|
password: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InstanceSignInForm: FC = () => {
|
export const InstanceSignInForm: React.FC = () => {
|
||||||
// search params
|
// search params
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const emailParam = searchParams.get("email") || undefined;
|
const emailParam = searchParams.get("email") || undefined;
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,9 @@
|
||||||
import type { FC, ReactNode } from "react";
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// hooks
|
// hooks
|
||||||
import { useInstance } from "@/hooks/store";
|
import { useInstance } from "@/hooks/store";
|
||||||
|
|
||||||
type InstanceProviderProps = {
|
export const InstanceProvider = observer<React.FC<React.PropsWithChildren>>((props) => {
|
||||||
children: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const InstanceProvider: FC<InstanceProviderProps> = observer((props) => {
|
|
||||||
const { children } = props;
|
const { children } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { fetchInstanceInfo } = useInstance();
|
const { fetchInstanceInfo } = useInstance();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { ReactNode } from "react";
|
|
||||||
import { createContext } from "react";
|
import { createContext } from "react";
|
||||||
// plane admin store
|
// plane admin store
|
||||||
import { RootStore } from "@/plane-admin/store/root.store";
|
import { RootStore } from "@/plane-admin/store/root.store";
|
||||||
|
|
@ -24,7 +23,7 @@ function initializeStore(initialData = {}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type StoreProviderProps = {
|
export type StoreProviderProps = {
|
||||||
children: ReactNode;
|
children: React.ReactNode;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
initialState?: any;
|
initialState?: any;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,12 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { FC, ReactNode } from "react";
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// hooks
|
// hooks
|
||||||
import { useInstance, useTheme, useUser } from "@/hooks/store";
|
import { useInstance, useTheme, useUser } from "@/hooks/store";
|
||||||
|
|
||||||
interface IUserProvider {
|
export const UserProvider = observer<React.FC<React.PropsWithChildren>>(({ children }) => {
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UserProvider: FC<IUserProvider> = observer(({ children }) => {
|
|
||||||
// hooks
|
// hooks
|
||||||
const { isSidebarCollapsed, toggleSidebar } = useTheme();
|
const { isSidebarCollapsed, toggleSidebar } = useTheme();
|
||||||
const { currentUser, fetchCurrentUser } = useUser();
|
const { currentUser, fetchCurrentUser } = useUser();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { FC } from "react";
|
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "@plane/utils";
|
import { cn } from "@plane/utils";
|
||||||
|
|
||||||
|
|
@ -14,7 +13,7 @@ type Props = {
|
||||||
unavailable?: boolean;
|
unavailable?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AuthenticationMethodCard: FC<Props> = (props) => {
|
export const AuthenticationMethodCard: React.FC<Props> = (props) => {
|
||||||
const { name, description, icon, config, disabled = false, withBorder = true, unavailable = false } = props;
|
const { name, description, icon, config, disabled = false, withBorder = true, unavailable = false } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
// icons
|
// icons
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
// icons
|
// icons
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { Button } from "@plane/propel/button";
|
||||||
type Props = {
|
type Props = {
|
||||||
title: string;
|
title: string;
|
||||||
description?: React.ReactNode;
|
description?: React.ReactNode;
|
||||||
image?: any;
|
image?: string;
|
||||||
primaryButton?: {
|
primaryButton?: {
|
||||||
icon?: any;
|
icon?: any;
|
||||||
text: string;
|
text: string;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { FC } from "react";
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
// icons
|
// icons
|
||||||
|
|
@ -54,7 +53,7 @@ const defaultFromData: TFormData = {
|
||||||
is_telemetry_enabled: true,
|
is_telemetry_enabled: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const InstanceSetupForm: FC = (props) => {
|
export const InstanceSetupForm: React.FC = (props) => {
|
||||||
const {} = props;
|
const {} = props;
|
||||||
// search params
|
// search params
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
|
||||||
1
apps/admin/core/utils/public-asset.ts
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
export {};
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
extends: ["@plane/eslint-config/next.js"],
|
extends: ["@plane/eslint-config/next.js"],
|
||||||
|
ignorePatterns: [
|
||||||
|
"build/**",
|
||||||
|
"dist/**",
|
||||||
|
".vite/**",
|
||||||
|
],
|
||||||
rules: {
|
rules: {
|
||||||
"no-duplicate-imports": "off",
|
"no-duplicate-imports": "off",
|
||||||
"import/no-duplicates": ["error", { "prefer-inline": false }],
|
"import/no-duplicates": ["error", {"prefer-inline": false}],
|
||||||
"import/consistent-type-specifier-style": ["error", "prefer-top-level"],
|
"import/consistent-type-specifier-style": ["error", "prefer-top-level"],
|
||||||
"@typescript-eslint/no-import-type-side-effects": "error",
|
"@typescript-eslint/no-import-type-side-effects": "error",
|
||||||
"@typescript-eslint/consistent-type-imports": [
|
"@typescript-eslint/consistent-type-imports": [
|
||||||
|
|
@ -16,3 +21,5 @@ module.exports = {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
3
apps/space/.gitignore
vendored
|
|
@ -12,6 +12,9 @@
|
||||||
/.next/
|
/.next/
|
||||||
/out/
|
/out/
|
||||||
|
|
||||||
|
# react-router
|
||||||
|
/.react-router/
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,103 +1,86 @@
|
||||||
# syntax=docker/dockerfile:1.7
|
|
||||||
FROM node:22-alpine AS base
|
FROM node:22-alpine AS base
|
||||||
|
|
||||||
# Setup pnpm package manager with corepack and configure global bin directory for caching
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV TURBO_TELEMETRY_DISABLED=1
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN corepack enable
|
ENV CI=1
|
||||||
|
|
||||||
|
RUN corepack enable pnpm
|
||||||
|
|
||||||
|
# =========================================================================== #
|
||||||
|
|
||||||
# *****************************************************************************
|
|
||||||
# STAGE 1: Build the project
|
|
||||||
# *****************************************************************************
|
|
||||||
FROM base AS builder
|
FROM base AS builder
|
||||||
RUN apk add --no-cache libc6-compat
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ARG TURBO_VERSION=2.5.6
|
RUN pnpm add -g turbo@2.5.8
|
||||||
RUN corepack enable pnpm && pnpm add -g turbo@${TURBO_VERSION}
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
# Create a pruned workspace for just the space app
|
||||||
RUN turbo prune --scope=space --docker
|
RUN turbo prune --scope=space --docker
|
||||||
|
|
||||||
# *****************************************************************************
|
# =========================================================================== #
|
||||||
# STAGE 2: Install dependencies & build the project
|
|
||||||
# *****************************************************************************
|
|
||||||
FROM base AS installer
|
FROM base AS installer
|
||||||
|
|
||||||
RUN apk add --no-cache libc6-compat
|
# Build in production mode; we still install dev deps explicitly below
|
||||||
WORKDIR /app
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
# Public envs required at build time (pick up via process.env)
|
||||||
|
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||||
|
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||||
|
ARG NEXT_PUBLIC_API_BASE_PATH="/api"
|
||||||
|
ENV NEXT_PUBLIC_API_BASE_PATH=$NEXT_PUBLIC_API_BASE_PATH
|
||||||
|
|
||||||
|
ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
|
||||||
|
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
|
||||||
|
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
|
||||||
|
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
|
||||||
|
|
||||||
|
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
|
||||||
|
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
|
||||||
|
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
|
||||||
|
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
|
||||||
|
|
||||||
|
ARG NEXT_PUBLIC_LIVE_BASE_URL=""
|
||||||
|
ENV NEXT_PUBLIC_LIVE_BASE_URL=$NEXT_PUBLIC_LIVE_BASE_URL
|
||||||
|
ARG NEXT_PUBLIC_LIVE_BASE_PATH="/live"
|
||||||
|
ENV NEXT_PUBLIC_LIVE_BASE_PATH=$NEXT_PUBLIC_LIVE_BASE_PATH
|
||||||
|
|
||||||
|
ARG NEXT_PUBLIC_WEB_BASE_URL=""
|
||||||
|
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
|
||||||
|
ARG NEXT_PUBLIC_WEB_BASE_PATH=""
|
||||||
|
ENV NEXT_PUBLIC_WEB_BASE_PATH=$NEXT_PUBLIC_WEB_BASE_PATH
|
||||||
|
|
||||||
|
ARG NEXT_PUBLIC_WEBSITE_URL="https://plane.so"
|
||||||
|
ENV NEXT_PUBLIC_WEBSITE_URL=$NEXT_PUBLIC_WEBSITE_URL
|
||||||
|
ARG NEXT_PUBLIC_SUPPORT_EMAIL="support@plane.so"
|
||||||
|
ENV NEXT_PUBLIC_SUPPORT_EMAIL=$NEXT_PUBLIC_SUPPORT_EMAIL
|
||||||
|
|
||||||
COPY .gitignore .gitignore
|
COPY .gitignore .gitignore
|
||||||
COPY --from=builder /app/out/json/ .
|
COPY --from=builder /app/out/json/ .
|
||||||
COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
|
COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
|
||||||
RUN corepack enable pnpm
|
|
||||||
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm fetch --store-dir=/pnpm/store
|
|
||||||
|
|
||||||
|
# Fetch dependencies to cache store, then install offline with dev deps
|
||||||
|
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm fetch --store-dir=/pnpm/store
|
||||||
COPY --from=builder /app/out/full/ .
|
COPY --from=builder /app/out/full/ .
|
||||||
COPY turbo.json turbo.json
|
COPY turbo.json turbo.json
|
||||||
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store
|
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store --prod=false
|
||||||
|
|
||||||
ARG NEXT_PUBLIC_API_BASE_URL=""
|
|
||||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
|
||||||
|
|
||||||
ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
|
|
||||||
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
|
|
||||||
|
|
||||||
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
|
|
||||||
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
|
|
||||||
|
|
||||||
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
|
|
||||||
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
|
|
||||||
|
|
||||||
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
|
|
||||||
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
|
|
||||||
|
|
||||||
ARG NEXT_PUBLIC_WEB_BASE_URL=""
|
|
||||||
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
|
|
||||||
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
|
||||||
ENV TURBO_TELEMETRY_DISABLED=1
|
|
||||||
|
|
||||||
|
# Build only the space package
|
||||||
RUN pnpm turbo run build --filter=space
|
RUN pnpm turbo run build --filter=space
|
||||||
|
|
||||||
# *****************************************************************************
|
# =========================================================================== #
|
||||||
# STAGE 3: Copy the project and start it
|
|
||||||
# *****************************************************************************
|
|
||||||
FROM base AS runner
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Don't run production as root
|
FROM nginx:1.27-alpine AS production
|
||||||
RUN addgroup --system --gid 1001 nodejs
|
|
||||||
RUN adduser --system --uid 1001 nextjs
|
|
||||||
USER nextjs
|
|
||||||
|
|
||||||
# Automatically leverage output traces to reduce image size
|
COPY apps/space/nginx/nginx.conf /etc/nginx/nginx.conf
|
||||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
COPY --from=installer /app/apps/space/build/client /usr/share/nginx/html/spaces
|
||||||
COPY --from=installer /app/apps/space/.next/standalone ./
|
|
||||||
COPY --from=installer /app/apps/space/.next/static ./apps/space/.next/static
|
|
||||||
COPY --from=installer /app/apps/space/public ./apps/space/public
|
|
||||||
|
|
||||||
ARG NEXT_PUBLIC_API_BASE_URL=""
|
|
||||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
|
||||||
|
|
||||||
ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
|
|
||||||
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
|
|
||||||
|
|
||||||
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
|
|
||||||
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
|
|
||||||
|
|
||||||
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
|
|
||||||
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
|
|
||||||
|
|
||||||
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
|
|
||||||
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
|
|
||||||
|
|
||||||
ARG NEXT_PUBLIC_WEB_BASE_URL=""
|
|
||||||
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
|
|
||||||
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
|
||||||
ENV TURBO_TELEMETRY_DISABLED=1
|
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
CMD ["node", "apps/space/server.js"]
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD curl -fsS http://127.0.0.1:3000/ >/dev/null || exit 1
|
||||||
|
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
|
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
import { notFound, redirect } from "next/navigation";
|
|
||||||
// plane imports
|
|
||||||
import { SitesProjectPublishService } from "@plane/services";
|
|
||||||
import type { TProjectPublishSettings } from "@plane/types";
|
|
||||||
|
|
||||||
const publishService = new SitesProjectPublishService();
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
params: {
|
|
||||||
workspaceSlug: string;
|
|
||||||
projectId: string;
|
|
||||||
};
|
|
||||||
searchParams: Record<"board" | "peekId", string | string[] | undefined>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function IssuesPage(props: Props) {
|
|
||||||
const { params, searchParams } = props;
|
|
||||||
// query params
|
|
||||||
const { workspaceSlug, projectId } = params;
|
|
||||||
const { board, peekId } = searchParams;
|
|
||||||
|
|
||||||
let response: TProjectPublishSettings | undefined = undefined;
|
|
||||||
try {
|
|
||||||
response = await publishService.retrieveSettingsByProjectId(workspaceSlug, projectId);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error fetching project publish settings:", error);
|
|
||||||
notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
let url = "";
|
|
||||||
if (response?.entity_name === "project") {
|
|
||||||
url = `/issues/${response?.anchor}`;
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
if (board) params.append("board", String(board));
|
|
||||||
if (peekId) params.append("peekId", String(peekId));
|
|
||||||
if (params.toString()) url += `?${params.toString()}`;
|
|
||||||
redirect(url);
|
|
||||||
} else {
|
|
||||||
notFound();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
53
apps/space/app/[workspaceSlug]/[projectId]/page.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { redirect } from "react-router";
|
||||||
|
import type { ClientLoaderFunctionArgs } from "react-router";
|
||||||
|
// plane imports
|
||||||
|
import { SitesProjectPublishService } from "@plane/services";
|
||||||
|
import type { TProjectPublishSettings } from "@plane/types";
|
||||||
|
// components
|
||||||
|
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||||
|
|
||||||
|
const publishService = new SitesProjectPublishService();
|
||||||
|
|
||||||
|
export const clientLoader = async ({ params, request }: ClientLoaderFunctionArgs) => {
|
||||||
|
const { workspaceSlug, projectId } = params;
|
||||||
|
|
||||||
|
// Validate required params
|
||||||
|
if (!workspaceSlug || !projectId) {
|
||||||
|
throw redirect("/404");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract query params from the request URL
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const board = url.searchParams.get("board");
|
||||||
|
const peekId = url.searchParams.get("peekId");
|
||||||
|
|
||||||
|
let response: TProjectPublishSettings | undefined = undefined;
|
||||||
|
|
||||||
|
try {
|
||||||
|
response = await publishService.retrieveSettingsByProjectId(workspaceSlug, projectId);
|
||||||
|
} catch {
|
||||||
|
throw redirect("/404");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response?.entity_name === "project") {
|
||||||
|
let redirectUrl = `/issues/${response?.anchor}`;
|
||||||
|
const urlParams = new URLSearchParams();
|
||||||
|
if (board) urlParams.append("board", String(board));
|
||||||
|
if (peekId) urlParams.append("peekId", String(peekId));
|
||||||
|
if (urlParams.toString()) redirectUrl += `?${urlParams.toString()}`;
|
||||||
|
|
||||||
|
throw redirect(redirectUrl);
|
||||||
|
} else {
|
||||||
|
throw redirect("/404");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function IssuesPage() {
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen min-h-[500px] w-full justify-center items-center">
|
||||||
|
<LogoSpinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 6 KiB After Width: | Height: | Size: 6 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 466 B After Width: | Height: | Size: 466 B |
|
Before Width: | Height: | Size: 761 B After Width: | Height: | Size: 761 B |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 954 KiB After Width: | Height: | Size: 954 KiB |
|
Before Width: | Height: | Size: 418 KiB After Width: | Height: | Size: 418 KiB |
|
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 4 KiB |
|
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 4 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 190 KiB After Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 438 B After Width: | Height: | Size: 438 B |
|
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 1 KiB |
|
Before Width: | Height: | Size: 742 B After Width: | Height: | Size: 742 B |
|
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
33
apps/space/app/compat/next/helper.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
/**
|
||||||
|
* Ensures that a URL has a trailing slash while preserving query parameters and fragments
|
||||||
|
* @param url - The URL to process
|
||||||
|
* @returns The URL with a trailing slash added to the pathname (if not already present)
|
||||||
|
*/
|
||||||
|
export function ensureTrailingSlash(url: string): string {
|
||||||
|
try {
|
||||||
|
// Handle relative URLs by creating a URL object with a dummy base
|
||||||
|
const urlObj = new URL(url, "http://dummy.com");
|
||||||
|
|
||||||
|
// Don't modify root path
|
||||||
|
if (urlObj.pathname === "/") {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add trailing slash if it doesn't exist
|
||||||
|
if (!urlObj.pathname.endsWith("/")) {
|
||||||
|
urlObj.pathname += "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
// For relative URLs, return just the path + search + hash
|
||||||
|
if (url.startsWith("/")) {
|
||||||
|
return urlObj.pathname + urlObj.search + urlObj.hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For absolute URLs, return the full URL
|
||||||
|
return urlObj.toString();
|
||||||
|
} catch (error) {
|
||||||
|
// If URL parsing fails, return the original URL
|
||||||
|
console.warn("Failed to parse URL for trailing slash enforcement:", url, error);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
apps/space/app/compat/next/image.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// Minimal shim so code using next/image compiles under React Router + Vite
|
||||||
|
// without changing call sites. It just renders a native img.
|
||||||
|
|
||||||
|
type NextImageProps = React.ImgHTMLAttributes<HTMLImageElement> & {
|
||||||
|
src: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Image: React.FC<NextImageProps> = ({ src, alt = "", ...rest }) => <img src={src} alt={alt} {...rest} />;
|
||||||
|
|
||||||
|
export default Image;
|
||||||
24
apps/space/app/compat/next/link.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { Link as RRLink } from "react-router";
|
||||||
|
import { ensureTrailingSlash } from "./helper";
|
||||||
|
|
||||||
|
type NextLinkProps = React.ComponentProps<"a"> & {
|
||||||
|
href: string;
|
||||||
|
replace?: boolean;
|
||||||
|
prefetch?: boolean; // next.js prop, ignored
|
||||||
|
scroll?: boolean; // next.js prop, ignored
|
||||||
|
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} />;
|
||||||
|
|
||||||
|
export default Link;
|
||||||
38
apps/space/app/compat/next/navigation.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { useLocation, useNavigate, useParams as useParamsRR, useSearchParams as useSearchParamsRR } from "react-router";
|
||||||
|
import { ensureTrailingSlash } from "./helper";
|
||||||
|
|
||||||
|
export function useRouter() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
push: (to: string) => navigate(ensureTrailingSlash(to)),
|
||||||
|
replace: (to: string) => navigate(ensureTrailingSlash(to), { replace: true }),
|
||||||
|
back: () => navigate(-1),
|
||||||
|
forward: () => navigate(1),
|
||||||
|
refresh: () => {
|
||||||
|
location.reload();
|
||||||
|
},
|
||||||
|
prefetch: async (_to: string) => {
|
||||||
|
// no-op in this shim
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[navigate]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePathname(): string {
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
return pathname;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSearchParams(): URLSearchParams {
|
||||||
|
const [searchParams] = useSearchParamsRR();
|
||||||
|
return searchParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useParams() {
|
||||||
|
return useParamsRR();
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
import { Outlet } from "react-router";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// components
|
// components
|
||||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||||
|
|
@ -10,14 +11,10 @@ import { IssuesNavbarRoot } from "@/components/issues/navbar";
|
||||||
// hooks
|
// hooks
|
||||||
import { usePublish, usePublishList } from "@/hooks/store/publish";
|
import { usePublish, usePublishList } from "@/hooks/store/publish";
|
||||||
import { useIssueFilter } from "@/hooks/store/use-issue-filter";
|
import { useIssueFilter } from "@/hooks/store/use-issue-filter";
|
||||||
|
import type { Route } from "./+types/client-layout";
|
||||||
|
|
||||||
type Props = {
|
const IssuesClientLayout = observer((props: Route.ComponentProps) => {
|
||||||
children: React.ReactNode;
|
const { anchor } = props.params;
|
||||||
anchor: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const IssuesClientLayout = observer((props: Props) => {
|
|
||||||
const { children, anchor } = props;
|
|
||||||
// store hooks
|
// store hooks
|
||||||
const { fetchPublishSettings } = usePublishList();
|
const { fetchPublishSettings } = usePublishList();
|
||||||
const publishSettings = usePublish(anchor);
|
const publishSettings = usePublish(anchor);
|
||||||
|
|
@ -57,9 +54,13 @@ export const IssuesClientLayout = observer((props: Props) => {
|
||||||
<div className="relative flex h-[60px] flex-shrink-0 select-none items-center border-b border-custom-border-300 bg-custom-sidebar-background-100">
|
<div className="relative flex h-[60px] flex-shrink-0 select-none items-center border-b border-custom-border-300 bg-custom-sidebar-background-100">
|
||||||
<IssuesNavbarRoot publishSettings={publishSettings} />
|
<IssuesNavbarRoot publishSettings={publishSettings} />
|
||||||
</div>
|
</div>
|
||||||
<div className="relative h-full w-full overflow-hidden bg-custom-background-90">{children}</div>
|
<div className="relative h-full w-full overflow-hidden bg-custom-background-90">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<PoweredBy />
|
<PoweredBy />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export default IssuesClientLayout;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import { IssuesClientLayout } from "./client-layout";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
params: {
|
params: {
|
||||||
|
|
@ -9,6 +7,7 @@ type Props = {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: Convert into SSR in order to generate metadata
|
||||||
export async function generateMetadata({ params }: Props) {
|
export async function generateMetadata({ params }: Props) {
|
||||||
const { anchor } = params;
|
const { anchor } = params;
|
||||||
const DEFAULT_TITLE = "Plane";
|
const DEFAULT_TITLE = "Plane";
|
||||||
|
|
@ -49,9 +48,7 @@ export async function generateMetadata({ params }: Props) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function IssuesLayout(props: Props) {
|
export default async function IssuesLayout(_props: Props) {
|
||||||
const { children, params } = props;
|
// return <IssuesClientLayout params={{ anchor }}>{children}</IssuesClientLayout>;
|
||||||
const { anchor } = params;
|
return null;
|
||||||
|
|
||||||
return <IssuesClientLayout anchor={anchor}>{children}</IssuesClientLayout>;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useParams, useSearchParams } from "next/navigation";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// components
|
// components
|
||||||
import { IssuesLayoutsRoot } from "@/components/issues/issue-layouts";
|
import { IssuesLayoutsRoot } from "@/components/issues/issue-layouts";
|
||||||
|
|
@ -10,16 +10,10 @@ import { usePublish } from "@/hooks/store/publish";
|
||||||
import { useLabel } from "@/hooks/store/use-label";
|
import { useLabel } from "@/hooks/store/use-label";
|
||||||
import { useStates } from "@/hooks/store/use-state";
|
import { useStates } from "@/hooks/store/use-state";
|
||||||
|
|
||||||
type Props = {
|
const IssuesPage = observer(() => {
|
||||||
params: {
|
|
||||||
anchor: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const IssuesPage = observer((props: Props) => {
|
|
||||||
const { params } = props;
|
|
||||||
const { anchor } = params;
|
|
||||||
// params
|
// params
|
||||||
|
const params = useParams<{ anchor: string }>();
|
||||||
|
const { anchor } = params;
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const peekId = searchParams.get("peekId") || undefined;
|
const peekId = searchParams.get("peekId") || undefined;
|
||||||
// store
|
// store
|
||||||
|
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
import type { Metadata } from "next";
|
|
||||||
// helpers
|
|
||||||
import { SPACE_BASE_PATH } from "@plane/constants";
|
|
||||||
// styles
|
|
||||||
import "@/styles/globals.css";
|
|
||||||
// components
|
|
||||||
import { AppProvider } from "./provider";
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: "Plane Publish | Make your Plane boards public with one-click",
|
|
||||||
description: "Plane Publish is a customer feedback management tool built on top of plane.so",
|
|
||||||
openGraph: {
|
|
||||||
title: "Plane Publish | Make your Plane boards public with one-click",
|
|
||||||
description: "Plane Publish is a customer feedback management tool built on top of plane.so",
|
|
||||||
url: "https://sites.plane.so/",
|
|
||||||
},
|
|
||||||
keywords:
|
|
||||||
"software development, customer feedback, software, accelerate, code management, release management, project management, work item tracking, agile, scrum, kanban, collaboration",
|
|
||||||
twitter: {
|
|
||||||
site: "@planepowers",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href={`${SPACE_BASE_PATH}/favicon/apple-touch-icon.png`} />
|
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href={`${SPACE_BASE_PATH}/favicon/favicon-32x32.png`} />
|
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href={`${SPACE_BASE_PATH}/favicon/favicon-16x16.png`} />
|
|
||||||
<link rel="manifest" href={`${SPACE_BASE_PATH}/site.webmanifest.json`} />
|
|
||||||
<link rel="shortcut icon" href={`${SPACE_BASE_PATH}/favicon/favicon.ico`} />
|
|
||||||
<meta name="robots" content="noindex, nofollow" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="editor-portal" />
|
|
||||||
<AppProvider>
|
|
||||||
<>{children}</>
|
|
||||||
</AppProvider>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
// assets
|
// assets
|
||||||
import SomethingWentWrongImage from "public/something-went-wrong.svg";
|
import SomethingWentWrongImage from "@/app/assets/something-went-wrong.svg?url";
|
||||||
|
|
||||||
const NotFound = () => (
|
const NotFound = () => (
|
||||||
<div className="h-screen w-screen grid place-items-center">
|
<div className="h-screen w-screen grid place-items-center">
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,18 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { ReactNode, FC } from "react";
|
|
||||||
import { ThemeProvider } from "next-themes";
|
import { ThemeProvider } from "next-themes";
|
||||||
// components
|
// components
|
||||||
import { TranslationProvider } from "@plane/i18n";
|
import { TranslationProvider } from "@plane/i18n";
|
||||||
|
import { AppProgressBar } from "@/lib/b-progress";
|
||||||
import { InstanceProvider } from "@/lib/instance-provider";
|
import { InstanceProvider } from "@/lib/instance-provider";
|
||||||
import { StoreProvider } from "@/lib/store-provider";
|
import { StoreProvider } from "@/lib/store-provider";
|
||||||
import { ToastProvider } from "@/lib/toast-provider";
|
import { ToastProvider } from "@/lib/toast-provider";
|
||||||
|
|
||||||
interface IAppProvider {
|
export function AppProviders({ children }: { children: React.ReactNode }) {
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AppProvider: FC<IAppProvider> = (props) => {
|
|
||||||
const { children } = props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem>
|
<ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem>
|
||||||
<StoreProvider>
|
<StoreProvider>
|
||||||
|
<AppProgressBar />
|
||||||
<TranslationProvider>
|
<TranslationProvider>
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
<InstanceProvider>{children}</InstanceProvider>
|
<InstanceProvider>{children}</InstanceProvider>
|
||||||
|
|
@ -26,4 +21,4 @@ export const AppProvider: FC<IAppProvider> = (props) => {
|
||||||
</StoreProvider>
|
</StoreProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
66
apps/space/app/root.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { Links, Meta, Outlet, Scripts } from "react-router";
|
||||||
|
import type { LinksFunction } from "react-router";
|
||||||
|
// styles
|
||||||
|
import "@/styles/globals.css";
|
||||||
|
// assets
|
||||||
|
import appleTouchIcon from "@/app/assets/favicon/apple-touch-icon.png?url";
|
||||||
|
import favicon16 from "@/app/assets/favicon/favicon-16x16.png?url";
|
||||||
|
import favicon32 from "@/app/assets/favicon/favicon-32x32.png?url";
|
||||||
|
import faviconIco from "@/app/assets/favicon/favicon.ico?url";
|
||||||
|
// types
|
||||||
|
import type { Route } from "./+types/root";
|
||||||
|
// local imports
|
||||||
|
import ErrorPage from "./error";
|
||||||
|
import { AppProviders } from "./providers";
|
||||||
|
|
||||||
|
const APP_TITLE = "Plane Publish | Make your Plane boards public with one-click";
|
||||||
|
const APP_DESCRIPTION = "Plane Publish is a customer feedback management tool built on top of plane.so";
|
||||||
|
|
||||||
|
export const links: LinksFunction = () => [
|
||||||
|
{ rel: "apple-touch-icon", sizes: "180x180", href: appleTouchIcon },
|
||||||
|
{ rel: "icon", type: "image/png", sizes: "32x32", href: favicon32 },
|
||||||
|
{ rel: "icon", type: "image/png", sizes: "16x16", href: favicon16 },
|
||||||
|
{ rel: "shortcut icon", href: faviconIco },
|
||||||
|
{ rel: "manifest", href: `/site.webmanifest.json` },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charSet="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
|
<Meta />
|
||||||
|
<Links />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="editor-portal" />
|
||||||
|
<AppProviders>{children}</AppProviders>
|
||||||
|
<Scripts />
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const meta: Route.MetaFunction = () => [
|
||||||
|
{ title: APP_TITLE },
|
||||||
|
{ name: "description", content: APP_DESCRIPTION },
|
||||||
|
{ property: "og:title", content: APP_TITLE },
|
||||||
|
{ property: "og:description", content: APP_DESCRIPTION },
|
||||||
|
{ property: "og:url", content: "https://sites.plane.so/" },
|
||||||
|
{
|
||||||
|
name: "keywords",
|
||||||
|
content:
|
||||||
|
"software development, customer feedback, software, accelerate, code management, release management, project management, work item tracking, agile, scrum, kanban, collaboration",
|
||||||
|
},
|
||||||
|
{ name: "twitter:site", content: "@planepowers" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Root() {
|
||||||
|
return <Outlet />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ErrorBoundary() {
|
||||||
|
return <ErrorPage />;
|
||||||
|
}
|
||||||
10
apps/space/app/routes.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import type { RouteConfig } from "@react-router/dev/routes";
|
||||||
|
import { index, layout, route } from "@react-router/dev/routes";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
index("./page.tsx"),
|
||||||
|
route(":workspaceSlug/:projectId", "./[workspaceSlug]/[projectId]/page.tsx"),
|
||||||
|
layout("./issues/[anchor]/client-layout.tsx", [route("issues/:anchor", "./issues/[anchor]/page.tsx")]),
|
||||||
|
// Catch-all route for 404 handling
|
||||||
|
route("*", "./not-found.tsx"),
|
||||||
|
] satisfies RouteConfig;
|
||||||
10
apps/space/app/types/next-image.d.ts
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
declare module "next/image" {
|
||||||
|
import type { FC, ImgHTMLAttributes } from "react";
|
||||||
|
|
||||||
|
type NextImageProps = ImgHTMLAttributes<HTMLImageElement> & {
|
||||||
|
src: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Image: FC<NextImageProps>;
|
||||||
|
export default Image;
|
||||||
|
}
|
||||||
14
apps/space/app/types/next-link.d.ts
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
declare module "next/link" {
|
||||||
|
import type { FC, ComponentProps } from "react";
|
||||||
|
|
||||||
|
type NextLinkProps = ComponentProps<"a"> & {
|
||||||
|
href: string;
|
||||||
|
replace?: boolean;
|
||||||
|
prefetch?: boolean;
|
||||||
|
scroll?: boolean;
|
||||||
|
shallow?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Link: FC<NextLinkProps>;
|
||||||
|
export default Link;
|
||||||
|
}
|
||||||
14
apps/space/app/types/next-navigation.d.ts
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
declare module "next/navigation" {
|
||||||
|
export function useRouter(): {
|
||||||
|
push: (url: string) => void;
|
||||||
|
replace: (url: string) => void;
|
||||||
|
back: () => void;
|
||||||
|
forward: () => void;
|
||||||
|
refresh: () => void;
|
||||||
|
prefetch: (url: string) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function usePathname(): string;
|
||||||
|
export function useSearchParams(): URLSearchParams;
|
||||||
|
export function useParams<T = Record<string, string>>(): T;
|
||||||
|
}
|
||||||
5
apps/space/app/types/react-router-virtual.d.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
declare module "virtual:react-router/server-build" {
|
||||||
|
import type { ServerBuild } from "react-router";
|
||||||
|
const serverBuild: ServerBuild;
|
||||||
|
export default serverBuild;
|
||||||
|
}
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import useSWR from "swr";
|
|
||||||
// components
|
|
||||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
|
||||||
import { PoweredBy } from "@/components/common/powered-by";
|
|
||||||
import { SomethingWentWrongError } from "@/components/issues/issue-layouts/error";
|
|
||||||
// hooks
|
|
||||||
import { usePublish, usePublishList } from "@/hooks/store/publish";
|
|
||||||
// Plane web
|
|
||||||
import { ViewNavbarRoot } from "@/plane-web/components/navbar";
|
|
||||||
import { useView } from "@/plane-web/hooks/store";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
children: React.ReactNode;
|
|
||||||
params: {
|
|
||||||
anchor: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const ViewsLayout = observer((props: Props) => {
|
|
||||||
const { children, params } = props;
|
|
||||||
// params
|
|
||||||
const { anchor } = params;
|
|
||||||
// store hooks
|
|
||||||
const { fetchPublishSettings } = usePublishList();
|
|
||||||
const { viewData, fetchViewDetails } = useView();
|
|
||||||
const publishSettings = usePublish(anchor);
|
|
||||||
|
|
||||||
// fetch publish settings && view details
|
|
||||||
const { error } = useSWR(
|
|
||||||
anchor ? `PUBLISHED_VIEW_SETTINGS_${anchor}` : null,
|
|
||||||
anchor
|
|
||||||
? async () => {
|
|
||||||
const promises = [];
|
|
||||||
promises.push(fetchPublishSettings(anchor));
|
|
||||||
promises.push(fetchViewDetails(anchor));
|
|
||||||
await Promise.all(promises);
|
|
||||||
}
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
if (error) return <SomethingWentWrongError />;
|
|
||||||
|
|
||||||
if (!publishSettings || !viewData) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center h-screen w-full">
|
|
||||||
<LogoSpinner />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative flex h-screen min-h-[500px] w-screen flex-col overflow-hidden">
|
|
||||||
<div className="relative flex h-[60px] flex-shrink-0 select-none items-center border-b border-custom-border-300 bg-custom-sidebar-background-100">
|
|
||||||
<ViewNavbarRoot publishSettings={publishSettings} />
|
|
||||||
</div>
|
|
||||||
<div className="relative h-full w-full overflow-hidden bg-custom-background-90">{children}</div>
|
|
||||||
<PoweredBy />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default ViewsLayout;
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
|
||||||
import { useSearchParams } from "next/navigation";
|
|
||||||
// components
|
|
||||||
import { PoweredBy } from "@/components/common/powered-by";
|
|
||||||
// hooks
|
|
||||||
import { usePublish } from "@/hooks/store/publish";
|
|
||||||
// plane-web
|
|
||||||
import { ViewLayoutsRoot } from "@/plane-web/components/issue-layouts/root";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
params: {
|
|
||||||
anchor: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const ViewsPage = observer((props: Props) => {
|
|
||||||
const { params } = props;
|
|
||||||
const { anchor } = params;
|
|
||||||
// params
|
|
||||||
const searchParams = useSearchParams();
|
|
||||||
const peekId = searchParams.get("peekId") || undefined;
|
|
||||||
|
|
||||||
const publishSettings = usePublish(anchor);
|
|
||||||
|
|
||||||
if (!publishSettings) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewLayoutsRoot peekId={peekId} publishSettings={publishSettings} />
|
|
||||||
<PoweredBy />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
export default ViewsPage;
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { FC } from "react";
|
|
||||||
import { Info } from "lucide-react";
|
import { Info } from "lucide-react";
|
||||||
import { CloseIcon } from "@plane/propel/icons";
|
import { CloseIcon } from "@plane/propel/icons";
|
||||||
// helpers
|
// helpers
|
||||||
|
|
@ -11,7 +10,7 @@ type TAuthBanner = {
|
||||||
handleBannerData?: (bannerData: TAuthErrorInfo | undefined) => void;
|
handleBannerData?: (bannerData: TAuthErrorInfo | undefined) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AuthBanner: FC<TAuthBanner> = (props) => {
|
export const AuthBanner: React.FC<TAuthBanner> = (props) => {
|
||||||
const { bannerData, handleBannerData } = props;
|
const { bannerData, handleBannerData } = props;
|
||||||
|
|
||||||
if (!bannerData) return <></>;
|
if (!bannerData) return <></>;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { FC } from "react";
|
|
||||||
// helpers
|
// helpers
|
||||||
import { EAuthModes } from "@/types/auth";
|
import { EAuthModes } from "@/types/auth";
|
||||||
|
|
||||||
|
|
@ -28,7 +27,7 @@ const Titles: TAuthHeaderDetails = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AuthHeader: FC<TAuthHeader> = (props) => {
|
export const AuthHeader: React.FC<TAuthHeader> = (props) => {
|
||||||
const { authMode } = props;
|
const { authMode } = props;
|
||||||
|
|
||||||
const getHeaderSubHeader = (mode: EAuthModes | null): TAuthHeaderContent => {
|
const getHeaderSubHeader = (mode: EAuthModes | null): TAuthHeaderContent => {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { FC } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
|
|
@ -11,7 +10,12 @@ import { API_BASE_URL } from "@plane/constants";
|
||||||
import { SitesAuthService } from "@plane/services";
|
import { SitesAuthService } from "@plane/services";
|
||||||
import type { IEmailCheckData } from "@plane/types";
|
import type { IEmailCheckData } from "@plane/types";
|
||||||
import { OAuthOptions } from "@plane/ui";
|
import { OAuthOptions } from "@plane/ui";
|
||||||
// components
|
// assets
|
||||||
|
import GiteaLogo from "@/app/assets/logos/gitea-logo.svg?url";
|
||||||
|
import GithubLightLogo from "@/app/assets/logos/github-black.png?url";
|
||||||
|
import GithubDarkLogo from "@/app/assets/logos/github-dark.svg?url";
|
||||||
|
import GitlabLogo from "@/app/assets/logos/gitlab-logo.svg?url";
|
||||||
|
import GoogleLogo from "@/app/assets/logos/google-logo.svg?url";
|
||||||
// helpers
|
// helpers
|
||||||
import type { TAuthErrorInfo } from "@/helpers/authentication.helper";
|
import type { TAuthErrorInfo } from "@/helpers/authentication.helper";
|
||||||
import { EErrorAlertType, authErrorHandler, EAuthenticationErrorCodes } from "@/helpers/authentication.helper";
|
import { EErrorAlertType, authErrorHandler, EAuthenticationErrorCodes } from "@/helpers/authentication.helper";
|
||||||
|
|
@ -19,12 +23,6 @@ import { EErrorAlertType, authErrorHandler, EAuthenticationErrorCodes } from "@/
|
||||||
import { useInstance } from "@/hooks/store/use-instance";
|
import { useInstance } from "@/hooks/store/use-instance";
|
||||||
// types
|
// types
|
||||||
import { EAuthModes, EAuthSteps } from "@/types/auth";
|
import { EAuthModes, EAuthSteps } from "@/types/auth";
|
||||||
// assets
|
|
||||||
import GithubLightLogo from "/public/logos/github-black.png";
|
|
||||||
import GithubDarkLogo from "/public/logos/github-dark.svg";
|
|
||||||
import GitlabLogo from "/public/logos/gitlab-logo.svg";
|
|
||||||
import GoogleLogo from "/public/logos/google-logo.svg";
|
|
||||||
import GiteaLogo from "/public/logos/gitea-logo.svg";
|
|
||||||
// local imports
|
// local imports
|
||||||
import { TermsAndConditions } from "../terms-and-conditions";
|
import { TermsAndConditions } from "../terms-and-conditions";
|
||||||
import { AuthBanner } from "./auth-banner";
|
import { AuthBanner } from "./auth-banner";
|
||||||
|
|
@ -35,7 +33,7 @@ import { AuthUniqueCodeForm } from "./unique-code";
|
||||||
|
|
||||||
const authService = new SitesAuthService();
|
const authService = new SitesAuthService();
|
||||||
|
|
||||||
export const AuthRoot: FC = observer(() => {
|
export const AuthRoot: React.FC = observer(() => {
|
||||||
// router params
|
// router params
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const emailParam = searchParams.get("email") || undefined;
|
const emailParam = searchParams.get("email") || undefined;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { FC, FormEvent } from "react";
|
import type { FormEvent } from "react";
|
||||||
import { useMemo, useRef, useState } from "react";
|
import { useMemo, useRef, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// icons
|
// icons
|
||||||
|
|
@ -19,7 +19,7 @@ type TAuthEmailForm = {
|
||||||
onSubmit: (data: IEmailCheckData) => Promise<void>;
|
onSubmit: (data: IEmailCheckData) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AuthEmailForm: FC<TAuthEmailForm> = observer((props) => {
|
export const AuthEmailForm: React.FC<TAuthEmailForm> = observer((props) => {
|
||||||
const { onSubmit, defaultEmail } = props;
|
const { onSubmit, defaultEmail } = props;
|
||||||
// states
|
// states
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { FC } from "react";
|
|
||||||
import React from "react";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isSignUp?: boolean;
|
isSignUp?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TermsAndConditions: FC<Props> = (props) => {
|
export const TermsAndConditions: React.FC<Props> = (props) => {
|
||||||
const { isSignUp = false } = props;
|
const { isSignUp = false } = props;
|
||||||
return (
|
return (
|
||||||
<span className="flex items-center justify-center py-6">
|
<span className="flex items-center justify-center py-6">
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,13 @@
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { PlaneLockup } from "@plane/propel/icons";
|
import { PlaneLockup } from "@plane/propel/icons";
|
||||||
|
// assets
|
||||||
|
import UserLoggedInImage from "@/app/assets/user-logged-in.svg?url";
|
||||||
// components
|
// components
|
||||||
import { PoweredBy } from "@/components/common/powered-by";
|
import { PoweredBy } from "@/components/common/powered-by";
|
||||||
import { UserAvatar } from "@/components/issues/navbar/user-avatar";
|
import { UserAvatar } from "@/components/issues/navbar/user-avatar";
|
||||||
// hooks
|
// hooks
|
||||||
import { useUser } from "@/hooks/store/use-user";
|
import { useUser } from "@/hooks/store/use-user";
|
||||||
// assets
|
|
||||||
import UserLoggedInImage from "@/public/user-logged-in.svg";
|
|
||||||
|
|
||||||
export const UserLoggedIn = observer(() => {
|
export const UserLoggedIn = observer(() => {
|
||||||
// store hooks
|
// store hooks
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
// assets
|
// assets
|
||||||
import LogoSpinnerDark from "@/public/images/logo-spinner-dark.gif";
|
import LogoSpinnerDark from "@/app/assets/images/logo-spinner-dark.gif?url";
|
||||||
import LogoSpinnerLight from "@/public/images/logo-spinner-light.gif";
|
import LogoSpinnerLight from "@/app/assets/images/logo-spinner-light.gif?url";
|
||||||
|
|
||||||
export const LogoSpinner = () => {
|
export const LogoSpinner = () => {
|
||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { FC } from "react";
|
|
||||||
import { WEBSITE_URL } from "@plane/constants";
|
import { WEBSITE_URL } from "@plane/constants";
|
||||||
// assets
|
// assets
|
||||||
import { PlaneLogo } from "@plane/propel/icons";
|
import { PlaneLogo } from "@plane/propel/icons";
|
||||||
|
|
@ -9,7 +8,7 @@ type TPoweredBy = {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PoweredBy: FC<TPoweredBy> = (props) => {
|
export const PoweredBy: React.FC<TPoweredBy> = (props) => {
|
||||||
// props
|
// props
|
||||||
const { disabled = false } = props;
|
const { disabled = false } = props;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,13 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { FC } from "react";
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { Button } from "@plane/propel/button";
|
import { Button } from "@plane/propel/button";
|
||||||
// assets
|
// assets
|
||||||
import InstanceFailureDarkImage from "public/instance/instance-failure-dark.svg";
|
import InstanceFailureDarkImage from "@/app/assets/instance/instance-failure-dark.svg?url";
|
||||||
import InstanceFailureImage from "public/instance/instance-failure.svg";
|
import InstanceFailureImage from "@/app/assets/instance/instance-failure.svg?url";
|
||||||
|
|
||||||
export const InstanceFailureView: FC = () => {
|
export const InstanceFailureView: React.FC = () => {
|
||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
|
|
||||||
const instanceImage = resolvedTheme === "dark" ? InstanceFailureDarkImage : InstanceFailureImage;
|
const instanceImage = resolvedTheme === "dark" ? InstanceFailureDarkImage : InstanceFailureImage;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { FC } from "react";
|
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { cloneDeep } from "lodash-es";
|
import { cloneDeep } from "lodash-es";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
|
@ -16,7 +15,7 @@ type TIssueAppliedFilters = {
|
||||||
anchor: string;
|
anchor: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueAppliedFilters: FC<TIssueAppliedFilters> = observer((props) => {
|
export const IssueAppliedFilters: React.FC<TIssueAppliedFilters> = observer((props) => {
|
||||||
const { anchor } = props;
|
const { anchor } = props;
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { FC } from "react";
|
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { cloneDeep } from "lodash-es";
|
import { cloneDeep } from "lodash-es";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
|
@ -21,7 +20,7 @@ type IssueFiltersDropdownProps = {
|
||||||
anchor: string;
|
anchor: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueFiltersDropdown: FC<IssueFiltersDropdownProps> = observer((props) => {
|
export const IssueFiltersDropdown: React.FC<IssueFiltersDropdownProps> = observer((props) => {
|
||||||
const { anchor } = props;
|
const { anchor } = props;
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
// assets
|
// assets
|
||||||
import SomethingWentWrongImage from "public/something-went-wrong.svg";
|
import SomethingWentWrongImage from "@/app/assets/something-went-wrong.svg?url";
|
||||||
|
|
||||||
export const SomethingWentWrongError = () => (
|
export const SomethingWentWrongError = () => (
|
||||||
<div className="grid min-h-screen w-full place-items-center p-6">
|
<div className="grid min-h-screen w-full place-items-center p-6">
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { FC } from "react";
|
|
||||||
import React from "react";
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Circle } from "lucide-react";
|
import { Circle } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
|
|
@ -14,7 +12,7 @@ interface IHeaderGroupByCard {
|
||||||
count: number;
|
count: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
|
export const HeaderGroupByCard: React.FC<IHeaderGroupByCard> = observer((props) => {
|
||||||
const { icon, title, count } = props;
|
const { icon, title, count } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import type { FC } from "react";
|
|
||||||
import React from "react";
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Circle } from "lucide-react";
|
import { Circle } from "lucide-react";
|
||||||
import { ChevronDownIcon, ChevronUpIcon } from "@plane/propel/icons";
|
import { ChevronDownIcon, ChevronUpIcon } from "@plane/propel/icons";
|
||||||
|
|
@ -13,7 +11,7 @@ interface IHeaderSubGroupByCard {
|
||||||
toggleExpanded: () => void;
|
toggleExpanded: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HeaderSubGroupByCard: FC<IHeaderSubGroupByCard> = observer((props) => {
|
export const HeaderSubGroupByCard: React.FC<IHeaderSubGroupByCard> = observer((props) => {
|
||||||
const { icon, title, count, isExpanded, toggleExpanded } = props;
|
const { icon, title, count, isExpanded, toggleExpanded } = props;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import type { FC, MutableRefObject } from "react";
|
import type { MutableRefObject } from "react";
|
||||||
// types
|
// types
|
||||||
import type { IIssueDisplayProperties } from "@plane/types";
|
import type { IIssueDisplayProperties } from "@plane/types";
|
||||||
import { IssueBlock } from "./block";
|
import { IssueBlock } from "./block";
|
||||||
|
|
@ -10,7 +10,7 @@ interface Props {
|
||||||
containerRef: MutableRefObject<HTMLDivElement | null>;
|
containerRef: MutableRefObject<HTMLDivElement | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const IssueBlocksList: FC<Props> = (props) => {
|
export const IssueBlocksList: React.FC<Props> = (props) => {
|
||||||
const { issueIds = [], groupId, displayProperties } = props;
|
const { issueIds = [], groupId, displayProperties } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { FC } from "react";
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
@ -23,7 +22,7 @@ type Props = {
|
||||||
publishSettings: PublishStore;
|
publishSettings: PublishStore;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssuesLayoutsRoot: FC<Props> = observer((props) => {
|
export const IssuesLayoutsRoot: React.FC<Props> = observer((props) => {
|
||||||
const { peekId, publishSettings } = props;
|
const { peekId, publishSettings } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { getIssueFilters } = useIssueFilter();
|
const { getIssueFilters } = useIssueFilter();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
import type { ReactNode } from "react";
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// plane imports
|
// plane imports
|
||||||
import type { IIssueDisplayProperties } from "@plane/types";
|
import type { IIssueDisplayProperties } from "@plane/types";
|
||||||
|
|
@ -7,7 +6,7 @@ interface IWithDisplayPropertiesHOC {
|
||||||
displayProperties: IIssueDisplayProperties;
|
displayProperties: IIssueDisplayProperties;
|
||||||
shouldRenderProperty?: (displayProperties: IIssueDisplayProperties) => boolean;
|
shouldRenderProperty?: (displayProperties: IIssueDisplayProperties) => boolean;
|
||||||
displayPropertyKey: keyof IIssueDisplayProperties | (keyof IIssueDisplayProperties)[];
|
displayPropertyKey: keyof IIssueDisplayProperties | (keyof IIssueDisplayProperties)[];
|
||||||
children: ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WithDisplayPropertiesHOC = observer(
|
export const WithDisplayPropertiesHOC = observer(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { FC } from "react";
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
|
|
@ -25,7 +24,7 @@ export type NavbarControlsProps = {
|
||||||
publishSettings: PublishStore;
|
publishSettings: PublishStore;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NavbarControls: FC<NavbarControlsProps> = observer((props) => {
|
export const NavbarControls: React.FC<NavbarControlsProps> = observer((props) => {
|
||||||
// props
|
// props
|
||||||
const { publishSettings } = props;
|
const { publishSettings } = props;
|
||||||
// router
|
// router
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { FC } from "react";
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
// ui
|
// ui
|
||||||
|
|
@ -20,7 +19,7 @@ type Props = {
|
||||||
anchor: string;
|
anchor: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssuesLayoutSelection: FC<Props> = observer((props) => {
|
export const IssuesLayoutSelection: React.FC<Props> = observer((props) => {
|
||||||
const { anchor } = props;
|
const { anchor } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { FC } from "react";
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { ProjectIcon } from "@plane/propel/icons";
|
import { ProjectIcon } from "@plane/propel/icons";
|
||||||
// components
|
// components
|
||||||
|
|
@ -14,7 +13,7 @@ type Props = {
|
||||||
publishSettings: PublishStore;
|
publishSettings: PublishStore;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssuesNavbarRoot: FC<Props> = observer((props) => {
|
export const IssuesNavbarRoot: React.FC<Props> = observer((props) => {
|
||||||
const { publishSettings } = props;
|
const { publishSettings } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const { project_details } = publishSettings;
|
const { project_details } = publishSettings;
|
||||||
|
|
|
||||||