diff --git a/apps/app/components/project/publish-project/modal.tsx b/apps/app/components/project/publish-project/modal.tsx index 5f9d9ae2cbd..b22a496f5d9 100644 --- a/apps/app/components/project/publish-project/modal.tsx +++ b/apps/app/components/project/publish-project/modal.tsx @@ -1,28 +1,38 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; // next imports import { useRouter } from "next/router"; // react-hook-form -import { useForm } from "react-hook-form"; +import { Controller, useForm } from "react-hook-form"; // headless ui import { Dialog, Transition } from "@headlessui/react"; // ui components -import { ToggleSwitch, PrimaryButton, SecondaryButton } from "components/ui"; +import { ToggleSwitch, PrimaryButton, SecondaryButton, Icon, DangerButton } from "components/ui"; import { CustomPopover } from "./popover"; // mobx react lite import { observer } from "mobx-react-lite"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; import { RootStore } from "store/root"; -import { IProjectPublishSettingsViews } from "store/project-publish"; +import { IProjectPublishSettings, TProjectPublishViews } from "store/project-publish"; // hooks import useToast from "hooks/use-toast"; import useProjectDetails from "hooks/use-project-details"; +import useUser from "hooks/use-user"; type Props = { // user: ICurrentUserResponse | undefined; }; -const defaultValues: Partial = { +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, @@ -31,70 +41,73 @@ const defaultValues: Partial = { views: ["list", "kanban"], }; -const viewOptions = [ - { key: "list", value: "List" }, - { key: "kanban", value: "Kanban" }, - // { key: "calendar", value: "Calendar" }, - // { key: "gantt", value: "Gantt" }, - // { key: "spreadsheet", value: "Spreadsheet" }, +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(() => { + const [isUnpublishing, setIsUnpublishing] = useState(false); + const [isUpdateRequired, setIsUpdateRequired] = useState(false); + + const plane_deploy_url = process.env.NEXT_PUBLIC_DEPLOY_URL ?? "http://localhost:4000"; + + const router = useRouter(); + const { workspaceSlug } = router.query; + const store: RootStore = useMobxStore(); const { projectPublish } = store; - const { projectDetails, mutateProjectDetails } = useProjectDetails(); + const { user } = useUser(); - const { setToastAlert } = useToast(); - const handleToastAlert = (title: string, type: string, message: string) => { - setToastAlert({ - title: title || "Title", - type: "error" || "warning", - message: message || "Message", - }); - }; - - const { NEXT_PUBLIC_DEPLOY_URL } = process.env; - const plane_deploy_url = NEXT_PUBLIC_DEPLOY_URL - ? NEXT_PUBLIC_DEPLOY_URL - : "http://localhost:3001"; + const { mutateProjectDetails } = useProjectDetails(); - const router = useRouter(); - const { workspaceSlug } = router.query; + const { setToastAlert } = useToast(); const { - formState: { errors, isSubmitting }, + control, + formState: { isSubmitting }, + getValues, handleSubmit, reset, watch, - setValue, - } = useForm({ + } = useForm({ defaultValues, - reValidateMode: "onChange", }); const handleClose = () => { projectPublish.handleProjectModal(null); + + setIsUpdateRequired(false); reset({ ...defaultValues }); }; + // prefill form with the saved settings if the project is already published useEffect(() => { if ( projectPublish.projectPublishSettings && - projectPublish.projectPublishSettings != "not-initialized" + projectPublish.projectPublishSettings !== "not-initialized" ) { - let userBoards: string[] = []; + let userBoards: TProjectPublishViews[] = []; + if (projectPublish.projectPublishSettings?.views) { - const _views: IProjectPublishSettingsViews | null = - projectPublish.projectPublishSettings?.views || null; - if (_views != null) { - if (_views.list) userBoards.push("list"); - if (_views.kanban) userBoards.push("kanban"); - if (_views.calendar) userBoards.push("calendar"); - if (_views.gantt) userBoards.push("gantt"); - if (_views.spreadsheet) userBoards.push("spreadsheet"); - userBoards = userBoards && userBoards.length > 0 ? userBoards : ["list"]; - } + const savedViews = projectPublish.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 = { @@ -105,126 +118,105 @@ export const PublishProjectModal: React.FC = observer(() => { inbox: projectPublish.projectPublishSettings?.inbox || null, views: userBoards, }; + reset({ ...updatedData }); } }, [reset, projectPublish.projectPublishSettings]); + // fetch publish settings useEffect(() => { + if (!workspaceSlug) return; + if ( projectPublish.projectPublishModal && - workspaceSlug && - projectPublish.project_id != null && + projectPublish.project_id !== null && projectPublish?.projectPublishSettings === "not-initialized" ) { projectPublish.getProjectSettingsAsync( - workspaceSlug as string, - projectPublish.project_id as string, + workspaceSlug.toString(), + projectPublish.project_id, null ); } }, [workspaceSlug, projectPublish, projectPublish.projectPublishModal]); - const onSettingsPublish = async (formData: any) => { - if (formData.views && formData.views.length > 0) { - const payload = { - comments: formData.comments || false, - reactions: formData.reactions || false, - votes: formData.votes || false, - inbox: formData.inbox || null, - views: { - list: formData.views.includes("list") || false, - kanban: formData.views.includes("kanban") || false, - calendar: formData.views.includes("calendar") || false, - gantt: formData.views.includes("gantt") || false, - spreadsheet: formData.views.includes("spreadsheet") || false, - }, - }; - - const _workspaceSlug = workspaceSlug; - const _projectId = projectPublish.project_id; - - return projectPublish - .createProjectSettingsAsync(_workspaceSlug as string, _projectId as string, payload, null) - .then((response) => { - mutateProjectDetails(); - handleClose(); - console.log("_projectId", _projectId); - if (_projectId) - window.open(`${plane_deploy_url}/${_workspaceSlug}/${_projectId}`, "_blank"); - return response; - }) - .catch((error) => { - console.error("error", error); - return error; - }); - } else { - handleToastAlert("Missing fields", "warning", "Please select at least one view to publish"); - } - }; + const handlePublishProject = async (payload: IProjectPublishSettings) => { + if (!workspaceSlug || !user) return; - const onSettingsUpdate = async (key: string, value: any) => { - const payload = { - comments: key === "comments" ? value : watch("comments"), - reactions: key === "reactions" ? value : watch("reactions"), - votes: key === "votes" ? value : watch("votes"), - inbox: key === "inbox" ? value : watch("inbox"), - views: - key === "views" - ? { - list: value.includes("list") ? true : false, - kanban: value.includes("kanban") ? true : false, - calendar: value.includes("calendar") ? true : false, - gantt: value.includes("gantt") ? true : false, - spreadsheet: value.includes("spreadsheet") ? true : false, - } - : { - list: watch("views").includes("list") ? true : false, - kanban: watch("views").includes("kanban") ? true : false, - calendar: watch("views").includes("calendar") ? true : false, - gantt: watch("views").includes("gantt") ? true : false, - spreadsheet: watch("views").includes("spreadsheet") ? true : false, - }, - }; + const projectId = projectPublish.project_id; return projectPublish - .updateProjectSettingsAsync( - workspaceSlug as string, - projectPublish.project_id as string, - watch("id"), + .createProjectSettingsAsync( + workspaceSlug.toString(), + projectId?.toString() ?? "", payload, - null + user ) .then((response) => { mutateProjectDetails(); + handleClose(); + if (projectId) window.open(`${plane_deploy_url}/${workspaceSlug}/${projectId}`, "_blank"); return response; }) + .catch((error) => { + console.error("error", error); + return error; + }); + }; + + const handleUpdatePublishSettings = async (payload: IProjectPublishSettings) => { + if (!workspaceSlug || !user) return; + + await projectPublish + .updateProjectSettingsAsync( + workspaceSlug.toString(), + projectPublish.project_id?.toString() ?? "", + payload.id ?? "", + payload, + user + ) + .then((res) => { + mutateProjectDetails(); + + setToastAlert({ + type: "success", + title: "Success!", + message: "Publish settings updated successfully!", + }); + + handleClose(); + return res; + }) .catch((error) => { console.log("error", error); return error; }); }; - const onSettingsUnPublish = async (formData: any) => + const handleUnpublishProject = async (publishId: string) => { + if (!workspaceSlug || !publishId) return; + + setIsUnpublishing(true); + projectPublish .deleteProjectSettingsAsync( - workspaceSlug as string, + workspaceSlug.toString(), projectPublish.project_id as string, - formData?.id, + publishId, null ) - .then((response) => { + .then((res) => { mutateProjectDetails(); - reset({ ...defaultValues }); + handleClose(); - return response; + return res; }) - .catch((error) => { - console.error("error", error); - return error; - }); + .catch((err) => err) + .finally(() => setIsUnpublishing(false)); + }; const CopyLinkToClipboard = ({ copy_link }: { copy_link: string }) => { - const [status, setStatus] = React.useState(false); + const [status, setStatus] = useState(false); const copyText = () => { navigator.clipboard.writeText(copy_link); @@ -244,6 +236,68 @@ export const PublishProjectModal: React.FC = observer(() => { ); }; + const handleFormSubmit = async (formData: FormData) => { + if (!formData.views || formData.views.length === 0) { + setToastAlert({ + 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 (watch("id")) await handleUpdatePublishSettings({ id: watch("id") ?? "", ...payload }); + else await handlePublishProject(payload); + }; + + // check if an update is required or not + const checkIfUpdateIsRequired = () => { + if ( + !projectPublish.projectPublishSettings || + projectPublish.projectPublishSettings === "not-initialized" + ) + return; + + const currentSettings = projectPublish.projectPublishSettings as IProjectPublishSettings; + 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 ( @@ -270,200 +324,190 @@ export const PublishProjectModal: React.FC = observer(() => { leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > - - {/* heading */} -
-
Publish
- {projectPublish.loader && ( -
Changes saved
- )} -
- close + +
+ {/* heading */} +
+
Publish
+ {watch("id") && ( + handleUnpublishProject(watch("id") ?? "")} + className="!px-2 !py-1.5" + loading={isUnpublishing} + > + {isUnpublishing ? "Unpublishing..." : "Unpublish"} + + )}
-
- - {/* content */} -
- {watch("id") && ( -
-
- - radio_button_checked - + + {/* content */} +
+
+
+ {`${plane_deploy_url}/${workspaceSlug}/${projectPublish.project_id}`} +
+
+
-
This project is live on web
- )} -
-
- {`${plane_deploy_url}/${workspaceSlug}/${projectPublish.project_id}`} -
-
- - +
This project is live on web
+
+ )} -
-
-
Views
-
- 0 - ? viewOptions - .filter( - (_view) => watch("views").includes(_view.key) && _view.value - ) - .map((_view) => _view.value) - .join(", ") - : `` - } - placeholder="Select views" - > - <> - {viewOptions && - viewOptions.length > 0 && - viewOptions.map((_view) => ( -
{ - const _views = - watch("views") && watch("views").length > 0 - ? watch("views").includes(_view?.key) - ? watch("views").filter((_o: string) => _o !== _view?.key) - : [...watch("views"), _view?.key] - : [_view?.key]; - setValue("views", _views); - if (watch("id") != null) onSettingsUpdate("views", _views); - }} - > -
{_view.value}
+
+
+
Views
+ ( + 0 + ? viewOptions + .filter((v) => value.includes(v.key)) + .map((v) => v.label) + .join(", ") + : `` + } + placeholder="Select views" + > + <> + {viewOptions.map((option) => (
{ + const _views = + value.length > 0 + ? value.includes(option.key) + ? value.filter((_o: string) => _o !== option.key) + : [...value, option.key] + : [option.key]; + + if (_views.length === 0) return; + + onChange(_views); + checkIfUpdateIsRequired(); + }} > - {watch("views") && - watch("views").length > 0 && - watch("views").includes(_view.key) && ( - - done - +
{option.label}
+
+ {value.length > 0 && value.includes(option.key) && ( + )} +
-
- ))} - - -
-
- - {/*
-
Allow comments
-
- { - const _comments = !watch("comments"); - setValue("comments", _comments); - if (watch("id") != null) onSettingsUpdate("comments", _comments); - }} - size="sm" + ))} + + + )} />
-
*/} - {/*
-
Allow reactions
-
- { - const _reactions = !watch("reactions"); - setValue("reactions", _reactions); - if (watch("id") != null) onSettingsUpdate("reactions", _reactions); - }} - size="sm" +
+
Allow comments
+ ( + { + onChange(val); + checkIfUpdateIsRequired(); + }} + size="sm" + /> + )} />
-
*/} - - {/*
-
Allow Voting
-
- { - const _votes = !watch("votes"); - setValue("votes", _votes); - if (watch("id") != null) onSettingsUpdate("votes", _votes); - }} - size="sm" +
+
Allow reactions
+ ( + { + onChange(val); + checkIfUpdateIsRequired(); + }} + size="sm" + /> + )} />
-
*/} - - {/*
-
Allow issue proposals
-
- { - setValue("inbox", !watch("inbox")); - }} - size="sm" +
+
Allow voting
+ ( + { + onChange(val); + checkIfUpdateIsRequired(); + }} + size="sm" + /> + )} />
+ + {/*
+
Allow issue proposals
+ ( + + )} + />
*/} +
-
- {/* modal handlers */} -
-
-
- public + {/* modal handlers */} +
+
+ +
Anyone with the link can access
+
+
+ Cancel + {watch("id") ? ( + <> + {isUpdateRequired && ( + + {isSubmitting ? "Updating..." : "Update settings"} + + )} + + ) : ( + + {isSubmitting ? "Publishing..." : "Publish"} + + )}
-
Anyone with the link can access
-
-
- Cancel - {watch("id") != null ? ( - - {isSubmitting ? "Unpublishing..." : "Unpublish"} - - ) : ( - - {isSubmitting ? "Publishing..." : "Publish"} - - )}
-
+
diff --git a/apps/app/components/project/publish-project/popover.tsx b/apps/app/components/project/publish-project/popover.tsx index 623675b9f7a..5ab2d6432d5 100644 --- a/apps/app/components/project/publish-project/popover.tsx +++ b/apps/app/components/project/publish-project/popover.tsx @@ -1,6 +1,9 @@ import React, { Fragment } from "react"; + // headless ui import { Popover, Transition } from "@headlessui/react"; +// icons +import { Icon } from "components/ui"; export const CustomPopover = ({ children, @@ -16,18 +19,14 @@ export const CustomPopover = ({ {({ open }) => ( <> -
- {label ? label : placeholder ? placeholder : "Select"} -
-
+
{label ?? placeholder}
+
{!open ? ( - expand_more + ) : ( - expand_less + )}
@@ -41,8 +40,8 @@ export const CustomPopover = ({ leaveFrom="opacity-100 translate-y-0" leaveTo="opacity-0 translate-y-1" > - -
+ +
{children}
diff --git a/apps/app/components/project/single-sidebar-project.tsx b/apps/app/components/project/single-sidebar-project.tsx index 7bfca0d2c4f..6fbdbbaf0fd 100644 --- a/apps/app/components/project/single-sidebar-project.tsx +++ b/apps/app/components/project/single-sidebar-project.tsx @@ -26,7 +26,6 @@ import { SettingsOutlined, } from "@mui/icons-material"; // helpers -import { truncateText } from "helpers/string.helper"; import { renderEmoji } from "helpers/emoji.helper"; // types import { IProject } from "types"; @@ -265,11 +264,10 @@ export const SingleSidebarProject: React.FC = observer( >
- ios_share +
-
Publish
+
{project.is_deployed ? "Publish settings" : "Publish"}
- {/* */} )} diff --git a/apps/app/store/project-publish.tsx b/apps/app/store/project-publish.tsx index 1b27d5fff9c..ffc45f5464a 100644 --- a/apps/app/store/project-publish.tsx +++ b/apps/app/store/project-publish.tsx @@ -4,21 +4,11 @@ import { RootStore } from "./root"; // services import ProjectServices from "services/project-publish.service"; -export type IProjectPublishSettingsViewKeys = - | "list" - | "gantt" - | "kanban" - | "calendar" - | "spreadsheet" - | string; - -export interface IProjectPublishSettingsViews { - list: boolean; - gantt: boolean; - kanban: boolean; - calendar: boolean; - spreadsheet: boolean; -} +export type TProjectPublishViews = "list" | "gantt" | "kanban" | "calendar" | "spreadsheet"; + +export type TProjectPublishViewsSettings = { + [key in TProjectPublishViews]: boolean; +}; export interface IProjectPublishSettings { id?: string; @@ -26,8 +16,8 @@ export interface IProjectPublishSettings { comments: boolean; reactions: boolean; votes: boolean; - views: IProjectPublishSettingsViews; - inbox: null; + views: TProjectPublishViewsSettings; + inbox: string | null; } export interface IProjectPublishStore {