From fdcd63dc804d99d2f375f021755765987405a13f Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com> Date: Fri, 27 Dec 2024 09:44:59 +0530 Subject: [PATCH] [WEB-2880] fix: workspace initiatives (#2028) * fix: initiative comment delete endpoint * chore: initiative order updated in app sidebar * fix: create/update initiative modal description section * chore: initiatve detail spacing and modal project icon improvement * chore: project type and initiative store updated * chore: initiative project activity added * chore: initiative validation and improvements --- .../initiatives/components/form.tsx | 178 +++++++++--------- .../projects/project-list-item/index.tsx | 15 +- .../project-list-item/quick-actions.tsx | 15 +- .../initiatives/detail/projects/root.tsx | 87 +++++++-- .../initiatives/detail/projects/title.tsx | 14 +- .../initiatives/detail/section/root.tsx | 104 +++++----- .../sidebar/acitivity-tab/activity-icon.tsx | 3 +- .../sidebar/acitivity-tab/activity-items.tsx | 11 ++ .../sidebar/properties-tab/root.tsx | 34 +++- web/ee/constants/dashboard.ts | 2 +- web/ee/services/initiative.service.ts | 2 +- web/ee/store/initiatives/initiatives.store.ts | 2 +- 12 files changed, 286 insertions(+), 181 deletions(-) diff --git a/web/ee/components/initiatives/components/form.tsx b/web/ee/components/initiatives/components/form.tsx index 47d9cb7b07..ad6017b013 100644 --- a/web/ee/components/initiatives/components/form.tsx +++ b/web/ee/components/initiatives/components/form.tsx @@ -1,6 +1,7 @@ import React, { FC, useState } from "react"; import { useParams } from "next/navigation"; import { EFileAssetType } from "@plane/types/src/enums"; +import { cn } from "@plane/utils"; import { Button, Input, setToast, TOAST_TYPE } from "@plane/ui"; // components import { DateRangeDropdown, MemberDropdown, ProjectDropdown } from "@/components/dropdowns"; @@ -79,114 +80,105 @@ export const CreateUpdateInitiativeForm: FC = (props) => { } }} > -
-
-

- {initiativeDetail ? "Update" : "Create"} Initiative -

-
-
-
+
+

{formData.id ? "Update" : "Create"} Initiative

+ +
+
handleNameChange(e.target.value)} - hasError={Boolean(errors?.name)} - placeholder="Title" - className="w-full text-base" + placeholder="Initiative name" + className="w-full resize-none text-base" + hasError={Boolean(errors.name)} tabIndex={1} - maxLength={255} - required autoFocus /> - {errors?.name} + {errors.name &&
{errors.name}
}
-
-

" - : formData?.description_html - } - value={formData?.description_html} - workspaceSlug={workspaceSlug.toString()} - workspaceId={currentWorkspace.id} - dragDropEnabled={false} - onChange={(description_json: object, description_html: string) => { - handleFormDataChange("description_html", description_html); +
+

" : formData?.description_html + } + value={formData?.description_html} + workspaceSlug={workspaceSlug.toString()} + workspaceId={currentWorkspace.id} + dragDropEnabled={false} + onChange={(description_json: object, description_html: string) => { + handleFormDataChange("description_html", description_html); + }} + placeholder={getDescriptionPlaceholder} + searchMentionCallback={searchEntity} + editorClassName="text-xs" + containerClassName="resize-none min-h-24 text-xs border-[0.5px] border-custom-border-200 rounded-md px-3 py-2" + tabIndex={2} + uploadFile={async (file) => { + try { + const { asset_id } = await fileService.uploadWorkspaceAsset( + workspaceSlug.toString(), + { + entity_identifier: initiativeDetail?.id ?? "", + entity_type: EFileAssetType.INITIATIVE_DESCRIPTION, + }, + file + ); + return asset_id; + } catch (error) { + console.log("Error in uploading issue asset:", error); + throw new Error("Asset upload failed. Please try again later."); + } + }} + /> + +
+
+ { + handleFormDataChange("project_ids", val); }} - placeholder={getDescriptionPlaceholder} - searchMentionCallback={searchEntity} - editorClassName="text-xs" - containerClassName="resize-none min-h-24 max-h-64 overflow-y-scroll vertical-scrollbar scrollbar-sm text-xs border-[0.5px] border-custom-border-200 rounded-md px-3 py-2" + value={formData.project_ids || []} + multiple + showTooltip tabIndex={2} - uploadFile={async (file) => { - try { - const { asset_id } = await fileService.uploadWorkspaceAsset( - workspaceSlug.toString(), - { - entity_identifier: initiativeDetail?.id ?? "", - entity_type: EFileAssetType.INITIATIVE_DESCRIPTION, - }, - file - ); - return asset_id; - } catch (error) { - console.log("Error in uploading issue asset:", error); - throw new Error("Asset upload failed. Please try again later."); - } - }} />
-
-
- { - handleFormDataChange("project_ids", val); - }} - value={formData.project_ids || []} - multiple - showTooltip - tabIndex={2} - /> -
- { + handleFormDataChange("start_date", val?.from ? renderFormattedPayloadDate(val.from) : null); + handleFormDataChange("end_date", val?.to ? renderFormattedPayloadDate(val.to) : null); + }} + placeholder={{ + from: "Start date", + to: "End date", + }} + hideIcon={{ + to: true, + }} + tabIndex={3} + /> + +
+ handleFormDataChange("lead", val)} + multiple={false} buttonVariant="border-with-text" - className="h-7" - value={{ - from: getDate(formData?.start_date), - to: getDate(formData?.end_date), - }} - onSelect={(val) => { - handleFormDataChange("start_date", val?.from ? renderFormattedPayloadDate(val.from) : null); - handleFormDataChange("end_date", val?.to ? renderFormattedPayloadDate(val.to) : null); - }} - placeholder={{ - from: "Start date", - to: "End date", - }} - hideIcon={{ - to: true, - }} - tabIndex={3} + placeholder="Lead" + tabIndex={4} + showUserDetails /> - -
- handleFormDataChange("lead", val)} - multiple={false} - buttonVariant="border-with-text" - placeholder="Lead" - tabIndex={4} - showUserDetails - /> -
diff --git a/web/ee/components/initiatives/detail/projects/project-list-item/index.tsx b/web/ee/components/initiatives/detail/projects/project-list-item/index.tsx index a2909bdd8b..8b1292d477 100644 --- a/web/ee/components/initiatives/detail/projects/project-list-item/index.tsx +++ b/web/ee/components/initiatives/detail/projects/project-list-item/index.tsx @@ -10,6 +10,8 @@ import { usePlatformOS } from "@/hooks/use-platform-os"; // Plane-web import Attributes from "@/plane-web/components/projects/layouts/attributes"; // +import { useWorkspaceFeatures } from "@/plane-web/hooks/store"; +import { EWorkspaceFeatures } from "@/plane-web/types/workspace-feature"; import { QuickActions } from "./quick-actions"; type Props = { @@ -23,9 +25,12 @@ export const ProjectItem = observer((props: Props) => { const { getProjectById, updateProject } = useProject(); const { currentWorkspace } = useWorkspace(); const { isMobile } = usePlatformOS(); + const { isWorkspaceFeatureEnabled } = useWorkspaceFeatures(); const parentRef = useRef(null); + const isProjectGroupingEnabled = isWorkspaceFeatureEnabled(EWorkspaceFeatures.IS_PROJECT_GROUPING_ENABLED); + const projectDetails = getProjectById(projectId); if (!projectDetails || !currentWorkspace) return ; @@ -41,6 +46,9 @@ export const ProjectItem = observer((props: Props) => { } quickActionElement={
+ {projectDetails.total_issues > 0 && ( + {`${projectDetails.completed_issues}/${projectDetails.total_issues}`} + )} { workspaceSlug={workspaceSlug.toString()} currentWorkspace={currentWorkspace} containerClass="px-0 py-0 md:pb-4 lg:py-2" - displayProperties={{ state: true, priority: true, lead: true, date: true }} + displayProperties={{ + state: isProjectGroupingEnabled, + priority: isProjectGroupingEnabled, + lead: true, + date: true, + }} />
= observer((props: Props) => { const { workspaceSlug, initiativeId, project } = props; - // derived states - const projectLink = `${workspaceSlug}/projects/${project.id}/issues`; - + // store hooks const { - initiative: { updateInitiative }, + initiative: { getInitiativeById, updateInitiative }, } = useInitiatives(); + // derived states + const projectLink = `${workspaceSlug}/projects/${project.id}/issues`; + const initiative = initiativeId ? getInitiativeById(initiativeId) : undefined; + + const shouldRenderRemove = (initiative?.project_ids || [])?.length > 1; + + // handler const handleCopyText = () => copyUrlToClipboard(projectLink).then(() => setToast({ @@ -46,7 +51,7 @@ export const QuickActions: React.FC = observer((props: Props) => { action: () => updateInitiative(workspaceSlug, initiativeId, { project_ids: [project.id] }), title: "Remove", icon: Trash2, - shouldRender: true, + shouldRender: shouldRenderRemove, }, ]; diff --git a/web/ee/components/initiatives/detail/projects/root.tsx b/web/ee/components/initiatives/detail/projects/root.tsx index 61940bde8e..bc86261450 100644 --- a/web/ee/components/initiatives/detail/projects/root.tsx +++ b/web/ee/components/initiatives/detail/projects/root.tsx @@ -3,10 +3,14 @@ import React, { FC, useState } from "react"; import { observer } from "mobx-react"; // Plane -import { Collapsible } from "@plane/ui"; +import { Collapsible, setToast, TOAST_TYPE } from "@plane/ui"; +// components +import { ProjectMultiSelectModal } from "@/components/project"; +// hooks +import { useProject } from "@/hooks/store"; // Plane-web import { useInitiatives } from "@/plane-web/hooks/store/use-initiatives"; -// +// local components import { InitiativeProjectsCollapsibleContent } from "./content"; import { InitiativeProjectsCollapsibleTitle } from "./title"; @@ -20,11 +24,11 @@ export const ProjectsCollapsible: FC = observer((props) => { const { workspaceSlug, initiativeId, disabled = false } = props; // store hooks const [isOpen, setIsOpen] = useState(false); - - // store hooks + const [isModalOpen, setIsModalOpen] = useState(false); const { - initiative: { getInitiativeById }, + initiative: { getInitiativeById, updateInitiative }, } = useInitiatives(); + const { workspaceProjectIds } = useProject(); // derived values const initiative = getInitiativeById(initiativeId); @@ -33,19 +37,68 @@ export const ProjectsCollapsible: FC = observer((props) => { if (!projectsIds) return <>; + const handleOnClick = () => { + setIsModalOpen(true); + }; + + const handleProjectsUpdate = async (initiativeProjectIds: string[]) => { + if (!initiativeId) return; + + if (initiativeProjectIds.length === 0) { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Please select at least one project.", + }); + return; + } + + await updateInitiative(workspaceSlug?.toString(), initiativeId, { project_ids: initiativeProjectIds }) + .then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: `Initiative projects updated successfully.`, + }); + }) + .catch((error) => { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: error?.error ?? `Failed to update initiative projects. Please try again!`, + }); + }); + }; + return ( - setIsOpen((prevState) => !prevState)} - title={} - buttonClassName="w-full" - > - + setIsModalOpen(false)} + onSubmit={handleProjectsUpdate} + selectedProjectIds={projectsIds} + projectIds={workspaceProjectIds || []} /> - + setIsOpen((prevState) => !prevState)} + title={ + + } + buttonClassName="w-full" + > + + + ); }); diff --git a/web/ee/components/initiatives/detail/projects/title.tsx b/web/ee/components/initiatives/detail/projects/title.tsx index 97c9d31ecb..521ec76f1c 100644 --- a/web/ee/components/initiatives/detail/projects/title.tsx +++ b/web/ee/components/initiatives/detail/projects/title.tsx @@ -1,19 +1,19 @@ "use client"; import React, { FC, useMemo } from "react"; import { observer } from "mobx-react"; +import { Plus } from "lucide-react"; // Plane import { CollapsibleButton } from "@plane/ui"; -// components -import { IssueLinksActionButton } from "@/components/issues/issue-detail-widgets"; type Props = { isOpen: boolean; projectsIds: string[] | undefined; + handleOnClick: () => void; disabled: boolean; }; export const InitiativeProjectsCollapsibleTitle: FC = observer((props) => { - const { isOpen, projectsIds, disabled } = props; + const { isOpen, projectsIds, handleOnClick, disabled } = props; const projectsCount = projectsIds?.length ?? 0; @@ -32,7 +32,13 @@ export const InitiativeProjectsCollapsibleTitle: FC = observer((props) => isOpen={isOpen} title="Projects" indicatorElement={indicatorElement} - actionItemElement={!disabled && } + actionItemElement={ + !disabled && ( + + ) + } /> ); }); diff --git a/web/ee/components/initiatives/detail/section/root.tsx b/web/ee/components/initiatives/detail/section/root.tsx index dcc43fe978..e146443e12 100644 --- a/web/ee/components/initiatives/detail/section/root.tsx +++ b/web/ee/components/initiatives/detail/section/root.tsx @@ -58,62 +58,60 @@ export const InitiativeDetailSection = observer((props: Props) => { "max-w-2/3": !initiativesSidebarCollapsed, })} > - - +
+ -
- - -
- -
- - - Add link -
- } - disabled={disabled} - /> - - - Attach -
- } - disabled={disabled} - /> -
+ +
+ +
+ + + Add link +
+ } + disabled={disabled} + /> + + + Attach +
+ } + disabled={disabled} + />
- - {initiativesLinks && initiativesLinks.length > 0 && ( - - )} - {initiativesAttachments && initiativesAttachments.length > 0 && ( - - )} -
- + + {initiativesLinks && initiativesLinks.length > 0 && ( + + )} + {initiativesAttachments && initiativesAttachments.length > 0 && ( + + )} + +
); }); diff --git a/web/ee/components/initiatives/initiative-details/sidebar/acitivity-tab/activity-icon.tsx b/web/ee/components/initiatives/initiative-details/sidebar/acitivity-tab/activity-icon.tsx index 14380ebb71..cd9f214734 100644 --- a/web/ee/components/initiatives/initiative-details/sidebar/acitivity-tab/activity-icon.tsx +++ b/web/ee/components/initiatives/initiative-details/sidebar/acitivity-tab/activity-icon.tsx @@ -1,7 +1,7 @@ "use client"; import React from "react"; -import { CalendarDays, Link, MessageSquare, Network, Paperclip, Users } from "lucide-react"; +import { Briefcase, CalendarDays, Link, MessageSquare, Network, Paperclip, Users } from "lucide-react"; import { InitiativeIcon } from "@plane/ui"; import { TInitiativeActivity } from "@/plane-web/types/initiative"; @@ -14,6 +14,7 @@ const iconMap = { end_date: CalendarDays, link: Link, attachment: Paperclip, + projects: Briefcase, } as const; interface InitiativeActivityIconProps { diff --git a/web/ee/components/initiatives/initiative-details/sidebar/acitivity-tab/activity-items.tsx b/web/ee/components/initiatives/initiative-details/sidebar/acitivity-tab/activity-items.tsx index e904d57be7..1c4e122f31 100644 --- a/web/ee/components/initiatives/initiative-details/sidebar/acitivity-tab/activity-items.tsx +++ b/web/ee/components/initiatives/initiative-details/sidebar/acitivity-tab/activity-items.tsx @@ -117,6 +117,17 @@ export const InitiativeActivityItem: FC = observer((pro )} ); + case "projects": + return ( + <> + {activity.new_value === "" ? `removed a project` : `added a project`}{" "} + {activity.new_value === "" ? ( + {activity.old_value} + ) : ( + {activity.new_value} + )} + + ); default: return <>; } diff --git a/web/ee/components/initiatives/initiative-details/sidebar/properties-tab/root.tsx b/web/ee/components/initiatives/initiative-details/sidebar/properties-tab/root.tsx index 44a72e6b7d..b9d618bd37 100644 --- a/web/ee/components/initiatives/initiative-details/sidebar/properties-tab/root.tsx +++ b/web/ee/components/initiatives/initiative-details/sidebar/properties-tab/root.tsx @@ -3,6 +3,7 @@ import React from "react"; import { observer } from "mobx-react"; import { Briefcase, Calendar, UserCircle2 } from "lucide-react"; +import { setToast, TOAST_TYPE } from "@plane/ui"; // components import { DateRangeDropdown, MemberDropdown, ProjectDropdown } from "@/components/dropdowns"; import { ButtonAvatars } from "@/components/dropdowns/member/avatar"; @@ -44,11 +45,36 @@ export const InitiativeSidebarPropertiesRoot: React.FC - updateInitiative && + const handleProjects = (ids: string | string[]) => { + const projectIds = ids ? (Array.isArray(ids) ? ids : [ids]) : []; + + if (projectIds.length === 0) { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Please select at least one project.", + }); + return; + } + updateInitiative(workspaceSlug.toString(), initiative.id, { - project_ids: ids ? (Array.isArray(ids) ? ids : [ids]) : null, - }); + project_ids: projectIds, + }) + .then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: "Initiative projects updated successfully.", + }); + }) + .catch((error) => { + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: error?.error ?? "Failed to update initiative projects. Please try again!", + }); + }); + }; return (
diff --git a/web/ee/constants/dashboard.ts b/web/ee/constants/dashboard.ts index bb9b38590f..8cb4c0ea28 100644 --- a/web/ee/constants/dashboard.ts +++ b/web/ee/constants/dashboard.ts @@ -41,12 +41,12 @@ export const SIDEBAR_WORKSPACE_MENU: Partial item !== undefined); export { SIDEBAR_USER_MENU_ITEMS }; diff --git a/web/ee/services/initiative.service.ts b/web/ee/services/initiative.service.ts index 6921d3c710..a9c83dfaae 100644 --- a/web/ee/services/initiative.service.ts +++ b/web/ee/services/initiative.service.ts @@ -203,7 +203,7 @@ export class InitiativeService extends APIService { } async deleteInitiativeComment(workspaceSlug: string, initiativeId: string, commentId: string): Promise { - return this.delete(`/api/workspaces/${workspaceSlug}/initiatives/${initiativeId}/links/${commentId}/`) + return this.delete(`/api/workspaces/${workspaceSlug}/initiatives/${initiativeId}/comments/${commentId}/`) .then((res) => res?.data) .catch((err) => { throw err?.response?.data; diff --git a/web/ee/store/initiatives/initiatives.store.ts b/web/ee/store/initiatives/initiatives.store.ts index cd2aed662f..2e27ae69ef 100644 --- a/web/ee/store/initiatives/initiatives.store.ts +++ b/web/ee/store/initiatives/initiatives.store.ts @@ -237,7 +237,7 @@ export class InitiativeStore implements IInitiativeStore { }); await this.initiativeService.updateInitiative(workspaceSlug, initiativeId, payload); - + this.initiativeCommentActivities.fetchActivities(workspaceSlug, initiativeId); return; } catch (error) { runInAction(() => {