mirror of
https://github.com/makeplane/plane.git
synced 2026-07-13 22:09:12 +02:00
[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
This commit is contained in:
committed by
GitHub
parent
c1bad55295
commit
fdcd63dc80
@@ -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> = (props) => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="space-y-5 p-5">
|
||||
<div className="flex items-center gap-x-3">
|
||||
<h3 className="text-xl font-medium text-custom-text-200">
|
||||
{initiativeDetail ? "Update" : "Create"} Initiative
|
||||
</h3>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-1">
|
||||
<div className="space-y-3 p-5 pb-4">
|
||||
<h3 className="text-xl font-medium text-custom-text-200">{formData.id ? "Update" : "Create"} Initiative</h3>
|
||||
|
||||
<div className={cn("flex items-center gap-2 w-full", errors.name && "items-start")}>
|
||||
<div className="space-y-1 flew-grow w-full">
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
value={formData?.name}
|
||||
value={formData.name}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
<span className="text-xs text-red-500">{errors?.name}</span>
|
||||
{errors.name && <div className="text-red-500 text-xs">{errors.name}</div>}
|
||||
</div>
|
||||
<div className="border-[0.5px] border-custom-border-200 rounded-lg relative">
|
||||
<RichTextEditor
|
||||
id="initiative-modal-editor"
|
||||
initialValue={
|
||||
!formData?.description_html || formData?.description_html === ""
|
||||
? "<p></p>"
|
||||
: 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);
|
||||
</div>
|
||||
<RichTextEditor
|
||||
id="initiative-modal-editor"
|
||||
initialValue={
|
||||
!formData?.description_html || formData?.description_html === "" ? "<p></p>" : 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.");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="h-7">
|
||||
<ProjectDropdown
|
||||
buttonVariant={"border-with-text"}
|
||||
onChange={(val) => {
|
||||
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.");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="h-7">
|
||||
<ProjectDropdown
|
||||
buttonVariant={"border-with-text"}
|
||||
onChange={(val) => {
|
||||
handleFormDataChange("project_ids", val);
|
||||
}}
|
||||
value={formData.project_ids || []}
|
||||
multiple
|
||||
showTooltip
|
||||
tabIndex={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<DateRangeDropdown
|
||||
<DateRangeDropdown
|
||||
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}
|
||||
/>
|
||||
|
||||
<div className="h-7">
|
||||
<MemberDropdown
|
||||
value={formData?.lead ?? ""}
|
||||
onChange={(val) => 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
|
||||
/>
|
||||
|
||||
<div className="h-7">
|
||||
<MemberDropdown
|
||||
value={formData?.lead ?? ""}
|
||||
onChange={(val) => handleFormDataChange("lead", val)}
|
||||
multiple={false}
|
||||
buttonVariant="border-with-text"
|
||||
placeholder="Lead"
|
||||
tabIndex={4}
|
||||
showUserDetails
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 <Spinner />;
|
||||
@@ -41,6 +46,9 @@ export const ProjectItem = observer((props: Props) => {
|
||||
}
|
||||
quickActionElement={
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{projectDetails.total_issues > 0 && (
|
||||
<span className="text-sm font-medium text-custom-text-400 px-1">{`${projectDetails.completed_issues}/${projectDetails.total_issues}`}</span>
|
||||
)}
|
||||
<Attributes
|
||||
project={projectDetails}
|
||||
isArchived={projectDetails.archived_at !== null}
|
||||
@@ -48,7 +56,12 @@ export const ProjectItem = observer((props: Props) => {
|
||||
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,
|
||||
}}
|
||||
/>
|
||||
<div className={cn("hidden md:flex")}>
|
||||
<QuickActions
|
||||
|
||||
@@ -17,13 +17,18 @@ type Props = {
|
||||
|
||||
export const QuickActions: React.FC<Props> = 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<Props> = observer((props: Props) => {
|
||||
action: () => updateInitiative(workspaceSlug, initiativeId, { project_ids: [project.id] }),
|
||||
title: "Remove",
|
||||
icon: Trash2,
|
||||
shouldRender: true,
|
||||
shouldRender: shouldRenderRemove,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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<Props> = 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<Props> = 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 (
|
||||
<Collapsible
|
||||
isOpen={isOpen}
|
||||
onToggle={() => setIsOpen((prevState) => !prevState)}
|
||||
title={<InitiativeProjectsCollapsibleTitle isOpen={isOpen} projectsIds={projectsIds} disabled={disabled} />}
|
||||
buttonClassName="w-full"
|
||||
>
|
||||
<InitiativeProjectsCollapsibleContent
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectIds={projectsIds}
|
||||
initiativeId={initiativeId}
|
||||
disabled={disabled}
|
||||
<>
|
||||
<ProjectMultiSelectModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
onSubmit={handleProjectsUpdate}
|
||||
selectedProjectIds={projectsIds}
|
||||
projectIds={workspaceProjectIds || []}
|
||||
/>
|
||||
</Collapsible>
|
||||
<Collapsible
|
||||
isOpen={isOpen}
|
||||
onToggle={() => setIsOpen((prevState) => !prevState)}
|
||||
title={
|
||||
<InitiativeProjectsCollapsibleTitle
|
||||
isOpen={isOpen}
|
||||
projectsIds={projectsIds}
|
||||
disabled={disabled}
|
||||
handleOnClick={handleOnClick}
|
||||
/>
|
||||
}
|
||||
buttonClassName="w-full"
|
||||
>
|
||||
<InitiativeProjectsCollapsibleContent
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectIds={projectsIds}
|
||||
initiativeId={initiativeId}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</Collapsible>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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<Props> = 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<Props> = observer((props) =>
|
||||
isOpen={isOpen}
|
||||
title="Projects"
|
||||
indicatorElement={indicatorElement}
|
||||
actionItemElement={!disabled && <IssueLinksActionButton disabled={disabled} />}
|
||||
actionItemElement={
|
||||
!disabled && (
|
||||
<button type="button" onClick={handleOnClick} disabled={disabled}>
|
||||
<Plus className="h-4 w-4" />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -58,62 +58,60 @@ export const InitiativeDetailSection = observer((props: Props) => {
|
||||
"max-w-2/3": !initiativesSidebarCollapsed,
|
||||
})}
|
||||
>
|
||||
<ContentWrapper variant={ERowVariant.REGULAR}>
|
||||
<IssueLinkCreateUpdateModal
|
||||
isModalOpen={isLinkModalOpen}
|
||||
handleOnClose={handleOnClose}
|
||||
linkOperations={linkOperations}
|
||||
<IssueLinkCreateUpdateModal
|
||||
isModalOpen={isLinkModalOpen}
|
||||
handleOnClose={handleOnClose}
|
||||
linkOperations={linkOperations}
|
||||
/>
|
||||
<div className="rounded-lg flex flex-col w-full gap-3 space-y-4">
|
||||
<InitiativeTitleInput
|
||||
value={initiative.name}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
isSubmitting={isSubmitting}
|
||||
setIsSubmitting={setIsSubmitting}
|
||||
initiativeId={initiative.id}
|
||||
/>
|
||||
<div className="rounded-lg flex flex-col w-full gap-3 space-y-4">
|
||||
<InitiativeTitleInput
|
||||
value={initiative.name}
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
isSubmitting={isSubmitting}
|
||||
setIsSubmitting={setIsSubmitting}
|
||||
initiativeId={initiative.id}
|
||||
/>
|
||||
<InitiativeDescriptionInput
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
initiativeId={initiative.id}
|
||||
initialValue={initiative.description_html ?? undefined}
|
||||
setIsSubmitting={setIsSubmitting}
|
||||
containerClassName="min-h-[120px] w-full border-none"
|
||||
/>
|
||||
<div className="flex justify-between">
|
||||
<InitiativeReactions workspaceSlug={workspaceSlug.toString()} initiativeId={initiative.id} />
|
||||
<div className="flex items-center gap-2">
|
||||
<InitiativeLinksActionButton
|
||||
customButton={
|
||||
<div className="flex items-center gap-1 p-2 text-custom-text-300 hover:text-custom-text-100">
|
||||
<Link className="h-3.5 w-3.5 flex-shrink-0" strokeWidth={2} />
|
||||
<span className="text-sm font-medium">Add link</span>
|
||||
</div>
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<InitiativeAttachmentActionButton
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
initiativeId={initiative.id}
|
||||
customButton={
|
||||
<div className="flex items-center gap-1 p-2 text-custom-text-300 hover:text-custom-text-100">
|
||||
<Paperclip className="h-3.5 w-3.5 flex-shrink-0" strokeWidth={2} />
|
||||
<span className="text-sm font-medium">Attach</span>
|
||||
</div>
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<InitiativeDescriptionInput
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
initiativeId={initiative.id}
|
||||
initialValue={initiative.description_html ?? undefined}
|
||||
setIsSubmitting={setIsSubmitting}
|
||||
containerClassName="min-h-[120px] w-full border-none"
|
||||
/>
|
||||
<div className="flex justify-between">
|
||||
<InitiativeReactions workspaceSlug={workspaceSlug.toString()} initiativeId={initiative.id} />
|
||||
<div className="flex items-center gap-2">
|
||||
<InitiativeLinksActionButton
|
||||
customButton={
|
||||
<div className="flex items-center gap-1 p-2 text-custom-text-300 hover:text-custom-text-100">
|
||||
<Link className="h-3.5 w-3.5 flex-shrink-0" strokeWidth={2} />
|
||||
<span className="text-sm font-medium">Add link</span>
|
||||
</div>
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<InitiativeAttachmentActionButton
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
initiativeId={initiative.id}
|
||||
customButton={
|
||||
<div className="flex items-center gap-1 p-2 text-custom-text-300 hover:text-custom-text-100">
|
||||
<Paperclip className="h-3.5 w-3.5 flex-shrink-0" strokeWidth={2} />
|
||||
<span className="text-sm font-medium">Attach</span>
|
||||
</div>
|
||||
}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<InitiativeProgressRoot workspaceSlug={workspaceSlug.toString()} initiativeId={initiative.id} />
|
||||
{initiativesLinks && initiativesLinks.length > 0 && (
|
||||
<LinksCollapsible workspaceSlug={workspaceSlug.toString()} initiativeId={initiative.id} />
|
||||
)}
|
||||
{initiativesAttachments && initiativesAttachments.length > 0 && (
|
||||
<AttachmentsCollapsible workspaceSlug={workspaceSlug.toString()} initiativeId={initiative.id} />
|
||||
)}
|
||||
<ProjectsCollapsible workspaceSlug={workspaceSlug.toString()} initiativeId={initiative.id} />
|
||||
</div>
|
||||
</ContentWrapper>
|
||||
<InitiativeProgressRoot workspaceSlug={workspaceSlug.toString()} initiativeId={initiative.id} />
|
||||
{initiativesLinks && initiativesLinks.length > 0 && (
|
||||
<LinksCollapsible workspaceSlug={workspaceSlug.toString()} initiativeId={initiative.id} />
|
||||
)}
|
||||
{initiativesAttachments && initiativesAttachments.length > 0 && (
|
||||
<AttachmentsCollapsible workspaceSlug={workspaceSlug.toString()} initiativeId={initiative.id} />
|
||||
)}
|
||||
<ProjectsCollapsible workspaceSlug={workspaceSlug.toString()} initiativeId={initiative.id} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -117,6 +117,17 @@ export const InitiativeActivityItem: FC<TInitiativeActivityItem> = observer((pro
|
||||
)}
|
||||
</>
|
||||
);
|
||||
case "projects":
|
||||
return (
|
||||
<>
|
||||
{activity.new_value === "" ? `removed a project` : `added a project`}{" "}
|
||||
{activity.new_value === "" ? (
|
||||
<span className="font-medium text-custom-text-100">{activity.old_value}</span>
|
||||
) : (
|
||||
<span className="font-medium text-custom-text-100">{activity.new_value}</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
|
||||
@@ -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<TInitiativeSidebarPropert
|
||||
lead: id,
|
||||
});
|
||||
|
||||
const handleProjects = (ids: string | string[]) =>
|
||||
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 (
|
||||
<div className={`mb-2 space-y-2.5 ${disabled ? "opacity-60" : ""}`}>
|
||||
|
||||
@@ -41,12 +41,12 @@ export const SIDEBAR_WORKSPACE_MENU: Partial<Record<TSidebarWorkspaceMenuItemKey
|
||||
};
|
||||
|
||||
export const SIDEBAR_WORKSPACE_MENU_ITEMS: TSidebarWorkspaceMenuItems[] = [
|
||||
SIDEBAR_WORKSPACE_MENU?.initiatives,
|
||||
SIDEBAR_WORKSPACE_MENU?.teams,
|
||||
SIDEBAR_WORKSPACE_MENU?.projects,
|
||||
SIDEBAR_WORKSPACE_MENU?.["all-issues"],
|
||||
SIDEBAR_WORKSPACE_MENU?.["active-cycles"],
|
||||
SIDEBAR_WORKSPACE_MENU?.analytics,
|
||||
SIDEBAR_WORKSPACE_MENU?.initiatives,
|
||||
].filter((item): item is TSidebarWorkspaceMenuItems => item !== undefined);
|
||||
|
||||
export { SIDEBAR_USER_MENU_ITEMS };
|
||||
|
||||
@@ -203,7 +203,7 @@ export class InitiativeService extends APIService {
|
||||
}
|
||||
|
||||
async deleteInitiativeComment(workspaceSlug: string, initiativeId: string, commentId: string): Promise<void> {
|
||||
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;
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
Reference in New Issue
Block a user