import { Fragment, useEffect, useState } from "react"; import { observer } from "mobx-react"; 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"; // helpers import { SPACE_BASE_PATH, SPACE_BASE_URL } from "@/helpers/common.helper"; // hooks import { useProjectPublish } from "@/hooks/store"; // store import { IProjectPublishSettings, TProjectPublishViews } from "@/store/project/project-publish.store"; // local components import { CustomPopover } from "./popover"; type Props = { isOpen: boolean; project: IProject; onClose: () => void; }; type FormData = { anchor: string; id: string | null; is_comments_enabled: boolean; is_reactions_enabled: boolean; is_votes_enabled: boolean; inbox: string | null; views: TProjectPublishViews[]; }; const defaultValues: FormData = { anchor: "", id: null, is_comments_enabled: false, is_reactions_enabled: false, is_votes_enabled: false, inbox: null, views: ["list", "kanban"], }; const VIEW_OPTIONS: { key: TProjectPublishViews; label: string; }[] = [ { key: "list", label: "List" }, { key: "kanban", label: "Kanban" }, ]; export const PublishProjectModal: React.FC = observer((props) => { const { isOpen, project, onClose } = props; // states const [isUnPublishing, setIsUnPublishing] = useState(false); const [isUpdateRequired, setIsUpdateRequired] = useState(false); // router const router = useRouter(); const { workspaceSlug } = router.query; // store hooks const { fetchPublishSettings, getPublishSettingsByProjectID, publishProject, updatePublishSettings, unPublishProject, fetchSettingsLoader, } = useProjectPublish(); // derived values const projectPublishSettings = getPublishSettingsByProjectID(project.id); // 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?.anchor) return; let userBoards: TProjectPublishViews[] = []; if (projectPublishSettings?.view_props) { const savedViews = projectPublishSettings?.view_props; 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, is_comments_enabled: !!projectPublishSettings?.is_comments_enabled, is_reactions_enabled: !!projectPublishSettings?.is_reactions_enabled, is_votes_enabled: !!projectPublishSettings?.is_votes_enabled, inbox: projectPublishSettings?.inbox || null, views: userBoards, }; reset({ ...updatedData }); }, [reset, projectPublishSettings, isOpen]); // fetch publish settings useEffect(() => { if (!workspaceSlug || !isOpen) return; if (!projectPublishSettings) { fetchPublishSettings(workspaceSlug.toString(), project.id); } }, [fetchPublishSettings, isOpen, project, projectPublishSettings, workspaceSlug]); const handlePublishProject = async (payload: IProjectPublishSettings) => { if (!workspaceSlug) return; return publishProject(workspaceSlug.toString(), project.id, payload); }; const handleUpdatePublishSettings = async (payload: IProjectPublishSettings) => { if (!workspaceSlug) return; await updatePublishSettings(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) .catch(() => setToast({ type: TOAST_TYPE.ERROR, title: "Error!", message: "Something went wrong while unpublishing 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 = { is_comments_enabled: formData.is_comments_enabled, is_reactions_enabled: formData.is_reactions_enabled, is_votes_enabled: formData.is_votes_enabled, inbox: formData.inbox, view_props: { 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({ anchor: watch("anchor") ?? "", id: watch("id") ?? "", ...payload, }); else await handlePublishProject(payload); }; // check if an update is required or not const checkIfUpdateIsRequired = () => { if (!projectPublishSettings || !projectPublishSettings) return; const currentSettings = projectPublishSettings; const newSettings = getValues(); if ( currentSettings.is_comments_enabled !== newSettings.is_comments_enabled || currentSettings.is_reactions_enabled !== newSettings.is_reactions_enabled || currentSettings.is_votes_enabled !== newSettings.is_votes_enabled ) { setIsUpdateRequired(true); return; } let viewCheckFlag = 0; VIEW_OPTIONS.forEach((option) => { if (currentSettings.view_props?.[option.key] !== newSettings.views.includes(option.key)) viewCheckFlag++; }); if (viewCheckFlag !== 0) { setIsUpdateRequired(true); return; } setIsUpdateRequired(false); }; const SPACE_URL = (SPACE_BASE_URL === "" ? window.location.origin : SPACE_BASE_URL) + SPACE_BASE_PATH; return (
{/* heading */}
Publish
{project.is_deployed && ( )}
{/* content */} {fetchSettingsLoader ? ( ) : (
{project.is_deployed && projectPublishSettings && ( <>
{`${SPACE_URL}/issues/${projectPublishSettings.anchor}`}
This project is live on web
)}
Views
( 0 ? VIEW_OPTIONS.filter((v) => value.includes(v.key)) .map((v) => v.label) .join(", ") : `` } placeholder="Select views" > <> {VIEW_OPTIONS.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 && ( )} ) : ( )}
)}
); });