From 23f4d5418fed3b8a7717d7566835d170bbc6ef36 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Thu, 6 Jun 2024 18:12:56 +0530 Subject: [PATCH] [WEB-1516] refactor: publish project modal and types (#4716) * refacotr: project publish * chore: rename service names * chore: is_deployed changed to anchor * chore: update is_deployed key --------- Co-authored-by: NarayanBavisetti --- apiserver/plane/app/serializers/project.py | 4 +- apiserver/plane/app/views/project/base.py | 11 +- packages/types/src/index.d.ts | 1 + packages/types/src/project/projects.d.ts | 2 +- .../types => packages/types/src}/publish.d.ts | 23 +- space/app/[workspaceSlug]/[projectId]/page.ts | 8 +- space/services/publish.service.ts | 5 +- space/store/publish/publish.store.ts | 7 +- space/store/publish/publish_list.store.ts | 3 +- space/types/project.d.ts | 18 - web/components/headers/project-issues.tsx | 8 +- .../issue-detail/issue-activity/root.tsx | 8 +- .../project/publish-project/index.ts | 1 + .../project/publish-project/index.tsx | 2 - .../project/publish-project/modal.tsx | 580 +++++++----------- .../project/publish-project/popover.tsx | 47 -- web/components/project/sidebar-list-item.tsx | 2 +- .../project/project-publish.service.ts | 21 +- web/store/project/project-publish.store.ts | 63 +- 19 files changed, 295 insertions(+), 519 deletions(-) rename {space/types => packages/types/src}/publish.d.ts (57%) delete mode 100644 space/types/project.d.ts create mode 100644 web/components/project/publish-project/index.ts delete mode 100644 web/components/project/publish-project/index.tsx delete mode 100644 web/components/project/publish-project/popover.tsx diff --git a/apiserver/plane/app/serializers/project.py b/apiserver/plane/app/serializers/project.py index d9ea99f1e..1bbc580c1 100644 --- a/apiserver/plane/app/serializers/project.py +++ b/apiserver/plane/app/serializers/project.py @@ -114,7 +114,7 @@ class ProjectListSerializer(DynamicBaseSerializer): is_member = serializers.BooleanField(read_only=True) sort_order = serializers.FloatField(read_only=True) member_role = serializers.IntegerField(read_only=True) - is_deployed = serializers.BooleanField(read_only=True) + anchor = serializers.CharField(read_only=True) members = serializers.SerializerMethodField() def get_members(self, obj): @@ -148,7 +148,7 @@ class ProjectDetailSerializer(BaseSerializer): is_member = serializers.BooleanField(read_only=True) sort_order = serializers.FloatField(read_only=True) member_role = serializers.IntegerField(read_only=True) - is_deployed = serializers.BooleanField(read_only=True) + anchor = serializers.CharField(read_only=True) class Meta: model = Project diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py index f6f8e951c..7e3326e02 100644 --- a/apiserver/plane/app/views/project/base.py +++ b/apiserver/plane/app/views/project/base.py @@ -137,12 +137,11 @@ class ProjectViewSet(BaseViewSet): ).values("role") ) .annotate( - is_deployed=Exists( - DeployBoard.objects.filter( - project_id=OuterRef("pk"), - workspace__slug=self.kwargs.get("slug"), - ) - ) + anchor=DeployBoard.objects.filter( + entity_name="project", + entity_identifier=OuterRef("pk"), + workspace__slug=self.kwargs.get("slug"), + ).values("anchor") ) .annotate(sort_order=Subquery(sort_order)) .prefetch_related( diff --git a/packages/types/src/index.d.ts b/packages/types/src/index.d.ts index 94277e8b6..25c2b255b 100644 --- a/packages/types/src/index.d.ts +++ b/packages/types/src/index.d.ts @@ -27,3 +27,4 @@ export * from "./webhook"; export * from "./workspace-views"; export * from "./common"; export * from "./pragmatic"; +export * from "./publish"; diff --git a/packages/types/src/project/projects.d.ts b/packages/types/src/project/projects.d.ts index ee974fd63..59ccf73b6 100644 --- a/packages/types/src/project/projects.d.ts +++ b/packages/types/src/project/projects.d.ts @@ -32,7 +32,7 @@ export interface IProject { estimate: string | null; id: string; identifier: string; - is_deployed: boolean; + anchor: string | null; is_favorite: boolean; is_member: boolean; logo_props: TLogoProps; diff --git a/space/types/publish.d.ts b/packages/types/src/publish.d.ts similarity index 57% rename from space/types/publish.d.ts rename to packages/types/src/publish.d.ts index 482cbafec..883ef8dd6 100644 --- a/space/types/publish.d.ts +++ b/packages/types/src/publish.d.ts @@ -1,8 +1,25 @@ -import { IWorkspaceLite } from "@plane/types"; -import { TProjectDetails, TViewDetails } from "@/types/project"; +import { IProject, IProjectLite, IWorkspaceLite } from "@plane/types"; export type TPublishEntityType = "project"; +export type TProjectPublishLayouts = + | "calendar" + | "gantt" + | "kanban" + | "list" + | "spreadsheet"; + +export type TPublishViewProps = { + calendar?: boolean; + gantt?: boolean; + kanban?: boolean; + list?: boolean; + spreadsheet?: boolean; +}; + +export type TProjectDetails = IProjectLite & + Pick; + export type TPublishSettings = { anchor: string | undefined; is_comments_enabled: boolean; @@ -17,7 +34,7 @@ export type TPublishSettings = { is_reactions_enabled: boolean; updated_at: string | undefined; updated_by: string | undefined; - view_props: TViewDetails | undefined; + view_props: TViewProps | undefined; is_votes_enabled: boolean; workspace: string | undefined; workspace_detail: IWorkspaceLite | undefined; diff --git a/space/app/[workspaceSlug]/[projectId]/page.ts b/space/app/[workspaceSlug]/[projectId]/page.ts index 8af878397..4f18e8bd5 100644 --- a/space/app/[workspaceSlug]/[projectId]/page.ts +++ b/space/app/[workspaceSlug]/[projectId]/page.ts @@ -1,8 +1,8 @@ import { notFound, redirect } from "next/navigation"; +// types +import { TPublishSettings } from "@plane/types"; // services import PublishService from "@/services/publish.service"; -// types -import { TPublishSettings } from "@/types/publish"; const publishService = new PublishService(); @@ -29,8 +29,8 @@ export default async function IssuesPage(props: Props) { } let url = ""; - if (response.entity_name === "project") { - url = `/issues/${response.anchor}`; + if (response?.entity_name === "project") { + url = `/issues/${response?.anchor}`; const params = new URLSearchParams(); if (board) params.append("board", board); if (peekId) params.append("peekId", peekId); diff --git a/space/services/publish.service.ts b/space/services/publish.service.ts index c0bd3d8de..0275142c8 100644 --- a/space/services/publish.service.ts +++ b/space/services/publish.service.ts @@ -1,8 +1,9 @@ +// types +import { TPublishSettings } from "@plane/types"; +// helpers import { API_BASE_URL } from "@/helpers/common.helper"; // services import { APIService } from "@/services/api.service"; -// types -import { TPublishSettings } from "@/types/publish"; class PublishService extends APIService { constructor() { diff --git a/space/store/publish/publish.store.ts b/space/store/publish/publish.store.ts index 5cad121db..29cbc53ab 100644 --- a/space/store/publish/publish.store.ts +++ b/space/store/publish/publish.store.ts @@ -1,11 +1,8 @@ import { observable, makeObservable, computed } from "mobx"; // types -import { IWorkspaceLite } from "@plane/types"; +import { IWorkspaceLite, TProjectDetails, TPublishEntityType, TPublishSettings, TPublishViewProps } from "@plane/types"; // store types import { RootStore } from "@/store/root.store"; -// types -import { TProjectDetails, TViewDetails } from "@/types/project"; -import { TPublishEntityType, TPublishSettings } from "@/types/publish"; export interface IPublishStore extends TPublishSettings { // computed @@ -30,7 +27,7 @@ export class PublishStore implements IPublishStore { is_reactions_enabled: boolean; updated_at: string | undefined; updated_by: string | undefined; - view_props: TViewDetails | undefined; + view_props: TPublishViewProps | undefined; is_votes_enabled: boolean; workspace: string | undefined; workspace_detail: IWorkspaceLite | undefined; diff --git a/space/store/publish/publish_list.store.ts b/space/store/publish/publish_list.store.ts index 91db19f7a..b6722115d 100644 --- a/space/store/publish/publish_list.store.ts +++ b/space/store/publish/publish_list.store.ts @@ -1,11 +1,12 @@ import set from "lodash/set"; import { makeObservable, observable, runInAction, action } from "mobx"; +// types +import { TPublishSettings } from "@plane/types"; // services import PublishService from "@/services/publish.service"; // store import { PublishStore } from "@/store/publish/publish.store"; // store -import { TPublishSettings } from "@/types/publish"; import { RootStore } from "../root.store"; export interface IPublishListStore { diff --git a/space/types/project.d.ts b/space/types/project.d.ts deleted file mode 100644 index c0ae02583..000000000 --- a/space/types/project.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { TLogoProps } from "@plane/types"; - -export type TViewDetails = { - list: boolean; - gantt: boolean; - kanban: boolean; - calendar: boolean; - spreadsheet: boolean; -}; - -export type TProjectDetails = { - id: string; - identifier: string; - name: string; - cover_image: string | undefined; - logo_props: TLogoProps; - description: string; -}; diff --git a/web/components/headers/project-issues.tsx b/web/components/headers/project-issues.tsx index 7042d2c28..71d787148 100644 --- a/web/components/headers/project-issues.tsx +++ b/web/components/headers/project-issues.tsx @@ -15,7 +15,7 @@ import { DisplayFiltersSelection, FiltersDropdown, FilterSelection, LayoutSelect import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "@/constants/issue"; import { EUserProjectRoles } from "@/constants/project"; // helpers -import { SPACE_BASE_PATH, SPACE_BASE_URL } from "@/helpers/common.helper"; +import { SPACE_BASE_URL } from "@/helpers/common.helper"; import { calculateTotalFilters } from "@/helpers/filter.helper"; // hooks import { @@ -100,7 +100,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => { [workspaceSlug, projectId, updateFilters] ); - const DEPLOY_URL = SPACE_BASE_URL + SPACE_BASE_PATH; + const publishedURL = `${SPACE_BASE_URL}/issues/${currentProjectDetails?.anchor}`; const canUserCreateIssue = currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); @@ -159,9 +159,9 @@ export const ProjectIssuesHeader: React.FC = observer(() => { ) : null} - {currentProjectDetails?.is_deployed && DEPLOY_URL && ( + {currentProjectDetails?.anchor && ( = observer((props) => { workspaceSlug={workspaceSlug} issueId={issueId} activityOperations={activityOperations} - showAccessSpecifier={project.is_deployed} + showAccessSpecifier={!!project.anchor} disabled={disabled} /> {!disabled && ( @@ -150,7 +150,7 @@ export const IssueActivity: FC = observer((props) => { projectId={projectId} workspaceSlug={workspaceSlug} activityOperations={activityOperations} - showAccessSpecifier={project.is_deployed} + showAccessSpecifier={!!project.anchor} /> )} @@ -161,7 +161,7 @@ export const IssueActivity: FC = observer((props) => { workspaceSlug={workspaceSlug} issueId={issueId} activityOperations={activityOperations} - showAccessSpecifier={project.is_deployed} + showAccessSpecifier={!!project.anchor} disabled={disabled} /> {!disabled && ( @@ -170,7 +170,7 @@ export const IssueActivity: FC = observer((props) => { projectId={projectId} workspaceSlug={workspaceSlug} activityOperations={activityOperations} - showAccessSpecifier={project.is_deployed} + showAccessSpecifier={!!project.anchor} /> )} diff --git a/web/components/project/publish-project/index.ts b/web/components/project/publish-project/index.ts new file mode 100644 index 000000000..031608e25 --- /dev/null +++ b/web/components/project/publish-project/index.ts @@ -0,0 +1 @@ +export * from "./modal"; diff --git a/web/components/project/publish-project/index.tsx b/web/components/project/publish-project/index.tsx deleted file mode 100644 index 6bebc086c..000000000 --- a/web/components/project/publish-project/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./modal"; -export * from "./popover"; diff --git a/web/components/project/publish-project/modal.tsx b/web/components/project/publish-project/modal.tsx index 1c547aa8d..bec4e7f17 100644 --- a/web/components/project/publish-project/modal.tsx +++ b/web/components/project/publish-project/modal.tsx @@ -1,22 +1,19 @@ -import { Fragment, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { observer } from "mobx-react"; import { useRouter } from "next/router"; import { Controller, useForm } from "react-hook-form"; +import { Check, ExternalLink, Globe2 } from "lucide-react"; +// types +import { IProject, TProjectPublishLayouts, TPublishSettings } from "@plane/types"; // 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"; +import { Button, Loader, ToggleSwitch, TOAST_TYPE, setToast, CustomSelect } from "@plane/ui"; +// components +import { EModalWidth, ModalCore } from "@/components/core"; // helpers -import { SPACE_BASE_PATH, SPACE_BASE_URL } from "@/helpers/common.helper"; +import { SPACE_BASE_URL } from "@/helpers/common.helper"; +import { copyTextToClipboard } from "@/helpers/string.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; @@ -24,28 +21,19 @@ type Props = { 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, +const defaultValues: Partial = { is_comments_enabled: false, is_reactions_enabled: false, is_votes_enabled: false, inbox: null, - views: ["list", "kanban"], + view_props: { + list: true, + kanban: true, + }, }; const VIEW_OPTIONS: { - key: TProjectPublishViews; + key: TProjectPublishLayouts; label: string; }[] = [ { key: "list", label: "List" }, @@ -56,7 +44,6 @@ 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; @@ -74,8 +61,7 @@ export const PublishProjectModal: React.FC = observer((props) => { // form info const { control, - formState: { isSubmitting }, - getValues, + formState: { isDirty, isSubmitting }, handleSubmit, reset, watch, @@ -85,43 +71,8 @@ export const PublishProjectModal: React.FC = observer((props) => { 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; @@ -131,30 +82,24 @@ export const PublishProjectModal: React.FC = observer((props) => { } }, [fetchPublishSettings, isOpen, project, projectPublishSettings, workspaceSlug]); - const handlePublishProject = async (payload: IProjectPublishSettings) => { + const handlePublishProject = async (payload: Partial) => { if (!workspaceSlug) return; - - return publishProject(workspaceSlug.toString(), project.id, payload); + await publishProject(workspaceSlug.toString(), project.id, payload); }; - const handleUpdatePublishSettings = async (payload: IProjectPublishSettings) => { - if (!workspaceSlug) return; + const handleUpdatePublishSettings = async (payload: Partial) => { + if (!workspaceSlug || !payload.id) 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; + 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; + }); }; const handleUnPublishProject = async (publishId: string) => { @@ -173,29 +118,15 @@ export const PublishProjectModal: React.FC = observer((props) => { .finally(() => setIsUnPublishing(false)); }; - const CopyLinkToClipboard = ({ copy_link }: { copy_link: string }) => { - const [status, setStatus] = useState(false); + const selectedLayouts = Object.entries(watch("view_props") ?? {}) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .filter(([key, value]) => value) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .map(([key, value]) => key) + .filter((l) => VIEW_OPTIONS.find((o) => o.key === l)); - 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) { + const handleFormSubmit = async (formData: Partial) => { + if (!selectedLayouts || selectedLayouts.length === 0) { setToast({ type: TOAST_TYPE.ERROR, title: "Error!", @@ -204,282 +135,195 @@ export const PublishProjectModal: React.FC = observer((props) => { return; } - const payload = { + const payload: Partial = { + id: formData.id, 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"), - }, + view_props: formData.view_props, }; - if (project.is_deployed) - await handleUpdatePublishSettings({ - anchor: watch("anchor") ?? "", - id: watch("id") ?? "", - ...payload, - }); + if (formData.id && project.anchor) await handleUpdatePublishSettings(payload); else await handlePublishProject(payload); }; - // check if an update is required or not - const checkIfUpdateIsRequired = () => { - if (!projectPublishSettings || !projectPublishSettings) return; + // prefill form values for already published projects + useEffect(() => { + if (!projectPublishSettings?.anchor) 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++; + reset({ + ...defaultValues, + ...projectPublishSettings, }); + }, [projectPublishSettings, reset]); - if (viewCheckFlag !== 0) { - setIsUpdateRequired(true); - return; - } + const publishLink = `${SPACE_BASE_URL}/issues/${projectPublishSettings?.anchor}`; - setIsUpdateRequired(false); - }; - - const SPACE_URL = (SPACE_BASE_URL === "" ? window.location.origin : SPACE_BASE_URL) + SPACE_BASE_PATH; + const handleCopyLink = () => + copyTextToClipboard(publishLink).then(() => + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "", + message: "Published page link copied successfully.", + }) + ); 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 && ( - - )} - - ) : ( - - )} -
- )} -
-
-
-
-
+ +
+
+
Publish page
+ {project.anchor && ( + + )}
-
-
+ + {/* content */} + {fetchSettingsLoader ? ( + + + + + + + ) : ( +
+ {project.anchor && projectPublishSettings && ( + <> +
+ + {publishLink} + +
+ + + + +
+
+

+ + + + + This project is now live on web +

+ + )} +
+
+
Views
+ ( + selectedLayouts.includes(o.key)) + .map((o) => o.label) + .join(", ")} + onChange={(val: TProjectPublishLayouts) => { + if (selectedLayouts.length === 1 && selectedLayouts[0] === val) return; + onChange({ + ...value, + [val]: !value?.[val], + }); + }} + buttonClassName="border-none" + placement="bottom-end" + > + {VIEW_OPTIONS.map((option) => ( + + {option.label} + {selectedLayouts.includes(option.key) && } + + ))} + + )} + /> +
+
+
Allow comments
+ ( + + )} + /> +
+
+
Allow reactions
+ ( + + )} + /> +
+
+
Allow voting
+ ( + + )} + /> +
+
+
+ )} + + {/* modal handlers */} +
+
+ +
Anyone with the link can access
+
+ {!fetchSettingsLoader && ( +
+ + {project.anchor ? ( + isDirty && ( + + ) + ) : ( + + )} +
+ )} +
+ + ); }); diff --git a/web/components/project/publish-project/popover.tsx b/web/components/project/publish-project/popover.tsx deleted file mode 100644 index 3de915788..000000000 --- a/web/components/project/publish-project/popover.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React, { Fragment } from "react"; - -// headless ui -import { ChevronDown, ChevronUp } from "lucide-react"; -import { Popover, Transition } from "@headlessui/react"; -// icons - -export const CustomPopover = ({ - children, - label, - placeholder = "Select", -}: { - children: React.ReactNode; - label?: string; - placeholder?: string; -}) => ( -
- - {({ open }) => ( - <> - -
{label ?? placeholder}
-
- {!open ? : } -
-
- - - -
- {children} -
-
-
- - )} -
-
-); diff --git a/web/components/project/sidebar-list-item.tsx b/web/components/project/sidebar-list-item.tsx index bf8e823d6..2bf79de72 100644 --- a/web/components/project/sidebar-list-item.tsx +++ b/web/components/project/sidebar-list-item.tsx @@ -396,7 +396,7 @@ export const ProjectSidebarListItem: React.FC = observer((props) => {
-
{project.is_deployed ? "Publish settings" : "Publish"}
+
{project.anchor ? "Publish settings" : "Publish"}
)} diff --git a/web/services/project/project-publish.service.ts b/web/services/project/project-publish.service.ts index 59c7ed089..b0e3ca686 100644 --- a/web/services/project/project-publish.service.ts +++ b/web/services/project/project-publish.service.ts @@ -1,15 +1,16 @@ +// types +import { TPublishSettings } from "@plane/types"; +// helpers import { API_BASE_URL } from "@/helpers/common.helper"; // services import { APIService } from "@/services/api.service"; -// types -import { IProjectPublishSettings } from "@/store/project/project-publish.store"; export class ProjectPublishService extends APIService { constructor() { super(API_BASE_URL); } - async getProjectSettingsAsync(workspaceSlug: string, projectID: string): Promise { + async fetchPublishSettings(workspaceSlug: string, projectID: string): Promise { return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectID}/project-deploy-boards/`) .then((response) => response?.data) .catch((error) => { @@ -17,11 +18,11 @@ export class ProjectPublishService extends APIService { }); } - async createProjectSettingsAsync( + async publishProject( workspaceSlug: string, projectID: string, - data: IProjectPublishSettings - ): Promise { + data: Partial + ): Promise { return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectID}/project-deploy-boards/`, data) .then((response) => response?.data) .catch((error) => { @@ -29,12 +30,12 @@ export class ProjectPublishService extends APIService { }); } - async updateProjectSettingsAsync( + async updatePublishSettings( workspaceSlug: string, projectID: string, project_publish_id: string, - data: IProjectPublishSettings - ): Promise { + data: Partial + ): Promise { return this.patch( `/api/workspaces/${workspaceSlug}/projects/${projectID}/project-deploy-boards/${project_publish_id}/`, data @@ -45,7 +46,7 @@ export class ProjectPublishService extends APIService { }); } - async deleteProjectSettingsAsync(workspaceSlug: string, projectID: string, project_publish_id: string): Promise { + async unpublishProject(workspaceSlug: string, projectID: string, project_publish_id: string): Promise { return this.delete( `/api/workspaces/${workspaceSlug}/projects/${projectID}/project-deploy-boards/${project_publish_id}/` ) diff --git a/web/store/project/project-publish.store.ts b/web/store/project/project-publish.store.ts index 25c2516f5..4e08fd6c6 100644 --- a/web/store/project/project-publish.store.ts +++ b/web/store/project/project-publish.store.ts @@ -2,48 +2,33 @@ import set from "lodash/set"; import unset from "lodash/unset"; import { observable, action, makeObservable, runInAction } from "mobx"; // types -import { ProjectPublishService } from "@/services/project"; -import { ProjectRootStore } from "./"; +import { TPublishSettings } from "@plane/types"; // services - -export type TProjectPublishViews = "list" | "gantt" | "kanban" | "calendar" | "spreadsheet"; - -export type TProjectPublishViewsSettings = { - [key in TProjectPublishViews]: boolean; -}; - -export interface IProjectPublishSettings { - anchor?: string; - id?: string; - project?: string; - is_comments_enabled: boolean; - is_reactions_enabled: boolean; - is_votes_enabled: boolean; - view_props: TProjectPublishViewsSettings; - inbox: string | null; -} +import { ProjectPublishService } from "@/services/project"; +// store +import { ProjectRootStore } from "@/store/project"; export interface IProjectPublishStore { // states generalLoader: boolean; fetchSettingsLoader: boolean; // observables - publishSettingsMap: Record; // projectID => IProjectPublishSettings + publishSettingsMap: Record; // projectID => TPublishSettings // helpers - getPublishSettingsByProjectID: (projectID: string) => IProjectPublishSettings | undefined; + getPublishSettingsByProjectID: (projectID: string) => TPublishSettings | undefined; // actions - fetchPublishSettings: (workspaceSlug: string, projectID: string) => Promise; + fetchPublishSettings: (workspaceSlug: string, projectID: string) => Promise; updatePublishSettings: ( workspaceSlug: string, projectID: string, projectPublishId: string, - data: IProjectPublishSettings - ) => Promise; + data: Partial + ) => Promise; publishProject: ( workspaceSlug: string, projectID: string, - data: IProjectPublishSettings - ) => Promise; + data: Partial + ) => Promise; unPublishProject: (workspaceSlug: string, projectID: string, projectPublishId: string) => Promise; } @@ -52,7 +37,7 @@ export class ProjectPublishStore implements IProjectPublishStore { generalLoader: boolean = false; fetchSettingsLoader: boolean = false; // observables - publishSettingsMap: Record = {}; + publishSettingsMap: Record = {}; // root store projectRootStore: ProjectRootStore; // services @@ -80,9 +65,9 @@ export class ProjectPublishStore implements IProjectPublishStore { /** * @description returns the publish settings of a particular project * @param {string} projectID - * @returns {IProjectPublishSettings | undefined} + * @returns {TPublishSettings | undefined} */ - getPublishSettingsByProjectID = (projectID: string): IProjectPublishSettings | undefined => + getPublishSettingsByProjectID = (projectID: string): TPublishSettings | undefined => this.publishSettingsMap?.[projectID] ?? undefined; /** @@ -96,7 +81,7 @@ export class ProjectPublishStore implements IProjectPublishStore { runInAction(() => { this.fetchSettingsLoader = true; }); - const response = await this.projectPublishService.getProjectSettingsAsync(workspaceSlug, projectID); + const response = await this.projectPublishService.fetchPublishSettings(workspaceSlug, projectID); runInAction(() => { set(this.publishSettingsMap, [projectID], response); @@ -118,15 +103,15 @@ export class ProjectPublishStore implements IProjectPublishStore { * @param data * @returns */ - publishProject = async (workspaceSlug: string, projectID: string, data: IProjectPublishSettings) => { + publishProject = async (workspaceSlug: string, projectID: string, data: Partial) => { try { runInAction(() => { this.generalLoader = true; }); - const response = await this.projectPublishService.createProjectSettingsAsync(workspaceSlug, projectID, data); + const response = await this.projectPublishService.publishProject(workspaceSlug, projectID, data); runInAction(() => { set(this.publishSettingsMap, [projectID], response); - set(this.projectRootStore.project.projectMap, [projectID, "is_deployed"], true); + set(this.projectRootStore.project.projectMap, [projectID, "anchor"], response.anchor); this.generalLoader = false; }); return response; @@ -150,13 +135,13 @@ export class ProjectPublishStore implements IProjectPublishStore { workspaceSlug: string, projectID: string, projectPublishId: string, - data: IProjectPublishSettings + data: Partial ) => { try { runInAction(() => { this.generalLoader = true; }); - const response = await this.projectPublishService.updateProjectSettingsAsync( + const response = await this.projectPublishService.updatePublishSettings( workspaceSlug, projectID, projectPublishId, @@ -187,14 +172,10 @@ export class ProjectPublishStore implements IProjectPublishStore { runInAction(() => { this.generalLoader = true; }); - const response = await this.projectPublishService.deleteProjectSettingsAsync( - workspaceSlug, - projectID, - projectPublishId - ); + const response = await this.projectPublishService.unpublishProject(workspaceSlug, projectID, projectPublishId); runInAction(() => { unset(this.publishSettingsMap, [projectID]); - set(this.projectRootStore.project.projectMap, [projectID, "is_deployed"], false); + set(this.projectRootStore.project.projectMap, [projectID, "anchor"], null); this.generalLoader = false; }); return response;