import { Fragment, useEffect, useState } from "react"; import { observer } from "mobx-react-lite"; import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; // ui import { Check, CircleDot, Globe2 } from "lucide-react"; import { Dialog, Transition } from "@headlessui/react"; // icons import { IProject } from "@plane/types"; // ui import { Button, Loader, ToggleSwitch, TOAST_TYPE, setToast } from "@plane/ui"; // hooks import { useProjectPublish } from "@/hooks/store"; // store import { IProjectPublishSettings, TProjectPublishViews } from "@/store/project/project-publish.store"; // types // local components import { CustomPopover } from "./popover"; type Props = { isOpen: boolean; project: IProject; onClose: () => void; }; type FormData = { id: string | null; comments: boolean; reactions: boolean; votes: boolean; inbox: string | null; views: TProjectPublishViews[]; }; const defaultValues: FormData = { id: null, comments: false, reactions: false, votes: false, inbox: null, views: ["list", "kanban"], }; const viewOptions: { key: TProjectPublishViews; label: string; }[] = [ { key: "list", label: "List" }, { key: "kanban", label: "Kanban" }, // { key: "calendar", label: "Calendar" }, // { key: "gantt", label: "Gantt" }, // { key: "spreadsheet", label: "Spreadsheet" }, ]; export const PublishProjectModal: React.FC = observer((props) => { const { isOpen, project, onClose } = props; // states const [isUnPublishing, setIsUnPublishing] = useState(false); const [isUpdateRequired, setIsUpdateRequired] = useState(false); let plane_deploy_url = process.env.NEXT_PUBLIC_DEPLOY_URL; if (typeof window !== "undefined" && !plane_deploy_url) plane_deploy_url = window.location.protocol + "//" + window.location.host + "/spaces"; // router const router = useRouter(); const { workspaceSlug } = router.query; // store hooks const { projectPublishSettings, getProjectSettingsAsync, publishProject, updateProjectSettingsAsync, unPublishProject, fetchSettingsLoader, } = useProjectPublish(); // form info const { control, formState: { isSubmitting }, getValues, handleSubmit, reset, watch, } = useForm({ defaultValues, }); const handleClose = () => { onClose(); setIsUpdateRequired(false); reset({ ...defaultValues }); }; // prefill form with the saved settings if the project is already published useEffect(() => { if (projectPublishSettings && projectPublishSettings !== "not-initialized") { let userBoards: TProjectPublishViews[] = []; if (projectPublishSettings?.views) { const savedViews = projectPublishSettings?.views; if (!savedViews) return; if (savedViews.list) userBoards.push("list"); if (savedViews.kanban) userBoards.push("kanban"); if (savedViews.calendar) userBoards.push("calendar"); if (savedViews.gantt) userBoards.push("gantt"); if (savedViews.spreadsheet) userBoards.push("spreadsheet"); userBoards = userBoards && userBoards.length > 0 ? userBoards : ["list"]; } const updatedData = { id: projectPublishSettings?.id || null, comments: projectPublishSettings?.comments || false, reactions: projectPublishSettings?.reactions || false, votes: projectPublishSettings?.votes || false, inbox: projectPublishSettings?.inbox || null, views: userBoards, }; reset({ ...updatedData }); } }, [reset, projectPublishSettings, isOpen]); // fetch publish settings useEffect(() => { if (!workspaceSlug || !isOpen) return; if (projectPublishSettings === "not-initialized") { getProjectSettingsAsync(workspaceSlug.toString(), project.id); } }, [isOpen, workspaceSlug, project, projectPublishSettings, getProjectSettingsAsync]); const handlePublishProject = async (payload: IProjectPublishSettings) => { if (!workspaceSlug) return; return publishProject(workspaceSlug.toString(), project.id, payload) .then((res) => { handleClose(); // window.open(`${plane_deploy_url}/${workspaceSlug}/${project.id}`, "_blank"); return res; }) .catch((err) => err); }; const handleUpdatePublishSettings = async (payload: IProjectPublishSettings) => { if (!workspaceSlug) return; await updateProjectSettingsAsync(workspaceSlug.toString(), project.id, payload.id ?? "", payload) .then((res) => { setToast({ type: TOAST_TYPE.SUCCESS, title: "Success!", message: "Publish settings updated successfully!", }); handleClose(); return res; }) .catch((error) => { console.error("error", error); return error; }); }; const handleUnPublishProject = async (publishId: string) => { if (!workspaceSlug || !publishId) return; setIsUnPublishing(true); await unPublishProject(workspaceSlug.toString(), project.id, publishId) .then((res) => { handleClose(); return res; }) .catch(() => setToast({ type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong while un-publishing the project.", }) ) .finally(() => setIsUnPublishing(false)); }; const CopyLinkToClipboard = ({ copy_link }: { copy_link: string }) => { const [status, setStatus] = useState(false); const copyText = () => { navigator.clipboard.writeText(copy_link); setStatus(true); setTimeout(() => { setStatus(false); }, 1000); }; return (
copyText()} > {status ? "Copied" : "Copy Link"}
); }; const handleFormSubmit = async (formData: FormData) => { if (!formData.views || formData.views.length === 0) { setToast({ type: TOAST_TYPE.ERROR, title: "Error!", message: "Please select at least one view layout to publish the project.", }); return; } const payload = { comments: formData.comments, reactions: formData.reactions, votes: formData.votes, inbox: formData.inbox, views: { list: formData.views.includes("list"), kanban: formData.views.includes("kanban"), calendar: formData.views.includes("calendar"), gantt: formData.views.includes("gantt"), spreadsheet: formData.views.includes("spreadsheet"), }, }; if (project.is_deployed) await handleUpdatePublishSettings({ id: watch("id") ?? "", ...payload }); else await handlePublishProject(payload); }; // check if an update is required or not const checkIfUpdateIsRequired = () => { if (!projectPublishSettings || projectPublishSettings === "not-initialized") return; const currentSettings = projectPublishSettings; const newSettings = getValues(); if ( currentSettings.comments !== newSettings.comments || currentSettings.reactions !== newSettings.reactions || currentSettings.votes !== newSettings.votes ) { setIsUpdateRequired(true); return; } let viewCheckFlag = 0; viewOptions.forEach((option) => { if (currentSettings.views[option.key] !== newSettings.views.includes(option.key)) viewCheckFlag++; }); if (viewCheckFlag !== 0) { setIsUpdateRequired(true); return; } setIsUpdateRequired(false); }; return (
{/* heading */}
Publish
{project.is_deployed && ( )}
{/* content */} {fetchSettingsLoader ? ( ) : (
{project.is_deployed && ( <>
{`${plane_deploy_url}/${workspaceSlug}/${project.id}`}
This project is live on web
)}
Views
( 0 ? viewOptions .filter((v) => value.includes(v.key)) .map((v) => v.label) .join(", ") : `` } placeholder="Select views" > <> {viewOptions.map((option) => (
{ const optionViews = value.length > 0 ? value.includes(option.key) ? value.filter((_o: string) => _o !== option.key) : [...value, option.key] : [option.key]; if (optionViews.length === 0) return; onChange(optionViews); checkIfUpdateIsRequired(); }} >
{option.label}
{value.length > 0 && value.includes(option.key) && ( )}
))}
)} />
Allow comments
( { onChange(val); checkIfUpdateIsRequired(); }} size="sm" /> )} />
Allow reactions
( { onChange(val); checkIfUpdateIsRequired(); }} size="sm" /> )} />
Allow voting
( { onChange(val); checkIfUpdateIsRequired(); }} size="sm" /> )} />
{/* toggle inbox */} {/*
Allow issue proposals
( )} />
*/}
)} {/* modal handlers */}
Anyone with the link can access
{!fetchSettingsLoader && (
{project.is_deployed ? ( <> {isUpdateRequired && ( )} ) : ( )}
)}
); });