mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
chore: refactored and resolved build issues on the issues and issue detail page (#3340)
* fix: handled undefined issue_id in list layout * dev: issue detail store and optimization * dev: issue filter and list operations * fix: typo on labels update * dev: Handled all issues in the list layout in project issues * dev: handled kanban and auick add issue in swimlanes * chore: fixed peekoverview in kanban * chore: fixed peekoverview in calendar * chore: fixed peekoverview in gantt * chore: updated quick add in the gantt chart * chore: handled issue detail properties and resolved build issues --------- Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
This commit is contained in:
parent
09603cf189
commit
73eed69aa6
@ -77,7 +77,7 @@ class DynamicBaseSerializer(BaseSerializer):
|
|||||||
"assignees": UserLiteSerializer,
|
"assignees": UserLiteSerializer,
|
||||||
"labels": LabelSerializer,
|
"labels": LabelSerializer,
|
||||||
"issue_cycle": CycleIssueSerializer,
|
"issue_cycle": CycleIssueSerializer,
|
||||||
"parent": IssueFlatSerializer,
|
"parent": IssueSerializer,
|
||||||
}
|
}
|
||||||
|
|
||||||
self.fields[field] = expansion[field](many=True if field in ["members", "assignees", "labels", "issue_cycle"] else False)
|
self.fields[field] = expansion[field](many=True if field in ["members", "assignees", "labels", "issue_cycle"] else False)
|
||||||
@ -119,6 +119,7 @@ class DynamicBaseSerializer(BaseSerializer):
|
|||||||
"assignees": UserLiteSerializer,
|
"assignees": UserLiteSerializer,
|
||||||
"labels": LabelSerializer,
|
"labels": LabelSerializer,
|
||||||
"issue_cycle": CycleIssueSerializer,
|
"issue_cycle": CycleIssueSerializer,
|
||||||
|
"parent": IssueSerializer,
|
||||||
}
|
}
|
||||||
# Check if field in expansion then expand the field
|
# Check if field in expansion then expand the field
|
||||||
if expand in expansion:
|
if expand in expansion:
|
||||||
|
@ -175,7 +175,7 @@ class IssueCreateSerializer(BaseSerializer):
|
|||||||
|
|
||||||
def update(self, instance, validated_data):
|
def update(self, instance, validated_data):
|
||||||
assignees = validated_data.pop("assignee_ids", None)
|
assignees = validated_data.pop("assignee_ids", None)
|
||||||
labels = validated_data.pop("labels_ids", None)
|
labels = validated_data.pop("label_ids", None)
|
||||||
|
|
||||||
# Related models
|
# Related models
|
||||||
project_id = instance.project_id
|
project_id = instance.project_id
|
||||||
|
2
packages/types/src/issues.d.ts
vendored
2
packages/types/src/issues.d.ts
vendored
@ -222,7 +222,7 @@ export type GroupByColumnTypes =
|
|||||||
export interface IGroupByColumn {
|
export interface IGroupByColumn {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
Icon: ReactElement | undefined;
|
icon: ReactElement | undefined;
|
||||||
payload: Partial<TIssue>;
|
payload: Partial<TIssue>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,5 +17,5 @@ export type TIssueReactionMap = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type TIssueReactionIdMap = {
|
export type TIssueReactionIdMap = {
|
||||||
[issue_id: string]: string[];
|
[issue_id: string]: { [reaction: string]: string[] };
|
||||||
};
|
};
|
||||||
|
13
packages/types/src/view-props.d.ts
vendored
13
packages/types/src/view-props.d.ts
vendored
@ -1,4 +1,9 @@
|
|||||||
export type TIssueLayouts = "list" | "kanban" | "calendar" | "spreadsheet" | "gantt_chart";
|
export type TIssueLayouts =
|
||||||
|
| "list"
|
||||||
|
| "kanban"
|
||||||
|
| "calendar"
|
||||||
|
| "spreadsheet"
|
||||||
|
| "gantt_chart";
|
||||||
|
|
||||||
export type TIssueGroupByOptions =
|
export type TIssueGroupByOptions =
|
||||||
| "state"
|
| "state"
|
||||||
@ -108,10 +113,16 @@ export interface IIssueDisplayProperties {
|
|||||||
updated_on?: boolean;
|
updated_on?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TIssueKanbanFilters = {
|
||||||
|
group_by: string[];
|
||||||
|
sub_group_by: string[];
|
||||||
|
};
|
||||||
|
|
||||||
export interface IIssueFilters {
|
export interface IIssueFilters {
|
||||||
filters: IIssueFilterOptions | undefined;
|
filters: IIssueFilterOptions | undefined;
|
||||||
displayFilters: IIssueDisplayFilterOptions | undefined;
|
displayFilters: IIssueDisplayFilterOptions | undefined;
|
||||||
displayProperties: IIssueDisplayProperties | undefined;
|
displayProperties: IIssueDisplayProperties | undefined;
|
||||||
|
kanbanFilters: TIssueKanbanFilters | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IIssueFiltersResponse {
|
export interface IIssueFiltersResponse {
|
||||||
|
6
packages/types/src/workspace-views.d.ts
vendored
6
packages/types/src/workspace-views.d.ts
vendored
@ -29,4 +29,8 @@ export interface IWorkspaceView {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TStaticViewTypes = "all-issues" | "assigned" | "created" | "subscribed";
|
export type TStaticViewTypes =
|
||||||
|
| "all-issues"
|
||||||
|
| "assigned"
|
||||||
|
| "created"
|
||||||
|
| "subscribed";
|
||||||
|
@ -3,5 +3,4 @@ export * from "./modals";
|
|||||||
export * from "./sidebar";
|
export * from "./sidebar";
|
||||||
export * from "./theme";
|
export * from "./theme";
|
||||||
export * from "./activity";
|
export * from "./activity";
|
||||||
export * from "./reaction-selector";
|
|
||||||
export * from "./image-picker-popover";
|
export * from "./image-picker-popover";
|
||||||
|
@ -126,7 +126,7 @@ export const SidebarProgressStats: React.FC<Props> = ({
|
|||||||
</Tab.List>
|
</Tab.List>
|
||||||
<Tab.Panels className="flex w-full items-center justify-between text-custom-text-200">
|
<Tab.Panels className="flex w-full items-center justify-between text-custom-text-200">
|
||||||
<Tab.Panel as="div" className="flex h-44 w-full flex-col gap-1.5 overflow-y-auto pt-3.5">
|
<Tab.Panel as="div" className="flex h-44 w-full flex-col gap-1.5 overflow-y-auto pt-3.5">
|
||||||
{distribution.assignees.length > 0 ? (
|
{distribution?.assignees.length > 0 ? (
|
||||||
distribution.assignees.map((assignee, index) => {
|
distribution.assignees.map((assignee, index) => {
|
||||||
if (assignee.assignee_id)
|
if (assignee.assignee_id)
|
||||||
return (
|
return (
|
||||||
|
@ -7,7 +7,7 @@ import { useChart } from "components/gantt-chart/hooks";
|
|||||||
// ui
|
// ui
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { GanttInlineCreateIssueForm, IssueGanttSidebarBlock } from "components/issues";
|
import { GanttQuickAddIssueForm, IssueGanttSidebarBlock } from "components/issues";
|
||||||
// helpers
|
// helpers
|
||||||
import { findTotalDaysInRange } from "helpers/date-time.helper";
|
import { findTotalDaysInRange } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
@ -169,7 +169,7 @@ export const IssueGanttSidebar: React.FC<Props> = (props) => {
|
|||||||
{droppableProvided.placeholder}
|
{droppableProvided.placeholder}
|
||||||
</>
|
</>
|
||||||
{enableQuickIssueCreate && !disableIssueCreation && (
|
{enableQuickIssueCreate && !disableIssueCreation && (
|
||||||
<GanttInlineCreateIssueForm quickAddCallback={quickAddCallback} viewId={viewId} />
|
<GanttQuickAddIssueForm quickAddCallback={quickAddCallback} viewId={viewId} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -7,7 +7,13 @@ import { AlertTriangle, CheckCircle2, Clock, Copy, ExternalLink, Inbox, XCircle
|
|||||||
// hooks
|
// hooks
|
||||||
import { useProjectState, useUser, useInboxIssues } from "hooks/store";
|
import { useProjectState, useUser, useInboxIssues } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { IssueDescriptionForm, IssueDetailsSidebar, IssueReaction, IssueUpdateStatus } from "components/issues";
|
import {
|
||||||
|
IssueDescriptionForm,
|
||||||
|
// FIXME: have to replace this once the issue details page is ready --issue-detail--
|
||||||
|
// IssueDetailsSidebar,
|
||||||
|
// IssueReaction,
|
||||||
|
IssueUpdateStatus,
|
||||||
|
} from "components/issues";
|
||||||
import { InboxIssueActivity } from "components/inbox";
|
import { InboxIssueActivity } from "components/inbox";
|
||||||
// ui
|
// ui
|
||||||
import { Loader, StateGroupIcon } from "@plane/ui";
|
import { Loader, StateGroupIcon } from "@plane/ui";
|
||||||
@ -226,7 +232,9 @@ export const InboxMainContent: React.FC = observer(() => {
|
|||||||
)}
|
)}
|
||||||
<IssueUpdateStatus isSubmitting={isSubmitting} issueDetail={issueDetails} />
|
<IssueUpdateStatus isSubmitting={isSubmitting} issueDetail={issueDetails} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
|
{/* FIXME: have to replace this once the issue details page is ready --issue-detail-- */}
|
||||||
|
{/* <div>
|
||||||
<IssueDescriptionForm
|
<IssueDescriptionForm
|
||||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
@ -239,26 +247,28 @@ export const InboxMainContent: React.FC = observer(() => {
|
|||||||
handleFormSubmit={submitChanges}
|
handleFormSubmit={submitChanges}
|
||||||
isAllowed={isAllowed || currentUser?.id === issueDetails.created_by}
|
isAllowed={isAllowed || currentUser?.id === issueDetails.created_by}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
{workspaceSlug && projectId && (
|
{/* FIXME: have to replace this once the issue details page is ready --issue-detail-- */}
|
||||||
|
{/* {workspaceSlug && projectId && (
|
||||||
<IssueReaction
|
<IssueReaction
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
projectId={projectId.toString()}
|
projectId={projectId.toString()}
|
||||||
issueId={issueDetails.id}
|
issueId={issueDetails.id}
|
||||||
/>
|
/>
|
||||||
)}
|
)} */}
|
||||||
<InboxIssueActivity issueDetails={issueDetails} />
|
<InboxIssueActivity issueDetails={issueDetails} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="basis-1/3 space-y-5 border-custom-border-200 py-5">
|
<div className="basis-1/3 space-y-5 border-custom-border-200 py-5">
|
||||||
<IssueDetailsSidebar
|
{/* FIXME: have to replace this once the issue details page is ready --issue-detail-- */}
|
||||||
|
{/* <IssueDetailsSidebar
|
||||||
control={control}
|
control={control}
|
||||||
issueDetail={issueDetails}
|
issueDetail={issueDetails}
|
||||||
submitChanges={submitChanges}
|
submitChanges={submitChanges}
|
||||||
watch={watch}
|
watch={watch}
|
||||||
fieldsToShow={["assignee", "priority", "estimate", "dueDate", "label", "state"]}
|
fieldsToShow={["assignee", "priority", "estimate", "dueDate", "label", "state"]}
|
||||||
/>
|
/> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
@ -11,15 +11,15 @@ import { TAttachmentOperations } from "./root";
|
|||||||
type TAttachmentOperationsModal = Exclude<TAttachmentOperations, "remove">;
|
type TAttachmentOperationsModal = Exclude<TAttachmentOperations, "remove">;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
workspaceSlug: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
handleAttachmentOperations: TAttachmentOperationsModal;
|
handleAttachmentOperations: TAttachmentOperationsModal;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueAttachmentUpload: React.FC<Props> = observer((props) => {
|
export const IssueAttachmentUpload: React.FC<Props> = observer((props) => {
|
||||||
const { disabled = false, handleAttachmentOperations } = props;
|
const { workspaceSlug, disabled = false, handleAttachmentOperations } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const {
|
const {
|
||||||
router: { workspaceSlug },
|
|
||||||
config: { envConfig },
|
config: { envConfig },
|
||||||
} = useApplication();
|
} = useApplication();
|
||||||
// states
|
// states
|
||||||
|
@ -1,13 +1,17 @@
|
|||||||
import { FC, useMemo } from "react";
|
import { FC, useMemo } from "react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useApplication, useIssueDetail } from "hooks/store";
|
import { useIssueDetail } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { IssueAttachmentUpload } from "./attachment-upload";
|
import { IssueAttachmentUpload } from "./attachment-upload";
|
||||||
import { IssueAttachmentsList } from "./attachments-list";
|
import { IssueAttachmentsList } from "./attachments-list";
|
||||||
|
|
||||||
export type TIssueAttachmentRoot = {
|
export type TIssueAttachmentRoot = {
|
||||||
isEditable: boolean;
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
issueId: string;
|
||||||
|
is_archived: boolean;
|
||||||
|
is_editable: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TAttachmentOperations = {
|
export type TAttachmentOperations = {
|
||||||
@ -17,20 +21,17 @@ export type TAttachmentOperations = {
|
|||||||
|
|
||||||
export const IssueAttachmentRoot: FC<TIssueAttachmentRoot> = (props) => {
|
export const IssueAttachmentRoot: FC<TIssueAttachmentRoot> = (props) => {
|
||||||
// props
|
// props
|
||||||
const { isEditable } = props;
|
const { workspaceSlug, projectId, issueId, is_archived, is_editable } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const {
|
const { createAttachment, removeAttachment } = useIssueDetail();
|
||||||
router: { workspaceSlug, projectId },
|
|
||||||
} = useApplication();
|
|
||||||
const { peekIssue, createAttachment, removeAttachment } = useIssueDetail();
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
const handleAttachmentOperations: TAttachmentOperations = useMemo(
|
const handleAttachmentOperations: TAttachmentOperations = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
create: async (data: FormData) => {
|
create: async (data: FormData) => {
|
||||||
try {
|
try {
|
||||||
if (!workspaceSlug || !projectId || !peekIssue?.issueId) throw new Error("Missing required fields");
|
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields");
|
||||||
await createAttachment(workspaceSlug, projectId, peekIssue?.issueId, data);
|
await createAttachment(workspaceSlug, projectId, issueId, data);
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
message: "The attachment has been successfully uploaded",
|
message: "The attachment has been successfully uploaded",
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -46,8 +47,8 @@ export const IssueAttachmentRoot: FC<TIssueAttachmentRoot> = (props) => {
|
|||||||
},
|
},
|
||||||
remove: async (attachmentId: string) => {
|
remove: async (attachmentId: string) => {
|
||||||
try {
|
try {
|
||||||
if (!workspaceSlug || !projectId || !peekIssue?.issueId) throw new Error("Missing required fields");
|
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields");
|
||||||
await removeAttachment(workspaceSlug, projectId, peekIssue?.issueId, attachmentId);
|
await removeAttachment(workspaceSlug, projectId, issueId, attachmentId);
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
message: "The attachment has been successfully removed",
|
message: "The attachment has been successfully removed",
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -62,14 +63,18 @@ export const IssueAttachmentRoot: FC<TIssueAttachmentRoot> = (props) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[workspaceSlug, projectId, peekIssue, createAttachment, removeAttachment, setToastAlert]
|
[workspaceSlug, projectId, issueId, createAttachment, removeAttachment, setToastAlert]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative py-3 space-y-3">
|
<div className="relative py-3 space-y-3">
|
||||||
<h3 className="text-lg">Attachments</h3>
|
<h3 className="text-lg">Attachments</h3>
|
||||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||||
<IssueAttachmentUpload disabled={isEditable} handleAttachmentOperations={handleAttachmentOperations} />
|
<IssueAttachmentUpload
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
disabled={is_editable}
|
||||||
|
handleAttachmentOperations={handleAttachmentOperations}
|
||||||
|
/>
|
||||||
<IssueAttachmentsList handleAttachmentOperations={handleAttachmentOperations} />
|
<IssueAttachmentsList handleAttachmentOperations={handleAttachmentOperations} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,7 +5,7 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { useUser } from "hooks/store";
|
import { useUser } from "hooks/store";
|
||||||
import useCommentReaction from "hooks/use-comment-reaction";
|
import useCommentReaction from "hooks/use-comment-reaction";
|
||||||
// ui
|
// ui
|
||||||
import { ReactionSelector } from "components/core";
|
// import { ReactionSelector } from "components/core";
|
||||||
// helper
|
// helper
|
||||||
import { renderEmoji } from "helpers/emoji.helper";
|
import { renderEmoji } from "helpers/emoji.helper";
|
||||||
// types
|
// types
|
||||||
@ -47,7 +47,8 @@ export const CommentReaction: FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-2 flex items-center gap-1.5">
|
<div className="mt-2 flex items-center gap-1.5">
|
||||||
{!readonly && (
|
{/* FIXME: have to replace this once the issue details page is ready --issue-detail-- */}
|
||||||
|
{/* {!readonly && (
|
||||||
<ReactionSelector
|
<ReactionSelector
|
||||||
size="md"
|
size="md"
|
||||||
position="top"
|
position="top"
|
||||||
@ -58,7 +59,7 @@ export const CommentReaction: FC<Props> = observer((props) => {
|
|||||||
}
|
}
|
||||||
onSelect={handleReactionClick}
|
onSelect={handleReactionClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)} */}
|
||||||
|
|
||||||
{Object.keys(groupedReactions || {}).map(
|
{Object.keys(groupedReactions || {}).map(
|
||||||
(reaction) =>
|
(reaction) =>
|
||||||
|
@ -8,6 +8,7 @@ import { TextArea } from "@plane/ui";
|
|||||||
import { RichTextEditor } from "@plane/rich-text-editor";
|
import { RichTextEditor } from "@plane/rich-text-editor";
|
||||||
// types
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
|
import { TIssueOperations } from "./issue-detail";
|
||||||
// services
|
// services
|
||||||
import { FileService } from "services/file.service";
|
import { FileService } from "services/file.service";
|
||||||
import { useMention } from "hooks/store";
|
import { useMention } from "hooks/store";
|
||||||
@ -18,14 +19,16 @@ export interface IssueDescriptionFormValues {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IssueDetailsProps {
|
export interface IssueDetailsProps {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
issueId: string;
|
||||||
issue: {
|
issue: {
|
||||||
name: string;
|
name: string;
|
||||||
description_html: string;
|
description_html: string;
|
||||||
id: string;
|
id: string;
|
||||||
project_id?: string;
|
project_id?: string;
|
||||||
};
|
};
|
||||||
workspaceSlug: string;
|
issueOperations: TIssueOperations;
|
||||||
handleFormSubmit: (value: IssueDescriptionFormValues) => Promise<void>;
|
|
||||||
isAllowed: boolean;
|
isAllowed: boolean;
|
||||||
isSubmitting: "submitting" | "submitted" | "saved";
|
isSubmitting: "submitting" | "submitted" | "saved";
|
||||||
setIsSubmitting: (value: "submitting" | "submitted" | "saved") => void;
|
setIsSubmitting: (value: "submitting" | "submitted" | "saved") => void;
|
||||||
@ -34,7 +37,7 @@ export interface IssueDetailsProps {
|
|||||||
const fileService = new FileService();
|
const fileService = new FileService();
|
||||||
|
|
||||||
export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
|
export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
|
||||||
const { issue, handleFormSubmit, workspaceSlug, isAllowed, isSubmitting, setIsSubmitting } = props;
|
const { workspaceSlug, projectId, issueId, issue, issueOperations, isAllowed, isSubmitting, setIsSubmitting } = props;
|
||||||
// states
|
// states
|
||||||
const [characterLimit, setCharacterLimit] = useState(false);
|
const [characterLimit, setCharacterLimit] = useState(false);
|
||||||
|
|
||||||
@ -75,12 +78,12 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
|
|||||||
async (formData: Partial<TIssue>) => {
|
async (formData: Partial<TIssue>) => {
|
||||||
if (!formData?.name || formData?.name.length === 0 || formData?.name.length > 255) return;
|
if (!formData?.name || formData?.name.length === 0 || formData?.name.length > 255) return;
|
||||||
|
|
||||||
await handleFormSubmit({
|
await issueOperations.update(workspaceSlug, projectId, issueId, {
|
||||||
name: formData.name ?? "",
|
name: formData.name ?? "",
|
||||||
description_html: formData.description_html ?? "<p></p>",
|
description_html: formData.description_html ?? "<p></p>",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[handleFormSubmit]
|
[workspaceSlug, projectId, issueId, issueOperations]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -1,21 +1,22 @@
|
|||||||
export * from "./attachment";
|
export * from "./attachment";
|
||||||
export * from "./comment";
|
export * from "./comment";
|
||||||
export * from "./issue-modal";
|
export * from "./issue-modal";
|
||||||
export * from "./sidebar-select";
|
|
||||||
export * from "./view-select";
|
export * from "./view-select";
|
||||||
export * from "./activity";
|
export * from "./activity";
|
||||||
export * from "./delete-issue-modal";
|
export * from "./delete-issue-modal";
|
||||||
export * from "./description-form";
|
export * from "./description-form";
|
||||||
export * from "./issue-layouts";
|
export * from "./issue-layouts";
|
||||||
export * from "./peek-overview";
|
|
||||||
export * from "./main-content";
|
|
||||||
export * from "./parent-issues-list-modal";
|
export * from "./parent-issues-list-modal";
|
||||||
export * from "./sidebar";
|
|
||||||
export * from "./label";
|
export * from "./label";
|
||||||
export * from "./issue-reaction";
|
|
||||||
export * from "./confirm-issue-discard";
|
export * from "./confirm-issue-discard";
|
||||||
export * from "./issue-update-status";
|
export * from "./issue-update-status";
|
||||||
|
|
||||||
|
// issue details
|
||||||
|
export * from "./issue-detail";
|
||||||
|
|
||||||
|
export * from "./peek-overview";
|
||||||
|
|
||||||
// draft issue
|
// draft issue
|
||||||
export * from "./draft-issue-form";
|
export * from "./draft-issue-form";
|
||||||
export * from "./draft-issue-modal";
|
export * from "./draft-issue-modal";
|
||||||
@ -23,6 +24,3 @@ export * from "./delete-draft-issue-modal";
|
|||||||
|
|
||||||
// archived issue
|
// archived issue
|
||||||
export * from "./delete-archived-issue-modal";
|
export * from "./delete-archived-issue-modal";
|
||||||
|
|
||||||
// issue links
|
|
||||||
export * from "./issue-links";
|
|
||||||
|
103
web/components/issues/issue-detail/cycle-select.tsx
Normal file
103
web/components/issues/issue-detail/cycle-select.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import React, { ReactNode, useState } from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import useSWR from "swr";
|
||||||
|
// hooks
|
||||||
|
import { useCycle, useIssueDetail } from "hooks/store";
|
||||||
|
// ui
|
||||||
|
import { ContrastIcon, CustomSearchSelect, Spinner, Tooltip } from "@plane/ui";
|
||||||
|
// types
|
||||||
|
import type { TIssueOperations } from "./root";
|
||||||
|
|
||||||
|
type TIssueCycleSelect = {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
issueId: string;
|
||||||
|
issueOperations: TIssueOperations;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IssueCycleSelect: React.FC<TIssueCycleSelect> = observer((props) => {
|
||||||
|
const { workspaceSlug, projectId, issueId, issueOperations, disabled = false } = props;
|
||||||
|
// hooks
|
||||||
|
const { getCycleById, currentProjectIncompleteCycleIds, fetchAllCycles } = useCycle();
|
||||||
|
const {
|
||||||
|
issue: { getIssueById },
|
||||||
|
} = useIssueDetail();
|
||||||
|
// state
|
||||||
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
|
|
||||||
|
useSWR(workspaceSlug && projectId ? `PROJECT_${projectId}_ISSUE_${issueId}_CYCLES` : null, async () => {
|
||||||
|
if (workspaceSlug && projectId) await fetchAllCycles(workspaceSlug, projectId);
|
||||||
|
});
|
||||||
|
|
||||||
|
const issue = getIssueById(issueId);
|
||||||
|
const projectCycleIds = currentProjectIncompleteCycleIds;
|
||||||
|
const issueCycle = (issue && issue.cycle_id && getCycleById(issue.cycle_id)) || undefined;
|
||||||
|
const disableSelect = disabled || isUpdating;
|
||||||
|
|
||||||
|
const handleIssueCycleChange = async (cycleId: string) => {
|
||||||
|
if (!cycleId) return;
|
||||||
|
setIsUpdating(true);
|
||||||
|
if (issue && issue.cycle_id === cycleId)
|
||||||
|
await issueOperations.removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId);
|
||||||
|
else await issueOperations.addIssueToCycle(workspaceSlug, projectId, cycleId, [issueId]);
|
||||||
|
setIsUpdating(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
type TDropdownOptions = { value: string; query: string; content: ReactNode }[];
|
||||||
|
const options: TDropdownOptions | undefined = projectCycleIds
|
||||||
|
? (projectCycleIds
|
||||||
|
.map((cycleId) => {
|
||||||
|
const cycle = getCycleById(cycleId) || undefined;
|
||||||
|
if (!cycle) return undefined;
|
||||||
|
return {
|
||||||
|
value: cycle.id,
|
||||||
|
query: cycle.name,
|
||||||
|
content: (
|
||||||
|
<div className="flex items-center gap-1.5 truncate">
|
||||||
|
<span className="flex h-3.5 w-3.5 flex-shrink-0 items-center justify-center">
|
||||||
|
<ContrastIcon />
|
||||||
|
</span>
|
||||||
|
<span className="flex-grow truncate">{cycle.name}</span>
|
||||||
|
</div>
|
||||||
|
) as ReactNode,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((cycle) => cycle !== undefined) as TDropdownOptions)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<CustomSearchSelect
|
||||||
|
value={issue?.cycle_id}
|
||||||
|
onChange={(value: any) => handleIssueCycleChange(value)}
|
||||||
|
options={options}
|
||||||
|
customButton={
|
||||||
|
<div>
|
||||||
|
<Tooltip position="left" tooltipContent={`${issueCycle ? issueCycle?.name : "No cycle"}`}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`flex w-full items-center rounded bg-custom-background-80 px-2.5 py-0.5 text-xs ${
|
||||||
|
disableSelect ? "cursor-not-allowed" : ""
|
||||||
|
} max-w-[10rem]`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`flex items-center gap-1.5 truncate ${
|
||||||
|
issueCycle ? "text-custom-text-100" : "text-custom-text-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="flex-shrink-0">{issueCycle && <ContrastIcon className="h-3.5 w-3.5" />}</span>
|
||||||
|
<span className="truncate">{issueCycle ? issueCycle?.name : "No cycle"}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
width="max-w-[10rem]"
|
||||||
|
noChevron
|
||||||
|
disabled={disableSelect}
|
||||||
|
/>
|
||||||
|
{isUpdating && <Spinner className="h-4 w-4" />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
14
web/components/issues/issue-detail/index.ts
Normal file
14
web/components/issues/issue-detail/index.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export * from "./root";
|
||||||
|
|
||||||
|
export * from "./main-content";
|
||||||
|
export * from "./sidebar";
|
||||||
|
|
||||||
|
// select
|
||||||
|
export * from "./cycle-select";
|
||||||
|
export * from "./module-select";
|
||||||
|
export * from "./parent-select";
|
||||||
|
export * from "./relation-select";
|
||||||
|
export * from "./parent";
|
||||||
|
export * from "./label";
|
||||||
|
export * from "./subscription";
|
||||||
|
export * from "./links";
|
163
web/components/issues/issue-detail/label/create-label.tsx
Normal file
163
web/components/issues/issue-detail/label/create-label.tsx
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
import { FC, useState, Fragment, useEffect } from "react";
|
||||||
|
import { Plus, X } from "lucide-react";
|
||||||
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
import { TwitterPicker } from "react-color";
|
||||||
|
import { Popover, Transition } from "@headlessui/react";
|
||||||
|
// hooks
|
||||||
|
import { useIssueDetail } from "hooks/store";
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
// ui
|
||||||
|
import { Input } from "@plane/ui";
|
||||||
|
// types
|
||||||
|
import { TLabelOperations } from "./root";
|
||||||
|
import { IIssueLabel } from "@plane/types";
|
||||||
|
|
||||||
|
type ILabelCreate = {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
issueId: string;
|
||||||
|
labelOperations: TLabelOperations;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultValues: Partial<IIssueLabel> = {
|
||||||
|
name: "",
|
||||||
|
color: "#ff0000",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LabelCreate: FC<ILabelCreate> = (props) => {
|
||||||
|
const { workspaceSlug, projectId, issueId, labelOperations, disabled = false } = props;
|
||||||
|
// hooks
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
const {
|
||||||
|
issue: { getIssueById },
|
||||||
|
} = useIssueDetail();
|
||||||
|
// state
|
||||||
|
const [isCreateToggle, setIsCreateToggle] = useState(false);
|
||||||
|
const handleIsCreateToggle = () => setIsCreateToggle(!isCreateToggle);
|
||||||
|
// react hook form
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
formState: { errors, isSubmitting },
|
||||||
|
reset,
|
||||||
|
control,
|
||||||
|
setFocus,
|
||||||
|
} = useForm<Partial<IIssueLabel>>({
|
||||||
|
defaultValues,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isCreateToggle) return;
|
||||||
|
|
||||||
|
setFocus("name");
|
||||||
|
reset();
|
||||||
|
}, [isCreateToggle, reset, setFocus]);
|
||||||
|
|
||||||
|
const handleLabel = async (formData: Partial<IIssueLabel>) => {
|
||||||
|
if (!workspaceSlug || !projectId || isSubmitting) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const issue = getIssueById(issueId);
|
||||||
|
const labelResponse = await labelOperations.createLabel(workspaceSlug, projectId, formData);
|
||||||
|
const currentLabels = [...(issue?.label_ids || []), labelResponse.id];
|
||||||
|
await labelOperations.updateIssue(workspaceSlug, projectId, issueId, { label_ids: currentLabels });
|
||||||
|
reset(defaultValues);
|
||||||
|
} catch (error) {
|
||||||
|
setToastAlert({
|
||||||
|
title: "Label creation failed",
|
||||||
|
type: "error",
|
||||||
|
message: "Label creation failed. Please try again sometime later.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className="flex-shrink-0 transition-all relative flex items-center gap-1 cursor-pointer border border-custom-border-100 rounded-full text-xs p-0.5 px-2 group hover:border-red-500/50 hover:bg-red-500/20"
|
||||||
|
onClick={handleIsCreateToggle}
|
||||||
|
>
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
{isCreateToggle ? (
|
||||||
|
<X className="transition-all h-2.5 w-2.5 group-hover:text-red-500" />
|
||||||
|
) : (
|
||||||
|
<Plus className="transition-all h-2.5 w-2.5 group-hover:text-red-500" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-shrink-0">{isCreateToggle ? "Cancel" : "New"}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isCreateToggle && (
|
||||||
|
<form className="flex items-center gap-x-2" onSubmit={handleSubmit(handleLabel)}>
|
||||||
|
<div>
|
||||||
|
<Controller
|
||||||
|
name="color"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<Popover className="relative">
|
||||||
|
<>
|
||||||
|
<Popover.Button className="grid place-items-center outline-none">
|
||||||
|
{value && value?.trim() !== "" && (
|
||||||
|
<span
|
||||||
|
className="h-6 w-6 rounded"
|
||||||
|
style={{
|
||||||
|
backgroundColor: value ?? "black",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Popover.Button>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
enter="transition ease-out duration-200"
|
||||||
|
enterFrom="opacity-0 translate-y-1"
|
||||||
|
enterTo="opacity-100 translate-y-0"
|
||||||
|
leave="transition ease-in duration-150"
|
||||||
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
|
leaveTo="opacity-0 translate-y-1"
|
||||||
|
>
|
||||||
|
<Popover.Panel className="absolute z-10 mt-1.5 max-w-xs px-2 sm:px-0">
|
||||||
|
<TwitterPicker color={value} onChange={(value) => onChange(value.hex)} />
|
||||||
|
</Popover.Panel>
|
||||||
|
</Transition>
|
||||||
|
</>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="name"
|
||||||
|
rules={{
|
||||||
|
required: "This is required",
|
||||||
|
}}
|
||||||
|
render={({ field: { value, onChange, ref } }) => (
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
value={value ?? ""}
|
||||||
|
onChange={onChange}
|
||||||
|
ref={ref}
|
||||||
|
hasError={Boolean(errors.name)}
|
||||||
|
placeholder="Title"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="grid place-items-center rounded bg-red-500 p-1.5"
|
||||||
|
onClick={() => setIsCreateToggle(false)}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4 text-white" />
|
||||||
|
</button>
|
||||||
|
<button type="submit" className="grid place-items-center rounded bg-green-500 p-1.5" disabled={isSubmitting}>
|
||||||
|
<Plus className="h-4 w-4 text-white" />
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
5
web/components/issues/issue-detail/label/index.ts
Normal file
5
web/components/issues/issue-detail/label/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export * from "./root";
|
||||||
|
|
||||||
|
export * from "./label-list";
|
||||||
|
export * from "./label-list-item";
|
||||||
|
export * from "./create-label";
|
52
web/components/issues/issue-detail/label/label-list-item.tsx
Normal file
52
web/components/issues/issue-detail/label/label-list-item.tsx
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
// types
|
||||||
|
import { TLabelOperations } from "./root";
|
||||||
|
import { useIssueDetail, useLabel } from "hooks/store";
|
||||||
|
|
||||||
|
type TLabelListItem = {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
issueId: string;
|
||||||
|
labelId: string;
|
||||||
|
labelOperations: TLabelOperations;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LabelListItem: FC<TLabelListItem> = (props) => {
|
||||||
|
const { workspaceSlug, projectId, issueId, labelId, labelOperations } = props;
|
||||||
|
// hooks
|
||||||
|
const {
|
||||||
|
issue: { getIssueById },
|
||||||
|
} = useIssueDetail();
|
||||||
|
const { getLabelById } = useLabel();
|
||||||
|
|
||||||
|
const issue = getIssueById(issueId);
|
||||||
|
const label = getLabelById(labelId);
|
||||||
|
|
||||||
|
const handleLabel = async () => {
|
||||||
|
if (issue) {
|
||||||
|
const currentLabels = issue.label_ids.filter((_labelId) => _labelId !== labelId);
|
||||||
|
await labelOperations.updateIssue(workspaceSlug, projectId, issueId, { label_ids: currentLabels });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!label) return <></>;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={labelId}
|
||||||
|
className="transition-all relative flex items-center gap-1 cursor-pointer border border-custom-border-100 rounded-full text-xs p-0.5 px-1 group hover:border-red-500/50 hover:bg-red-500/20"
|
||||||
|
onClick={handleLabel}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="rounded-full h-2 w-2 flex-shrink-0"
|
||||||
|
style={{
|
||||||
|
backgroundColor: label.color ?? "#000000",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex-shrink-0">{label.name}</div>
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<X className="transition-all h-2.5 w-2.5 group-hover:text-red-500" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
40
web/components/issues/issue-detail/label/label-list.tsx
Normal file
40
web/components/issues/issue-detail/label/label-list.tsx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
// components
|
||||||
|
import { LabelListItem } from "./label-list-item";
|
||||||
|
// hooks
|
||||||
|
import { useIssueDetail } from "hooks/store";
|
||||||
|
// types
|
||||||
|
import { TLabelOperations } from "./root";
|
||||||
|
|
||||||
|
type TLabelList = {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
issueId: string;
|
||||||
|
labelOperations: TLabelOperations;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LabelList: FC<TLabelList> = (props) => {
|
||||||
|
const { workspaceSlug, projectId, issueId, labelOperations } = props;
|
||||||
|
// hooks
|
||||||
|
const {
|
||||||
|
issue: { getIssueById },
|
||||||
|
} = useIssueDetail();
|
||||||
|
|
||||||
|
const issue = getIssueById(issueId);
|
||||||
|
const issueLabels = issue?.label_ids || undefined;
|
||||||
|
|
||||||
|
if (!issue || !issueLabels) return <></>;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{issueLabels.map((labelId) => (
|
||||||
|
<LabelListItem
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
issueId={issueId}
|
||||||
|
labelId={labelId}
|
||||||
|
labelOperations={labelOperations}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
92
web/components/issues/issue-detail/label/root.tsx
Normal file
92
web/components/issues/issue-detail/label/root.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { FC, useMemo } from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
// components
|
||||||
|
import { LabelList, LabelCreate } from "./";
|
||||||
|
|
||||||
|
// hooks
|
||||||
|
import { useIssueDetail, useLabel } from "hooks/store";
|
||||||
|
// types
|
||||||
|
import { IIssueLabel, TIssue } from "@plane/types";
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
|
||||||
|
export type TIssueLabel = {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
issueId: string;
|
||||||
|
disabled: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TLabelOperations = {
|
||||||
|
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
||||||
|
createLabel: (workspaceSlug: string, projectId: string, data: Partial<IIssueLabel>) => Promise<any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IssueLabel: FC<TIssueLabel> = observer((props) => {
|
||||||
|
const { workspaceSlug, projectId, issueId, disabled = false } = props;
|
||||||
|
// hooks
|
||||||
|
const { updateIssue } = useIssueDetail();
|
||||||
|
const {
|
||||||
|
project: { createLabel },
|
||||||
|
} = useLabel();
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
const labelOperations: TLabelOperations = useMemo(
|
||||||
|
() => ({
|
||||||
|
updateIssue: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
|
||||||
|
try {
|
||||||
|
await updateIssue(workspaceSlug, projectId, issueId, data);
|
||||||
|
setToastAlert({
|
||||||
|
title: "Issue updated successfully",
|
||||||
|
type: "success",
|
||||||
|
message: "Issue updated successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setToastAlert({
|
||||||
|
title: "Issue update failed",
|
||||||
|
type: "error",
|
||||||
|
message: "Issue update failed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createLabel: async (workspaceSlug: string, projectId: string, data: Partial<IIssueLabel>) => {
|
||||||
|
try {
|
||||||
|
const labelResponse = await createLabel(workspaceSlug, projectId, data);
|
||||||
|
setToastAlert({
|
||||||
|
title: "Label created successfully",
|
||||||
|
type: "success",
|
||||||
|
message: "Label created successfully",
|
||||||
|
});
|
||||||
|
return labelResponse;
|
||||||
|
} catch (error) {
|
||||||
|
setToastAlert({
|
||||||
|
title: "Label creation failed",
|
||||||
|
type: "error",
|
||||||
|
message: "Label creation failed",
|
||||||
|
});
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[updateIssue, createLabel, setToastAlert]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex flex-wrap items-center gap-1">
|
||||||
|
<LabelList
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
issueId={issueId}
|
||||||
|
labelOperations={labelOperations}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* <div>select existing labels</div> */}
|
||||||
|
|
||||||
|
<LabelCreate
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
issueId={issueId}
|
||||||
|
labelOperations={labelOperations}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,9 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
|
type TLabelExistingSelect = {};
|
||||||
|
|
||||||
|
export const LabelExistingSelect: FC<TLabelExistingSelect> = (props) => {
|
||||||
|
const {} = props;
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
};
|
@ -42,7 +42,7 @@ export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = (props)
|
|||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
handleModal(false);
|
handleModal(false);
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
reset(defaultValues);
|
reset(preloadedData ? preloadedData : defaultValues);
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
}, 500);
|
}, 500);
|
||||||
};
|
};
|
4
web/components/issues/issue-detail/links/index.ts
Normal file
4
web/components/issues/issue-detail/links/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from "./root";
|
||||||
|
|
||||||
|
export * from "./links";
|
||||||
|
export * from "./link-detail";
|
@ -23,13 +23,17 @@ export const IssueLinkDetail: FC<TIssueLinkDetail> = (props) => {
|
|||||||
const { linkId, linkOperations, isNotAllowed } = props;
|
const { linkId, linkOperations, isNotAllowed } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const {
|
const {
|
||||||
|
toggleIssueLinkModal: toggleIssueLinkModalStore,
|
||||||
link: { getLinkById },
|
link: { getLinkById },
|
||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
// state
|
// state
|
||||||
const [isIssueLinkModalOpen, setIsIssueLinkModalOpen] = useState(false);
|
const [isIssueLinkModalOpen, setIsIssueLinkModalOpen] = useState(false);
|
||||||
const toggleIssueLinkModal = (modalToggle: boolean) => setIsIssueLinkModalOpen(modalToggle);
|
const toggleIssueLinkModal = (modalToggle: boolean) => {
|
||||||
|
toggleIssueLinkModalStore(modalToggle);
|
||||||
|
setIsIssueLinkModalOpen(modalToggle);
|
||||||
|
};
|
||||||
|
|
||||||
const linkDetail = getLinkById(linkId);
|
const linkDetail = getLinkById(linkId);
|
||||||
if (!linkDetail) return <></>;
|
if (!linkDetail) return <></>;
|
||||||
@ -74,7 +78,7 @@ export const IssueLinkDetail: FC<TIssueLinkDetail> = (props) => {
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setIsIssueLinkModalOpen(true);
|
toggleIssueLinkModal(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Pencil className="h-3 w-3 stroke-[1.5] text-custom-text-200" />
|
<Pencil className="h-3 w-3 stroke-[1.5] text-custom-text-200" />
|
@ -1,7 +1,7 @@
|
|||||||
import { FC, useMemo, useState } from "react";
|
import { FC, useCallback, useMemo, useState } from "react";
|
||||||
import { Plus } from "lucide-react";
|
import { Plus } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useApplication, useIssueDetail } from "hooks/store";
|
import { useIssueDetail } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { IssueLinkCreateUpdateModal } from "./create-update-link-modal";
|
import { IssueLinkCreateUpdateModal } from "./create-update-link-modal";
|
||||||
@ -16,21 +16,27 @@ export type TLinkOperations = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type TIssueLinkRoot = {
|
export type TIssueLinkRoot = {
|
||||||
uneditable: boolean;
|
workspaceSlug: string;
|
||||||
isAllowed: boolean;
|
projectId: string;
|
||||||
|
issueId: string;
|
||||||
|
is_editable: boolean;
|
||||||
|
is_archived: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const IssueLinkRoot: FC<TIssueLinkRoot> = (props) => {
|
export const IssueLinkRoot: FC<TIssueLinkRoot> = (props) => {
|
||||||
// props
|
// props
|
||||||
const { uneditable, isAllowed } = props;
|
const { workspaceSlug, projectId, issueId, is_editable, is_archived } = props;
|
||||||
// hooks
|
// hooks
|
||||||
const {
|
const { toggleIssueLinkModal: toggleIssueLinkModalStore, createLink, updateLink, removeLink } = useIssueDetail();
|
||||||
router: { workspaceSlug, projectId },
|
|
||||||
} = useApplication();
|
|
||||||
const { peekIssue, createLink, updateLink, removeLink } = useIssueDetail();
|
|
||||||
// state
|
// state
|
||||||
const [isIssueLinkModalOpen, setIsIssueLinkModalOpen] = useState(false);
|
const [isIssueLinkModal, setIsIssueLinkModal] = useState(false);
|
||||||
const toggleIssueLinkModal = (modalToggle: boolean) => setIsIssueLinkModalOpen(modalToggle);
|
const toggleIssueLinkModal = useCallback(
|
||||||
|
(modalToggle: boolean) => {
|
||||||
|
toggleIssueLinkModalStore(modalToggle);
|
||||||
|
setIsIssueLinkModal(modalToggle);
|
||||||
|
},
|
||||||
|
[toggleIssueLinkModalStore]
|
||||||
|
);
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
@ -38,8 +44,8 @@ export const IssueLinkRoot: FC<TIssueLinkRoot> = (props) => {
|
|||||||
() => ({
|
() => ({
|
||||||
create: async (data: Partial<TIssueLink>) => {
|
create: async (data: Partial<TIssueLink>) => {
|
||||||
try {
|
try {
|
||||||
if (!workspaceSlug || !projectId || !peekIssue?.issueId) throw new Error("Missing required fields");
|
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields");
|
||||||
await createLink(workspaceSlug, projectId, peekIssue?.issueId, data);
|
await createLink(workspaceSlug, projectId, issueId, data);
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
message: "The link has been successfully created",
|
message: "The link has been successfully created",
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -56,8 +62,8 @@ export const IssueLinkRoot: FC<TIssueLinkRoot> = (props) => {
|
|||||||
},
|
},
|
||||||
update: async (linkId: string, data: Partial<TIssueLink>) => {
|
update: async (linkId: string, data: Partial<TIssueLink>) => {
|
||||||
try {
|
try {
|
||||||
if (!workspaceSlug || !projectId || !peekIssue?.issueId) throw new Error("Missing required fields");
|
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields");
|
||||||
await updateLink(workspaceSlug, projectId, peekIssue?.issueId, linkId, data);
|
await updateLink(workspaceSlug, projectId, issueId, linkId, data);
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
message: "The link has been successfully updated",
|
message: "The link has been successfully updated",
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -74,8 +80,8 @@ export const IssueLinkRoot: FC<TIssueLinkRoot> = (props) => {
|
|||||||
},
|
},
|
||||||
remove: async (linkId: string) => {
|
remove: async (linkId: string) => {
|
||||||
try {
|
try {
|
||||||
if (!workspaceSlug || !projectId || !peekIssue?.issueId) throw new Error("Missing required fields");
|
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing required fields");
|
||||||
await removeLink(workspaceSlug, projectId, peekIssue?.issueId, linkId);
|
await removeLink(workspaceSlug, projectId, issueId, linkId);
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
message: "The link has been successfully removed",
|
message: "The link has been successfully removed",
|
||||||
type: "success",
|
type: "success",
|
||||||
@ -91,28 +97,28 @@ export const IssueLinkRoot: FC<TIssueLinkRoot> = (props) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[workspaceSlug, projectId, peekIssue, createLink, updateLink, removeLink, setToastAlert]
|
[workspaceSlug, projectId, issueId, createLink, updateLink, removeLink, setToastAlert, toggleIssueLinkModal]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<IssueLinkCreateUpdateModal
|
<IssueLinkCreateUpdateModal
|
||||||
isModalOpen={isIssueLinkModalOpen}
|
isModalOpen={isIssueLinkModal}
|
||||||
handleModal={toggleIssueLinkModal}
|
handleModal={toggleIssueLinkModal}
|
||||||
linkOperations={handleLinkOperations}
|
linkOperations={handleLinkOperations}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={`py-1 text-xs ${uneditable ? "opacity-60" : ""}`}>
|
<div className={`py-1 text-xs ${is_archived ? "opacity-60" : ""}`}>
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<h4>Links</h4>
|
<h4>Links</h4>
|
||||||
{isAllowed && (
|
{is_editable && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-custom-background-90 ${
|
className={`grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-custom-background-90 ${
|
||||||
uneditable ? "cursor-not-allowed" : "cursor-pointer"
|
is_archived ? "cursor-not-allowed" : "cursor-pointer"
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setIsIssueLinkModalOpen(true)}
|
onClick={() => toggleIssueLinkModal(true)}
|
||||||
disabled={uneditable}
|
disabled={is_archived}
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
130
web/components/issues/issue-detail/main-content.tsx
Normal file
130
web/components/issues/issue-detail/main-content.tsx
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
// hooks
|
||||||
|
import { useIssueDetail, useProject, useProjectState, useUser } from "hooks/store";
|
||||||
|
// components
|
||||||
|
import { IssueDescriptionForm, IssueAttachmentRoot, IssueUpdateStatus } from "components/issues";
|
||||||
|
import { IssueParentDetail } from "./parent";
|
||||||
|
import { IssueReaction } from "./reactions";
|
||||||
|
import { SubIssuesRoot } from "../sub-issues";
|
||||||
|
// ui
|
||||||
|
import { StateGroupIcon } from "@plane/ui";
|
||||||
|
// types
|
||||||
|
import { TIssueOperations } from "./root";
|
||||||
|
// constants
|
||||||
|
import { EUserProjectRoles } from "constants/project";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
issueId: string;
|
||||||
|
issueOperations: TIssueOperations;
|
||||||
|
is_archived: boolean;
|
||||||
|
is_editable: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IssueMainContent: React.FC<Props> = observer((props) => {
|
||||||
|
const { workspaceSlug, projectId, issueId, issueOperations, is_archived, is_editable } = props;
|
||||||
|
// states
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
||||||
|
// hooks
|
||||||
|
const {
|
||||||
|
currentUser,
|
||||||
|
membership: { currentProjectRole },
|
||||||
|
} = useUser();
|
||||||
|
const { getProjectById } = useProject();
|
||||||
|
const { projectStates } = useProjectState();
|
||||||
|
const {
|
||||||
|
issue: { getIssueById },
|
||||||
|
} = useIssueDetail();
|
||||||
|
|
||||||
|
const issue = getIssueById(issueId);
|
||||||
|
if (!issue) return <></>;
|
||||||
|
|
||||||
|
const projectDetails = projectId ? getProjectById(projectId) : null;
|
||||||
|
const currentIssueState = projectStates?.find((s) => s.id === issue.state_id);
|
||||||
|
|
||||||
|
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="rounded-lg space-y-4">
|
||||||
|
{issue.parent_id && (
|
||||||
|
<IssueParentDetail
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
issueId={issueId}
|
||||||
|
issue={issue}
|
||||||
|
issueOperations={issueOperations}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mb-2.5 flex items-center">
|
||||||
|
{currentIssueState && (
|
||||||
|
<StateGroupIcon
|
||||||
|
className="mr-3 h-4 w-4"
|
||||||
|
stateGroup={currentIssueState.group}
|
||||||
|
color={currentIssueState.color}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<IssueUpdateStatus isSubmitting={isSubmitting} issueDetail={issue} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<IssueDescriptionForm
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
issueId={issueId}
|
||||||
|
setIsSubmitting={(value) => setIsSubmitting(value)}
|
||||||
|
isSubmitting={isSubmitting}
|
||||||
|
issue={issue}
|
||||||
|
issueOperations={issueOperations}
|
||||||
|
isAllowed={isAllowed || !is_editable}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{currentUser && (
|
||||||
|
<IssueReaction
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
issueId={issueId}
|
||||||
|
currentUser={currentUser}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentUser && (
|
||||||
|
<SubIssuesRoot
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
issueId={issueId}
|
||||||
|
currentUser={currentUser}
|
||||||
|
is_archived={is_archived}
|
||||||
|
is_editable={is_editable}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* issue attachments */}
|
||||||
|
<IssueAttachmentRoot
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
issueId={issueId}
|
||||||
|
is_archived={is_archived}
|
||||||
|
is_editable={is_editable}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* <div className="space-y-5 pt-3">
|
||||||
|
<h3 className="text-lg text-custom-text-100">Comments/Activity</h3>
|
||||||
|
<IssueActivitySection
|
||||||
|
activity={issueActivity}
|
||||||
|
handleCommentUpdate={handleCommentUpdate}
|
||||||
|
handleCommentDelete={handleCommentDelete}
|
||||||
|
showAccessSpecifier={Boolean(projectDetails && projectDetails.is_deployed)}
|
||||||
|
/>
|
||||||
|
<AddComment
|
||||||
|
onSubmit={handleAddComment}
|
||||||
|
disabled={is_editable}
|
||||||
|
showAccessSpecifier={Boolean(projectDetails && projectDetails.is_deployed)}
|
||||||
|
/>
|
||||||
|
</div> */}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
103
web/components/issues/issue-detail/module-select.tsx
Normal file
103
web/components/issues/issue-detail/module-select.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import React, { ReactNode, useState } from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import useSWR from "swr";
|
||||||
|
// hooks
|
||||||
|
import { useModule, useIssueDetail } from "hooks/store";
|
||||||
|
// ui
|
||||||
|
import { CustomSearchSelect, DiceIcon, Spinner, Tooltip } from "@plane/ui";
|
||||||
|
// types
|
||||||
|
import type { TIssueOperations } from "./root";
|
||||||
|
|
||||||
|
type TIssueModuleSelect = {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
issueId: string;
|
||||||
|
issueOperations: TIssueOperations;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IssueModuleSelect: React.FC<TIssueModuleSelect> = observer((props) => {
|
||||||
|
const { workspaceSlug, projectId, issueId, issueOperations, disabled = false } = props;
|
||||||
|
// hooks
|
||||||
|
const { getModuleById, projectModuleIds, fetchModules } = useModule();
|
||||||
|
const {
|
||||||
|
issue: { getIssueById },
|
||||||
|
} = useIssueDetail();
|
||||||
|
// state
|
||||||
|
const [isUpdating, setIsUpdating] = useState(false);
|
||||||
|
|
||||||
|
useSWR(workspaceSlug && projectId ? `PROJECT_${projectId}_ISSUE_${issueId}_MODULES` : null, async () => {
|
||||||
|
if (workspaceSlug && projectId) await fetchModules(workspaceSlug, projectId);
|
||||||
|
});
|
||||||
|
|
||||||
|
const issue = getIssueById(issueId);
|
||||||
|
const issueModule = (issue && issue.module_id && getModuleById(issue.module_id)) || undefined;
|
||||||
|
const disableSelect = disabled || isUpdating;
|
||||||
|
|
||||||
|
const handleIssueModuleChange = async (moduleId: string) => {
|
||||||
|
if (!moduleId) return;
|
||||||
|
setIsUpdating(true);
|
||||||
|
if (issue && issue.module_id === moduleId)
|
||||||
|
await issueOperations.removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId);
|
||||||
|
else await issueOperations.addIssueToModule(workspaceSlug, projectId, moduleId, [issueId]);
|
||||||
|
setIsUpdating(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
type TDropdownOptions = { value: string; query: string; content: ReactNode }[];
|
||||||
|
const options: TDropdownOptions | undefined = projectModuleIds
|
||||||
|
? (projectModuleIds
|
||||||
|
.map((moduleId) => {
|
||||||
|
const _module = getModuleById(moduleId);
|
||||||
|
if (!_module) return undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: _module.id,
|
||||||
|
query: _module.name,
|
||||||
|
content: (
|
||||||
|
<div className="flex items-center gap-1.5 truncate">
|
||||||
|
<span className="flex h-3.5 w-3.5 flex-shrink-0 items-center justify-center">
|
||||||
|
<DiceIcon />
|
||||||
|
</span>
|
||||||
|
<span className="flex-grow truncate">{_module.name}</span>
|
||||||
|
</div>
|
||||||
|
) as ReactNode,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((_module) => _module !== undefined) as TDropdownOptions)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<CustomSearchSelect
|
||||||
|
value={issue?.module_id}
|
||||||
|
onChange={(value: any) => handleIssueModuleChange(value)}
|
||||||
|
options={options}
|
||||||
|
customButton={
|
||||||
|
<div>
|
||||||
|
<Tooltip position="left" tooltipContent={`${issueModule?.name ?? "No module"}`}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`flex w-full items-center rounded bg-custom-background-80 px-2.5 py-0.5 text-xs ${
|
||||||
|
disableSelect ? "cursor-not-allowed" : ""
|
||||||
|
} max-w-[10rem]`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`flex items-center gap-1.5 truncate ${
|
||||||
|
issueModule ? "text-custom-text-100" : "text-custom-text-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="flex-shrink-0">{issueModule && <DiceIcon className="h-3.5 w-3.5" />}</span>
|
||||||
|
<span className="truncate">{issueModule?.name ?? "No module"}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
width="max-w-[10rem]"
|
||||||
|
noChevron
|
||||||
|
disabled={disableSelect}
|
||||||
|
/>
|
||||||
|
{isUpdating && <Spinner className="h-4 w-4" />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
82
web/components/issues/issue-detail/parent-select.tsx
Normal file
82
web/components/issues/issue-detail/parent-select.tsx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
// hooks
|
||||||
|
import { useIssueDetail, useProject } from "hooks/store";
|
||||||
|
import { Spinner } from "@plane/ui";
|
||||||
|
// components
|
||||||
|
import { ParentIssuesListModal } from "components/issues";
|
||||||
|
import { TIssueOperations } from "./root";
|
||||||
|
|
||||||
|
type TIssueParentSelect = {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
issueId: string;
|
||||||
|
issueOperations: TIssueOperations;
|
||||||
|
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IssueParentSelect: React.FC<TIssueParentSelect> = observer(
|
||||||
|
({ workspaceSlug, projectId, issueId, issueOperations, disabled = false }) => {
|
||||||
|
// hooks
|
||||||
|
const { getProjectById } = useProject();
|
||||||
|
const {
|
||||||
|
issue: { getIssueById },
|
||||||
|
} = useIssueDetail();
|
||||||
|
// state
|
||||||
|
const { isParentIssueModalOpen, toggleParentIssueModal } = useIssueDetail();
|
||||||
|
const [updating, setUpdating] = useState(false);
|
||||||
|
|
||||||
|
const issue = getIssueById(issueId);
|
||||||
|
|
||||||
|
const parentIssue = issue && issue.parent_id ? getIssueById(issue.parent_id) : undefined;
|
||||||
|
const parentIssueProjectDetails =
|
||||||
|
parentIssue && parentIssue.project_id ? getProjectById(parentIssue.project_id) : undefined;
|
||||||
|
|
||||||
|
const handleParentIssue = async (_issueId: string | null = null) => {
|
||||||
|
setUpdating(true);
|
||||||
|
await issueOperations.update(workspaceSlug, projectId, issueId, { parent_id: _issueId }).finally(() => {
|
||||||
|
toggleParentIssueModal(false);
|
||||||
|
setUpdating(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!issue) return <></>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative flex items-center gap-2">
|
||||||
|
<ParentIssuesListModal
|
||||||
|
projectId={projectId}
|
||||||
|
issueId={issueId}
|
||||||
|
isOpen={isParentIssueModalOpen}
|
||||||
|
handleClose={() => toggleParentIssueModal(false)}
|
||||||
|
onChange={(issue: any) => handleParentIssue(issue?.id)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className={`flex items-center gap-2 rounded bg-custom-background-80 px-2.5 py-0.5 text-xs w-max max-w-max" ${
|
||||||
|
disabled ? "cursor-not-allowed" : "cursor-pointer "
|
||||||
|
}`}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<div onClick={() => toggleParentIssueModal(true)}>
|
||||||
|
{parentIssue ? (
|
||||||
|
`${parentIssueProjectDetails?.identifier}-${parentIssue.sequence_id}`
|
||||||
|
) : (
|
||||||
|
<span className="text-custom-text-200">Select issue</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{parentIssue && (
|
||||||
|
<div onClick={() => handleParentIssue(null)}>
|
||||||
|
<X className="h-2.5 w-2.5" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{updating && <Spinner className="h-4 w-4" />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
4
web/components/issues/issue-detail/parent/index.ts
Normal file
4
web/components/issues/issue-detail/parent/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from "./root";
|
||||||
|
|
||||||
|
export * from "./siblings";
|
||||||
|
export * from "./sibling-item";
|
72
web/components/issues/issue-detail/parent/root.tsx
Normal file
72
web/components/issues/issue-detail/parent/root.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { MinusCircle } from "lucide-react";
|
||||||
|
// component
|
||||||
|
import { IssueParentSiblings } from "./siblings";
|
||||||
|
// ui
|
||||||
|
import { CustomMenu } from "@plane/ui";
|
||||||
|
// hooks
|
||||||
|
import { useIssueDetail, useIssues, useProject, useProjectState } from "hooks/store";
|
||||||
|
// types
|
||||||
|
import { TIssueOperations } from "../root";
|
||||||
|
import { TIssue } from "@plane/types";
|
||||||
|
|
||||||
|
export type TIssueParentDetail = {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
issueId: string;
|
||||||
|
issue: TIssue;
|
||||||
|
issueOperations: TIssueOperations;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IssueParentDetail: FC<TIssueParentDetail> = (props) => {
|
||||||
|
const { workspaceSlug, projectId, issueId, issue, issueOperations } = props;
|
||||||
|
// hooks
|
||||||
|
const { issueMap } = useIssues();
|
||||||
|
const { peekIssue } = useIssueDetail();
|
||||||
|
const { getProjectById } = useProject();
|
||||||
|
const { getProjectStates } = useProjectState();
|
||||||
|
|
||||||
|
const parentIssue = issueMap?.[issue.parent_id || ""] || undefined;
|
||||||
|
|
||||||
|
const issueParentState = getProjectStates(parentIssue?.project_id)?.find(
|
||||||
|
(state) => state?.id === parentIssue?.state_id
|
||||||
|
);
|
||||||
|
const stateColor = issueParentState?.color || undefined;
|
||||||
|
|
||||||
|
if (!parentIssue) return <></>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mb-5 flex w-min items-center gap-3 whitespace-nowrap rounded-md border border-custom-border-300 bg-custom-background-80 px-2.5 py-1 text-xs">
|
||||||
|
<Link href={`/${peekIssue?.workspaceSlug}/projects/${parentIssue?.project_id}/issues/${parentIssue.id}`}>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex items-center gap-2.5">
|
||||||
|
<span className="block h-2 w-2 rounded-full" style={{ backgroundColor: stateColor }} />
|
||||||
|
<span className="flex-shrink-0 text-custom-text-200">
|
||||||
|
{getProjectById(parentIssue.project_id)?.identifier}-{parentIssue?.sequence_id}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="truncate text-custom-text-100">{(parentIssue?.name ?? "").substring(0, 50)}</span>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<CustomMenu ellipsis optionsClassName="p-1.5">
|
||||||
|
<div className="border-b border-custom-border-300 text-xs font-medium text-custom-text-200">
|
||||||
|
Sibling issues
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<IssueParentSiblings currentIssue={issue} parentIssue={parentIssue} />
|
||||||
|
|
||||||
|
<CustomMenu.MenuItem
|
||||||
|
onClick={() => issueOperations.update(workspaceSlug, projectId, issueId, { parent_id: null })}
|
||||||
|
className="flex items-center gap-2 py-2 text-red-500"
|
||||||
|
>
|
||||||
|
<MinusCircle className="h-4 w-4" />
|
||||||
|
<span> Remove Parent Issue</span>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
</CustomMenu>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
39
web/components/issues/issue-detail/parent/sibling-item.tsx
Normal file
39
web/components/issues/issue-detail/parent/sibling-item.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
// ui
|
||||||
|
import { CustomMenu, LayersIcon } from "@plane/ui";
|
||||||
|
// hooks
|
||||||
|
import { useIssueDetail, useProject } from "hooks/store";
|
||||||
|
|
||||||
|
type TIssueParentSiblingItem = {
|
||||||
|
issueId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IssueParentSiblingItem: FC<TIssueParentSiblingItem> = (props) => {
|
||||||
|
const { issueId } = props;
|
||||||
|
// hooks
|
||||||
|
const { getProjectById } = useProject();
|
||||||
|
const {
|
||||||
|
peekIssue,
|
||||||
|
issue: { getIssueById },
|
||||||
|
} = useIssueDetail();
|
||||||
|
|
||||||
|
const issueDetail = (issueId && getIssueById(issueId)) || undefined;
|
||||||
|
if (!issueDetail) return <></>;
|
||||||
|
|
||||||
|
const projectDetails = (issueDetail.project_id && getProjectById(issueDetail.project_id)) || undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CustomMenu.MenuItem key={issueDetail.id}>
|
||||||
|
<Link
|
||||||
|
href={`/${peekIssue?.workspaceSlug}/projects/${issueDetail?.project_id as string}/issues/${issueDetail.id}`}
|
||||||
|
className="flex items-center gap-2 py-2"
|
||||||
|
>
|
||||||
|
<LayersIcon className="h-4 w-4" />
|
||||||
|
{projectDetails?.identifier}-{issueDetail.sequence_id}
|
||||||
|
</Link>
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
51
web/components/issues/issue-detail/parent/siblings.tsx
Normal file
51
web/components/issues/issue-detail/parent/siblings.tsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
// components
|
||||||
|
import { IssueParentSiblingItem } from "./sibling-item";
|
||||||
|
// hooks
|
||||||
|
import { useIssueDetail } from "hooks/store";
|
||||||
|
// types
|
||||||
|
import { TIssue } from "@plane/types";
|
||||||
|
|
||||||
|
export type TIssueParentSiblings = {
|
||||||
|
currentIssue: TIssue;
|
||||||
|
parentIssue: TIssue;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IssueParentSiblings: FC<TIssueParentSiblings> = (props) => {
|
||||||
|
const { currentIssue, parentIssue } = props;
|
||||||
|
// hooks
|
||||||
|
const {
|
||||||
|
peekIssue,
|
||||||
|
fetchSubIssues,
|
||||||
|
subIssues: { subIssuesByIssueId },
|
||||||
|
} = useIssueDetail();
|
||||||
|
|
||||||
|
const { isLoading } = useSWR(
|
||||||
|
peekIssue && parentIssue
|
||||||
|
? `ISSUE_PARENT_CHILD_ISSUES_${peekIssue?.workspaceSlug}_${parentIssue.project_id}_${parentIssue.id}`
|
||||||
|
: null,
|
||||||
|
peekIssue && parentIssue
|
||||||
|
? () => fetchSubIssues(peekIssue?.workspaceSlug, parentIssue.project_id, parentIssue.id)
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
|
||||||
|
const subIssueIds = (parentIssue && subIssuesByIssueId(parentIssue.id)) || undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center gap-2 whitespace-nowrap px-1 py-1 text-left text-xs text-custom-text-200">
|
||||||
|
Loading
|
||||||
|
</div>
|
||||||
|
) : subIssueIds && subIssueIds.length > 0 ? (
|
||||||
|
subIssueIds.map((issueId) => currentIssue.id != issueId && <IssueParentSiblingItem issueId={issueId} />)
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-2 whitespace-nowrap px-1 py-1 text-left text-xs text-custom-text-200">
|
||||||
|
No sibling issues
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
4
web/components/issues/issue-detail/reactions/index.ts
Normal file
4
web/components/issues/issue-detail/reactions/index.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export * from "./reaction-selector";
|
||||||
|
|
||||||
|
export * from "./issue";
|
||||||
|
// export * from "./issue-comment";
|
103
web/components/issues/issue-detail/reactions/issue.tsx
Normal file
103
web/components/issues/issue-detail/reactions/issue.tsx
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
import { FC, useMemo } from "react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
// components
|
||||||
|
import { ReactionSelector } from "./reaction-selector";
|
||||||
|
// hooks
|
||||||
|
import { useIssueDetail } from "hooks/store";
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
// types
|
||||||
|
import { IUser } from "@plane/types";
|
||||||
|
import { renderEmoji } from "helpers/emoji.helper";
|
||||||
|
|
||||||
|
export type TIssueReaction = {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
issueId: string;
|
||||||
|
currentUser: IUser;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IssueReaction: FC<TIssueReaction> = observer((props) => {
|
||||||
|
const { workspaceSlug, projectId, issueId, currentUser } = props;
|
||||||
|
// hooks
|
||||||
|
const {
|
||||||
|
reaction: { getReactionsByIssueId, reactionsByUser },
|
||||||
|
createReaction,
|
||||||
|
removeReaction,
|
||||||
|
} = useIssueDetail();
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
const reactionIds = getReactionsByIssueId(issueId);
|
||||||
|
const userReactions = reactionsByUser(issueId, currentUser.id).map((r) => r.reaction);
|
||||||
|
|
||||||
|
const issueReactionOperations = useMemo(
|
||||||
|
() => ({
|
||||||
|
create: async (reaction: string) => {
|
||||||
|
try {
|
||||||
|
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields");
|
||||||
|
await createReaction(workspaceSlug, projectId, issueId, reaction);
|
||||||
|
setToastAlert({
|
||||||
|
title: "Reaction created successfully",
|
||||||
|
type: "success",
|
||||||
|
message: "Reaction created successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setToastAlert({
|
||||||
|
title: "Reaction creation failed",
|
||||||
|
type: "error",
|
||||||
|
message: "Reaction creation failed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
remove: async (reaction: string) => {
|
||||||
|
try {
|
||||||
|
if (!workspaceSlug || !projectId || !issueId) throw new Error("Missing fields");
|
||||||
|
await removeReaction(workspaceSlug, projectId, issueId, reaction, currentUser.id);
|
||||||
|
setToastAlert({
|
||||||
|
title: "Reaction removed successfully",
|
||||||
|
type: "success",
|
||||||
|
message: "Reaction removed successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setToastAlert({
|
||||||
|
title: "Reaction remove failed",
|
||||||
|
type: "error",
|
||||||
|
message: "Reaction remove failed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
react: async (reaction: string) => {
|
||||||
|
if (userReactions.includes(reaction)) await issueReactionOperations.remove(reaction);
|
||||||
|
else await issueReactionOperations.create(reaction);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[workspaceSlug, projectId, issueId, currentUser, createReaction, removeReaction, setToastAlert, userReactions]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-4 relative flex items-center gap-1.5">
|
||||||
|
<ReactionSelector size="md" position="top" value={userReactions} onSelect={issueReactionOperations.react} />
|
||||||
|
|
||||||
|
{reactionIds &&
|
||||||
|
Object.keys(reactionIds || {}).map(
|
||||||
|
(reaction) =>
|
||||||
|
reactionIds[reaction]?.length > 0 && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => issueReactionOperations.react(reaction)}
|
||||||
|
key={reaction}
|
||||||
|
className={`flex h-full items-center gap-1 rounded-md px-2 py-1 text-sm text-custom-text-100 ${
|
||||||
|
userReactions.includes(reaction) ? "bg-custom-primary-100/10" : "bg-custom-background-80"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span>{renderEmoji(reaction)}</span>
|
||||||
|
<span className={userReactions.includes(reaction) ? "text-custom-primary-100" : ""}>
|
||||||
|
{(reactionIds || {})[reaction].length}{" "}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
@ -1,5 +1,4 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { X, CopyPlus } from "lucide-react";
|
import { X, CopyPlus } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
@ -37,17 +36,16 @@ const issueRelationObject: Record<TIssueRelationTypes, TRelationObject> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type TIssueRelationSelect = {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
issueId: string;
|
issueId: string;
|
||||||
relationKey: TIssueRelationTypes;
|
relationKey: TIssueRelationTypes;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SidebarIssueRelationSelect: React.FC<Props> = observer((props) => {
|
export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((props) => {
|
||||||
const { issueId, relationKey, disabled = false } = props;
|
const { workspaceSlug, projectId, issueId, relationKey, disabled = false } = props;
|
||||||
// router
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId } = router.query;
|
|
||||||
// hooks
|
// hooks
|
||||||
const { currentUser } = useUser();
|
const { currentUser } = useUser();
|
||||||
const { getProjectById } = useProject();
|
const { getProjectById } = useProject();
|
199
web/components/issues/issue-detail/root.tsx
Normal file
199
web/components/issues/issue-detail/root.tsx
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
import { FC, useMemo } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
// components
|
||||||
|
import { IssueMainContent } from "./main-content";
|
||||||
|
import { IssueDetailsSidebar } from "./sidebar";
|
||||||
|
// ui
|
||||||
|
import { EmptyState } from "components/common";
|
||||||
|
// images
|
||||||
|
import emptyIssue from "public/empty-state/issue.svg";
|
||||||
|
// hooks
|
||||||
|
import { useIssueDetail } from "hooks/store";
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
// types
|
||||||
|
import { TIssue } from "@plane/types";
|
||||||
|
|
||||||
|
export type TIssueOperations = {
|
||||||
|
update: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
|
||||||
|
remove: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
|
||||||
|
addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
|
||||||
|
removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
|
||||||
|
addIssueToModule: (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => Promise<void>;
|
||||||
|
removeIssueFromModule: (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TIssueDetailRoot = {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
issueId: string;
|
||||||
|
is_archived?: boolean;
|
||||||
|
is_editable?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
||||||
|
const { workspaceSlug, projectId, issueId, is_archived = false, is_editable = true } = props;
|
||||||
|
// router
|
||||||
|
const router = useRouter();
|
||||||
|
// hooks
|
||||||
|
const {
|
||||||
|
issue: { getIssueById },
|
||||||
|
updateIssue,
|
||||||
|
removeIssue,
|
||||||
|
addIssueToCycle,
|
||||||
|
removeIssueFromCycle,
|
||||||
|
addIssueToModule,
|
||||||
|
removeIssueFromModule,
|
||||||
|
} = useIssueDetail();
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
const issueOperations: TIssueOperations = useMemo(
|
||||||
|
() => ({
|
||||||
|
update: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
|
||||||
|
try {
|
||||||
|
await updateIssue(workspaceSlug, projectId, issueId, data);
|
||||||
|
setToastAlert({
|
||||||
|
title: "Issue updated successfully",
|
||||||
|
type: "success",
|
||||||
|
message: "Issue updated successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setToastAlert({
|
||||||
|
title: "Issue update failed",
|
||||||
|
type: "error",
|
||||||
|
message: "Issue update failed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
remove: async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||||
|
try {
|
||||||
|
await removeIssue(workspaceSlug, projectId, issueId);
|
||||||
|
setToastAlert({
|
||||||
|
title: "Issue deleted successfully",
|
||||||
|
type: "success",
|
||||||
|
message: "Issue deleted successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setToastAlert({
|
||||||
|
title: "Issue delete failed",
|
||||||
|
type: "error",
|
||||||
|
message: "Issue delete failed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addIssueToCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => {
|
||||||
|
try {
|
||||||
|
await addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds);
|
||||||
|
setToastAlert({
|
||||||
|
title: "Cycle added to issue successfully",
|
||||||
|
type: "success",
|
||||||
|
message: "Issue added to issue successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setToastAlert({
|
||||||
|
title: "Cycle add to issue failed",
|
||||||
|
type: "error",
|
||||||
|
message: "Cycle add to issue failed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeIssueFromCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => {
|
||||||
|
try {
|
||||||
|
await removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId);
|
||||||
|
setToastAlert({
|
||||||
|
title: "Cycle removed from issue successfully",
|
||||||
|
type: "success",
|
||||||
|
message: "Cycle removed from issue successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setToastAlert({
|
||||||
|
title: "Cycle remove from issue failed",
|
||||||
|
type: "error",
|
||||||
|
message: "Cycle remove from issue failed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addIssueToModule: async (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => {
|
||||||
|
try {
|
||||||
|
await addIssueToModule(workspaceSlug, projectId, moduleId, issueIds);
|
||||||
|
setToastAlert({
|
||||||
|
title: "Module added to issue successfully",
|
||||||
|
type: "success",
|
||||||
|
message: "Module added to issue successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setToastAlert({
|
||||||
|
title: "Module add to issue failed",
|
||||||
|
type: "error",
|
||||||
|
message: "Module add to issue failed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeIssueFromModule: async (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => {
|
||||||
|
try {
|
||||||
|
await removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId);
|
||||||
|
setToastAlert({
|
||||||
|
title: "Module removed from issue successfully",
|
||||||
|
type: "success",
|
||||||
|
message: "Module removed from issue successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setToastAlert({
|
||||||
|
title: "Module remove from issue failed",
|
||||||
|
type: "error",
|
||||||
|
message: "Module remove from issue failed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
updateIssue,
|
||||||
|
removeIssue,
|
||||||
|
addIssueToCycle,
|
||||||
|
removeIssueFromCycle,
|
||||||
|
addIssueToModule,
|
||||||
|
removeIssueFromModule,
|
||||||
|
setToastAlert,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const issue = getIssueById(issueId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{!issue ? (
|
||||||
|
<EmptyState
|
||||||
|
image={emptyIssue}
|
||||||
|
title="Issue does not exist"
|
||||||
|
description="The issue you are looking for does not exist, has been archived, or has been deleted."
|
||||||
|
primaryButton={{
|
||||||
|
text: "View other issues",
|
||||||
|
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/issues`),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-full overflow-hidden">
|
||||||
|
<div className="h-full w-2/3 space-y-5 divide-y-2 divide-custom-border-300 overflow-y-auto p-5">
|
||||||
|
<IssueMainContent
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
issueId={issueId}
|
||||||
|
issueOperations={issueOperations}
|
||||||
|
is_archived={is_archived}
|
||||||
|
is_editable={is_editable}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="h-full w-1/3 space-y-5 overflow-hidden border-l border-custom-border-300 py-5">
|
||||||
|
<IssueDetailsSidebar
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
issueId={issueId}
|
||||||
|
issueOperations={issueOperations}
|
||||||
|
is_archived={is_archived}
|
||||||
|
is_editable={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,45 +1,40 @@
|
|||||||
import React, { useCallback, useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { mutate } from "swr";
|
import { CalendarDays, LinkIcon, Signal, Tag, Trash2, Triangle, LayoutPanelTop } from "lucide-react";
|
||||||
import { Controller, UseFormWatch } from "react-hook-form";
|
|
||||||
import { Bell, CalendarDays, LinkIcon, Signal, Tag, Trash2, Triangle, LayoutPanelTop } from "lucide-react";
|
|
||||||
// hooks
|
// hooks
|
||||||
import { useEstimate, useIssues, useProject, useProjectState, useUser } from "hooks/store";
|
import { useEstimate, useIssueDetail, useProject, useProjectState, useUser } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useUserIssueNotificationSubscription from "hooks/use-issue-notification-subscription";
|
|
||||||
// services
|
|
||||||
import { IssueService } from "services/issue";
|
|
||||||
import { ModuleService } from "services/module.service";
|
|
||||||
// components
|
// components
|
||||||
import {
|
import {
|
||||||
DeleteIssueModal,
|
DeleteIssueModal,
|
||||||
SidebarIssueRelationSelect,
|
|
||||||
SidebarCycleSelect,
|
|
||||||
SidebarModuleSelect,
|
|
||||||
SidebarParentSelect,
|
|
||||||
SidebarLabelSelect,
|
|
||||||
IssueLinkRoot,
|
IssueLinkRoot,
|
||||||
|
IssueRelationSelect,
|
||||||
|
IssueCycleSelect,
|
||||||
|
IssueModuleSelect,
|
||||||
|
IssueParentSelect,
|
||||||
|
IssueLabel,
|
||||||
} from "components/issues";
|
} from "components/issues";
|
||||||
|
import { IssueSubscription } from "./subscription";
|
||||||
import { EstimateDropdown, PriorityDropdown, ProjectMemberDropdown, StateDropdown } from "components/dropdowns";
|
import { EstimateDropdown, PriorityDropdown, ProjectMemberDropdown, StateDropdown } from "components/dropdowns";
|
||||||
// ui
|
// ui
|
||||||
import { CustomDatePicker } from "components/ui";
|
import { CustomDatePicker } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { Button, ContrastIcon, DiceIcon, DoubleCircleIcon, StateGroupIcon, UserGroupIcon } from "@plane/ui";
|
import { ContrastIcon, DiceIcon, DoubleCircleIcon, StateGroupIcon, UserGroupIcon } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { copyTextToClipboard } from "helpers/string.helper";
|
import { copyTextToClipboard } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import type { TIssue } from "@plane/types";
|
import type { TIssueOperations } from "./root";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { ISSUE_DETAILS } from "constants/fetch-keys";
|
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
import { EIssuesStoreType } from "constants/issue";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
control: any;
|
workspaceSlug: string;
|
||||||
submitChanges: (formData: any) => void;
|
projectId: string;
|
||||||
issueDetail: TIssue | undefined;
|
issueId: string;
|
||||||
watch: UseFormWatch<TIssue>;
|
issueOperations: TIssueOperations;
|
||||||
|
is_archived: boolean;
|
||||||
|
is_editable: boolean;
|
||||||
fieldsToShow?: (
|
fieldsToShow?: (
|
||||||
| "state"
|
| "state"
|
||||||
| "assignee"
|
| "assignee"
|
||||||
@ -60,74 +55,42 @@ type Props = {
|
|||||||
| "duplicate"
|
| "duplicate"
|
||||||
| "relates_to"
|
| "relates_to"
|
||||||
)[];
|
)[];
|
||||||
uneditable?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const issueService = new IssueService();
|
|
||||||
const moduleService = new ModuleService();
|
|
||||||
|
|
||||||
export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||||
const { control, submitChanges, issueDetail, watch: watchIssue, fieldsToShow = ["all"], uneditable = false } = props;
|
const {
|
||||||
// states
|
workspaceSlug,
|
||||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
projectId,
|
||||||
|
issueId,
|
||||||
|
issueOperations,
|
||||||
|
is_archived,
|
||||||
|
is_editable,
|
||||||
|
fieldsToShow = ["all"],
|
||||||
|
} = props;
|
||||||
|
// router
|
||||||
|
const router = useRouter();
|
||||||
|
const { inboxIssueId } = router.query;
|
||||||
// store hooks
|
// store hooks
|
||||||
const { getProjectById } = useProject();
|
const { getProjectById } = useProject();
|
||||||
const {
|
|
||||||
issues: { removeIssue },
|
|
||||||
} = useIssues(EIssuesStoreType.PROJECT);
|
|
||||||
const {
|
const {
|
||||||
currentUser,
|
currentUser,
|
||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const { projectStates } = useProjectState();
|
const { projectStates } = useProjectState();
|
||||||
const { areEstimatesEnabledForCurrentProject } = useEstimate();
|
const { areEstimatesEnabledForCurrentProject } = useEstimate();
|
||||||
// router
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId, issueId, inboxIssueId } = router.query;
|
|
||||||
|
|
||||||
const { loading, handleSubscribe, handleUnsubscribe, subscribed } = useUserIssueNotificationSubscription(
|
|
||||||
currentUser,
|
|
||||||
workspaceSlug,
|
|
||||||
projectId,
|
|
||||||
issueId
|
|
||||||
);
|
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
const {
|
||||||
|
issue: { getIssueById },
|
||||||
|
} = useIssueDetail();
|
||||||
|
// states
|
||||||
|
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||||
|
|
||||||
const handleCycleChange = useCallback(
|
const issue = getIssueById(issueId);
|
||||||
(cycleId: string) => {
|
if (!issue) return <></>;
|
||||||
if (!workspaceSlug || !projectId || !issueDetail || !currentUser) return;
|
|
||||||
|
|
||||||
issueService
|
|
||||||
.addIssueToCycle(workspaceSlug as string, projectId as string, cycleId, {
|
|
||||||
issues: [issueDetail.id],
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
mutate(ISSUE_DETAILS(issueId as string));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[workspaceSlug, projectId, issueId, issueDetail, currentUser]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleModuleChange = useCallback(
|
|
||||||
(moduleId: string) => {
|
|
||||||
if (!workspaceSlug || !projectId || !issueDetail || !currentUser) return;
|
|
||||||
|
|
||||||
moduleService
|
|
||||||
.addIssuesToModule(workspaceSlug as string, projectId as string, moduleId, {
|
|
||||||
issues: [issueDetail.id],
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
mutate(ISSUE_DETAILS(issueId as string));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[workspaceSlug, projectId, issueId, issueDetail, currentUser]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleCopyText = () => {
|
const handleCopyText = () => {
|
||||||
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||||
|
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`).then(() => {
|
||||||
copyTextToClipboard(`${originURL}/${workspaceSlug}/projects/${projectId}/issues/${issueDetail?.id}`).then(() => {
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Link Copied!",
|
title: "Link Copied!",
|
||||||
@ -136,7 +99,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const projectDetails = issueDetail ? getProjectById(issueDetail?.project_id) : null;
|
const projectDetails = issue ? getProjectById(issue.project_id) : null;
|
||||||
|
|
||||||
const showFirstSection =
|
const showFirstSection =
|
||||||
fieldsToShow.includes("all") ||
|
fieldsToShow.includes("all") ||
|
||||||
@ -155,28 +118,25 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
const showThirdSection =
|
const showThirdSection =
|
||||||
fieldsToShow.includes("all") || fieldsToShow.includes("cycle") || fieldsToShow.includes("module");
|
fieldsToShow.includes("all") || fieldsToShow.includes("cycle") || fieldsToShow.includes("module");
|
||||||
|
|
||||||
const startDate = watchIssue("start_date");
|
const minDate = issue.start_date ? new Date(issue.start_date) : null;
|
||||||
const targetDate = watchIssue("target_date");
|
|
||||||
|
|
||||||
const minDate = startDate ? new Date(startDate) : null;
|
|
||||||
minDate?.setDate(minDate.getDate());
|
minDate?.setDate(minDate.getDate());
|
||||||
|
|
||||||
const maxDate = targetDate ? new Date(targetDate) : null;
|
const maxDate = issue.target_date ? new Date(issue.target_date) : null;
|
||||||
maxDate?.setDate(maxDate.getDate());
|
maxDate?.setDate(maxDate.getDate());
|
||||||
|
|
||||||
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||||
|
|
||||||
const currentIssueState = projectStates?.find((s) => s.id === issueDetail?.state_id);
|
const currentIssueState = projectStates?.find((s) => s.id === issue.state_id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{workspaceSlug && projectId && issueDetail && (
|
{workspaceSlug && projectId && issue && (
|
||||||
<DeleteIssueModal
|
<DeleteIssueModal
|
||||||
handleClose={() => setDeleteIssueModal(false)}
|
handleClose={() => setDeleteIssueModal(false)}
|
||||||
isOpen={deleteIssueModal}
|
isOpen={deleteIssueModal}
|
||||||
data={issueDetail}
|
data={issue}
|
||||||
onSubmit={async () => {
|
onSubmit={async () => {
|
||||||
await removeIssue(workspaceSlug.toString(), projectId.toString(), issueDetail.id);
|
await issueOperations.remove(workspaceSlug, projectId, issueId);
|
||||||
router.push(`/${workspaceSlug}/projects/${projectId}/issues`);
|
router.push(`/${workspaceSlug}/projects/${projectId}/issues`);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -195,28 +155,22 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
<StateGroupIcon className="h-4 w-4" stateGroup="backlog" color="#ff7700" />
|
<StateGroupIcon className="h-4 w-4" stateGroup="backlog" color="#ff7700" />
|
||||||
) : null}
|
) : null}
|
||||||
<h4 className="text-lg font-medium text-custom-text-300">
|
<h4 className="text-lg font-medium text-custom-text-300">
|
||||||
{projectDetails?.identifier}-{issueDetail?.sequence_id}
|
{projectDetails?.identifier}-{issue?.sequence_id}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
{issueDetail?.created_by !== currentUser?.id &&
|
{currentUser && !is_archived && (fieldsToShow.includes("all") || fieldsToShow.includes("subscribe")) && (
|
||||||
!issueDetail?.assignee_ids.includes(currentUser?.id ?? "") &&
|
<IssueSubscription
|
||||||
!router.pathname.includes("[archivedIssueId]") &&
|
workspaceSlug={workspaceSlug}
|
||||||
(fieldsToShow.includes("all") || fieldsToShow.includes("subscribe")) && (
|
projectId={projectId}
|
||||||
<Button
|
issueId={issueId}
|
||||||
size="sm"
|
currentUserId={currentUser?.id}
|
||||||
prependIcon={<Bell className="h-3 w-3" />}
|
disabled={!isAllowed || !is_editable}
|
||||||
variant="outline-primary"
|
/>
|
||||||
className="hover:!bg-custom-primary-100/20"
|
|
||||||
onClick={() => {
|
|
||||||
if (subscribed) handleUnsubscribe();
|
|
||||||
else handleSubscribe();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{loading ? "Loading..." : subscribed ? "Unsubscribe" : "Subscribe"}
|
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && (
|
|
||||||
|
{/* {(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="rounded-md border border-custom-border-200 p-2 shadow-sm duration-300 hover:bg-custom-background-90 focus:border-custom-primary focus:outline-none focus:ring-1 focus:ring-custom-primary"
|
className="rounded-md border border-custom-border-200 p-2 shadow-sm duration-300 hover:bg-custom-background-90 focus:border-custom-primary focus:outline-none focus:ring-1 focus:ring-custom-primary"
|
||||||
@ -224,8 +178,9 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
>
|
>
|
||||||
<LinkIcon className="h-3.5 w-3.5" />
|
<LinkIcon className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)} */}
|
||||||
{isAllowed && (fieldsToShow.includes("all") || fieldsToShow.includes("delete")) && (
|
|
||||||
|
{/* {isAllowed && (fieldsToShow.includes("all") || fieldsToShow.includes("delete")) && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="rounded-md border border-red-500 p-2 text-red-500 shadow-sm duration-300 hover:bg-red-500/20 focus:outline-none"
|
className="rounded-md border border-red-500 p-2 text-red-500 shadow-sm duration-300 hover:bg-red-500/20 focus:outline-none"
|
||||||
@ -233,12 +188,12 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
>
|
>
|
||||||
<Trash2 className="h-3.5 w-3.5" />
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-full w-full overflow-y-auto px-5">
|
<div className="h-full w-full overflow-y-auto px-5">
|
||||||
<div className={`divide-y-2 divide-custom-border-200 ${uneditable ? "opacity-60" : ""}`}>
|
<div className={`divide-y-2 divide-custom-border-200 ${is_editable ? "opacity-60" : ""}`}>
|
||||||
{showFirstSection && (
|
{showFirstSection && (
|
||||||
<div className="py-1">
|
<div className="py-1">
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && (
|
||||||
@ -247,71 +202,63 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
<DoubleCircleIcon className="h-4 w-4 flex-shrink-0" />
|
<DoubleCircleIcon className="h-4 w-4 flex-shrink-0" />
|
||||||
<p>State</p>
|
<p>State</p>
|
||||||
</div>
|
</div>
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="state"
|
|
||||||
render={({ field: { value } }) => (
|
|
||||||
<div className="h-5 sm:w-1/2">
|
<div className="h-5 sm:w-1/2">
|
||||||
<StateDropdown
|
<StateDropdown
|
||||||
value={value}
|
value={issue?.state_id ?? undefined}
|
||||||
onChange={(val) => submitChanges({ state: val })}
|
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { state_id: val })}
|
||||||
projectId={projectId?.toString() ?? ""}
|
projectId={projectId?.toString() ?? ""}
|
||||||
disabled={!isAllowed || uneditable}
|
disabled={!isAllowed || !is_editable}
|
||||||
buttonVariant="background-with-text"
|
buttonVariant="background-with-text"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("assignee")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("assignee")) && (
|
||||||
<div className="flex flex-wrap items-center py-2">
|
<div className="flex flex-wrap items-center py-2">
|
||||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:w-1/2">
|
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:w-1/2">
|
||||||
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
|
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
|
||||||
<p>Assignees</p>
|
<p>Assignees</p>
|
||||||
</div>
|
</div>
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="assignees"
|
|
||||||
render={({ field: { value } }) => (
|
|
||||||
<div className="h-5 sm:w-1/2">
|
<div className="h-5 sm:w-1/2">
|
||||||
<ProjectMemberDropdown
|
<ProjectMemberDropdown
|
||||||
value={value}
|
value={issue?.assignee_ids ?? undefined}
|
||||||
onChange={(val) => submitChanges({ assignees: val })}
|
onChange={(val) =>
|
||||||
disabled={!isAllowed || uneditable}
|
issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })
|
||||||
|
}
|
||||||
|
disabled={!isAllowed || !is_editable}
|
||||||
projectId={projectId?.toString() ?? ""}
|
projectId={projectId?.toString() ?? ""}
|
||||||
placeholder="Assignees"
|
placeholder="Assignees"
|
||||||
multiple
|
multiple
|
||||||
buttonVariant={value?.length > 0 ? "transparent-without-text" : "background-with-text"}
|
buttonVariant={
|
||||||
buttonClassName={value?.length > 0 ? "hover:bg-transparent px-0" : ""}
|
issue?.assignee_ids?.length > 0 ? "transparent-without-text" : "background-with-text"
|
||||||
|
}
|
||||||
|
buttonClassName={issue?.assignee_ids?.length > 0 ? "hover:bg-transparent px-0" : ""}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("priority")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("priority")) && (
|
||||||
<div className="flex flex-wrap items-center py-2">
|
<div className="flex flex-wrap items-center py-2">
|
||||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:w-1/2">
|
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:w-1/2">
|
||||||
<Signal className="h-4 w-4 flex-shrink-0" />
|
<Signal className="h-4 w-4 flex-shrink-0" />
|
||||||
<p>Priority</p>
|
<p>Priority</p>
|
||||||
</div>
|
</div>
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="priority"
|
|
||||||
render={({ field: { value } }) => (
|
|
||||||
<div className="h-5 sm:w-1/2">
|
<div className="h-5 sm:w-1/2">
|
||||||
<PriorityDropdown
|
<PriorityDropdown
|
||||||
value={value}
|
value={issue?.priority || undefined}
|
||||||
onChange={(val) => submitChanges({ priority: val })}
|
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { priority: val })}
|
||||||
disabled={!isAllowed || uneditable}
|
disabled={!isAllowed || !is_editable}
|
||||||
buttonVariant="background-with-text"
|
buttonVariant="background-with-text"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) &&
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) &&
|
||||||
areEstimatesEnabledForCurrentProject && (
|
areEstimatesEnabledForCurrentProject && (
|
||||||
<div className="flex flex-wrap items-center py-2">
|
<div className="flex flex-wrap items-center py-2">
|
||||||
@ -319,25 +266,23 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
<Triangle className="h-4 w-4 flex-shrink-0 " />
|
<Triangle className="h-4 w-4 flex-shrink-0 " />
|
||||||
<p>Estimate</p>
|
<p>Estimate</p>
|
||||||
</div>
|
</div>
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="estimate_point"
|
|
||||||
render={({ field: { value } }) => (
|
|
||||||
<div className="h-5 sm:w-1/2">
|
<div className="h-5 sm:w-1/2">
|
||||||
<EstimateDropdown
|
<EstimateDropdown
|
||||||
value={value}
|
value={issue?.estimate_point || null}
|
||||||
onChange={(val) => submitChanges({ estimate_point: val })}
|
onChange={(val) =>
|
||||||
projectId={projectId?.toString() ?? ""}
|
issueOperations.update(workspaceSlug, projectId, issueId, { estimate_point: val })
|
||||||
disabled={!isAllowed || uneditable}
|
}
|
||||||
|
projectId={projectId}
|
||||||
|
disabled={!isAllowed || !is_editable}
|
||||||
buttonVariant="background-with-text"
|
buttonVariant="background-with-text"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showSecondSection && (
|
{showSecondSection && (
|
||||||
<div className="py-1">
|
<div className="py-1">
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
|
||||||
@ -347,53 +292,54 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
<p>Parent</p>
|
<p>Parent</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:basis-1/2">
|
<div className="sm:basis-1/2">
|
||||||
<Controller
|
<IssueParentSelect
|
||||||
control={control}
|
workspaceSlug={workspaceSlug}
|
||||||
name="parent"
|
projectId={projectId}
|
||||||
render={({ field: { onChange } }) => (
|
issueId={issueId}
|
||||||
<SidebarParentSelect
|
issueOperations={issueOperations}
|
||||||
onChange={(val: string) => {
|
disabled={!isAllowed || !is_editable}
|
||||||
submitChanges({ parent: val });
|
|
||||||
onChange(val);
|
|
||||||
}}
|
|
||||||
issueDetails={issueDetail}
|
|
||||||
disabled={!isAllowed || uneditable}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("blocker")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("blocker")) && (
|
||||||
<SidebarIssueRelationSelect
|
<IssueRelationSelect
|
||||||
issueId={issueId as string}
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
issueId={issueId}
|
||||||
relationKey="blocking"
|
relationKey="blocking"
|
||||||
disabled={!isAllowed || uneditable}
|
disabled={!isAllowed || !is_editable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("blocked")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("blocked")) && (
|
||||||
<SidebarIssueRelationSelect
|
<IssueRelationSelect
|
||||||
issueId={issueId as string}
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
issueId={issueId}
|
||||||
relationKey="blocked_by"
|
relationKey="blocked_by"
|
||||||
disabled={!isAllowed || uneditable}
|
disabled={!isAllowed || !is_editable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("duplicate")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("duplicate")) && (
|
||||||
<SidebarIssueRelationSelect
|
<IssueRelationSelect
|
||||||
issueId={issueId as string}
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
issueId={issueId}
|
||||||
relationKey="duplicate"
|
relationKey="duplicate"
|
||||||
disabled={!isAllowed || uneditable}
|
disabled={!isAllowed || !is_editable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("relates_to")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("relates_to")) && (
|
||||||
<SidebarIssueRelationSelect
|
<IssueRelationSelect
|
||||||
issueId={issueId as string}
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
issueId={issueId}
|
||||||
relationKey="relates_to"
|
relationKey="relates_to"
|
||||||
disabled={!isAllowed || uneditable}
|
disabled={!isAllowed || !is_editable}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -404,27 +350,20 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
<p>Start date</p>
|
<p>Start date</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:basis-1/2">
|
<div className="sm:basis-1/2">
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="start_date"
|
|
||||||
render={({ field: { value } }) => (
|
|
||||||
<CustomDatePicker
|
<CustomDatePicker
|
||||||
placeholder="Start date"
|
placeholder="Start date"
|
||||||
value={value}
|
value={issue.start_date || undefined}
|
||||||
onChange={(val) =>
|
onChange={(val) =>
|
||||||
submitChanges({
|
issueOperations.update(workspaceSlug, projectId, issueId, { start_date: val })
|
||||||
start_date: val,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
className="border-none bg-custom-background-80"
|
className="border-none bg-custom-background-80"
|
||||||
maxDate={maxDate ?? undefined}
|
maxDate={maxDate ?? undefined}
|
||||||
disabled={!isAllowed || uneditable}
|
disabled={!isAllowed || !is_editable}
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("dueDate")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("dueDate")) && (
|
||||||
<div className="flex flex-wrap items-center py-2">
|
<div className="flex flex-wrap items-center py-2">
|
||||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
|
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
|
||||||
@ -432,23 +371,15 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
<p>Due date</p>
|
<p>Due date</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:basis-1/2">
|
<div className="sm:basis-1/2">
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="target_date"
|
|
||||||
render={({ field: { value } }) => (
|
|
||||||
<CustomDatePicker
|
<CustomDatePicker
|
||||||
placeholder="Due date"
|
placeholder="Due date"
|
||||||
value={value}
|
value={issue.target_date || undefined}
|
||||||
onChange={(val) =>
|
onChange={(val) =>
|
||||||
submitChanges({
|
issueOperations.update(workspaceSlug, projectId, issueId, { target_date: val })
|
||||||
target_date: val,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
className="border-none bg-custom-background-80"
|
className="border-none bg-custom-background-80"
|
||||||
minDate={minDate ?? undefined}
|
minDate={minDate ?? undefined}
|
||||||
disabled={!isAllowed || uneditable}
|
disabled={!isAllowed || !is_editable}
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -465,14 +396,17 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
<p>Cycle</p>
|
<p>Cycle</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<SidebarCycleSelect
|
<IssueCycleSelect
|
||||||
issueDetail={issueDetail}
|
workspaceSlug={workspaceSlug}
|
||||||
handleCycleChange={handleCycleChange}
|
projectId={projectId}
|
||||||
disabled={!isAllowed || uneditable}
|
issueId={issueId}
|
||||||
|
issueOperations={issueOperations}
|
||||||
|
disabled={!isAllowed || !is_editable}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("module")) && projectDetails?.module_view && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("module")) && projectDetails?.module_view && (
|
||||||
<div className="flex flex-wrap items-center py-2">
|
<div className="flex flex-wrap items-center py-2">
|
||||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:w-1/2">
|
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:w-1/2">
|
||||||
@ -480,10 +414,12 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
<p>Module</p>
|
<p>Module</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<SidebarModuleSelect
|
<IssueModuleSelect
|
||||||
issueDetail={issueDetail}
|
workspaceSlug={workspaceSlug}
|
||||||
handleModuleChange={handleModuleChange}
|
projectId={projectId}
|
||||||
disabled={!isAllowed || uneditable}
|
issueId={issueId}
|
||||||
|
issueOperations={issueOperations}
|
||||||
|
disabled={!isAllowed || !is_editable}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -499,19 +435,24 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
<p>Label</p>
|
<p>Label</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1 sm:w-1/2">
|
<div className="space-y-1 sm:w-1/2">
|
||||||
<SidebarLabelSelect
|
<IssueLabel
|
||||||
issueDetails={issueDetail}
|
workspaceSlug={workspaceSlug}
|
||||||
labelList={issueDetail?.label_ids ?? []}
|
projectId={projectId}
|
||||||
submitChanges={submitChanges}
|
issueId={issueId}
|
||||||
isNotAllowed={!isAllowed}
|
disabled={!isAllowed || !is_editable}
|
||||||
uneditable={uneditable || !isAllowed}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && (
|
{(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && (
|
||||||
<IssueLinkRoot uneditable={uneditable} isAllowed={isAllowed} />
|
<IssueLinkRoot
|
||||||
|
workspaceSlug={workspaceSlug}
|
||||||
|
projectId={projectId}
|
||||||
|
issueId={issueId}
|
||||||
|
is_editable={is_editable}
|
||||||
|
is_archived={is_archived}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
54
web/components/issues/issue-detail/subscription.tsx
Normal file
54
web/components/issues/issue-detail/subscription.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { FC, useState } from "react";
|
||||||
|
import { Bell } from "lucide-react";
|
||||||
|
import { observer } from "mobx-react-lite";
|
||||||
|
// UI
|
||||||
|
import { Button } from "@plane/ui";
|
||||||
|
// hooks
|
||||||
|
import { useIssueDetail } from "hooks/store";
|
||||||
|
|
||||||
|
export type TIssueSubscription = {
|
||||||
|
workspaceSlug: string;
|
||||||
|
projectId: string;
|
||||||
|
issueId: string;
|
||||||
|
currentUserId: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IssueSubscription: FC<TIssueSubscription> = observer((props) => {
|
||||||
|
const { workspaceSlug, projectId, issueId, currentUserId, disabled } = props;
|
||||||
|
// hooks
|
||||||
|
const {
|
||||||
|
issue: { getIssueById },
|
||||||
|
subscription: { getSubscriptionByIssueId },
|
||||||
|
createSubscription,
|
||||||
|
removeSubscription,
|
||||||
|
} = useIssueDetail();
|
||||||
|
// state
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const issue = getIssueById(issueId);
|
||||||
|
const subscription = getSubscriptionByIssueId(issueId);
|
||||||
|
|
||||||
|
const handleSubscription = () => {
|
||||||
|
setLoading(true);
|
||||||
|
if (subscription?.subscribed) removeSubscription(workspaceSlug, projectId, issueId);
|
||||||
|
else createSubscription(workspaceSlug, projectId, issueId);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (issue?.created_by === currentUserId || issue?.assignee_ids.includes(currentUserId)) return <></>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
prependIcon={<Bell className="h-3 w-3" />}
|
||||||
|
variant="outline-primary"
|
||||||
|
className="hover:!bg-custom-primary-100/20"
|
||||||
|
onClick={handleSubscription}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{loading ? "Loading..." : subscription?.subscribed ? "Unsubscribe" : "Subscribe"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
@ -4,12 +4,13 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { Draggable } from "@hello-pangea/dnd";
|
import { Draggable } from "@hello-pangea/dnd";
|
||||||
import { MoreHorizontal } from "lucide-react";
|
import { MoreHorizontal } from "lucide-react";
|
||||||
// components
|
// components
|
||||||
import { Tooltip } from "@plane/ui";
|
import { Tooltip, ControlLink } from "@plane/ui";
|
||||||
// hooks
|
// hooks
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
|
// ui
|
||||||
// types
|
// types
|
||||||
import { TIssue, TIssueMap } from "@plane/types";
|
import { TIssue, TIssueMap } from "@plane/types";
|
||||||
import { useProject, useProjectState } from "hooks/store";
|
import { useApplication, useIssueDetail, useProject, useProjectState } from "hooks/store";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
issues: TIssueMap | undefined;
|
issues: TIssueMap | undefined;
|
||||||
@ -23,21 +24,23 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
|
|||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
// hooks
|
// hooks
|
||||||
|
const {
|
||||||
|
router: { workspaceSlug, projectId },
|
||||||
|
} = useApplication();
|
||||||
const { getProjectById } = useProject();
|
const { getProjectById } = useProject();
|
||||||
const { getProjectStates } = useProjectState();
|
const { getProjectStates } = useProjectState();
|
||||||
|
const { setPeekIssue } = useIssueDetail();
|
||||||
// states
|
// states
|
||||||
const [isMenuActive, setIsMenuActive] = useState(false);
|
const [isMenuActive, setIsMenuActive] = useState(false);
|
||||||
|
|
||||||
const menuActionRef = useRef<HTMLDivElement | null>(null);
|
const menuActionRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
const handleIssuePeekOverview = (issue: TIssue) => {
|
const handleIssuePeekOverview = (issue: TIssue) =>
|
||||||
const { query } = router;
|
workspaceSlug &&
|
||||||
|
issue &&
|
||||||
router.push({
|
issue.project_id &&
|
||||||
pathname: router.pathname,
|
issue.id &&
|
||||||
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project_id },
|
setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id });
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false));
|
useOutsideClickDetector(menuActionRef, () => setIsMenuActive(false));
|
||||||
|
|
||||||
@ -67,8 +70,14 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
|
|||||||
{...provided.draggableProps}
|
{...provided.draggableProps}
|
||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
onClick={() => handleIssuePeekOverview(issue)}
|
|
||||||
>
|
>
|
||||||
|
<ControlLink
|
||||||
|
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
|
||||||
|
target="_blank"
|
||||||
|
onClick={() => handleIssuePeekOverview(issue)}
|
||||||
|
className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
|
||||||
|
>
|
||||||
|
<>
|
||||||
{issue?.tempId !== undefined && (
|
{issue?.tempId !== undefined && (
|
||||||
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />
|
<div className="absolute left-0 top-0 z-[99999] h-full w-full animate-pulse bg-custom-background-100/20" />
|
||||||
)}
|
)}
|
||||||
@ -106,6 +115,8 @@ export const CalendarIssueBlocks: React.FC<Props> = observer((props) => {
|
|||||||
{quickActions(issue, customActionButton)}
|
{quickActions(issue, customActionButton)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
</ControlLink>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Draggable>
|
</Draggable>
|
||||||
|
@ -1,25 +1,26 @@
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
// ui
|
// ui
|
||||||
import { Tooltip, StateGroupIcon } from "@plane/ui";
|
import { Tooltip, StateGroupIcon, ControlLink } from "@plane/ui";
|
||||||
// helpers
|
// helpers
|
||||||
import { renderFormattedDate } from "helpers/date-time.helper";
|
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||||
// types
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
import { useProject, useProjectState } from "hooks/store";
|
import { useApplication, useIssueDetail, useProject, useProjectState } from "hooks/store";
|
||||||
|
|
||||||
export const IssueGanttBlock = ({ data }: { data: TIssue }) => {
|
export const IssueGanttBlock = ({ data }: { data: TIssue }) => {
|
||||||
const router = useRouter();
|
|
||||||
// hooks
|
// hooks
|
||||||
|
const {
|
||||||
|
router: { workspaceSlug },
|
||||||
|
} = useApplication();
|
||||||
const { getProjectStates } = useProjectState();
|
const { getProjectStates } = useProjectState();
|
||||||
|
const { setPeekIssue } = useIssueDetail();
|
||||||
|
|
||||||
const handleIssuePeekOverview = () => {
|
const handleIssuePeekOverview = () =>
|
||||||
const { query } = router;
|
workspaceSlug &&
|
||||||
|
data &&
|
||||||
router.push({
|
data.project_id &&
|
||||||
pathname: router.pathname,
|
data.id &&
|
||||||
query: { ...query, peekIssueId: data?.id, peekProjectId: data?.project_id },
|
setPeekIssue({ workspaceSlug, projectId: data.project_id, issueId: data.id });
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@ -49,24 +50,31 @@ export const IssueGanttBlock = ({ data }: { data: TIssue }) => {
|
|||||||
|
|
||||||
// rendering issues on gantt sidebar
|
// rendering issues on gantt sidebar
|
||||||
export const IssueGanttSidebarBlock = ({ data }: { data: TIssue }) => {
|
export const IssueGanttSidebarBlock = ({ data }: { data: TIssue }) => {
|
||||||
const router = useRouter();
|
|
||||||
// hooks
|
// hooks
|
||||||
const { getProjectStates } = useProjectState();
|
const { getProjectStates } = useProjectState();
|
||||||
const { getProjectById } = useProject();
|
const { getProjectById } = useProject();
|
||||||
|
const {
|
||||||
|
router: { workspaceSlug },
|
||||||
|
} = useApplication();
|
||||||
|
const { setPeekIssue } = useIssueDetail();
|
||||||
|
|
||||||
const handleIssuePeekOverview = () => {
|
const handleIssuePeekOverview = () =>
|
||||||
const { query } = router;
|
workspaceSlug &&
|
||||||
|
data &&
|
||||||
router.push({
|
data.project_id &&
|
||||||
pathname: router.pathname,
|
data.id &&
|
||||||
query: { ...query, peekIssueId: data?.id, peekProjectId: data?.project_id },
|
setPeekIssue({ workspaceSlug, projectId: data.project_id, issueId: data.id });
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const currentStateDetails =
|
const currentStateDetails =
|
||||||
getProjectStates(data?.project_id)?.find((state) => state?.id == data?.state_id) || undefined;
|
getProjectStates(data?.project_id)?.find((state) => state?.id == data?.state_id) || undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<ControlLink
|
||||||
|
href={`/${workspaceSlug}/projects/${data.project_id}/issues/${data.id}`}
|
||||||
|
target="_blank"
|
||||||
|
onClick={handleIssuePeekOverview}
|
||||||
|
className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
|
||||||
|
>
|
||||||
<div className="relative flex h-full w-full cursor-pointer items-center gap-2" onClick={handleIssuePeekOverview}>
|
<div className="relative flex h-full w-full cursor-pointer items-center gap-2" onClick={handleIssuePeekOverview}>
|
||||||
{currentStateDetails != undefined && (
|
{currentStateDetails != undefined && (
|
||||||
<StateGroupIcon stateGroup={currentStateDetails?.group} color={currentStateDetails?.color} />
|
<StateGroupIcon stateGroup={currentStateDetails?.group} color={currentStateDetails?.color} />
|
||||||
@ -78,5 +86,6 @@ export const IssueGanttSidebarBlock = ({ data }: { data: TIssue }) => {
|
|||||||
<span className="flex-grow truncate text-sm font-medium">{data?.name}</span>
|
<span className="flex-grow truncate text-sm font-medium">{data?.name}</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
</ControlLink>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { useEffect, useState, useRef } from "react";
|
import { useEffect, useState, useRef, FC } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useProject, useWorkspace } from "hooks/store";
|
import { useProject } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import useKeypress from "hooks/use-keypress";
|
import useKeypress from "hooks/use-keypress";
|
||||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
@ -12,9 +12,38 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
|||||||
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
||||||
import { createIssuePayload } from "helpers/issue.helper";
|
import { createIssuePayload } from "helpers/issue.helper";
|
||||||
// types
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
import { IProject, TIssue } from "@plane/types";
|
||||||
|
|
||||||
type Props = {
|
interface IInputProps {
|
||||||
|
formKey: string;
|
||||||
|
register: any;
|
||||||
|
setFocus: any;
|
||||||
|
projectDetail: IProject | null;
|
||||||
|
}
|
||||||
|
const Inputs: FC<IInputProps> = (props) => {
|
||||||
|
const { formKey, register, setFocus, projectDetail } = props;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFocus(formKey);
|
||||||
|
}, [formKey, setFocus]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex w-full items-center gap-3">
|
||||||
|
<div className="text-xs font-medium text-custom-text-400">{projectDetail?.identifier ?? "..."}</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
placeholder="Issue Title"
|
||||||
|
{...register(formKey, {
|
||||||
|
required: "Issue title is required.",
|
||||||
|
})}
|
||||||
|
className="w-full rounded-md bg-transparent px-2 py-3 text-sm font-medium leading-5 text-custom-text-200 outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type IGanttQuickAddIssueForm = {
|
||||||
prePopulatedData?: Partial<TIssue>;
|
prePopulatedData?: Partial<TIssue>;
|
||||||
onSuccess?: (data: TIssue) => Promise<void> | void;
|
onSuccess?: (data: TIssue) => Promise<void> | void;
|
||||||
quickAddCallback?: (
|
quickAddCallback?: (
|
||||||
@ -30,34 +59,25 @@ const defaultValues: Partial<TIssue> = {
|
|||||||
name: "",
|
name: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const Inputs = (props: any) => {
|
export const GanttQuickAddIssueForm: React.FC<IGanttQuickAddIssueForm> = observer((props) => {
|
||||||
const { register, setFocus } = props;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setFocus("name");
|
|
||||||
}, [setFocus]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
autoComplete="off"
|
|
||||||
placeholder="Issue Title"
|
|
||||||
{...register("name", {
|
|
||||||
required: "Issue title is required.",
|
|
||||||
})}
|
|
||||||
className="w-full rounded-md bg-transparent px-2 text-sm font-medium leading-5 text-custom-text-200 outline-none"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const GanttInlineCreateIssueForm: React.FC<Props> = observer((props) => {
|
|
||||||
const { prePopulatedData, quickAddCallback, viewId } = props;
|
const { prePopulatedData, quickAddCallback, viewId } = props;
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
// store hooks
|
// hooks
|
||||||
const { getWorkspaceBySlug } = useWorkspace();
|
const { getProjectById } = useProject();
|
||||||
const { currentProjectDetails } = useProject();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
const projectDetail = (projectId && getProjectById(projectId.toString())) || undefined;
|
||||||
|
|
||||||
|
const ref = useRef<HTMLFormElement>(null);
|
||||||
|
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const handleClose = () => setIsOpen(false);
|
||||||
|
|
||||||
|
useKeypress("Escape", handleClose);
|
||||||
|
useOutsideClickDetector(ref, handleClose);
|
||||||
|
|
||||||
// form info
|
// form info
|
||||||
const {
|
const {
|
||||||
reset,
|
reset,
|
||||||
@ -67,103 +87,67 @@ export const GanttInlineCreateIssueForm: React.FC<Props> = observer((props) => {
|
|||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
} = useForm<TIssue>({ defaultValues });
|
} = useForm<TIssue>({ defaultValues });
|
||||||
|
|
||||||
// ref
|
|
||||||
const ref = useRef<HTMLFormElement>(null);
|
|
||||||
|
|
||||||
// states
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
|
|
||||||
const handleClose = () => setIsOpen(false);
|
|
||||||
|
|
||||||
// hooks
|
|
||||||
useKeypress("Escape", handleClose);
|
|
||||||
useOutsideClickDetector(ref, handleClose);
|
|
||||||
const { setToastAlert } = useToast();
|
|
||||||
|
|
||||||
// derived values
|
|
||||||
const workspaceDetail = getWorkspaceBySlug(workspaceSlug?.toString()!);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) reset({ ...defaultValues });
|
if (!isOpen) reset({ ...defaultValues });
|
||||||
}, [isOpen, reset]);
|
}, [isOpen, reset]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!errors) return;
|
|
||||||
|
|
||||||
Object.keys(errors).forEach((key) => {
|
|
||||||
const error = errors[key as keyof TIssue];
|
|
||||||
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: error?.message?.toString() || "Some error occurred. Please try again.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, [errors, setToastAlert]);
|
|
||||||
|
|
||||||
const onSubmitHandler = async (formData: TIssue) => {
|
const onSubmitHandler = async (formData: TIssue) => {
|
||||||
if (isSubmitting || !workspaceSlug || !projectId) return;
|
if (isSubmitting || !workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
reset({ ...defaultValues });
|
reset({ ...defaultValues });
|
||||||
|
|
||||||
|
const targetDate = new Date();
|
||||||
|
targetDate.setDate(targetDate.getDate() + 1);
|
||||||
|
|
||||||
const payload = createIssuePayload(projectId.toString(), {
|
const payload = createIssuePayload(projectId.toString(), {
|
||||||
...(prePopulatedData ?? {}),
|
...(prePopulatedData ?? {}),
|
||||||
...formData,
|
...formData,
|
||||||
|
start_date: renderFormattedPayloadDate(new Date()),
|
||||||
|
target_date: renderFormattedPayloadDate(targetDate),
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (quickAddCallback) {
|
quickAddCallback &&
|
||||||
await quickAddCallback(workspaceSlug.toString(), projectId.toString(), payload, viewId);
|
(await quickAddCallback(workspaceSlug.toString(), projectId.toString(), { ...payload }, viewId));
|
||||||
}
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "success",
|
type: "success",
|
||||||
title: "Success!",
|
title: "Success!",
|
||||||
message: "Issue created successfully.",
|
message: "Issue created successfully.",
|
||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
Object.keys(err || {}).forEach((key) => {
|
|
||||||
const error = err?.[key];
|
|
||||||
const errorTitle = error ? (Array.isArray(error) ? error.join(", ") : error) : null;
|
|
||||||
|
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
type: "error",
|
type: "error",
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
message: errorTitle || "Some error occurred. Please try again.",
|
message: err?.message || "Some error occurred. Please try again.",
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isOpen && (
|
<div
|
||||||
|
className={`${errors && errors?.name && errors?.name?.message ? `border border-red-500/20 bg-red-500/10` : ``}`}
|
||||||
|
>
|
||||||
|
{isOpen ? (
|
||||||
|
<div className="shadow-custom-shadow-sm">
|
||||||
<form
|
<form
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className="mr-2.5 flex items-center gap-x-2 rounded border-[0.5px] border-custom-border-100 bg-custom-background-100 px-2 py-3 shadow-custom-shadow-2xs"
|
|
||||||
onSubmit={handleSubmit(onSubmitHandler)}
|
onSubmit={handleSubmit(onSubmitHandler)}
|
||||||
|
className="flex w-full items-center gap-x-3 border-[0.5px] border-custom-border-100 bg-custom-background-100 px-3"
|
||||||
>
|
>
|
||||||
<div className="h-3 w-3 flex-shrink-0 rounded-full border border-custom-border-1000" />
|
<Inputs formKey={"name"} register={register} setFocus={setFocus} projectDetail={projectDetail ?? null} />
|
||||||
<h4 className="text-xs text-custom-text-400">{currentProjectDetails?.identifier ?? "..."}</h4>
|
|
||||||
<Inputs register={register} setFocus={setFocus} />
|
|
||||||
</form>
|
</form>
|
||||||
)}
|
<div className="px-3 py-2 text-xs italic text-custom-text-200">{`Press 'Enter' to add another issue`}</div>
|
||||||
|
</div>
|
||||||
{isOpen && (
|
) : (
|
||||||
<p className="ml-3 mt-3 text-xs italic text-custom-text-200">
|
<div
|
||||||
Press {"'"}Enter{"'"} to add another issue
|
className="flex w-full cursor-pointer items-center gap-2 p-3 py-3 text-custom-primary-100"
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isOpen && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex items-center gap-x-[6px] rounded-md px-2 py-1 text-custom-primary-100"
|
|
||||||
onClick={() => setIsOpen(true)}
|
onClick={() => setIsOpen(true)}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
|
<PlusIcon className="h-3.5 w-3.5 stroke-2" />
|
||||||
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
|
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
|
||||||
</button>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -15,17 +15,16 @@ import { IProjectIssues, IProjectIssuesFilter } from "store/issue/project";
|
|||||||
//components
|
//components
|
||||||
import { KanBan } from "./default";
|
import { KanBan } from "./default";
|
||||||
import { KanBanSwimLanes } from "./swimlanes";
|
import { KanBanSwimLanes } from "./swimlanes";
|
||||||
import { DeleteIssueModal, IssuePeekOverview } from "components/issues";
|
import { DeleteIssueModal } from "components/issues";
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
import { useIssues } from "hooks/store/use-issues";
|
import { useIssues } from "hooks/store/use-issues";
|
||||||
import { handleDragDrop } from "./utils";
|
import { handleDragDrop } from "./utils";
|
||||||
import { IssueKanBanViewStore } from "store/issue/issue_kanban_view.store";
|
|
||||||
import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle";
|
import { ICycleIssues, ICycleIssuesFilter } from "store/issue/cycle";
|
||||||
import { IDraftIssues, IDraftIssuesFilter } from "store/issue/draft";
|
import { IDraftIssues, IDraftIssuesFilter } from "store/issue/draft";
|
||||||
import { IProfileIssues, IProfileIssuesFilter } from "store/issue/profile";
|
import { IProfileIssues, IProfileIssuesFilter } from "store/issue/profile";
|
||||||
import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module";
|
import { IModuleIssues, IModuleIssuesFilter } from "store/issue/module";
|
||||||
import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views";
|
import { IProjectViewIssues, IProjectViewIssuesFilter } from "store/issue/project-views";
|
||||||
import { TCreateModalStoreTypes } from "constants/issue";
|
import { EIssueFilterType, TCreateModalStoreTypes } from "constants/issue";
|
||||||
|
|
||||||
export interface IBaseKanBanLayout {
|
export interface IBaseKanBanLayout {
|
||||||
issues: IProjectIssues | ICycleIssues | IDraftIssues | IModuleIssues | IProjectViewIssues | IProfileIssues;
|
issues: IProjectIssues | ICycleIssues | IDraftIssues | IModuleIssues | IProjectViewIssues | IProfileIssues;
|
||||||
@ -69,7 +68,7 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
|||||||
} = props;
|
} = props;
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, peekIssueId, peekProjectId } = router.query;
|
const { workspaceSlug, projectId } = router.query;
|
||||||
// store hooks
|
// store hooks
|
||||||
const {
|
const {
|
||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
@ -78,9 +77,6 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
|||||||
// toast alert
|
// toast alert
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
// FIXME get from filters
|
|
||||||
const kanbanViewStore: IssueKanBanViewStore = {} as IssueKanBanViewStore;
|
|
||||||
|
|
||||||
const issueIds = issues?.groupedIssueIds || [];
|
const issueIds = issues?.groupedIssueIds || [];
|
||||||
|
|
||||||
const displayFilters = issuesFilter?.issueFilters?.displayFilters;
|
const displayFilters = issuesFilter?.issueFilters?.displayFilters;
|
||||||
@ -211,10 +207,19 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKanBanToggle = (toggle: "groupByHeaderMinMax" | "subgroupByIssuesVisibility", value: string) => {
|
const handleKanbanFilters = (toggle: "group_by" | "sub_group_by", value: string) => {
|
||||||
kanbanViewStore.handleKanBanToggle(toggle, value);
|
if (workspaceSlug && projectId) {
|
||||||
|
let _kanbanFilters = issuesFilter?.issueFilters?.kanbanFilters?.[toggle] || [];
|
||||||
|
if (_kanbanFilters.includes(value)) _kanbanFilters = _kanbanFilters.filter((_value) => _value != value);
|
||||||
|
else _kanbanFilters.push(value);
|
||||||
|
issuesFilter.updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.KANBAN_FILTERS, {
|
||||||
|
[toggle]: _kanbanFilters,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const kanbanFilters = issuesFilter?.issueFilters?.kanbanFilters || { group_by: [], sub_group_by: [] };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DeleteIssueModal
|
<DeleteIssueModal
|
||||||
@ -230,8 +235,9 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={`relative h-max min-h-full w-max min-w-full bg-custom-background-90 px-3`}>
|
<div className="relative h-full w-max min-w-full bg-custom-background-90 px-2">
|
||||||
<DragDropContext onDragStart={onDragStart} onDragEnd={onDragEnd}>
|
<DragDropContext onDragStart={onDragStart} onDragEnd={onDragEnd}>
|
||||||
|
{/* drag and delete component */}
|
||||||
<div
|
<div
|
||||||
className={`fixed left-1/2 -translate-x-1/2 ${
|
className={`fixed left-1/2 -translate-x-1/2 ${
|
||||||
isDragStarted ? "z-40" : ""
|
isDragStarted ? "z-40" : ""
|
||||||
@ -262,8 +268,8 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
|||||||
group_by={group_by}
|
group_by={group_by}
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
quickActions={renderQuickActions}
|
quickActions={renderQuickActions}
|
||||||
kanBanToggle={kanbanViewStore?.kanBanToggle}
|
handleKanbanFilters={handleKanbanFilters}
|
||||||
handleKanBanToggle={handleKanBanToggle}
|
kanbanFilters={kanbanFilters}
|
||||||
enableQuickIssueCreate={enableQuickAdd}
|
enableQuickIssueCreate={enableQuickAdd}
|
||||||
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
|
showEmptyGroup={userDisplayFilters?.show_empty_groups || true}
|
||||||
quickAddCallback={issues?.quickAddIssue}
|
quickAddCallback={issues?.quickAddIssue}
|
||||||
@ -275,15 +281,6 @@ export const BaseKanBanRoot: React.FC<IBaseKanBanLayout> = observer((props: IBas
|
|||||||
/>
|
/>
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* {workspaceSlug && peekIssueId && peekProjectId && (
|
|
||||||
<IssuePeekOverview
|
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
|
||||||
projectId={peekProjectId.toString()}
|
|
||||||
issueId={peekIssueId.toString()}
|
|
||||||
handleIssue={async (issueToUpdate) => await handleIssues(issueToUpdate as TIssue, EIssueActions.UPDATE)}
|
|
||||||
/>
|
|
||||||
)} */}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -5,12 +5,11 @@ import { observer } from "mobx-react-lite";
|
|||||||
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
|
import { WithDisplayPropertiesHOC } from "../properties/with-display-properties-HOC";
|
||||||
import { IssueProperties } from "../properties/all-properties";
|
import { IssueProperties } from "../properties/all-properties";
|
||||||
// ui
|
// ui
|
||||||
import { Tooltip } from "@plane/ui";
|
import { Tooltip, ControlLink } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
|
import { TIssue, IIssueDisplayProperties, IIssueMap } from "@plane/types";
|
||||||
import { EIssueActions } from "../types";
|
import { EIssueActions } from "../types";
|
||||||
import { useRouter } from "next/router";
|
import { useApplication, useIssueDetail, useProject } from "hooks/store";
|
||||||
import { useProject } from "hooks/store";
|
|
||||||
|
|
||||||
interface IssueBlockProps {
|
interface IssueBlockProps {
|
||||||
issueId: string;
|
issueId: string;
|
||||||
@ -34,24 +33,23 @@ interface IssueDetailsBlockProps {
|
|||||||
|
|
||||||
const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((props: IssueDetailsBlockProps) => {
|
const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((props: IssueDetailsBlockProps) => {
|
||||||
const { issue, handleIssues, quickActions, isReadOnly, displayProperties } = props;
|
const { issue, handleIssues, quickActions, isReadOnly, displayProperties } = props;
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
// hooks
|
// hooks
|
||||||
const { getProjectById } = useProject();
|
const { getProjectById } = useProject();
|
||||||
|
const {
|
||||||
|
router: { workspaceSlug, projectId },
|
||||||
|
} = useApplication();
|
||||||
|
const { setPeekIssue } = useIssueDetail();
|
||||||
|
|
||||||
const updateIssue = (issueToUpdate: TIssue) => {
|
const updateIssue = (issueToUpdate: TIssue) => {
|
||||||
if (issueToUpdate) handleIssues(issueToUpdate, EIssueActions.UPDATE);
|
if (issueToUpdate) handleIssues(issueToUpdate, EIssueActions.UPDATE);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleIssuePeekOverview = () => {
|
const handleIssuePeekOverview = (issue: TIssue) =>
|
||||||
const { query } = router;
|
workspaceSlug &&
|
||||||
|
issue &&
|
||||||
router.push({
|
issue.project_id &&
|
||||||
pathname: router.pathname,
|
issue.id &&
|
||||||
query: { ...query, peekIssueId: issue?.id, peekProjectId: issue?.project_id },
|
setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id });
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -63,11 +61,18 @@ const KanbanIssueDetailsBlock: React.FC<IssueDetailsBlockProps> = observer((prop
|
|||||||
<div className="absolute -top-1 right-0 hidden group-hover/kanban-block:block">{quickActions(issue)}</div>
|
<div className="absolute -top-1 right-0 hidden group-hover/kanban-block:block">{quickActions(issue)}</div>
|
||||||
</div>
|
</div>
|
||||||
</WithDisplayPropertiesHOC>
|
</WithDisplayPropertiesHOC>
|
||||||
|
|
||||||
|
<ControlLink
|
||||||
|
href={`/${workspaceSlug}/projects/${projectId}/issues/${issue.id}`}
|
||||||
|
target="_blank"
|
||||||
|
onClick={() => handleIssuePeekOverview(issue)}
|
||||||
|
className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
|
||||||
|
>
|
||||||
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
|
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
|
||||||
<div className="line-clamp-2 text-sm font-medium text-custom-text-100" onClick={handleIssuePeekOverview}>
|
<span>{issue.name}</span>
|
||||||
{issue.name}
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
</ControlLink>
|
||||||
|
|
||||||
<IssueProperties
|
<IssueProperties
|
||||||
className="flex flex-wrap items-center gap-2 whitespace-nowrap"
|
className="flex flex-wrap items-center gap-2 whitespace-nowrap"
|
||||||
issue={issue}
|
issue={issue}
|
||||||
|
@ -14,6 +14,7 @@ import {
|
|||||||
IIssueMap,
|
IIssueMap,
|
||||||
TSubGroupedIssues,
|
TSubGroupedIssues,
|
||||||
TUnGroupedIssues,
|
TUnGroupedIssues,
|
||||||
|
TIssueKanbanFilters,
|
||||||
} from "@plane/types";
|
} from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
import { EIssueActions } from "../types";
|
import { EIssueActions } from "../types";
|
||||||
@ -30,8 +31,8 @@ export interface IGroupByKanBan {
|
|||||||
isDragDisabled: boolean;
|
isDragDisabled: boolean;
|
||||||
handleIssues: (issue: TIssue, action: EIssueActions) => void;
|
handleIssues: (issue: TIssue, action: EIssueActions) => void;
|
||||||
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
|
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
|
||||||
kanBanToggle: any;
|
kanbanFilters: TIssueKanbanFilters;
|
||||||
handleKanBanToggle: any;
|
handleKanbanFilters: any;
|
||||||
enableQuickIssueCreate?: boolean;
|
enableQuickIssueCreate?: boolean;
|
||||||
quickAddCallback?: (
|
quickAddCallback?: (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
@ -57,8 +58,8 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
|||||||
isDragDisabled,
|
isDragDisabled,
|
||||||
handleIssues,
|
handleIssues,
|
||||||
quickActions,
|
quickActions,
|
||||||
kanBanToggle,
|
kanbanFilters,
|
||||||
handleKanBanToggle,
|
handleKanbanFilters,
|
||||||
enableQuickIssueCreate,
|
enableQuickIssueCreate,
|
||||||
quickAddCallback,
|
quickAddCallback,
|
||||||
viewId,
|
viewId,
|
||||||
@ -77,40 +78,44 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
|||||||
|
|
||||||
if (!list) return null;
|
if (!list) return null;
|
||||||
|
|
||||||
const verticalAlignPosition = (_list: IGroupByColumn) => kanBanToggle?.groupByHeaderMinMax.includes(_list.id);
|
const visibilityGroupBy = (_list: IGroupByColumn) =>
|
||||||
|
sub_group_by ? false : kanbanFilters?.group_by.includes(_list.id) ? true : false;
|
||||||
|
|
||||||
const isGroupByCreatedBy = group_by === "created_by";
|
const isGroupByCreatedBy = group_by === "created_by";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-full w-full gap-3">
|
<div className={`relative w-full flex gap-3 overflow-hidden ${sub_group_by ? "h-full" : "h-full"}`}>
|
||||||
{list &&
|
{list &&
|
||||||
list.length > 0 &&
|
list.length > 0 &&
|
||||||
list.map((_list: IGroupByColumn) => {
|
list.map((_list: IGroupByColumn) => {
|
||||||
const verticalPosition = verticalAlignPosition(_list);
|
const groupByVisibilityToggle = visibilityGroupBy(_list);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`relative flex flex-shrink-0 flex-col ${!verticalPosition ? `w-[340px]` : ``} group`}
|
className={`relative flex flex-shrink-0 flex-col h-full group ${
|
||||||
key={_list.id}
|
groupByVisibilityToggle ? `` : `w-[340px]`
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{sub_group_by === null && (
|
{sub_group_by === null && (
|
||||||
<div className="sticky top-0 z-[2] w-full flex-shrink-0 bg-custom-background-90 py-1">
|
<div className="flex-shrink-0 sticky top-0 z-[2] w-full bg-custom-background-90 py-1">
|
||||||
<HeaderGroupByCard
|
<HeaderGroupByCard
|
||||||
sub_group_by={sub_group_by}
|
sub_group_by={sub_group_by}
|
||||||
group_by={group_by}
|
group_by={group_by}
|
||||||
column_id={_list.id}
|
column_id={_list.id}
|
||||||
icon={_list.Icon}
|
icon={_list.icon}
|
||||||
title={_list.name}
|
title={_list.name}
|
||||||
count={(issueIds as TGroupedIssues)?.[_list.id]?.length || 0}
|
count={(issueIds as TGroupedIssues)?.[_list.id]?.length || 0}
|
||||||
kanBanToggle={kanBanToggle}
|
|
||||||
handleKanBanToggle={handleKanBanToggle}
|
|
||||||
issuePayload={_list.payload}
|
issuePayload={_list.payload}
|
||||||
disableIssueCreation={disableIssueCreation || isGroupByCreatedBy}
|
disableIssueCreation={disableIssueCreation || isGroupByCreatedBy}
|
||||||
currentStore={currentStore}
|
currentStore={currentStore}
|
||||||
addIssuesToView={addIssuesToView}
|
addIssuesToView={addIssuesToView}
|
||||||
|
kanbanFilters={kanbanFilters}
|
||||||
|
handleKanbanFilters={handleKanbanFilters}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!groupByVisibilityToggle && (
|
||||||
<KanbanGroup
|
<KanbanGroup
|
||||||
groupId={_list.id}
|
groupId={_list.id}
|
||||||
issuesMap={issuesMap}
|
issuesMap={issuesMap}
|
||||||
@ -127,8 +132,9 @@ const GroupByKanBan: React.FC<IGroupByKanBan> = observer((props) => {
|
|||||||
viewId={viewId}
|
viewId={viewId}
|
||||||
disableIssueCreation={disableIssueCreation}
|
disableIssueCreation={disableIssueCreation}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
verticalPosition={verticalPosition}
|
groupByVisibilityToggle={groupByVisibilityToggle}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@ -145,8 +151,8 @@ export interface IKanBan {
|
|||||||
sub_group_id?: string;
|
sub_group_id?: string;
|
||||||
handleIssues: (issue: TIssue, action: EIssueActions) => void;
|
handleIssues: (issue: TIssue, action: EIssueActions) => void;
|
||||||
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
|
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
|
||||||
kanBanToggle: any;
|
kanbanFilters: TIssueKanbanFilters;
|
||||||
handleKanBanToggle: any;
|
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
|
||||||
showEmptyGroup: boolean;
|
showEmptyGroup: boolean;
|
||||||
enableQuickIssueCreate?: boolean;
|
enableQuickIssueCreate?: boolean;
|
||||||
quickAddCallback?: (
|
quickAddCallback?: (
|
||||||
@ -172,8 +178,8 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
|||||||
sub_group_id = "null",
|
sub_group_id = "null",
|
||||||
handleIssues,
|
handleIssues,
|
||||||
quickActions,
|
quickActions,
|
||||||
kanBanToggle,
|
kanbanFilters,
|
||||||
handleKanBanToggle,
|
handleKanbanFilters,
|
||||||
enableQuickIssueCreate,
|
enableQuickIssueCreate,
|
||||||
quickAddCallback,
|
quickAddCallback,
|
||||||
viewId,
|
viewId,
|
||||||
@ -186,7 +192,6 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
|||||||
const issueKanBanView = useKanbanView();
|
const issueKanBanView = useKanbanView();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-full w-full">
|
|
||||||
<GroupByKanBan
|
<GroupByKanBan
|
||||||
issuesMap={issuesMap}
|
issuesMap={issuesMap}
|
||||||
issueIds={issueIds}
|
issueIds={issueIds}
|
||||||
@ -197,8 +202,8 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
|||||||
isDragDisabled={!issueKanBanView?.canUserDragDrop}
|
isDragDisabled={!issueKanBanView?.canUserDragDrop}
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
quickActions={quickActions}
|
quickActions={quickActions}
|
||||||
kanBanToggle={kanBanToggle}
|
kanbanFilters={kanbanFilters}
|
||||||
handleKanBanToggle={handleKanBanToggle}
|
handleKanbanFilters={handleKanbanFilters}
|
||||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||||
quickAddCallback={quickAddCallback}
|
quickAddCallback={quickAddCallback}
|
||||||
viewId={viewId}
|
viewId={viewId}
|
||||||
@ -207,6 +212,5 @@ export const KanBan: React.FC<IKanBan> = observer((props) => {
|
|||||||
addIssuesToView={addIssuesToView}
|
addIssuesToView={addIssuesToView}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -11,7 +11,7 @@ import useToast from "hooks/use-toast";
|
|||||||
// mobx
|
// mobx
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// types
|
// types
|
||||||
import { TIssue, ISearchIssueResponse } from "@plane/types";
|
import { TIssue, ISearchIssueResponse, TIssueKanbanFilters } from "@plane/types";
|
||||||
import { TCreateModalStoreTypes } from "constants/issue";
|
import { TCreateModalStoreTypes } from "constants/issue";
|
||||||
|
|
||||||
interface IHeaderGroupByCard {
|
interface IHeaderGroupByCard {
|
||||||
@ -21,8 +21,8 @@ interface IHeaderGroupByCard {
|
|||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
title: string;
|
title: string;
|
||||||
count: number;
|
count: number;
|
||||||
kanBanToggle: any;
|
kanbanFilters: TIssueKanbanFilters;
|
||||||
handleKanBanToggle: any;
|
handleKanbanFilters: any;
|
||||||
issuePayload: Partial<TIssue>;
|
issuePayload: Partial<TIssue>;
|
||||||
disableIssueCreation?: boolean;
|
disableIssueCreation?: boolean;
|
||||||
currentStore?: TCreateModalStoreTypes;
|
currentStore?: TCreateModalStoreTypes;
|
||||||
@ -36,14 +36,14 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
|
|||||||
icon,
|
icon,
|
||||||
title,
|
title,
|
||||||
count,
|
count,
|
||||||
kanBanToggle,
|
kanbanFilters,
|
||||||
handleKanBanToggle,
|
handleKanbanFilters,
|
||||||
issuePayload,
|
issuePayload,
|
||||||
disableIssueCreation,
|
disableIssueCreation,
|
||||||
currentStore,
|
currentStore,
|
||||||
addIssuesToView,
|
addIssuesToView,
|
||||||
} = props;
|
} = props;
|
||||||
const verticalAlignPosition = kanBanToggle?.groupByHeaderMinMax.includes(column_id);
|
const verticalAlignPosition = sub_group_by ? false : kanbanFilters?.group_by.includes(column_id);
|
||||||
|
|
||||||
const [isOpen, setIsOpen] = React.useState(false);
|
const [isOpen, setIsOpen] = React.useState(false);
|
||||||
const [openExistingIssueListModal, setOpenExistingIssueListModal] = React.useState(false);
|
const [openExistingIssueListModal, setOpenExistingIssueListModal] = React.useState(false);
|
||||||
@ -117,7 +117,7 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
|
|||||||
{sub_group_by === null && (
|
{sub_group_by === null && (
|
||||||
<div
|
<div
|
||||||
className="flex h-[20px] w-[20px] flex-shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-sm transition-all hover:bg-custom-background-80"
|
className="flex h-[20px] w-[20px] flex-shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-sm transition-all hover:bg-custom-background-80"
|
||||||
onClick={() => handleKanBanToggle("groupByHeaderMinMax", column_id)}
|
onClick={() => handleKanbanFilters("group_by", column_id)}
|
||||||
>
|
>
|
||||||
{verticalAlignPosition ? (
|
{verticalAlignPosition ? (
|
||||||
<Maximize2 width={14} strokeWidth={2} />
|
<Maximize2 width={14} strokeWidth={2} />
|
||||||
|
@ -1,26 +1,26 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
// lucide icons
|
|
||||||
import { Circle, ChevronDown, ChevronUp } from "lucide-react";
|
import { Circle, ChevronDown, ChevronUp } from "lucide-react";
|
||||||
// mobx
|
// mobx
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
|
import { TIssueKanbanFilters } from "@plane/types";
|
||||||
|
|
||||||
interface IHeaderSubGroupByCard {
|
interface IHeaderSubGroupByCard {
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
title: string;
|
title: string;
|
||||||
count: number;
|
count: number;
|
||||||
column_id: string;
|
column_id: string;
|
||||||
kanBanToggle: any;
|
kanbanFilters: TIssueKanbanFilters;
|
||||||
handleKanBanToggle: any;
|
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HeaderSubGroupByCard = observer(
|
export const HeaderSubGroupByCard = observer(
|
||||||
({ icon, title, count, column_id, kanBanToggle, handleKanBanToggle }: IHeaderSubGroupByCard) => (
|
({ icon, title, count, column_id, kanbanFilters, handleKanbanFilters }: IHeaderSubGroupByCard) => (
|
||||||
<div className={`relative flex w-full flex-shrink-0 flex-row items-center gap-2 rounded-sm p-1.5`}>
|
<div className={`relative flex w-full flex-shrink-0 flex-row items-center gap-2 rounded-sm p-1.5`}>
|
||||||
<div
|
<div
|
||||||
className="flex h-[20px] w-[20px] flex-shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-sm transition-all hover:bg-custom-background-80"
|
className="flex h-[20px] w-[20px] flex-shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-sm transition-all hover:bg-custom-background-80"
|
||||||
onClick={() => handleKanBanToggle("subgroupByIssuesVisibility", column_id)}
|
onClick={() => handleKanbanFilters("sub_group_by", column_id)}
|
||||||
>
|
>
|
||||||
{kanBanToggle?.subgroupByIssuesVisibility.includes(column_id) ? (
|
{kanbanFilters?.sub_group_by.includes(column_id) ? (
|
||||||
<ChevronDown width={14} strokeWidth={2} />
|
<ChevronDown width={14} strokeWidth={2} />
|
||||||
) : (
|
) : (
|
||||||
<ChevronUp width={14} strokeWidth={2} />
|
<ChevronUp width={14} strokeWidth={2} />
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
import { Droppable } from "@hello-pangea/dnd";
|
import { Droppable } from "@hello-pangea/dnd";
|
||||||
|
// hooks
|
||||||
|
import { useProjectState } from "hooks/store";
|
||||||
|
//components
|
||||||
|
import { KanbanIssueBlocksList, KanBanQuickAddIssueForm } from ".";
|
||||||
//types
|
//types
|
||||||
import {
|
import {
|
||||||
TGroupedIssues,
|
TGroupedIssues,
|
||||||
@ -9,10 +13,6 @@ import {
|
|||||||
TUnGroupedIssues,
|
TUnGroupedIssues,
|
||||||
} from "@plane/types";
|
} from "@plane/types";
|
||||||
import { EIssueActions } from "../types";
|
import { EIssueActions } from "../types";
|
||||||
// hooks
|
|
||||||
import { useProjectState } from "hooks/store";
|
|
||||||
//components
|
|
||||||
import { KanBanQuickAddIssueForm, KanbanIssueBlocksList } from ".";
|
|
||||||
|
|
||||||
interface IKanbanGroup {
|
interface IKanbanGroup {
|
||||||
groupId: string;
|
groupId: string;
|
||||||
@ -35,7 +35,7 @@ interface IKanbanGroup {
|
|||||||
viewId?: string;
|
viewId?: string;
|
||||||
disableIssueCreation?: boolean;
|
disableIssueCreation?: boolean;
|
||||||
canEditProperties: (projectId: string | undefined) => boolean;
|
canEditProperties: (projectId: string | undefined) => boolean;
|
||||||
verticalPosition: any;
|
groupByVisibilityToggle: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const KanbanGroup = (props: IKanbanGroup) => {
|
export const KanbanGroup = (props: IKanbanGroup) => {
|
||||||
@ -46,7 +46,6 @@ export const KanbanGroup = (props: IKanbanGroup) => {
|
|||||||
sub_group_by,
|
sub_group_by,
|
||||||
issuesMap,
|
issuesMap,
|
||||||
displayProperties,
|
displayProperties,
|
||||||
verticalPosition,
|
|
||||||
issueIds,
|
issueIds,
|
||||||
isDragDisabled,
|
isDragDisabled,
|
||||||
handleIssues,
|
handleIssues,
|
||||||
@ -56,47 +55,66 @@ export const KanbanGroup = (props: IKanbanGroup) => {
|
|||||||
disableIssueCreation,
|
disableIssueCreation,
|
||||||
quickAddCallback,
|
quickAddCallback,
|
||||||
viewId,
|
viewId,
|
||||||
|
groupByVisibilityToggle,
|
||||||
} = props;
|
} = props;
|
||||||
|
// hooks
|
||||||
const projectState = useProjectState();
|
const projectState = useProjectState();
|
||||||
|
|
||||||
const prePopulateQuickAddData = (groupByKey: string | null, value: string) => {
|
const prePopulateQuickAddData = (
|
||||||
|
groupByKey: string | null,
|
||||||
|
subGroupByKey: string | null,
|
||||||
|
groupValue: string,
|
||||||
|
subGroupValue: string
|
||||||
|
) => {
|
||||||
const defaultState = projectState.projectStates?.find((state) => state.default);
|
const defaultState = projectState.projectStates?.find((state) => state.default);
|
||||||
let preloadedData: object = { state_id: defaultState?.id };
|
let preloadedData: object = { state_id: defaultState?.id };
|
||||||
|
|
||||||
if (groupByKey) {
|
if (groupByKey) {
|
||||||
if (groupByKey === "state") {
|
if (groupByKey === "state") {
|
||||||
preloadedData = { ...preloadedData, state_id: value };
|
preloadedData = { ...preloadedData, state_id: groupValue };
|
||||||
} else if (groupByKey === "priority") {
|
} else if (groupByKey === "priority") {
|
||||||
preloadedData = { ...preloadedData, priority: value };
|
preloadedData = { ...preloadedData, priority: groupValue };
|
||||||
} else if (groupByKey === "labels" && value != "None") {
|
} else if (groupByKey === "labels" && groupValue != "None") {
|
||||||
preloadedData = { ...preloadedData, label_ids: [value] };
|
preloadedData = { ...preloadedData, label_ids: [groupValue] };
|
||||||
} else if (groupByKey === "assignees" && value != "None") {
|
} else if (groupByKey === "assignees" && groupValue != "None") {
|
||||||
preloadedData = { ...preloadedData, assignee_ids: [value] };
|
preloadedData = { ...preloadedData, assignee_ids: [groupValue] };
|
||||||
} else if (groupByKey === "created_by") {
|
} else if (groupByKey === "created_by") {
|
||||||
preloadedData = { ...preloadedData };
|
preloadedData = { ...preloadedData };
|
||||||
} else {
|
} else {
|
||||||
preloadedData = { ...preloadedData, [groupByKey]: value };
|
preloadedData = { ...preloadedData, [groupByKey]: groupValue };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subGroupByKey) {
|
||||||
|
if (subGroupByKey === "state") {
|
||||||
|
preloadedData = { ...preloadedData, state_id: subGroupValue };
|
||||||
|
} else if (subGroupByKey === "priority") {
|
||||||
|
preloadedData = { ...preloadedData, priority: subGroupValue };
|
||||||
|
} else if (subGroupByKey === "labels" && subGroupValue != "None") {
|
||||||
|
preloadedData = { ...preloadedData, label_ids: [subGroupValue] };
|
||||||
|
} else if (subGroupByKey === "assignees" && subGroupValue != "None") {
|
||||||
|
preloadedData = { ...preloadedData, assignee_ids: [subGroupValue] };
|
||||||
|
} else if (subGroupByKey === "created_by") {
|
||||||
|
preloadedData = { ...preloadedData };
|
||||||
|
} else {
|
||||||
|
preloadedData = { ...preloadedData, [subGroupByKey]: subGroupValue };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return preloadedData;
|
return preloadedData;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isGroupByCreatedBy = group_by === "created_by";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${verticalPosition ? `min-h-[150px] w-[0px] overflow-hidden` : `w-full transition-all`}`}>
|
<div className={`relative w-full h-full overflow-hidden transition-all`}>
|
||||||
<Droppable droppableId={`${groupId}__${sub_group_id}`}>
|
<Droppable droppableId={`${groupId}__${sub_group_id}`}>
|
||||||
{(provided: any, snapshot: any) => (
|
{(provided: any, snapshot: any) => (
|
||||||
<div
|
<div
|
||||||
className={`relative h-full w-full transition-all ${
|
className={`relative h-full w-full transition-all ${
|
||||||
snapshot.isDraggingOver ? `bg-custom-background-80` : ``
|
sub_group_by ? `` : `overflow-hidden overflow-y-auto`
|
||||||
}`}
|
} ${snapshot.isDraggingOver ? `bg-custom-background-80` : ``}`}
|
||||||
{...provided.droppableProps}
|
{...provided.droppableProps}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
>
|
>
|
||||||
{!verticalPosition ? (
|
|
||||||
<KanbanIssueBlocksList
|
<KanbanIssueBlocksList
|
||||||
sub_group_id={sub_group_id}
|
sub_group_id={sub_group_id}
|
||||||
columnId={groupId}
|
columnId={groupId}
|
||||||
@ -108,28 +126,26 @@ export const KanbanGroup = (props: IKanbanGroup) => {
|
|||||||
quickActions={quickActions}
|
quickActions={quickActions}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
/>
|
/>
|
||||||
) : null}
|
|
||||||
|
|
||||||
{provided.placeholder}
|
{provided.placeholder}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Droppable>
|
|
||||||
|
|
||||||
<div className="sticky bottom-0 z-[0] w-full flex-shrink-0 bg-custom-background-90 py-1">
|
{enableQuickIssueCreate && !disableIssueCreation && (
|
||||||
{enableQuickIssueCreate && !disableIssueCreation && !isGroupByCreatedBy && (
|
<div className="w-full bg-custom-background-90 py-0.5 sticky bottom-0">
|
||||||
<KanBanQuickAddIssueForm
|
<KanBanQuickAddIssueForm
|
||||||
formKey="name"
|
formKey="name"
|
||||||
groupId={groupId}
|
groupId={groupId}
|
||||||
subGroupId={sub_group_id}
|
subGroupId={sub_group_id}
|
||||||
prePopulatedData={{
|
prePopulatedData={{
|
||||||
...(group_by && prePopulateQuickAddData(group_by, groupId)),
|
...(group_by && prePopulateQuickAddData(group_by, sub_group_by, groupId, sub_group_id)),
|
||||||
...(sub_group_by && sub_group_id !== "null" && { [sub_group_by]: sub_group_id }),
|
|
||||||
}}
|
}}
|
||||||
quickAddCallback={quickAddCallback}
|
quickAddCallback={quickAddCallback}
|
||||||
viewId={viewId}
|
viewId={viewId}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -120,13 +120,13 @@ export const KanBanQuickAddIssueForm: React.FC<IKanBanQuickAddIssueForm> = obser
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
{isOpen ? (
|
{isOpen ? (
|
||||||
<div className="shadow-custom-shadow-sm">
|
<div className="shadow-custom-shadow-sm m-1.5 rounded overflow-hidden">
|
||||||
<form
|
<form
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onSubmit={handleSubmit(onSubmitHandler)}
|
onSubmit={handleSubmit(onSubmitHandler)}
|
||||||
className="flex w-full items-center gap-x-3 border-[0.5px] border-t-0 border-custom-border-100 bg-custom-background-100 px-3"
|
className="flex w-full items-center gap-x-3 bg-custom-background-100 p-3"
|
||||||
>
|
>
|
||||||
<Inputs formKey={formKey} register={register} setFocus={setFocus} projectDetail={projectDetail} />
|
<Inputs formKey={formKey} register={register} setFocus={setFocus} projectDetail={projectDetail} />
|
||||||
</form>
|
</form>
|
||||||
@ -141,6 +141,6 @@ export const KanBanQuickAddIssueForm: React.FC<IKanBanQuickAddIssueForm> = obser
|
|||||||
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
|
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -13,6 +13,7 @@ import {
|
|||||||
IIssueMap,
|
IIssueMap,
|
||||||
TSubGroupedIssues,
|
TSubGroupedIssues,
|
||||||
TUnGroupedIssues,
|
TUnGroupedIssues,
|
||||||
|
TIssueKanbanFilters,
|
||||||
} from "@plane/types";
|
} from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
import { EIssueActions } from "../types";
|
import { EIssueActions } from "../types";
|
||||||
@ -25,16 +26,16 @@ interface ISubGroupSwimlaneHeader {
|
|||||||
sub_group_by: string | null;
|
sub_group_by: string | null;
|
||||||
group_by: string | null;
|
group_by: string | null;
|
||||||
list: IGroupByColumn[];
|
list: IGroupByColumn[];
|
||||||
kanBanToggle: any;
|
kanbanFilters: TIssueKanbanFilters;
|
||||||
handleKanBanToggle: any;
|
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
|
||||||
}
|
}
|
||||||
const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
|
const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
|
||||||
issueIds,
|
issueIds,
|
||||||
sub_group_by,
|
sub_group_by,
|
||||||
group_by,
|
group_by,
|
||||||
list,
|
list,
|
||||||
kanBanToggle,
|
kanbanFilters,
|
||||||
handleKanBanToggle,
|
handleKanbanFilters,
|
||||||
}) => (
|
}) => (
|
||||||
<div className="relative flex h-max min-h-full w-full items-center">
|
<div className="relative flex h-max min-h-full w-full items-center">
|
||||||
{list &&
|
{list &&
|
||||||
@ -45,11 +46,11 @@ const SubGroupSwimlaneHeader: React.FC<ISubGroupSwimlaneHeader> = ({
|
|||||||
sub_group_by={sub_group_by}
|
sub_group_by={sub_group_by}
|
||||||
group_by={group_by}
|
group_by={group_by}
|
||||||
column_id={_list.id}
|
column_id={_list.id}
|
||||||
icon={_list.Icon}
|
icon={_list.icon}
|
||||||
title={_list.name}
|
title={_list.name}
|
||||||
count={(issueIds as TGroupedIssues)?.[_list.id]?.length || 0}
|
count={(issueIds as TGroupedIssues)?.[_list.id]?.length || 0}
|
||||||
kanBanToggle={kanBanToggle}
|
kanbanFilters={kanbanFilters}
|
||||||
handleKanBanToggle={handleKanBanToggle}
|
handleKanbanFilters={handleKanbanFilters}
|
||||||
issuePayload={_list.payload}
|
issuePayload={_list.payload}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -64,8 +65,8 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader {
|
|||||||
displayProperties: IIssueDisplayProperties | undefined;
|
displayProperties: IIssueDisplayProperties | undefined;
|
||||||
handleIssues: (issue: TIssue, action: EIssueActions) => void;
|
handleIssues: (issue: TIssue, action: EIssueActions) => void;
|
||||||
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
|
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
|
||||||
kanBanToggle: any;
|
kanbanFilters: TIssueKanbanFilters;
|
||||||
handleKanBanToggle: any;
|
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
|
||||||
isDragStarted?: boolean;
|
isDragStarted?: boolean;
|
||||||
disableIssueCreation?: boolean;
|
disableIssueCreation?: boolean;
|
||||||
currentStore?: TCreateModalStoreTypes;
|
currentStore?: TCreateModalStoreTypes;
|
||||||
@ -90,8 +91,8 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
|||||||
handleIssues,
|
handleIssues,
|
||||||
quickActions,
|
quickActions,
|
||||||
displayProperties,
|
displayProperties,
|
||||||
kanBanToggle,
|
kanbanFilters,
|
||||||
handleKanBanToggle,
|
handleKanbanFilters,
|
||||||
showEmptyGroup,
|
showEmptyGroup,
|
||||||
enableQuickIssueCreate,
|
enableQuickIssueCreate,
|
||||||
canEditProperties,
|
canEditProperties,
|
||||||
@ -123,13 +124,14 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
|||||||
icon={_list.Icon}
|
icon={_list.Icon}
|
||||||
title={_list.name || ""}
|
title={_list.name || ""}
|
||||||
count={calculateIssueCount(_list.id)}
|
count={calculateIssueCount(_list.id)}
|
||||||
kanBanToggle={kanBanToggle}
|
kanbanFilters={kanbanFilters}
|
||||||
handleKanBanToggle={handleKanBanToggle}
|
handleKanbanFilters={handleKanbanFilters}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full border-b border-dashed border-custom-border-400" />
|
<div className="w-full border-b border-dashed border-custom-border-400" />
|
||||||
</div>
|
</div>
|
||||||
{!kanBanToggle?.subgroupByIssuesVisibility.includes(_list.id) && (
|
|
||||||
|
{!kanbanFilters?.sub_group_by.includes(_list.id) && (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<KanBan
|
<KanBan
|
||||||
issuesMap={issuesMap}
|
issuesMap={issuesMap}
|
||||||
@ -140,8 +142,8 @@ const SubGroupSwimlane: React.FC<ISubGroupSwimlane> = observer((props) => {
|
|||||||
sub_group_id={_list.id}
|
sub_group_id={_list.id}
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
quickActions={quickActions}
|
quickActions={quickActions}
|
||||||
kanBanToggle={kanBanToggle}
|
kanbanFilters={kanbanFilters}
|
||||||
handleKanBanToggle={handleKanBanToggle}
|
handleKanbanFilters={handleKanbanFilters}
|
||||||
showEmptyGroup={showEmptyGroup}
|
showEmptyGroup={showEmptyGroup}
|
||||||
enableQuickIssueCreate={enableQuickIssueCreate}
|
enableQuickIssueCreate={enableQuickIssueCreate}
|
||||||
canEditProperties={canEditProperties}
|
canEditProperties={canEditProperties}
|
||||||
@ -165,8 +167,8 @@ export interface IKanBanSwimLanes {
|
|||||||
group_by: string | null;
|
group_by: string | null;
|
||||||
handleIssues: (issue: TIssue, action: EIssueActions) => void;
|
handleIssues: (issue: TIssue, action: EIssueActions) => void;
|
||||||
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
|
quickActions: (issue: TIssue, customActionButton?: React.ReactElement) => React.ReactNode;
|
||||||
kanBanToggle: any;
|
kanbanFilters: TIssueKanbanFilters;
|
||||||
handleKanBanToggle: any;
|
handleKanbanFilters: (toggle: "group_by" | "sub_group_by", value: string) => void;
|
||||||
showEmptyGroup: boolean;
|
showEmptyGroup: boolean;
|
||||||
isDragStarted?: boolean;
|
isDragStarted?: boolean;
|
||||||
disableIssueCreation?: boolean;
|
disableIssueCreation?: boolean;
|
||||||
@ -192,8 +194,8 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
|||||||
group_by,
|
group_by,
|
||||||
handleIssues,
|
handleIssues,
|
||||||
quickActions,
|
quickActions,
|
||||||
kanBanToggle,
|
kanbanFilters,
|
||||||
handleKanBanToggle,
|
handleKanbanFilters,
|
||||||
showEmptyGroup,
|
showEmptyGroup,
|
||||||
isDragStarted,
|
isDragStarted,
|
||||||
disableIssueCreation,
|
disableIssueCreation,
|
||||||
@ -227,8 +229,8 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
|||||||
issueIds={issueIds}
|
issueIds={issueIds}
|
||||||
group_by={group_by}
|
group_by={group_by}
|
||||||
sub_group_by={sub_group_by}
|
sub_group_by={sub_group_by}
|
||||||
kanBanToggle={kanBanToggle}
|
kanbanFilters={kanbanFilters}
|
||||||
handleKanBanToggle={handleKanBanToggle}
|
handleKanbanFilters={handleKanbanFilters}
|
||||||
list={groupByList}
|
list={groupByList}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -243,8 +245,8 @@ export const KanBanSwimLanes: React.FC<IKanBanSwimLanes> = observer((props) => {
|
|||||||
sub_group_by={sub_group_by}
|
sub_group_by={sub_group_by}
|
||||||
handleIssues={handleIssues}
|
handleIssues={handleIssues}
|
||||||
quickActions={quickActions}
|
quickActions={quickActions}
|
||||||
kanBanToggle={kanBanToggle}
|
kanbanFilters={kanbanFilters}
|
||||||
handleKanBanToggle={handleKanBanToggle}
|
handleKanbanFilters={handleKanbanFilters}
|
||||||
showEmptyGroup={showEmptyGroup}
|
showEmptyGroup={showEmptyGroup}
|
||||||
isDragStarted={isDragStarted}
|
isDragStarted={isDragStarted}
|
||||||
disableIssueCreation={disableIssueCreation}
|
disableIssueCreation={disableIssueCreation}
|
||||||
|
@ -8,6 +8,41 @@ import { IProjectViewIssues } from "store/issue/project-views";
|
|||||||
import { IWorkspaceIssues } from "store/issue/workspace";
|
import { IWorkspaceIssues } from "store/issue/workspace";
|
||||||
import { TGroupedIssues, IIssueMap, TSubGroupedIssues, TUnGroupedIssues } from "@plane/types";
|
import { TGroupedIssues, IIssueMap, TSubGroupedIssues, TUnGroupedIssues } from "@plane/types";
|
||||||
|
|
||||||
|
const handleSortOrder = (destinationIssues: string[], destinationIndex: number, issueMap: IIssueMap) => {
|
||||||
|
const sortOrderDefaultValue = 65535;
|
||||||
|
let currentIssueState = {};
|
||||||
|
|
||||||
|
if (destinationIssues && destinationIssues.length > 0) {
|
||||||
|
if (destinationIndex === 0) {
|
||||||
|
const destinationIssueId = destinationIssues[destinationIndex];
|
||||||
|
currentIssueState = {
|
||||||
|
...currentIssueState,
|
||||||
|
sort_order: issueMap[destinationIssueId].sort_order - sortOrderDefaultValue,
|
||||||
|
};
|
||||||
|
} else if (destinationIndex === destinationIssues.length) {
|
||||||
|
const destinationIssueId = destinationIssues[destinationIndex - 1];
|
||||||
|
currentIssueState = {
|
||||||
|
...currentIssueState,
|
||||||
|
sort_order: issueMap[destinationIssueId].sort_order + sortOrderDefaultValue,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const destinationTopIssueId = destinationIssues[destinationIndex - 1];
|
||||||
|
const destinationBottomIssueId = destinationIssues[destinationIndex];
|
||||||
|
currentIssueState = {
|
||||||
|
...currentIssueState,
|
||||||
|
sort_order: (issueMap[destinationTopIssueId].sort_order + issueMap[destinationBottomIssueId].sort_order) / 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currentIssueState = {
|
||||||
|
...currentIssueState,
|
||||||
|
sort_order: sortOrderDefaultValue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentIssueState;
|
||||||
|
};
|
||||||
|
|
||||||
export const handleDragDrop = async (
|
export const handleDragDrop = async (
|
||||||
source: DraggableLocation | null | undefined,
|
source: DraggableLocation | null | undefined,
|
||||||
destination: DraggableLocation | null | undefined,
|
destination: DraggableLocation | null | undefined,
|
||||||
@ -50,7 +85,7 @@ export const handleDragDrop = async (
|
|||||||
!sourceGroupByColumnId ||
|
!sourceGroupByColumnId ||
|
||||||
!destinationGroupByColumnId ||
|
!destinationGroupByColumnId ||
|
||||||
!sourceSubGroupByColumnId ||
|
!sourceSubGroupByColumnId ||
|
||||||
!sourceGroupByColumnId
|
!destinationSubGroupByColumnId
|
||||||
)
|
)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -76,9 +111,9 @@ export const handleDragDrop = async (
|
|||||||
const [removed] = sourceIssues.splice(source.index, 1);
|
const [removed] = sourceIssues.splice(source.index, 1);
|
||||||
const removedIssueDetail = issueMap[removed];
|
const removedIssueDetail = issueMap[removed];
|
||||||
|
|
||||||
if (subGroupBy && sourceSubGroupByColumnId && destinationSubGroupByColumnId) {
|
|
||||||
updateIssue = {
|
updateIssue = {
|
||||||
id: removedIssueDetail?.id,
|
id: removedIssueDetail?.id,
|
||||||
|
project_id: removedIssueDetail?.project_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
// for both horizontal and vertical dnd
|
// for both horizontal and vertical dnd
|
||||||
@ -87,81 +122,38 @@ export const handleDragDrop = async (
|
|||||||
...handleSortOrder(destinationIssues, destination.index, issueMap),
|
...handleSortOrder(destinationIssues, destination.index, issueMap),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (subGroupBy && sourceSubGroupByColumnId && destinationSubGroupByColumnId) {
|
||||||
if (sourceSubGroupByColumnId === destinationSubGroupByColumnId) {
|
if (sourceSubGroupByColumnId === destinationSubGroupByColumnId) {
|
||||||
if (sourceGroupByColumnId != destinationGroupByColumnId) {
|
if (sourceGroupByColumnId != destinationGroupByColumnId) {
|
||||||
if (groupBy === "state") updateIssue = { ...updateIssue, state: destinationGroupByColumnId };
|
if (groupBy === "state") updateIssue = { ...updateIssue, state_id: destinationGroupByColumnId };
|
||||||
if (groupBy === "priority") updateIssue = { ...updateIssue, priority: destinationGroupByColumnId };
|
if (groupBy === "priority") updateIssue = { ...updateIssue, priority: destinationGroupByColumnId };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (subGroupBy === "state")
|
if (subGroupBy === "state")
|
||||||
updateIssue = {
|
updateIssue = {
|
||||||
...updateIssue,
|
...updateIssue,
|
||||||
state: destinationSubGroupByColumnId,
|
state_id: destinationSubGroupByColumnId,
|
||||||
priority: destinationGroupByColumnId,
|
priority: destinationGroupByColumnId,
|
||||||
};
|
};
|
||||||
if (subGroupBy === "priority")
|
if (subGroupBy === "priority")
|
||||||
updateIssue = {
|
updateIssue = {
|
||||||
...updateIssue,
|
...updateIssue,
|
||||||
state: destinationGroupByColumnId,
|
state_id: destinationGroupByColumnId,
|
||||||
priority: destinationSubGroupByColumnId,
|
priority: destinationSubGroupByColumnId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
updateIssue = {
|
|
||||||
id: removedIssueDetail?.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
// for both horizontal and vertical dnd
|
|
||||||
updateIssue = {
|
|
||||||
...updateIssue,
|
|
||||||
...handleSortOrder(destinationIssues, destination.index, issueMap),
|
|
||||||
};
|
|
||||||
|
|
||||||
// for horizontal dnd
|
// for horizontal dnd
|
||||||
if (sourceColumnId != destinationColumnId) {
|
if (sourceColumnId != destinationColumnId) {
|
||||||
if (groupBy === "state") updateIssue = { ...updateIssue, state: destinationGroupByColumnId };
|
if (groupBy === "state") updateIssue = { ...updateIssue, state_id: destinationGroupByColumnId };
|
||||||
if (groupBy === "priority") updateIssue = { ...updateIssue, priority: destinationGroupByColumnId };
|
if (groupBy === "priority") updateIssue = { ...updateIssue, priority: destinationGroupByColumnId };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updateIssue && updateIssue?.id) {
|
if (updateIssue && updateIssue?.id) {
|
||||||
if (viewId) return await store?.updateIssue(workspaceSlug, projectId, updateIssue.id, updateIssue); //, viewId);
|
if (viewId)
|
||||||
else return await store?.updateIssue(workspaceSlug, projectId, updateIssue.id, updateIssue);
|
return await store?.updateIssue(workspaceSlug, updateIssue.project_id, updateIssue.id, updateIssue, viewId);
|
||||||
|
else return await store?.updateIssue(workspaceSlug, updateIssue.project_id, updateIssue.id, updateIssue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSortOrder = (destinationIssues: string[], destinationIndex: number, issueMap: IIssueMap) => {
|
|
||||||
const sortOrderDefaultValue = 65535;
|
|
||||||
let currentIssueState = {};
|
|
||||||
|
|
||||||
if (destinationIssues && destinationIssues.length > 0) {
|
|
||||||
if (destinationIndex === 0) {
|
|
||||||
const destinationIssueId = destinationIssues[destinationIndex];
|
|
||||||
currentIssueState = {
|
|
||||||
...currentIssueState,
|
|
||||||
sort_order: issueMap[destinationIssueId].sort_order - sortOrderDefaultValue,
|
|
||||||
};
|
|
||||||
} else if (destinationIndex === destinationIssues.length) {
|
|
||||||
const destinationIssueId = destinationIssues[destinationIndex - 1];
|
|
||||||
currentIssueState = {
|
|
||||||
...currentIssueState,
|
|
||||||
sort_order: issueMap[destinationIssueId].sort_order + sortOrderDefaultValue,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
const destinationTopIssueId = destinationIssues[destinationIndex - 1];
|
|
||||||
const destinationBottomIssueId = destinationIssues[destinationIndex];
|
|
||||||
currentIssueState = {
|
|
||||||
...currentIssueState,
|
|
||||||
sort_order: (issueMap[destinationTopIssueId].sort_order + issueMap[destinationBottomIssueId].sort_order) / 2,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
currentIssueState = {
|
|
||||||
...currentIssueState,
|
|
||||||
sort_order: sortOrderDefaultValue,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentIssueState;
|
|
||||||
};
|
|
||||||
|
@ -138,15 +138,6 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
|
|||||||
addIssuesToView={addIssuesToView}
|
addIssuesToView={addIssuesToView}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* {workspaceSlug && peekIssueId && peekProjectId && (
|
|
||||||
<IssuePeekOverview
|
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
|
||||||
projectId={peekProjectId.toString()}
|
|
||||||
issueId={peekIssueId.toString()}
|
|
||||||
handleIssue={async (issueToUpdate) => await handleIssues(issueToUpdate as TIssue, EIssueActions.UPDATE)}
|
|
||||||
/>
|
|
||||||
)} */}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -62,7 +62,7 @@ export const IssueBlock: React.FC<IssueBlockProps> = observer((props: IssueBlock
|
|||||||
href={`/${workspaceSlug}/projects/${projectId}/issues/${issueId}`}
|
href={`/${workspaceSlug}/projects/${projectId}/issues/${issueId}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
onClick={() => handleIssuePeekOverview(issue)}
|
onClick={() => handleIssuePeekOverview(issue)}
|
||||||
className="w-full line-clamp-1 cursor-pointer text-sm font-medium text-custom-text-100"
|
className="w-full line-clamp-1 cursor-pointer text-sm text-custom-text-100"
|
||||||
>
|
>
|
||||||
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
|
<Tooltip tooltipHeading="Title" tooltipContent={issue.name}>
|
||||||
<span>{issue.name}</span>
|
<span>{issue.name}</span>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { CalendarCheck2, CalendarClock, Layers, Link, Paperclip } from "lucide-react";
|
import { CalendarCheck2, CalendarClock, Layers, Link, Paperclip } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useLabel } from "hooks/store";
|
import { useEstimate, useLabel } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { IssuePropertyLabels } from "../properties/labels";
|
import { IssuePropertyLabels } from "../properties/labels";
|
||||||
import { Tooltip } from "@plane/ui";
|
import { Tooltip } from "@plane/ui";
|
||||||
@ -29,6 +29,7 @@ export interface IIssueProperties {
|
|||||||
export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
||||||
const { issue, handleIssues, displayProperties, isReadOnly, className } = props;
|
const { issue, handleIssues, displayProperties, isReadOnly, className } = props;
|
||||||
const { labelMap } = useLabel();
|
const { labelMap } = useLabel();
|
||||||
|
const { areEstimatesEnabledForCurrentProject } = useEstimate();
|
||||||
|
|
||||||
const handleState = (stateId: string) => {
|
const handleState = (stateId: string) => {
|
||||||
handleIssues({ ...issue, state_id: stateId });
|
handleIssues({ ...issue, state_id: stateId });
|
||||||
@ -92,7 +93,6 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
|||||||
</WithDisplayPropertiesHOC>
|
</WithDisplayPropertiesHOC>
|
||||||
|
|
||||||
{/* label */}
|
{/* label */}
|
||||||
|
|
||||||
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="labels">
|
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="labels">
|
||||||
<IssuePropertyLabels
|
<IssuePropertyLabels
|
||||||
projectId={issue?.project_id || null}
|
projectId={issue?.project_id || null}
|
||||||
@ -122,6 +122,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
|||||||
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="due_date">
|
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="due_date">
|
||||||
<div className="h-5">
|
<div className="h-5">
|
||||||
<DateDropdown
|
<DateDropdown
|
||||||
|
minDate={issue.start_date ? new Date(issue.start_date) : undefined}
|
||||||
value={issue?.target_date ?? null}
|
value={issue?.target_date ?? null}
|
||||||
onChange={handleTargetDate}
|
onChange={handleTargetDate}
|
||||||
icon={<CalendarCheck2 className="h-3 w-3 flex-shrink-0" />}
|
icon={<CalendarCheck2 className="h-3 w-3 flex-shrink-0" />}
|
||||||
@ -148,6 +149,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
|||||||
</WithDisplayPropertiesHOC>
|
</WithDisplayPropertiesHOC>
|
||||||
|
|
||||||
{/* estimates */}
|
{/* estimates */}
|
||||||
|
{areEstimatesEnabledForCurrentProject && (
|
||||||
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="estimate">
|
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="estimate">
|
||||||
<div className="h-5">
|
<div className="h-5">
|
||||||
<EstimateDropdown
|
<EstimateDropdown
|
||||||
@ -159,6 +161,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</WithDisplayPropertiesHOC>
|
</WithDisplayPropertiesHOC>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* extra render properties */}
|
{/* extra render properties */}
|
||||||
{/* sub-issues */}
|
{/* sub-issues */}
|
||||||
|
@ -5,31 +5,53 @@ import useSWR from "swr";
|
|||||||
// mobx store
|
// mobx store
|
||||||
import { useIssues } from "hooks/store";
|
import { useIssues } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { ArchivedIssueListLayout, ArchivedIssueAppliedFiltersRoot } from "components/issues";
|
import { ArchivedIssueListLayout, ArchivedIssueAppliedFiltersRoot, ProjectEmptyState } from "components/issues";
|
||||||
import { EIssuesStoreType } from "constants/issue";
|
import { EIssuesStoreType } from "constants/issue";
|
||||||
|
// ui
|
||||||
|
import { Spinner } from "@plane/ui";
|
||||||
|
|
||||||
export const ArchivedIssueLayoutRoot: React.FC = observer(() => {
|
export const ArchivedIssueLayoutRoot: React.FC = observer(() => {
|
||||||
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
// hooks
|
||||||
|
const { issues, issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED);
|
||||||
|
|
||||||
const {
|
useSWR(
|
||||||
issues: { groupedIssueIds, fetchIssues },
|
workspaceSlug && projectId ? `ARCHIVED_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}` : null,
|
||||||
issuesFilter: { fetchFilters },
|
async () => {
|
||||||
} = useIssues(EIssuesStoreType.ARCHIVED);
|
|
||||||
|
|
||||||
useSWR(workspaceSlug && projectId ? `ARCHIVED_FILTERS_AND_ISSUES_${projectId.toString()}` : null, async () => {
|
|
||||||
if (workspaceSlug && projectId) {
|
if (workspaceSlug && projectId) {
|
||||||
await fetchFilters(workspaceSlug, projectId);
|
await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString());
|
||||||
await fetchIssues(workspaceSlug, projectId, groupedIssueIds ? "mutation" : "init-loader");
|
await issues?.fetchIssues(
|
||||||
|
workspaceSlug.toString(),
|
||||||
|
projectId.toString(),
|
||||||
|
issues?.groupedIssueIds ? "mutation" : "init-loader"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!workspaceSlug || !projectId) return <></>;
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||||
<ArchivedIssueAppliedFiltersRoot />
|
<ArchivedIssueAppliedFiltersRoot />
|
||||||
<div className="h-full w-full overflow-auto">
|
|
||||||
|
{issues?.loader === "init-loader" ? (
|
||||||
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{!issues?.groupedIssueIds ? (
|
||||||
|
// TODO: Replace this with project view empty state
|
||||||
|
<ProjectEmptyState />
|
||||||
|
) : (
|
||||||
|
<div className="relative h-full w-full overflow-auto">
|
||||||
<ArchivedIssueListLayout />
|
<ArchivedIssueListLayout />
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -21,36 +21,37 @@ import { Spinner } from "@plane/ui";
|
|||||||
import { EIssuesStoreType } from "constants/issue";
|
import { EIssuesStoreType } from "constants/issue";
|
||||||
|
|
||||||
export const CycleLayoutRoot: React.FC = observer(() => {
|
export const CycleLayoutRoot: React.FC = observer(() => {
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId, cycleId } = router.query;
|
||||||
|
// store hooks
|
||||||
|
const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE);
|
||||||
|
const { getCycleById } = useCycle();
|
||||||
|
// state
|
||||||
const [transferIssuesModal, setTransferIssuesModal] = useState(false);
|
const [transferIssuesModal, setTransferIssuesModal] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId, cycleId } = router.query as {
|
|
||||||
workspaceSlug: string;
|
|
||||||
projectId: string;
|
|
||||||
cycleId: string;
|
|
||||||
};
|
|
||||||
// store hooks
|
|
||||||
const {
|
|
||||||
issues: { loader, groupedIssueIds, fetchIssues },
|
|
||||||
issuesFilter: { issueFilters, fetchFilters },
|
|
||||||
} = useIssues(EIssuesStoreType.CYCLE);
|
|
||||||
const { getCycleById } = useCycle();
|
|
||||||
|
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId && cycleId ? `CYCLE_ISSUES_V3_${workspaceSlug}_${projectId}_${cycleId}` : null,
|
workspaceSlug && projectId && cycleId
|
||||||
|
? `CYCLE_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}_${cycleId.toString()}`
|
||||||
|
: null,
|
||||||
async () => {
|
async () => {
|
||||||
if (workspaceSlug && projectId && cycleId) {
|
if (workspaceSlug && projectId && cycleId) {
|
||||||
await fetchFilters(workspaceSlug, projectId, cycleId);
|
await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString(), cycleId.toString());
|
||||||
await fetchIssues(workspaceSlug, projectId, groupedIssueIds ? "mutation" : "init-loader", cycleId);
|
await issues?.fetchIssues(
|
||||||
|
workspaceSlug.toString(),
|
||||||
|
projectId.toString(),
|
||||||
|
issues?.groupedIssueIds ? "mutation" : "init-loader",
|
||||||
|
cycleId.toString()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
|
||||||
|
|
||||||
const cycleDetails = cycleId ? getCycleById(cycleId) : undefined;
|
const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined;
|
||||||
const cycleStatus = cycleDetails?.status.toLocaleLowerCase() ?? "draft";
|
const cycleStatus = cycleDetails?.status.toLocaleLowerCase() ?? "draft";
|
||||||
|
|
||||||
|
if (!workspaceSlug || !projectId || !cycleId) return <></>;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TransferIssuesModal handleClose={() => setTransferIssuesModal(false)} isOpen={transferIssuesModal} />
|
<TransferIssuesModal handleClose={() => setTransferIssuesModal(false)} isOpen={transferIssuesModal} />
|
||||||
@ -59,14 +60,18 @@ export const CycleLayoutRoot: React.FC = observer(() => {
|
|||||||
{cycleStatus === "completed" && <TransferIssues handleClick={() => setTransferIssuesModal(true)} />}
|
{cycleStatus === "completed" && <TransferIssues handleClick={() => setTransferIssuesModal(true)} />}
|
||||||
<CycleAppliedFiltersRoot />
|
<CycleAppliedFiltersRoot />
|
||||||
|
|
||||||
{loader === "init-loader" || !groupedIssueIds ? (
|
{issues?.loader === "init-loader" ? (
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{Object.keys(groupedIssueIds ?? {}).length == 0 ? (
|
{!issues?.groupedIssueIds ? (
|
||||||
<CycleEmptyState workspaceSlug={workspaceSlug} projectId={projectId} cycleId={cycleId} />
|
<CycleEmptyState
|
||||||
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
|
projectId={projectId.toString()}
|
||||||
|
cycleId={cycleId.toString()}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-full w-full overflow-auto">
|
<div className="h-full w-full overflow-auto">
|
||||||
{activeLayout === "list" ? (
|
{activeLayout === "list" ? (
|
||||||
|
@ -2,42 +2,56 @@ import React from "react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// mobx store
|
// hooks
|
||||||
import { useIssues } from "hooks/store";
|
import { useIssues } from "hooks/store";
|
||||||
|
// components
|
||||||
import { DraftIssueAppliedFiltersRoot } from "../filters/applied-filters/roots/draft-issue";
|
import { DraftIssueAppliedFiltersRoot } from "../filters/applied-filters/roots/draft-issue";
|
||||||
import { DraftIssueListLayout } from "../list/roots/draft-issue-root";
|
import { DraftIssueListLayout } from "../list/roots/draft-issue-root";
|
||||||
|
import { ProjectEmptyState } from "../empty-states";
|
||||||
|
// ui
|
||||||
import { Spinner } from "@plane/ui";
|
import { Spinner } from "@plane/ui";
|
||||||
import { DraftKanBanLayout } from "../kanban/roots/draft-issue-root";
|
import { DraftKanBanLayout } from "../kanban/roots/draft-issue-root";
|
||||||
|
// constants
|
||||||
import { EIssuesStoreType } from "constants/issue";
|
import { EIssuesStoreType } from "constants/issue";
|
||||||
|
|
||||||
export const DraftIssueLayoutRoot: React.FC = observer(() => {
|
export const DraftIssueLayoutRoot: React.FC = observer(() => {
|
||||||
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId } = router.query as { workspaceSlug: string; projectId: string };
|
const { workspaceSlug, projectId } = router.query;
|
||||||
|
// hooks
|
||||||
|
const { issues, issuesFilter } = useIssues(EIssuesStoreType.DRAFT);
|
||||||
|
|
||||||
const {
|
useSWR(
|
||||||
issues: { loader, groupedIssueIds, fetchIssues },
|
workspaceSlug && projectId ? `DRAFT_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}` : null,
|
||||||
issuesFilter: { issueFilters, fetchFilters },
|
async () => {
|
||||||
} = useIssues(EIssuesStoreType.DRAFT);
|
|
||||||
|
|
||||||
useSWR(workspaceSlug && projectId ? `DRAFT_FILTERS_AND_ISSUES_${projectId.toString()}` : null, async () => {
|
|
||||||
if (workspaceSlug && projectId) {
|
if (workspaceSlug && projectId) {
|
||||||
await fetchFilters(workspaceSlug, projectId);
|
await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString());
|
||||||
await fetchIssues(workspaceSlug, projectId, groupedIssueIds ? "mutation" : "init-loader");
|
await issues?.fetchIssues(
|
||||||
|
workspaceSlug.toString(),
|
||||||
|
projectId.toString(),
|
||||||
|
issues?.groupedIssueIds ? "mutation" : "init-loader"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const activeLayout = issueFilters?.displayFilters?.layout || undefined;
|
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout || undefined;
|
||||||
|
|
||||||
|
if (!workspaceSlug || !projectId) return <></>;
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||||
<DraftIssueAppliedFiltersRoot />
|
<DraftIssueAppliedFiltersRoot />
|
||||||
|
|
||||||
{loader === "init-loader" ? (
|
{issues?.loader === "init-loader" ? (
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
{!issues?.groupedIssueIds ? (
|
||||||
|
// TODO: Replace this with project view empty state
|
||||||
|
<ProjectEmptyState />
|
||||||
|
) : (
|
||||||
<div className="relative h-full w-full overflow-auto">
|
<div className="relative h-full w-full overflow-auto">
|
||||||
{activeLayout === "list" ? (
|
{activeLayout === "list" ? (
|
||||||
<DraftIssueListLayout />
|
<DraftIssueListLayout />
|
||||||
@ -45,6 +59,7 @@ export const DraftIssueLayoutRoot: React.FC = observer(() => {
|
|||||||
<DraftKanBanLayout />
|
<DraftKanBanLayout />
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
export * from "./cycle-layout-root";
|
|
||||||
export * from "./all-issue-layout-root";
|
|
||||||
export * from "./module-layout-root";
|
|
||||||
export * from "./project-layout-root";
|
export * from "./project-layout-root";
|
||||||
|
export * from "./module-layout-root";
|
||||||
|
export * from "./cycle-layout-root";
|
||||||
export * from "./project-view-layout-root";
|
export * from "./project-view-layout-root";
|
||||||
export * from "./archived-issue-layout-root";
|
export * from "./archived-issue-layout-root";
|
||||||
|
export * from "./draft-issue-layout-root";
|
||||||
|
export * from "./all-issue-layout-root";
|
||||||
|
@ -2,7 +2,6 @@ import React from "react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
// mobx store
|
// mobx store
|
||||||
import { useIssues } from "hooks/store";
|
import { useIssues } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
@ -20,42 +19,48 @@ import { Spinner } from "@plane/ui";
|
|||||||
import { EIssuesStoreType } from "constants/issue";
|
import { EIssuesStoreType } from "constants/issue";
|
||||||
|
|
||||||
export const ModuleLayoutRoot: React.FC = observer(() => {
|
export const ModuleLayoutRoot: React.FC = observer(() => {
|
||||||
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, moduleId } = router.query as {
|
const { workspaceSlug, projectId, moduleId } = router.query;
|
||||||
workspaceSlug: string;
|
// hooks
|
||||||
projectId: string;
|
const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE);
|
||||||
moduleId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const {
|
|
||||||
issues: { loader, groupedIssueIds, fetchIssues },
|
|
||||||
issuesFilter: { issueFilters, fetchFilters },
|
|
||||||
} = useIssues(EIssuesStoreType.MODULE);
|
|
||||||
|
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId && moduleId ? `MODULE_ISSUES_V3_${workspaceSlug}_${projectId}_${moduleId}` : null,
|
workspaceSlug && projectId && moduleId
|
||||||
|
? `MODULE_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}_${moduleId.toString()}`
|
||||||
|
: null,
|
||||||
async () => {
|
async () => {
|
||||||
if (workspaceSlug && projectId && moduleId) {
|
if (workspaceSlug && projectId && moduleId) {
|
||||||
await fetchFilters(workspaceSlug, projectId, moduleId);
|
await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString(), moduleId.toString());
|
||||||
await fetchIssues(workspaceSlug, projectId, groupedIssueIds ? "mutation" : "init-loader", moduleId);
|
await issues?.fetchIssues(
|
||||||
|
workspaceSlug.toString(),
|
||||||
|
projectId.toString(),
|
||||||
|
issues?.groupedIssueIds ? "mutation" : "init-loader",
|
||||||
|
moduleId.toString()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const activeLayout = issueFilters?.displayFilters?.layout || undefined;
|
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout || undefined;
|
||||||
|
|
||||||
|
if (!workspaceSlug || !projectId || !moduleId) return <></>;
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||||
<ModuleAppliedFiltersRoot />
|
<ModuleAppliedFiltersRoot />
|
||||||
|
|
||||||
{loader === "init-loader" || !groupedIssueIds ? (
|
{issues?.loader === "init-loader" ? (
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{Object.keys(groupedIssueIds ?? {}).length == 0 ? (
|
{!issues?.groupedIssueIds ? (
|
||||||
<ModuleEmptyState workspaceSlug={workspaceSlug} projectId={projectId} moduleId={moduleId} />
|
<ModuleEmptyState
|
||||||
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
|
projectId={projectId.toString()}
|
||||||
|
moduleId={moduleId.toString()}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="h-full w-full overflow-auto">
|
<div className="h-full w-full overflow-auto">
|
||||||
{activeLayout === "list" ? (
|
{activeLayout === "list" ? (
|
||||||
@ -71,7 +76,6 @@ export const ModuleLayoutRoot: React.FC = observer(() => {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* <ModuleEmptyState workspaceSlug={workspaceSlug} projectId={projectId} moduleId={moduleId} /> */}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { FC } from "react";
|
import { FC } from "react";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// components
|
// components
|
||||||
@ -15,23 +16,27 @@ import {
|
|||||||
// ui
|
// ui
|
||||||
import { Spinner } from "@plane/ui";
|
import { Spinner } from "@plane/ui";
|
||||||
// hooks
|
// hooks
|
||||||
import { useApplication, useIssues } from "hooks/store";
|
import { useIssues } from "hooks/store";
|
||||||
// constants
|
// constants
|
||||||
import { EIssuesStoreType } from "constants/issue";
|
import { EIssuesStoreType } from "constants/issue";
|
||||||
|
|
||||||
export const ProjectLayoutRoot: FC = observer(() => {
|
export const ProjectLayoutRoot: FC = observer(() => {
|
||||||
|
// router
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId } = router.query;
|
||||||
// hooks
|
// hooks
|
||||||
const {
|
|
||||||
router: { workspaceSlug, projectId },
|
|
||||||
} = useApplication();
|
|
||||||
const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT);
|
const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT);
|
||||||
|
|
||||||
useSWR(
|
const {} = useSWR(
|
||||||
workspaceSlug && projectId ? `PROJECT_ISSUES_${workspaceSlug}_${projectId}` : null,
|
workspaceSlug && projectId ? `PROJECT_ISSUES_${workspaceSlug}_${projectId}` : null,
|
||||||
async () => {
|
async () => {
|
||||||
if (workspaceSlug && projectId) {
|
if (workspaceSlug && projectId) {
|
||||||
await issuesFilter?.fetchFilters(workspaceSlug, projectId);
|
await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString());
|
||||||
await issues?.fetchIssues(workspaceSlug, projectId, issues?.groupedIssueIds ? "mutation" : "init-loader");
|
await issues?.fetchIssues(
|
||||||
|
workspaceSlug.toString(),
|
||||||
|
projectId.toString(),
|
||||||
|
issues?.groupedIssueIds ? "mutation" : "init-loader"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ revalidateOnFocus: false, refreshInterval: 600000, revalidateOnMount: true }
|
{ revalidateOnFocus: false, refreshInterval: 600000, revalidateOnMount: true }
|
||||||
@ -39,6 +44,7 @@ export const ProjectLayoutRoot: FC = observer(() => {
|
|||||||
|
|
||||||
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
|
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
|
||||||
|
|
||||||
|
if (!workspaceSlug || !projectId) return <></>;
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||||
<ProjectAppliedFiltersRoot />
|
<ProjectAppliedFiltersRoot />
|
||||||
@ -56,6 +62,13 @@ export const ProjectLayoutRoot: FC = observer(() => {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="relative h-full w-full overflow-auto bg-custom-background-90">
|
<div className="relative h-full w-full overflow-auto bg-custom-background-90">
|
||||||
|
{/* mutation loader */}
|
||||||
|
{issues?.loader === "mutation" && (
|
||||||
|
<div className="fixed w-[40px] h-[40px] z-50 right-[20px] top-[70px] flex justify-center items-center bg-custom-background-80 shadow-sm rounded">
|
||||||
|
<Spinner className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{activeLayout === "list" ? (
|
{activeLayout === "list" ? (
|
||||||
<ListLayout />
|
<ListLayout />
|
||||||
) : activeLayout === "kanban" ? (
|
) : activeLayout === "kanban" ? (
|
||||||
|
@ -6,6 +6,7 @@ import useSWR from "swr";
|
|||||||
import { useIssues } from "hooks/store";
|
import { useIssues } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import {
|
import {
|
||||||
|
ProjectEmptyState,
|
||||||
ProjectViewAppliedFiltersRoot,
|
ProjectViewAppliedFiltersRoot,
|
||||||
ProjectViewCalendarLayout,
|
ProjectViewCalendarLayout,
|
||||||
ProjectViewGanttLayout,
|
ProjectViewGanttLayout,
|
||||||
@ -17,40 +18,44 @@ import { Spinner } from "@plane/ui";
|
|||||||
import { EIssuesStoreType } from "constants/issue";
|
import { EIssuesStoreType } from "constants/issue";
|
||||||
|
|
||||||
export const ProjectViewLayoutRoot: React.FC = observer(() => {
|
export const ProjectViewLayoutRoot: React.FC = observer(() => {
|
||||||
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, viewId } = router.query as {
|
const { workspaceSlug, projectId, viewId } = router.query;
|
||||||
workspaceSlug: string;
|
// hooks
|
||||||
projectId: string;
|
const { issues, issuesFilter } = useIssues(EIssuesStoreType.PROJECT_VIEW);
|
||||||
viewId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const {
|
|
||||||
issues: { loader, groupedIssueIds, fetchIssues },
|
|
||||||
issuesFilter: { issueFilters, fetchFilters },
|
|
||||||
} = useIssues(EIssuesStoreType.PROJECT_VIEW);
|
|
||||||
|
|
||||||
useSWR(
|
useSWR(
|
||||||
workspaceSlug && projectId && viewId ? `PROJECT_VIEW_ISSUES_${workspaceSlug}_${projectId}` : null,
|
workspaceSlug && projectId && viewId ? `PROJECT_VIEW_ISSUES_${workspaceSlug}_${projectId}` : null,
|
||||||
async () => {
|
async () => {
|
||||||
if (workspaceSlug && projectId && viewId) {
|
if (workspaceSlug && projectId && viewId) {
|
||||||
await fetchFilters(workspaceSlug, projectId, viewId);
|
await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString(), viewId.toString());
|
||||||
await fetchIssues(workspaceSlug, projectId, groupedIssueIds ? "mutation" : "init-loader");
|
await issues?.fetchIssues(
|
||||||
|
workspaceSlug.toString(),
|
||||||
|
projectId.toString(),
|
||||||
|
issues?.groupedIssueIds ? "mutation" : "init-loader",
|
||||||
|
viewId.toString()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const activeLayout = issueFilters?.displayFilters?.layout;
|
const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout;
|
||||||
|
|
||||||
|
if (!workspaceSlug || !projectId || !viewId) return <></>;
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||||
<ProjectViewAppliedFiltersRoot />
|
<ProjectViewAppliedFiltersRoot />
|
||||||
|
|
||||||
{loader === "init-loader" ? (
|
{issues?.loader === "init-loader" ? (
|
||||||
<div className="flex h-full w-full items-center justify-center">
|
<div className="flex h-full w-full items-center justify-center">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
{!issues?.groupedIssueIds ? (
|
||||||
|
// TODO: Replace this with project view empty state
|
||||||
|
<ProjectEmptyState />
|
||||||
|
) : (
|
||||||
<div className="relative h-full w-full overflow-auto">
|
<div className="relative h-full w-full overflow-auto">
|
||||||
{activeLayout === "list" ? (
|
{activeLayout === "list" ? (
|
||||||
<ProjectViewListLayout />
|
<ProjectViewListLayout />
|
||||||
@ -64,6 +69,7 @@ export const ProjectViewLayoutRoot: React.FC = observer(() => {
|
|||||||
<ProjectViewSpreadsheetLayout />
|
<ProjectViewSpreadsheetLayout />
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -31,7 +31,7 @@ export const getGroupByColumns = (
|
|||||||
case "created_by":
|
case "created_by":
|
||||||
return getCreatedByColumns(member) as any;
|
return getCreatedByColumns(member) as any;
|
||||||
default:
|
default:
|
||||||
if (includeNone) return [{ id: `null`, name: `All Issues`, payload: {}, Icon: undefined }];
|
if (includeNone) return [{ id: `null`, name: `All Issues`, payload: {}, icon: undefined }];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -48,7 +48,7 @@ const getProjectColumns = (project: IProjectStore): IGroupByColumn[] | undefined
|
|||||||
return {
|
return {
|
||||||
id: project.id,
|
id: project.id,
|
||||||
name: project.name,
|
name: project.name,
|
||||||
Icon: <div className="w-6 h-6">{renderEmoji(project.emoji || "")}</div>,
|
icon: <div className="w-6 h-6">{renderEmoji(project.emoji || "")}</div>,
|
||||||
payload: { project_id: project.id },
|
payload: { project_id: project.id },
|
||||||
};
|
};
|
||||||
}) as any;
|
}) as any;
|
||||||
@ -61,7 +61,7 @@ const getStateColumns = (projectState: IStateStore): IGroupByColumn[] | undefine
|
|||||||
return projectStates.map((state) => ({
|
return projectStates.map((state) => ({
|
||||||
id: state.id,
|
id: state.id,
|
||||||
name: state.name,
|
name: state.name,
|
||||||
Icon: (
|
icon: (
|
||||||
<div className="w-3.5 h-3.5 rounded-full">
|
<div className="w-3.5 h-3.5 rounded-full">
|
||||||
<StateGroupIcon stateGroup={state.group} color={state.color} width="14" height="14" />
|
<StateGroupIcon stateGroup={state.group} color={state.color} width="14" height="14" />
|
||||||
</div>
|
</div>
|
||||||
@ -76,7 +76,7 @@ const getStateGroupColumns = () => {
|
|||||||
return stateGroups.map((stateGroup) => ({
|
return stateGroups.map((stateGroup) => ({
|
||||||
id: stateGroup.key,
|
id: stateGroup.key,
|
||||||
name: stateGroup.title,
|
name: stateGroup.title,
|
||||||
Icon: (
|
icon: (
|
||||||
<div className="w-3.5 h-3.5 rounded-full">
|
<div className="w-3.5 h-3.5 rounded-full">
|
||||||
<StateGroupIcon stateGroup={stateGroup.key} width="14" height="14" />
|
<StateGroupIcon stateGroup={stateGroup.key} width="14" height="14" />
|
||||||
</div>
|
</div>
|
||||||
@ -91,7 +91,7 @@ const getPriorityColumns = () => {
|
|||||||
return priorities.map((priority) => ({
|
return priorities.map((priority) => ({
|
||||||
id: priority.key,
|
id: priority.key,
|
||||||
name: priority.title,
|
name: priority.title,
|
||||||
Icon: <PriorityIcon priority={priority?.key} />,
|
icon: <PriorityIcon priority={priority?.key} />,
|
||||||
payload: { priority: priority.key },
|
payload: { priority: priority.key },
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
@ -108,7 +108,7 @@ const getLabelsColumns = (projectLabel: ILabelRootStore) => {
|
|||||||
return labels.map((label) => ({
|
return labels.map((label) => ({
|
||||||
id: label.id,
|
id: label.id,
|
||||||
name: label.name,
|
name: label.name,
|
||||||
Icon: (
|
icon: (
|
||||||
<div className="w-[12px] h-[12px] rounded-full" style={{ backgroundColor: label.color ? label.color : "#666" }} />
|
<div className="w-[12px] h-[12px] rounded-full" style={{ backgroundColor: label.color ? label.color : "#666" }} />
|
||||||
),
|
),
|
||||||
payload: label?.id === "None" ? {} : { label_ids: [label.id] },
|
payload: label?.id === "None" ? {} : { label_ids: [label.id] },
|
||||||
@ -128,12 +128,12 @@ const getAssigneeColumns = (member: IMemberRootStore) => {
|
|||||||
return {
|
return {
|
||||||
id: memberId,
|
id: memberId,
|
||||||
name: member?.display_name || "",
|
name: member?.display_name || "",
|
||||||
Icon: <Avatar name={member?.display_name} src={member?.avatar} size="md" />,
|
icon: <Avatar name={member?.display_name} src={member?.avatar} size="md" />,
|
||||||
payload: { assignee_ids: [memberId] },
|
payload: { assignee_ids: [memberId] },
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
assigneeColumns.push({ id: "None", name: "None", Icon: <Avatar size="md" />, payload: {} });
|
assigneeColumns.push({ id: "None", name: "None", icon: <Avatar size="md" />, payload: {} });
|
||||||
|
|
||||||
return assigneeColumns;
|
return assigneeColumns;
|
||||||
};
|
};
|
||||||
@ -151,7 +151,7 @@ const getCreatedByColumns = (member: IMemberRootStore) => {
|
|||||||
return {
|
return {
|
||||||
id: memberId,
|
id: memberId,
|
||||||
name: member?.display_name || "",
|
name: member?.display_name || "",
|
||||||
Icon: <Avatar name={member?.display_name} src={member?.avatar} size="md" />,
|
icon: <Avatar name={member?.display_name} src={member?.avatar} size="md" />,
|
||||||
payload: {},
|
payload: {},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -1 +0,0 @@
|
|||||||
export * from "./root";
|
|
@ -1,80 +0,0 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
|
||||||
// hooks
|
|
||||||
import { useUser } from "hooks/store";
|
|
||||||
import useIssueReaction from "hooks/use-issue-reaction";
|
|
||||||
// components
|
|
||||||
import { ReactionSelector } from "components/core";
|
|
||||||
// string helpers
|
|
||||||
import { renderEmoji } from "helpers/emoji.helper";
|
|
||||||
|
|
||||||
// types
|
|
||||||
type Props = {
|
|
||||||
workspaceSlug: string;
|
|
||||||
projectId: string;
|
|
||||||
issueId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const IssueReaction: React.FC<Props> = observer((props) => {
|
|
||||||
const { workspaceSlug, projectId, issueId } = props;
|
|
||||||
|
|
||||||
const { currentUser } = useUser();
|
|
||||||
|
|
||||||
const { reactions, groupedReactions, handleReactionCreate, handleReactionDelete } = useIssueReaction(
|
|
||||||
workspaceSlug,
|
|
||||||
projectId,
|
|
||||||
issueId
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleReactionClick = (reaction: string) => {
|
|
||||||
if (!workspaceSlug || !projectId || !issueId) return;
|
|
||||||
|
|
||||||
const isSelected = reactions?.some((r) => r.actor === currentUser?.id && r.reaction === reaction);
|
|
||||||
|
|
||||||
if (isSelected) {
|
|
||||||
handleReactionDelete(reaction);
|
|
||||||
} else {
|
|
||||||
handleReactionCreate(reaction);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mt-4 flex items-center gap-1.5">
|
|
||||||
<ReactionSelector
|
|
||||||
size="md"
|
|
||||||
position="top"
|
|
||||||
value={reactions?.filter((reaction) => reaction.actor === currentUser?.id).map((r) => r.reaction) || []}
|
|
||||||
onSelect={handleReactionClick}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{Object.keys(groupedReactions || {}).map(
|
|
||||||
(reaction) =>
|
|
||||||
groupedReactions?.[reaction]?.length &&
|
|
||||||
groupedReactions[reaction].length > 0 && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
handleReactionClick(reaction);
|
|
||||||
}}
|
|
||||||
key={reaction}
|
|
||||||
className={`flex h-full items-center gap-1 rounded-md px-2 py-1 text-sm text-custom-text-100 ${
|
|
||||||
reactions?.some((r) => r.actor === currentUser?.id && r.reaction === reaction)
|
|
||||||
? "bg-custom-primary-100/10"
|
|
||||||
: "bg-custom-background-80"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span>{renderEmoji(reaction)}</span>
|
|
||||||
<span
|
|
||||||
className={
|
|
||||||
reactions?.some((r) => r.actor === currentUser?.id && r.reaction === reaction)
|
|
||||||
? "text-custom-primary-100"
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{groupedReactions?.[reaction].length}{" "}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,271 +0,0 @@
|
|||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
import useSWR, { mutate } from "swr";
|
|
||||||
import { MinusCircle } from "lucide-react";
|
|
||||||
// hooks
|
|
||||||
import { useApplication, useIssues, useProject, useProjectState, useUser, useWorkspace } from "hooks/store";
|
|
||||||
import useToast from "hooks/use-toast";
|
|
||||||
// services
|
|
||||||
import { IssueService, IssueCommentService } from "services/issue";
|
|
||||||
// components
|
|
||||||
import {
|
|
||||||
IssueAttachmentRoot,
|
|
||||||
AddComment,
|
|
||||||
IssueActivitySection,
|
|
||||||
IssueDescriptionForm,
|
|
||||||
IssueReaction,
|
|
||||||
IssueUpdateStatus,
|
|
||||||
} from "components/issues";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { SubIssuesRoot } from "./sub-issues";
|
|
||||||
// ui
|
|
||||||
import { CustomMenu, LayersIcon, StateGroupIcon } from "@plane/ui";
|
|
||||||
// types
|
|
||||||
import { TIssue, IIssueActivity } from "@plane/types";
|
|
||||||
// fetch-keys
|
|
||||||
import { PROJECT_ISSUES_ACTIVITY, SUB_ISSUES } from "constants/fetch-keys";
|
|
||||||
// constants
|
|
||||||
import { EUserProjectRoles } from "constants/project";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
issueDetails: TIssue;
|
|
||||||
submitChanges: (formData: Partial<TIssue>) => Promise<void>;
|
|
||||||
uneditable?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
// services
|
|
||||||
const issueService = new IssueService();
|
|
||||||
const issueCommentService = new IssueCommentService();
|
|
||||||
|
|
||||||
export const IssueMainContent: React.FC<Props> = observer((props) => {
|
|
||||||
const { issueDetails, submitChanges, uneditable = false } = props;
|
|
||||||
// states
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
|
|
||||||
// router
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId, issueId } = router.query;
|
|
||||||
// toast alert
|
|
||||||
const { setToastAlert } = useToast();
|
|
||||||
const {
|
|
||||||
eventTracker: { postHogEventTracker },
|
|
||||||
} = useApplication();
|
|
||||||
const {
|
|
||||||
currentUser,
|
|
||||||
membership: { currentProjectRole },
|
|
||||||
} = useUser();
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
const { getProjectById } = useProject();
|
|
||||||
const { projectStates, getProjectStates } = useProjectState();
|
|
||||||
const { issueMap } = useIssues();
|
|
||||||
|
|
||||||
const projectDetails = projectId ? getProjectById(projectId.toString()) : null;
|
|
||||||
const currentIssueState = projectStates?.find((s) => s.id === issueDetails.state_id);
|
|
||||||
|
|
||||||
const { data: siblingIssues } = useSWR(
|
|
||||||
workspaceSlug && projectId && issueDetails?.parent_id ? SUB_ISSUES(issueDetails.parent_id) : null,
|
|
||||||
workspaceSlug && projectId && issueDetails?.parent_id
|
|
||||||
? () => issueService.subIssues(workspaceSlug.toString(), projectId.toString(), issueDetails.parent_id ?? "")
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
const siblingIssuesList = siblingIssues?.sub_issues.filter((i) => i.id !== issueDetails.id);
|
|
||||||
|
|
||||||
const { data: issueActivity, mutate: mutateIssueActivity } = useSWR(
|
|
||||||
workspaceSlug && projectId && issueId ? PROJECT_ISSUES_ACTIVITY(issueId.toString()) : null,
|
|
||||||
workspaceSlug && projectId && issueId
|
|
||||||
? () => issueService.getIssueActivities(workspaceSlug.toString(), projectId.toString(), issueId.toString())
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleCommentUpdate = async (commentId: string, data: Partial<IIssueActivity>) => {
|
|
||||||
if (!workspaceSlug || !projectId || !issueId) return;
|
|
||||||
|
|
||||||
await issueCommentService
|
|
||||||
.patchIssueComment(workspaceSlug as string, projectId as string, issueId as string, commentId, data)
|
|
||||||
.then((res) => {
|
|
||||||
mutateIssueActivity();
|
|
||||||
postHogEventTracker(
|
|
||||||
"COMMENT_UPDATED",
|
|
||||||
{
|
|
||||||
...res,
|
|
||||||
state: "SUCCESS",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
isGrouping: true,
|
|
||||||
groupType: "Workspace_metrics",
|
|
||||||
groupId: currentWorkspace?.id!,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCommentDelete = async (commentId: string) => {
|
|
||||||
if (!workspaceSlug || !projectId || !issueId || !currentUser) return;
|
|
||||||
|
|
||||||
mutateIssueActivity((prevData: any) => prevData?.filter((p: any) => p.id !== commentId), false);
|
|
||||||
|
|
||||||
await issueCommentService
|
|
||||||
.deleteIssueComment(workspaceSlug as string, projectId as string, issueId as string, commentId)
|
|
||||||
.then(() => {
|
|
||||||
mutateIssueActivity();
|
|
||||||
postHogEventTracker(
|
|
||||||
"COMMENT_DELETED",
|
|
||||||
{
|
|
||||||
state: "SUCCESS",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
isGrouping: true,
|
|
||||||
groupType: "Workspace_metrics",
|
|
||||||
groupId: currentWorkspace?.id!,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAddComment = async (formData: IIssueActivity) => {
|
|
||||||
if (!workspaceSlug || !issueDetails || !currentUser) return;
|
|
||||||
|
|
||||||
await issueCommentService
|
|
||||||
.createIssueComment(workspaceSlug.toString(), issueDetails.project_id, issueDetails.id, formData)
|
|
||||||
.then((res) => {
|
|
||||||
mutate(PROJECT_ISSUES_ACTIVITY(issueDetails.id));
|
|
||||||
postHogEventTracker(
|
|
||||||
"COMMENT_ADDED",
|
|
||||||
{
|
|
||||||
...res,
|
|
||||||
state: "SUCCESS",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
isGrouping: true,
|
|
||||||
groupType: "Workspace_metrics",
|
|
||||||
groupId: currentWorkspace?.id!,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch(() =>
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: "Comment could not be posted. Please try again.",
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
|
||||||
|
|
||||||
const parentDetail = issueMap?.[issueDetails.parent_id || ""] || undefined;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="rounded-lg">
|
|
||||||
{issueDetails?.parent_id && parentDetail ? (
|
|
||||||
<div className="mb-5 flex w-min items-center gap-3 whitespace-nowrap rounded-md border border-custom-border-300 bg-custom-background-80 px-2.5 py-1 text-xs">
|
|
||||||
<Link href={`/${workspaceSlug}/projects/${parentDetail?.project_id}/issues/${parentDetail.parent_id}`}>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="flex items-center gap-2.5">
|
|
||||||
<span
|
|
||||||
className="block h-2 w-2 rounded-full"
|
|
||||||
style={{
|
|
||||||
backgroundColor: getProjectStates(parentDetail?.project_id)?.find(
|
|
||||||
(state) => state?.id === parentDetail?.state_id
|
|
||||||
)?.color,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span className="flex-shrink-0 text-custom-text-200">
|
|
||||||
{getProjectById(parentDetail?.project_id)?.identifier}-{parentDetail?.sequence_id}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span className="truncate text-custom-text-100">{(parentDetail?.name ?? "").substring(0, 50)}</span>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<CustomMenu ellipsis optionsClassName="px-1.5">
|
|
||||||
{siblingIssuesList ? (
|
|
||||||
siblingIssuesList.length > 0 ? (
|
|
||||||
<>
|
|
||||||
<h2 className="mb-1 border-b border-custom-border-300 px-2 pb-1 text-xs font-medium text-custom-text-200">
|
|
||||||
Sibling issues
|
|
||||||
</h2>
|
|
||||||
{siblingIssuesList.map((issue) => (
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
key={issue.id}
|
|
||||||
onClick={() =>
|
|
||||||
router.push(`/${workspaceSlug}/projects/${projectId as string}/issues/${issue.id}`)
|
|
||||||
}
|
|
||||||
className="flex items-center gap-2 py-2"
|
|
||||||
>
|
|
||||||
<LayersIcon className="h-4 w-4" />
|
|
||||||
{getProjectById(issueDetails?.project_id)?.identifier}-{issue.sequence_id}
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<p className="flex items-center gap-2 whitespace-nowrap px-1 py-1 text-left text-xs text-custom-text-200">
|
|
||||||
No sibling issues
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
) : null}
|
|
||||||
<CustomMenu.MenuItem
|
|
||||||
onClick={() => submitChanges({ parent_id: null })}
|
|
||||||
className="flex items-center gap-2 py-2 text-red-500"
|
|
||||||
>
|
|
||||||
<MinusCircle className="h-4 w-4" />
|
|
||||||
<span> Remove Parent Issue</span>
|
|
||||||
</CustomMenu.MenuItem>
|
|
||||||
</CustomMenu>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div className="mb-2.5 flex items-center">
|
|
||||||
{currentIssueState && (
|
|
||||||
<StateGroupIcon
|
|
||||||
className="mr-3 h-4 w-4"
|
|
||||||
stateGroup={currentIssueState.group}
|
|
||||||
color={currentIssueState.color}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<IssueUpdateStatus isSubmitting={isSubmitting} issueDetail={issueDetails} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<IssueDescriptionForm
|
|
||||||
setIsSubmitting={(value) => setIsSubmitting(value)}
|
|
||||||
isSubmitting={isSubmitting}
|
|
||||||
workspaceSlug={workspaceSlug as string}
|
|
||||||
issue={issueDetails}
|
|
||||||
handleFormSubmit={submitChanges}
|
|
||||||
isAllowed={isAllowed || !uneditable}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{workspaceSlug && projectId && (
|
|
||||||
<IssueReaction
|
|
||||||
workspaceSlug={workspaceSlug.toString()}
|
|
||||||
projectId={projectId.toString()}
|
|
||||||
issueId={issueDetails.id}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mt-2 space-y-2">
|
|
||||||
<SubIssuesRoot parentIssue={issueDetails} user={currentUser ?? undefined} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* issue attachments */}
|
|
||||||
<IssueAttachmentRoot isEditable={uneditable} />
|
|
||||||
|
|
||||||
<div className="space-y-5 pt-3">
|
|
||||||
<h3 className="text-lg text-custom-text-100">Comments/Activity</h3>
|
|
||||||
<IssueActivitySection
|
|
||||||
activity={issueActivity}
|
|
||||||
handleCommentUpdate={handleCommentUpdate}
|
|
||||||
handleCommentDelete={handleCommentDelete}
|
|
||||||
showAccessSpecifier={Boolean(projectDetails && projectDetails.is_deployed)}
|
|
||||||
/>
|
|
||||||
<AddComment
|
|
||||||
onSubmit={handleAddComment}
|
|
||||||
disabled={uneditable}
|
|
||||||
showAccessSpecifier={Boolean(projectDetails && projectDetails.is_deployed)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,45 +1,35 @@
|
|||||||
import { FC, useState } from "react";
|
import { FC, useMemo } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { CalendarDays, Link2, Plus, Signal, Tag, Triangle, LayoutPanelTop } from "lucide-react";
|
import { CalendarDays, Signal, Tag, Triangle, LayoutPanelTop } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetail, useProject, useUser } from "hooks/store";
|
import { useIssueDetail, useProject, useUser } from "hooks/store";
|
||||||
// ui icons
|
// ui icons
|
||||||
import { DiceIcon, DoubleCircleIcon, UserGroupIcon, ContrastIcon } from "@plane/ui";
|
import { DiceIcon, DoubleCircleIcon, UserGroupIcon, ContrastIcon } from "@plane/ui";
|
||||||
import {
|
import { IssueLinkRoot, IssueCycleSelect, IssueModuleSelect, IssueParentSelect, IssueLabel } from "components/issues";
|
||||||
IssueLinkRoot,
|
|
||||||
SidebarCycleSelect,
|
|
||||||
SidebarLabelSelect,
|
|
||||||
SidebarModuleSelect,
|
|
||||||
SidebarParentSelect,
|
|
||||||
} from "components/issues";
|
|
||||||
import { EstimateDropdown, PriorityDropdown, ProjectMemberDropdown, StateDropdown } from "components/dropdowns";
|
import { EstimateDropdown, PriorityDropdown, ProjectMemberDropdown, StateDropdown } from "components/dropdowns";
|
||||||
// components
|
// components
|
||||||
import { CustomDatePicker } from "components/ui";
|
import { CustomDatePicker } from "components/ui";
|
||||||
import { LinkModal } from "components/core";
|
|
||||||
// types
|
// types
|
||||||
import { TIssue, TIssuePriorities, ILinkDetails, IIssueLink } from "@plane/types";
|
import { TIssue, TIssuePriorities } from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
|
|
||||||
interface IPeekOverviewProperties {
|
interface IPeekOverviewProperties {
|
||||||
issue: TIssue;
|
issue: TIssue;
|
||||||
issueUpdate: (issue: Partial<TIssue>) => void;
|
issueUpdate: (issue: Partial<TIssue>) => void;
|
||||||
issueLinkCreate: (data: IIssueLink) => Promise<ILinkDetails>;
|
|
||||||
issueLinkUpdate: (data: IIssueLink, linkId: string) => Promise<ILinkDetails>;
|
|
||||||
issueLinkDelete: (linkId: string) => Promise<void>;
|
|
||||||
disableUserActions: boolean;
|
disableUserActions: boolean;
|
||||||
|
issueOperations: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((props) => {
|
export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((props) => {
|
||||||
const { issue, issueUpdate, issueLinkCreate, issueLinkUpdate, issueLinkDelete, disableUserActions } = props;
|
const { issue, issueUpdate, disableUserActions, issueOperations } = props;
|
||||||
// states
|
|
||||||
const [selectedLinkToUpdate, setSelectedLinkToUpdate] = useState<ILinkDetails | null>(null);
|
|
||||||
// store hooks
|
// store hooks
|
||||||
const {
|
const {
|
||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const { fetchIssue, isIssueLinkModalOpen, toggleIssueLinkModal } = useIssueDetail();
|
const { currentUser } = useUser();
|
||||||
const { getProjectById } = useProject();
|
const { getProjectById } = useProject();
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -66,23 +56,6 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
|||||||
const handleTargetDate = (_targetDate: string | null) => {
|
const handleTargetDate = (_targetDate: string | null) => {
|
||||||
issueUpdate({ ...issue, target_date: _targetDate || undefined });
|
issueUpdate({ ...issue, target_date: _targetDate || undefined });
|
||||||
};
|
};
|
||||||
const handleParent = (_parent: string) => {
|
|
||||||
issueUpdate({ ...issue, parent_id: _parent });
|
|
||||||
};
|
|
||||||
const handleLabels = (formData: Partial<TIssue>) => {
|
|
||||||
issueUpdate({ ...issue, ...formData });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCycleOrModuleChange = async () => {
|
|
||||||
if (!workspaceSlug || !projectId) return;
|
|
||||||
|
|
||||||
await fetchIssue(workspaceSlug.toString(), projectId.toString(), issue.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditLink = (link: ILinkDetails) => {
|
|
||||||
setSelectedLinkToUpdate(link);
|
|
||||||
toggleIssueLinkModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const projectDetails = getProjectById(issue.project_id);
|
const projectDetails = getProjectById(issue.project_id);
|
||||||
const isEstimateEnabled = projectDetails?.estimate;
|
const isEstimateEnabled = projectDetails?.estimate;
|
||||||
@ -95,17 +68,6 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LinkModal
|
|
||||||
isOpen={isIssueLinkModalOpen}
|
|
||||||
handleClose={() => {
|
|
||||||
toggleIssueLinkModal(false);
|
|
||||||
setSelectedLinkToUpdate(null);
|
|
||||||
}}
|
|
||||||
data={selectedLinkToUpdate}
|
|
||||||
status={selectedLinkToUpdate ? true : false}
|
|
||||||
createIssueLink={issueLinkCreate}
|
|
||||||
updateIssueLink={issueLinkUpdate}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex w-full flex-col gap-5 py-5">
|
<div className="flex w-full flex-col gap-5 py-5">
|
||||||
{/* state */}
|
{/* state */}
|
||||||
@ -223,7 +185,13 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
|||||||
<p>Parent</p>
|
<p>Parent</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<SidebarParentSelect onChange={handleParent} issueDetails={issue} disabled={disableUserActions} />
|
<IssueParentSelect
|
||||||
|
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||||
|
projectId={projectId?.toString() ?? ""}
|
||||||
|
issueId={issue?.id}
|
||||||
|
issueOperations={issueOperations}
|
||||||
|
disabled={disableUserActions}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -238,10 +206,12 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
|||||||
<p>Cycle</p>
|
<p>Cycle</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<SidebarCycleSelect
|
<IssueCycleSelect
|
||||||
issueDetail={issue}
|
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||||
|
projectId={projectId?.toString() ?? ""}
|
||||||
|
issueId={issue?.id}
|
||||||
|
issueOperations={issueOperations}
|
||||||
disabled={disableUserActions}
|
disabled={disableUserActions}
|
||||||
handleIssueUpdate={handleCycleOrModuleChange}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -254,10 +224,12 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
|||||||
<p>Module</p>
|
<p>Module</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<SidebarModuleSelect
|
<IssueModuleSelect
|
||||||
issueDetail={issue}
|
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||||
|
projectId={projectId?.toString() ?? ""}
|
||||||
|
issueId={issue?.id}
|
||||||
|
issueOperations={issueOperations}
|
||||||
disabled={disableUserActions}
|
disabled={disableUserActions}
|
||||||
handleIssueUpdate={handleCycleOrModuleChange}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -269,12 +241,11 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
|||||||
<p>Label</p>
|
<p>Label</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-col gap-3">
|
<div className="flex w-full flex-col gap-3">
|
||||||
<SidebarLabelSelect
|
<IssueLabel
|
||||||
issueDetails={issue}
|
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||||
labelList={issue.label_ids}
|
projectId={projectId?.toString() ?? ""}
|
||||||
submitChanges={handleLabels}
|
issueId={issue?.id}
|
||||||
isNotAllowed={disableUserActions}
|
disabled={uneditable}
|
||||||
uneditable={disableUserActions}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -282,10 +253,14 @@ export const PeekOverviewProperties: FC<IPeekOverviewProperties> = observer((pro
|
|||||||
|
|
||||||
<span className="border-t border-custom-border-200" />
|
<span className="border-t border-custom-border-200" />
|
||||||
|
|
||||||
<div className="flex w-full flex-col gap-5 pt-5">
|
<div className="w-full pt-3">
|
||||||
<div className="flex flex-col gap-3">
|
<IssueLinkRoot
|
||||||
<IssueLinkRoot uneditable={uneditable} isAllowed={isAllowed} />
|
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||||
</div>
|
projectId={projectId?.toString() ?? ""}
|
||||||
|
issueId={issue?.id}
|
||||||
|
is_editable={uneditable}
|
||||||
|
is_archived={isAllowed}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import { FC, Fragment, useEffect, useState } from "react";
|
import { FC, Fragment, useEffect, useState, useMemo } from "react";
|
||||||
// router
|
// router
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
import { useIssueDetail, useIssues, useProject, useUser } from "hooks/store";
|
import { useIssueDetail, useIssues, useMember, useProject, useUser } from "hooks/store";
|
||||||
// components
|
// components
|
||||||
import { IssueView } from "components/issues";
|
import { IssueView } from "components/issues";
|
||||||
// helpers
|
// helpers
|
||||||
import { copyUrlToClipboard } from "helpers/string.helper";
|
import { copyUrlToClipboard } from "helpers/string.helper";
|
||||||
// types
|
// types
|
||||||
import { TIssue, IIssueLink } from "@plane/types";
|
import { TIssue } from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
import { EUserProjectRoles } from "constants/project";
|
import { EUserProjectRoles } from "constants/project";
|
||||||
import { EIssuesStoreType } from "constants/issue";
|
import { EIssuesStoreType } from "constants/issue";
|
||||||
@ -19,14 +19,25 @@ interface IIssuePeekOverview {
|
|||||||
isArchived?: boolean;
|
isArchived?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TIssuePeekOperations = {
|
||||||
|
addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise<void>;
|
||||||
|
removeIssueFromCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => Promise<void>;
|
||||||
|
addIssueToModule: (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => Promise<void>;
|
||||||
|
removeIssueFromModule: (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||||
const { isArchived = false } = props;
|
const { isArchived = false } = props;
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
// hooks
|
// hooks
|
||||||
|
const {
|
||||||
|
project: {},
|
||||||
|
} = useMember();
|
||||||
const { currentProjectDetails } = useProject();
|
const { currentProjectDetails } = useProject();
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
const {
|
const {
|
||||||
|
currentUser,
|
||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
const {
|
const {
|
||||||
@ -45,12 +56,10 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
removeReaction,
|
removeReaction,
|
||||||
createSubscription,
|
createSubscription,
|
||||||
removeSubscription,
|
removeSubscription,
|
||||||
createLink,
|
|
||||||
updateLink,
|
|
||||||
removeLink,
|
|
||||||
issue: { getIssueById, fetchIssue },
|
issue: { getIssueById, fetchIssue },
|
||||||
fetchActivities,
|
fetchActivities,
|
||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
|
const { addIssueToCycle, removeIssueFromCycle, addIssueToModule, removeIssueFromModule } = useIssueDetail();
|
||||||
// state
|
// state
|
||||||
const [loader, setLoader] = useState(false);
|
const [loader, setLoader] = useState(false);
|
||||||
|
|
||||||
@ -62,8 +71,7 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [peekIssue, fetchIssue]);
|
}, [peekIssue, fetchIssue]);
|
||||||
|
if (!peekIssue?.workspaceSlug || !peekIssue?.projectId || !peekIssue?.issueId) return <></>;
|
||||||
if (!peekIssue) return <></>;
|
|
||||||
|
|
||||||
const issue = getIssueById(peekIssue.issueId) || undefined;
|
const issue = getIssueById(peekIssue.issueId) || undefined;
|
||||||
|
|
||||||
@ -90,6 +98,76 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const issueOperations: TIssuePeekOperations = useMemo(
|
||||||
|
() => ({
|
||||||
|
addIssueToCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => {
|
||||||
|
try {
|
||||||
|
await addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds);
|
||||||
|
setToastAlert({
|
||||||
|
title: "Cycle added to issue successfully",
|
||||||
|
type: "success",
|
||||||
|
message: "Issue added to issue successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setToastAlert({
|
||||||
|
title: "Cycle add to issue failed",
|
||||||
|
type: "error",
|
||||||
|
message: "Cycle add to issue failed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeIssueFromCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => {
|
||||||
|
try {
|
||||||
|
await removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId);
|
||||||
|
setToastAlert({
|
||||||
|
title: "Cycle removed from issue successfully",
|
||||||
|
type: "success",
|
||||||
|
message: "Cycle removed from issue successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setToastAlert({
|
||||||
|
title: "Cycle remove from issue failed",
|
||||||
|
type: "error",
|
||||||
|
message: "Cycle remove from issue failed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addIssueToModule: async (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => {
|
||||||
|
try {
|
||||||
|
await addIssueToModule(workspaceSlug, projectId, moduleId, issueIds);
|
||||||
|
setToastAlert({
|
||||||
|
title: "Module added to issue successfully",
|
||||||
|
type: "success",
|
||||||
|
message: "Module added to issue successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setToastAlert({
|
||||||
|
title: "Module add to issue failed",
|
||||||
|
type: "error",
|
||||||
|
message: "Module add to issue failed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeIssueFromModule: async (workspaceSlug: string, projectId: string, moduleId: string, issueId: string) => {
|
||||||
|
try {
|
||||||
|
await removeIssueFromModule(workspaceSlug, projectId, moduleId, issueId);
|
||||||
|
setToastAlert({
|
||||||
|
title: "Module removed from issue successfully",
|
||||||
|
type: "success",
|
||||||
|
message: "Module removed from issue successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setToastAlert({
|
||||||
|
title: "Module remove from issue failed",
|
||||||
|
type: "error",
|
||||||
|
message: "Module remove from issue failed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[addIssueToCycle, removeIssueFromCycle, addIssueToModule, removeIssueFromModule, setToastAlert]
|
||||||
|
);
|
||||||
|
|
||||||
const issueUpdate = async (_data: Partial<TIssue>) => {
|
const issueUpdate = async (_data: Partial<TIssue>) => {
|
||||||
if (!issue) return;
|
if (!issue) return;
|
||||||
await updateIssue(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId, _data);
|
await updateIssue(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId, _data);
|
||||||
@ -104,7 +182,9 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
const issueReactionCreate = (reaction: string) =>
|
const issueReactionCreate = (reaction: string) =>
|
||||||
createReaction(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId, reaction);
|
createReaction(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId, reaction);
|
||||||
const issueReactionRemove = (reaction: string) =>
|
const issueReactionRemove = (reaction: string) =>
|
||||||
removeReaction(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId, reaction);
|
currentUser &&
|
||||||
|
currentUser.id &&
|
||||||
|
removeReaction(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId, reaction, currentUser.id);
|
||||||
|
|
||||||
const issueCommentCreate = (comment: any) =>
|
const issueCommentCreate = (comment: any) =>
|
||||||
createComment(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId, comment);
|
createComment(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId, comment);
|
||||||
@ -123,31 +203,22 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
const issueSubscriptionRemove = () =>
|
const issueSubscriptionRemove = () =>
|
||||||
removeSubscription(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId);
|
removeSubscription(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId);
|
||||||
|
|
||||||
const issueLinkCreate = (formData: IIssueLink) =>
|
|
||||||
createLink(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId, formData);
|
|
||||||
const issueLinkUpdate = (formData: IIssueLink, linkId: string) =>
|
|
||||||
updateLink(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId, linkId, formData);
|
|
||||||
const issueLinkDelete = (linkId: string) =>
|
|
||||||
removeLink(peekIssue.workspaceSlug, peekIssue.projectId, peekIssue.issueId, linkId);
|
|
||||||
|
|
||||||
const userRole = currentProjectRole ?? EUserProjectRoles.GUEST;
|
const userRole = currentProjectRole ?? EUserProjectRoles.GUEST;
|
||||||
const isLoading = !issue || loader ? true : false;
|
const isLoading = !issue || loader ? true : false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
{isLoading ? (
|
|
||||||
<></> // TODO: show the spinner
|
|
||||||
) : (
|
|
||||||
<IssueView
|
<IssueView
|
||||||
workspaceSlug={peekIssue.workspaceSlug}
|
workspaceSlug={peekIssue.workspaceSlug}
|
||||||
projectId={peekIssue.projectId}
|
projectId={peekIssue.projectId}
|
||||||
issueId={peekIssue.issueId}
|
issueId={peekIssue.issueId}
|
||||||
issue={issue}
|
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
isArchived={isArchived}
|
isArchived={isArchived}
|
||||||
|
issue={issue}
|
||||||
handleCopyText={handleCopyText}
|
handleCopyText={handleCopyText}
|
||||||
redirectToIssueDetail={redirectToIssueDetail}
|
redirectToIssueDetail={redirectToIssueDetail}
|
||||||
issueUpdate={issueUpdate}
|
issueUpdate={issueUpdate}
|
||||||
|
issueDelete={issueDelete}
|
||||||
issueReactionCreate={issueReactionCreate}
|
issueReactionCreate={issueReactionCreate}
|
||||||
issueReactionRemove={issueReactionRemove}
|
issueReactionRemove={issueReactionRemove}
|
||||||
issueCommentCreate={issueCommentCreate}
|
issueCommentCreate={issueCommentCreate}
|
||||||
@ -157,14 +228,10 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
|||||||
issueCommentReactionRemove={issueCommentReactionRemove}
|
issueCommentReactionRemove={issueCommentReactionRemove}
|
||||||
issueSubscriptionCreate={issueSubscriptionCreate}
|
issueSubscriptionCreate={issueSubscriptionCreate}
|
||||||
issueSubscriptionRemove={issueSubscriptionRemove}
|
issueSubscriptionRemove={issueSubscriptionRemove}
|
||||||
issueLinkCreate={issueLinkCreate}
|
|
||||||
issueLinkUpdate={issueLinkUpdate}
|
|
||||||
issueLinkDelete={issueLinkDelete}
|
|
||||||
handleDeleteIssue={issueDelete}
|
|
||||||
disableUserActions={[5, 10].includes(userRole)}
|
disableUserActions={[5, 10].includes(userRole)}
|
||||||
showCommentAccessSpecifier={currentProjectDetails?.is_deployed}
|
showCommentAccessSpecifier={currentProjectDetails?.is_deployed}
|
||||||
|
issueOperations={issueOperations}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -22,12 +22,18 @@ interface IIssueView {
|
|||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
issueId: string;
|
issueId: string;
|
||||||
issue: TIssue | undefined;
|
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
isArchived?: boolean;
|
isArchived?: boolean;
|
||||||
|
|
||||||
|
issue: TIssue | undefined;
|
||||||
|
|
||||||
handleCopyText: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
handleCopyText: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||||
redirectToIssueDetail: () => void;
|
redirectToIssueDetail: () => void;
|
||||||
|
|
||||||
issueUpdate: (issue: Partial<TIssue>) => void;
|
issueUpdate: (issue: Partial<TIssue>) => void;
|
||||||
|
issueDelete: () => Promise<void>;
|
||||||
|
|
||||||
issueReactionCreate: (reaction: string) => void;
|
issueReactionCreate: (reaction: string) => void;
|
||||||
issueReactionRemove: (reaction: string) => void;
|
issueReactionRemove: (reaction: string) => void;
|
||||||
issueCommentCreate: (comment: any) => void;
|
issueCommentCreate: (comment: any) => void;
|
||||||
@ -37,12 +43,11 @@ interface IIssueView {
|
|||||||
issueCommentReactionRemove: (commentId: string, reaction: string) => void;
|
issueCommentReactionRemove: (commentId: string, reaction: string) => void;
|
||||||
issueSubscriptionCreate: () => void;
|
issueSubscriptionCreate: () => void;
|
||||||
issueSubscriptionRemove: () => void;
|
issueSubscriptionRemove: () => void;
|
||||||
issueLinkCreate: (formData: IIssueLink) => Promise<ILinkDetails>;
|
|
||||||
issueLinkUpdate: (formData: IIssueLink, linkId: string) => Promise<ILinkDetails>;
|
|
||||||
issueLinkDelete: (linkId: string) => Promise<void>;
|
|
||||||
handleDeleteIssue: () => Promise<void>;
|
|
||||||
disableUserActions?: boolean;
|
disableUserActions?: boolean;
|
||||||
showCommentAccessSpecifier?: boolean;
|
showCommentAccessSpecifier?: boolean;
|
||||||
|
|
||||||
|
issueOperations: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
type TPeekModes = "side-peek" | "modal" | "full-screen";
|
type TPeekModes = "side-peek" | "modal" | "full-screen";
|
||||||
@ -75,6 +80,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
isArchived,
|
isArchived,
|
||||||
handleCopyText,
|
handleCopyText,
|
||||||
redirectToIssueDetail,
|
redirectToIssueDetail,
|
||||||
|
|
||||||
issueUpdate,
|
issueUpdate,
|
||||||
issueReactionCreate,
|
issueReactionCreate,
|
||||||
issueReactionRemove,
|
issueReactionRemove,
|
||||||
@ -85,12 +91,12 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
issueCommentReactionRemove,
|
issueCommentReactionRemove,
|
||||||
issueSubscriptionCreate,
|
issueSubscriptionCreate,
|
||||||
issueSubscriptionRemove,
|
issueSubscriptionRemove,
|
||||||
issueLinkCreate,
|
|
||||||
issueLinkUpdate,
|
issueDelete,
|
||||||
issueLinkDelete,
|
|
||||||
handleDeleteIssue,
|
|
||||||
disableUserActions = false,
|
disableUserActions = false,
|
||||||
showCommentAccessSpecifier = false,
|
showCommentAccessSpecifier = false,
|
||||||
|
|
||||||
|
issueOperations,
|
||||||
} = props;
|
} = props;
|
||||||
// states
|
// states
|
||||||
const [peekMode, setPeekMode] = useState<TPeekModes>("side-peek");
|
const [peekMode, setPeekMode] = useState<TPeekModes>("side-peek");
|
||||||
@ -109,7 +115,9 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
} = useIssueDetail();
|
} = useIssueDetail();
|
||||||
const { currentUser } = useUser();
|
const { currentUser } = useUser();
|
||||||
|
|
||||||
const removeRoutePeekId = () => setPeekIssue(undefined);
|
const removeRoutePeekId = () => {
|
||||||
|
setPeekIssue(undefined);
|
||||||
|
};
|
||||||
|
|
||||||
const issueReactions = reaction.getReactionsByIssueId(issueId) || [];
|
const issueReactions = reaction.getReactionsByIssueId(issueId) || [];
|
||||||
const issueActivity = activity.getActivitiesByIssueId(issueId);
|
const issueActivity = activity.getActivitiesByIssueId(issueId);
|
||||||
@ -126,7 +134,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
isOpen={isDeleteIssueModalOpen}
|
isOpen={isDeleteIssueModalOpen}
|
||||||
handleClose={() => toggleDeleteIssueModal(false)}
|
handleClose={() => toggleDeleteIssueModal(false)}
|
||||||
data={issue}
|
data={issue}
|
||||||
onSubmit={handleDeleteIssue}
|
onSubmit={issueDelete}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -135,7 +143,7 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
data={issue}
|
data={issue}
|
||||||
isOpen={isDeleteIssueModalOpen}
|
isOpen={isDeleteIssueModalOpen}
|
||||||
handleClose={() => toggleDeleteIssueModal(false)}
|
handleClose={() => toggleDeleteIssueModal(false)}
|
||||||
onSubmit={handleDeleteIssue}
|
onSubmit={issueDelete}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -257,10 +265,8 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
<PeekOverviewProperties
|
<PeekOverviewProperties
|
||||||
issue={issue}
|
issue={issue}
|
||||||
issueUpdate={issueUpdate}
|
issueUpdate={issueUpdate}
|
||||||
issueLinkCreate={issueLinkCreate}
|
|
||||||
issueLinkUpdate={issueLinkUpdate}
|
|
||||||
issueLinkDelete={issueLinkDelete}
|
|
||||||
disableUserActions={disableUserActions}
|
disableUserActions={disableUserActions}
|
||||||
|
issueOperations={issueOperations}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<IssueActivity
|
<IssueActivity
|
||||||
@ -316,10 +322,8 @@ export const IssueView: FC<IIssueView> = observer((props) => {
|
|||||||
<PeekOverviewProperties
|
<PeekOverviewProperties
|
||||||
issue={issue}
|
issue={issue}
|
||||||
issueUpdate={issueUpdate}
|
issueUpdate={issueUpdate}
|
||||||
issueLinkCreate={issueLinkCreate}
|
|
||||||
issueLinkUpdate={issueLinkUpdate}
|
|
||||||
issueLinkDelete={issueLinkDelete}
|
|
||||||
disableUserActions={disableUserActions}
|
disableUserActions={disableUserActions}
|
||||||
|
issueOperations={issueOperations}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -101,8 +101,7 @@ export const IssueLabelSelect: React.FC<Props> = observer((props) => {
|
|||||||
{isDropdownOpen && (
|
{isDropdownOpen && (
|
||||||
<Combobox.Options className="fixed z-10" static>
|
<Combobox.Options className="fixed z-10" static>
|
||||||
<div
|
<div
|
||||||
className="my-1 w-48 rounded border-[0.5px] border-custom-border-300
|
className="my-1 w-48 rounded border-[0.5px] border-custom-border-300 bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none"
|
||||||
bg-custom-background-100 px-2 py-2.5 text-xs shadow-custom-shadow-rg focus:outline-none"
|
|
||||||
ref={setPopperElement}
|
ref={setPopperElement}
|
||||||
style={styles.popper}
|
style={styles.popper}
|
||||||
{...attributes.popper}
|
{...attributes.popper}
|
||||||
|
@ -1,135 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import useSWR, { mutate } from "swr";
|
|
||||||
// hooks
|
|
||||||
import { useCycle, useIssues } from "hooks/store";
|
|
||||||
// services
|
|
||||||
import { CycleService } from "services/cycle.service";
|
|
||||||
// ui
|
|
||||||
import { ContrastIcon, CustomSearchSelect, Spinner, Tooltip } from "@plane/ui";
|
|
||||||
// types
|
|
||||||
import { TIssue } from "@plane/types";
|
|
||||||
// fetch-keys
|
|
||||||
import { CYCLE_ISSUES, INCOMPLETE_CYCLES_LIST, ISSUE_DETAILS } from "constants/fetch-keys";
|
|
||||||
import { EIssuesStoreType } from "constants/issue";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
issueDetail: TIssue | undefined;
|
|
||||||
handleCycleChange?: (cycleId: string) => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
handleIssueUpdate?: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
// services
|
|
||||||
const cycleService = new CycleService();
|
|
||||||
|
|
||||||
export const SidebarCycleSelect: React.FC<Props> = (props) => {
|
|
||||||
const { issueDetail, disabled = false, handleIssueUpdate, handleCycleChange } = props;
|
|
||||||
// router
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId } = router.query;
|
|
||||||
// mobx store
|
|
||||||
const {
|
|
||||||
issues: { removeIssueFromCycle, addIssueToCycle },
|
|
||||||
} = useIssues(EIssuesStoreType.CYCLE);
|
|
||||||
const { getCycleById } = useCycle();
|
|
||||||
|
|
||||||
const [isUpdating, setIsUpdating] = useState(false);
|
|
||||||
|
|
||||||
const { data: incompleteCycles } = useSWR(
|
|
||||||
workspaceSlug && projectId ? INCOMPLETE_CYCLES_LIST(projectId as string) : null,
|
|
||||||
workspaceSlug && projectId
|
|
||||||
? () => cycleService.getCyclesWithParams(workspaceSlug as string, projectId as string) // FIXME, "incomplete")
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleCycleStoreChange = async (cycleId: string) => {
|
|
||||||
if (!workspaceSlug || !issueDetail || !cycleId || !projectId) return;
|
|
||||||
|
|
||||||
setIsUpdating(true);
|
|
||||||
await addIssueToCycle(workspaceSlug.toString(), projectId?.toString(), cycleId, [issueDetail.id])
|
|
||||||
.then(async () => {
|
|
||||||
handleIssueUpdate && (await handleIssueUpdate());
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsUpdating(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveIssueFromCycle = (cycleId: string) => {
|
|
||||||
if (!workspaceSlug || !projectId || !issueDetail) return;
|
|
||||||
|
|
||||||
setIsUpdating(true);
|
|
||||||
removeIssueFromCycle(workspaceSlug.toString(), projectId.toString(), cycleId, issueDetail.id)
|
|
||||||
.then(async () => {
|
|
||||||
handleIssueUpdate && (await handleIssueUpdate());
|
|
||||||
mutate(ISSUE_DETAILS(issueDetail.id));
|
|
||||||
|
|
||||||
mutate(CYCLE_ISSUES(cycleId));
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsUpdating(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const options = incompleteCycles?.map((cycle) => ({
|
|
||||||
value: cycle.id,
|
|
||||||
query: cycle.name,
|
|
||||||
content: (
|
|
||||||
<div className="flex items-center gap-1.5 truncate">
|
|
||||||
<span className="flex h-3.5 w-3.5 flex-shrink-0 items-center justify-center">
|
|
||||||
<ContrastIcon />
|
|
||||||
</span>
|
|
||||||
<span className="flex-grow truncate">{cycle.name}</span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const issueCycle = (issueDetail && issueDetail.cycle_id && getCycleById(issueDetail.cycle_id)) || undefined;
|
|
||||||
|
|
||||||
const disableSelect = disabled || isUpdating;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<CustomSearchSelect
|
|
||||||
value={issueDetail?.cycle_id}
|
|
||||||
onChange={(value: any) => {
|
|
||||||
value === issueDetail?.cycle_id
|
|
||||||
? handleRemoveIssueFromCycle(issueDetail?.cycle_id ?? "")
|
|
||||||
: handleCycleChange
|
|
||||||
? handleCycleChange(value)
|
|
||||||
: handleCycleStoreChange(value);
|
|
||||||
}}
|
|
||||||
options={options}
|
|
||||||
customButton={
|
|
||||||
<div>
|
|
||||||
<Tooltip position="left" tooltipContent={`${issueCycle ? issueCycle?.name : "No cycle"}`}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`flex w-full items-center rounded bg-custom-background-80 px-2.5 py-0.5 text-xs ${
|
|
||||||
disableSelect ? "cursor-not-allowed" : ""
|
|
||||||
} max-w-[10rem]`}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`flex items-center gap-1.5 truncate ${
|
|
||||||
issueCycle ? "text-custom-text-100" : "text-custom-text-200"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span className="flex-shrink-0">{issueCycle && <ContrastIcon className="h-3.5 w-3.5" />}</span>
|
|
||||||
<span className="truncate">{issueCycle ? issueCycle?.name : "No cycle"}</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
width="max-w-[10rem]"
|
|
||||||
noChevron
|
|
||||||
disabled={disableSelect}
|
|
||||||
/>
|
|
||||||
{isUpdating && <Spinner className="h-4 w-4" />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
@ -1,5 +0,0 @@
|
|||||||
export * from "./relation";
|
|
||||||
export * from "./cycle";
|
|
||||||
export * from "./label";
|
|
||||||
export * from "./module";
|
|
||||||
export * from "./parent";
|
|
@ -1,225 +0,0 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import { TwitterPicker } from "react-color";
|
|
||||||
import { Popover, Transition } from "@headlessui/react";
|
|
||||||
import { Plus, X } from "lucide-react";
|
|
||||||
// hooks
|
|
||||||
import { useLabel } from "hooks/store";
|
|
||||||
import useToast from "hooks/use-toast";
|
|
||||||
// ui
|
|
||||||
import { Input } from "@plane/ui";
|
|
||||||
import { IssueLabelSelect } from "../select";
|
|
||||||
// types
|
|
||||||
import { TIssue, IIssueLabel } from "@plane/types";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
issueDetails: TIssue | undefined;
|
|
||||||
labelList: string[];
|
|
||||||
submitChanges: (formData: any) => void;
|
|
||||||
isNotAllowed: boolean;
|
|
||||||
uneditable: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultValues: Partial<IIssueLabel> = {
|
|
||||||
name: "",
|
|
||||||
color: "#ff0000",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SidebarLabelSelect: React.FC<Props> = observer((props) => {
|
|
||||||
const { issueDetails, labelList, submitChanges, isNotAllowed, uneditable } = props;
|
|
||||||
// states
|
|
||||||
const [createLabelForm, setCreateLabelForm] = useState(false);
|
|
||||||
// router
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId } = router.query;
|
|
||||||
// toast
|
|
||||||
const { setToastAlert } = useToast();
|
|
||||||
// mobx store
|
|
||||||
const {
|
|
||||||
project: { projectLabels, createLabel },
|
|
||||||
} = useLabel();
|
|
||||||
// form info
|
|
||||||
const {
|
|
||||||
handleSubmit,
|
|
||||||
formState: { errors, isSubmitting },
|
|
||||||
reset,
|
|
||||||
control,
|
|
||||||
setFocus,
|
|
||||||
} = useForm<Partial<IIssueLabel>>({
|
|
||||||
defaultValues,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleNewLabel = async (formData: Partial<IIssueLabel>) => {
|
|
||||||
if (!workspaceSlug || !projectId || isSubmitting) return;
|
|
||||||
|
|
||||||
await createLabel(workspaceSlug.toString(), projectId.toString(), formData)
|
|
||||||
.then((res) => {
|
|
||||||
reset(defaultValues);
|
|
||||||
|
|
||||||
submitChanges({ labels: [...(issueDetails?.label_ids ?? []), res.id] });
|
|
||||||
|
|
||||||
setCreateLabelForm(false);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "error",
|
|
||||||
title: "Error!",
|
|
||||||
message: error?.error ?? "Something went wrong. Please try again.",
|
|
||||||
});
|
|
||||||
reset(formData);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!createLabelForm) return;
|
|
||||||
|
|
||||||
setFocus("name");
|
|
||||||
reset();
|
|
||||||
}, [createLabelForm, reset, setFocus]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`flex flex-col gap-3 ${uneditable ? "opacity-60" : ""}`}>
|
|
||||||
<div className="flex flex-wrap gap-1">
|
|
||||||
{labelList?.map((labelId) => {
|
|
||||||
const label = projectLabels?.find((l) => l.id === labelId);
|
|
||||||
|
|
||||||
if (label)
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={label.id}
|
|
||||||
className={`group flex cursor-pointer items-center gap-1 rounded-2xl border border-custom-border-100 px-1 py-0.5 text-xs ${
|
|
||||||
isNotAllowed || uneditable ? "!cursor-not-allowed" : "hover:border-red-500/20 hover:bg-red-500/20"
|
|
||||||
}`}
|
|
||||||
onClick={() => {
|
|
||||||
const updatedLabels = labelList?.filter((l) => l !== labelId);
|
|
||||||
submitChanges({
|
|
||||||
labels: updatedLabels,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
disabled={uneditable}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="h-2 w-2 flex-shrink-0 rounded-full"
|
|
||||||
style={{
|
|
||||||
backgroundColor: label.color ?? "#000000",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{label.name}
|
|
||||||
<X className="h-2 w-2 group-hover:text-red-500" />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<IssueLabelSelect
|
|
||||||
setIsOpen={setCreateLabelForm}
|
|
||||||
value={issueDetails?.label_ids ?? []}
|
|
||||||
onChange={(val: any) => submitChanges({ labels: val })}
|
|
||||||
projectId={issueDetails?.project_id ?? ""}
|
|
||||||
label={
|
|
||||||
<span
|
|
||||||
className={`flex ${
|
|
||||||
isNotAllowed || uneditable ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-90"
|
|
||||||
} items-center gap-2 rounded-2xl border border-custom-border-100 px-2 py-0.5 text-xs text-custom-text-300 hover:text-custom-text-200`}
|
|
||||||
>
|
|
||||||
Select Label
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
disabled={uneditable}
|
|
||||||
/>
|
|
||||||
{!isNotAllowed && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`flex ${
|
|
||||||
isNotAllowed || uneditable ? "cursor-not-allowed" : "cursor-pointer hover:bg-custom-background-90"
|
|
||||||
} items-center gap-1 rounded-2xl border border-custom-border-100 px-2 py-0.5 text-xs text-custom-text-300 hover:text-custom-text-200`}
|
|
||||||
onClick={() => setCreateLabelForm((prevData) => !prevData)}
|
|
||||||
disabled={uneditable}
|
|
||||||
>
|
|
||||||
{createLabelForm ? (
|
|
||||||
<>
|
|
||||||
<X className="h-3 w-3" /> Cancel
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Plus className="h-3 w-3" /> New
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{createLabelForm && (
|
|
||||||
<form className="flex items-center gap-x-2" onSubmit={handleSubmit(handleNewLabel)}>
|
|
||||||
<div>
|
|
||||||
<Controller
|
|
||||||
name="color"
|
|
||||||
control={control}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<Popover className="relative">
|
|
||||||
<>
|
|
||||||
<Popover.Button className="grid place-items-center outline-none">
|
|
||||||
{value && value?.trim() !== "" && (
|
|
||||||
<span
|
|
||||||
className="h-6 w-6 rounded"
|
|
||||||
style={{
|
|
||||||
backgroundColor: value ?? "black",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Popover.Button>
|
|
||||||
|
|
||||||
<Transition
|
|
||||||
as={React.Fragment}
|
|
||||||
enter="transition ease-out duration-200"
|
|
||||||
enterFrom="opacity-0 translate-y-1"
|
|
||||||
enterTo="opacity-100 translate-y-0"
|
|
||||||
leave="transition ease-in duration-150"
|
|
||||||
leaveFrom="opacity-100 translate-y-0"
|
|
||||||
leaveTo="opacity-0 translate-y-1"
|
|
||||||
>
|
|
||||||
<Popover.Panel className="absolute z-10 mt-1.5 max-w-xs px-2 sm:px-0">
|
|
||||||
<TwitterPicker color={value} onChange={(value) => onChange(value.hex)} />
|
|
||||||
</Popover.Panel>
|
|
||||||
</Transition>
|
|
||||||
</>
|
|
||||||
</Popover>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name="name"
|
|
||||||
rules={{
|
|
||||||
required: "This is required",
|
|
||||||
}}
|
|
||||||
render={({ field: { value, onChange, ref } }) => (
|
|
||||||
<Input
|
|
||||||
id="name"
|
|
||||||
name="name"
|
|
||||||
type="text"
|
|
||||||
value={value ?? ""}
|
|
||||||
onChange={onChange}
|
|
||||||
ref={ref}
|
|
||||||
hasError={Boolean(errors.name)}
|
|
||||||
placeholder="Title"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="grid place-items-center rounded bg-red-500 p-2.5"
|
|
||||||
onClick={() => setCreateLabelForm(false)}
|
|
||||||
disabled={uneditable}
|
|
||||||
>
|
|
||||||
<X className="h-4 w-4 text-white" />
|
|
||||||
</button>
|
|
||||||
<button type="submit" className="grid place-items-center rounded bg-green-500 p-2.5" disabled={isSubmitting}>
|
|
||||||
<Plus className="h-4 w-4 text-white" />
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,128 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
import { mutate } from "swr";
|
|
||||||
// hooks
|
|
||||||
import { useIssues, useModule } from "hooks/store";
|
|
||||||
// ui
|
|
||||||
import { CustomSearchSelect, DiceIcon, Spinner, Tooltip } from "@plane/ui";
|
|
||||||
// types
|
|
||||||
import { TIssue } from "@plane/types";
|
|
||||||
// fetch-keys
|
|
||||||
import { ISSUE_DETAILS, MODULE_ISSUES } from "constants/fetch-keys";
|
|
||||||
import { EIssuesStoreType } from "constants/issue";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
issueDetail: TIssue | undefined;
|
|
||||||
handleModuleChange?: (moduleId: string) => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
handleIssueUpdate?: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SidebarModuleSelect: React.FC<Props> = observer((props) => {
|
|
||||||
const { issueDetail, disabled = false, handleIssueUpdate, handleModuleChange } = props;
|
|
||||||
// router
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId } = router.query;
|
|
||||||
// mobx store
|
|
||||||
const {
|
|
||||||
issues: { removeIssueFromModule, addIssueToModule },
|
|
||||||
} = useIssues(EIssuesStoreType.MODULE);
|
|
||||||
const { projectModuleIds, getModuleById } = useModule();
|
|
||||||
|
|
||||||
const [isUpdating, setIsUpdating] = useState(false);
|
|
||||||
|
|
||||||
const handleModuleStoreChange = async (moduleId: string) => {
|
|
||||||
if (!workspaceSlug || !issueDetail || !moduleId || !projectId) return;
|
|
||||||
|
|
||||||
setIsUpdating(true);
|
|
||||||
await addIssueToModule(workspaceSlug.toString(), projectId?.toString(), moduleId, [issueDetail.id])
|
|
||||||
.then(async () => {
|
|
||||||
handleIssueUpdate && (await handleIssueUpdate());
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsUpdating(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveIssueFromModule = (bridgeId: string, moduleId: string) => {
|
|
||||||
if (!workspaceSlug || !projectId || !issueDetail) return;
|
|
||||||
|
|
||||||
setIsUpdating(true);
|
|
||||||
removeIssueFromModule(workspaceSlug.toString(), projectId.toString(), moduleId, issueDetail.id)
|
|
||||||
.then(async () => {
|
|
||||||
handleIssueUpdate && (await handleIssueUpdate());
|
|
||||||
mutate(ISSUE_DETAILS(issueDetail.id));
|
|
||||||
|
|
||||||
mutate(MODULE_ISSUES(moduleId));
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsUpdating(false);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const options = projectModuleIds?.map((moduleId) => {
|
|
||||||
const moduleDetail = getModuleById(moduleId);
|
|
||||||
return {
|
|
||||||
value: moduleId,
|
|
||||||
query: moduleDetail?.name ?? "",
|
|
||||||
content: (
|
|
||||||
<div className="flex items-center gap-1.5 truncate">
|
|
||||||
<span className="flex h-3.5 w-3.5 flex-shrink-0 items-center justify-center">
|
|
||||||
<DiceIcon />
|
|
||||||
</span>
|
|
||||||
<span className="flex-grow truncate">{moduleDetail?.name}</span>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// derived values
|
|
||||||
const issueModule = (issueDetail && issueDetail?.module_id && getModuleById(issueDetail.module_id)) || undefined;
|
|
||||||
|
|
||||||
const disableSelect = disabled || isUpdating;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<CustomSearchSelect
|
|
||||||
value={issueDetail?.module_id}
|
|
||||||
onChange={(value: any) => {
|
|
||||||
value === issueDetail?.module_id
|
|
||||||
? handleRemoveIssueFromModule(issueModule?.id ?? "", issueDetail?.module_id ?? "")
|
|
||||||
: handleModuleChange
|
|
||||||
? handleModuleChange(value)
|
|
||||||
: handleModuleStoreChange(value);
|
|
||||||
}}
|
|
||||||
options={options}
|
|
||||||
customButton={
|
|
||||||
<div>
|
|
||||||
<Tooltip position="left" tooltipContent={`${issueModule?.name ?? "No module"}`}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`flex w-full items-center rounded bg-custom-background-80 px-2.5 py-0.5 text-xs ${
|
|
||||||
disableSelect ? "cursor-not-allowed" : ""
|
|
||||||
} max-w-[10rem]`}
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className={`flex items-center gap-1.5 truncate ${
|
|
||||||
issueModule ? "text-custom-text-100" : "text-custom-text-200"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<span className="flex-shrink-0">{issueModule && <DiceIcon className="h-3.5 w-3.5" />}</span>
|
|
||||||
<span className="truncate">{issueModule?.name ?? "No module"}</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
width="max-w-[10rem]"
|
|
||||||
noChevron
|
|
||||||
disabled={disableSelect}
|
|
||||||
/>
|
|
||||||
{isUpdating && <Spinner className="h-4 w-4" />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
@ -1,69 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
// hooks
|
|
||||||
import { useIssueDetail, useIssues, useProject } from "hooks/store";
|
|
||||||
// components
|
|
||||||
import { ParentIssuesListModal } from "components/issues";
|
|
||||||
// icons
|
|
||||||
import { X } from "lucide-react";
|
|
||||||
// types
|
|
||||||
import { TIssue, ISearchIssueResponse } from "@plane/types";
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
onChange: (value: string) => void;
|
|
||||||
issueDetails: TIssue | undefined;
|
|
||||||
disabled?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SidebarParentSelect: React.FC<Props> = observer(({ onChange, issueDetails, disabled = false }) => {
|
|
||||||
const [selectedParentIssue, setSelectedParentIssue] = useState<ISearchIssueResponse | null>(null);
|
|
||||||
|
|
||||||
const { isParentIssueModalOpen, toggleParentIssueModal } = useIssueDetail();
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { projectId, issueId } = router.query;
|
|
||||||
|
|
||||||
// hooks
|
|
||||||
const { getProjectById } = useProject();
|
|
||||||
const { issueMap } = useIssues();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ParentIssuesListModal
|
|
||||||
isOpen={isParentIssueModalOpen}
|
|
||||||
handleClose={() => toggleParentIssueModal(false)}
|
|
||||||
onChange={(issue) => {
|
|
||||||
onChange(issue.id);
|
|
||||||
setSelectedParentIssue(issue);
|
|
||||||
}}
|
|
||||||
issueId={issueId as string}
|
|
||||||
projectId={projectId as string}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
className={`flex items-center gap-2 rounded bg-custom-background-80 px-2.5 py-0.5 text-xs w-max max-w-max" ${
|
|
||||||
disabled ? "cursor-not-allowed" : "cursor-pointer "
|
|
||||||
}`}
|
|
||||||
onClick={() => {
|
|
||||||
if (issueDetails?.parent_id) {
|
|
||||||
onChange("");
|
|
||||||
setSelectedParentIssue(null);
|
|
||||||
} else {
|
|
||||||
toggleParentIssueModal(true);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
{selectedParentIssue && issueDetails?.parent_id ? (
|
|
||||||
`${selectedParentIssue.project__identifier}-${selectedParentIssue.sequence_id}`
|
|
||||||
) : !selectedParentIssue && issueDetails?.parent_id ? (
|
|
||||||
`${getProjectById(issueDetails.parent_id)?.identifier}-${issueMap[issueDetails.parent_id]?.sequence_id}`
|
|
||||||
) : (
|
|
||||||
<span className="text-custom-text-200">Select issue</span>
|
|
||||||
)}
|
|
||||||
{issueDetails?.parent_id && <X className="h-2.5 w-2.5" />}
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
});
|
|
@ -7,7 +7,7 @@ import { IssueProperty } from "./properties";
|
|||||||
import { CustomMenu, Tooltip } from "@plane/ui";
|
import { CustomMenu, Tooltip } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { IUser, TIssue, TIssueSubIssues } from "@plane/types";
|
import { IUser, TIssue, TIssueSubIssues } from "@plane/types";
|
||||||
import { ISubIssuesRootLoaders, ISubIssuesRootLoadersHandler } from "./root";
|
// import { ISubIssuesRootLoaders, ISubIssuesRootLoadersHandler } from "./root";
|
||||||
import { useIssueDetail, useProject, useProjectState } from "hooks/store";
|
import { useIssueDetail, useProject, useProjectState } from "hooks/store";
|
||||||
|
|
||||||
export interface ISubIssues {
|
export interface ISubIssues {
|
||||||
@ -24,8 +24,8 @@ export interface ISubIssues {
|
|||||||
user: IUser | undefined;
|
user: IUser | undefined;
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
removeIssueFromSubIssues: (parentIssueId: string, issue: TIssue) => void;
|
removeIssueFromSubIssues: (parentIssueId: string, issue: TIssue) => void;
|
||||||
issuesLoader: ISubIssuesRootLoaders;
|
issuesLoader: any; // FIXME: ISubIssuesRootLoaders replace with any
|
||||||
handleIssuesLoader: ({ key, issueId }: ISubIssuesRootLoadersHandler) => void;
|
handleIssuesLoader: ({ key, issueId }: any) => void; // FIXME: ISubIssuesRootLoadersHandler replace with any
|
||||||
copyText: (text: string) => void;
|
copyText: (text: string) => void;
|
||||||
handleIssueCrudOperation: (
|
handleIssueCrudOperation: (
|
||||||
key: "create" | "existing" | "edit" | "delete",
|
key: "create" | "existing" | "edit" | "delete",
|
||||||
|
@ -3,7 +3,7 @@ import { useMemo } from "react";
|
|||||||
import { SubIssues } from "./issue";
|
import { SubIssues } from "./issue";
|
||||||
// types
|
// types
|
||||||
import { IUser, TIssue } from "@plane/types";
|
import { IUser, TIssue } from "@plane/types";
|
||||||
import { ISubIssuesRootLoaders, ISubIssuesRootLoadersHandler } from "./root";
|
// import { ISubIssuesRootLoaders, ISubIssuesRootLoadersHandler } from "./root";
|
||||||
|
|
||||||
// fetch keys
|
// fetch keys
|
||||||
import { useIssueDetail } from "hooks/store";
|
import { useIssueDetail } from "hooks/store";
|
||||||
@ -16,8 +16,8 @@ export interface ISubIssuesRootList {
|
|||||||
user: IUser | undefined;
|
user: IUser | undefined;
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
removeIssueFromSubIssues: (parentIssueId: string, issue: TIssue) => void;
|
removeIssueFromSubIssues: (parentIssueId: string, issue: TIssue) => void;
|
||||||
issuesLoader: ISubIssuesRootLoaders;
|
issuesLoader: any; // FIXME: replace ISubIssuesRootLoaders with any
|
||||||
handleIssuesLoader: ({ key, issueId }: ISubIssuesRootLoadersHandler) => void;
|
handleIssuesLoader: ({ key, issueId }: any) => void; // FIXME: replace ISubIssuesRootLoadersHandler with any
|
||||||
copyText: (text: string) => void;
|
copyText: (text: string) => void;
|
||||||
handleIssueCrudOperation: (
|
handleIssueCrudOperation: (
|
||||||
key: "create" | "existing" | "edit" | "delete",
|
key: "create" | "existing" | "edit" | "delete",
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import React, { useCallback } from "react";
|
import React, { useCallback, useMemo, useState } from "react";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import useSWR, { mutate } from "swr";
|
|
||||||
import { Plus, ChevronRight, ChevronDown } from "lucide-react";
|
import { Plus, ChevronRight, ChevronDown } from "lucide-react";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssues, useUser } from "hooks/store";
|
import { useIssueDetail, useIssues, useUser } from "hooks/store";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// components
|
// components
|
||||||
import { ExistingIssuesListModal } from "components/core";
|
import { ExistingIssuesListModal } from "components/core";
|
||||||
@ -26,63 +24,91 @@ import { EUserProjectRoles } from "constants/project";
|
|||||||
import { EIssuesStoreType } from "constants/issue";
|
import { EIssuesStoreType } from "constants/issue";
|
||||||
|
|
||||||
export interface ISubIssuesRoot {
|
export interface ISubIssuesRoot {
|
||||||
parentIssue: TIssue;
|
workspaceSlug: string;
|
||||||
user: IUser | undefined;
|
projectId: string;
|
||||||
}
|
|
||||||
|
|
||||||
export interface ISubIssuesRootLoaders {
|
|
||||||
visibility: string[];
|
|
||||||
delete: string[];
|
|
||||||
sub_issues: string[];
|
|
||||||
}
|
|
||||||
export interface ISubIssuesRootLoadersHandler {
|
|
||||||
key: "visibility" | "delete" | "sub_issues";
|
|
||||||
issueId: string;
|
issueId: string;
|
||||||
|
currentUser: IUser;
|
||||||
|
is_archived: boolean;
|
||||||
|
is_editable: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const issueService = new IssueService();
|
|
||||||
|
|
||||||
export const SubIssuesRoot: React.FC<ISubIssuesRoot> = observer((props) => {
|
export const SubIssuesRoot: React.FC<ISubIssuesRoot> = observer((props) => {
|
||||||
const { parentIssue, user } = props;
|
const { workspaceSlug, projectId, issueId, currentUser, is_archived, is_editable } = props;
|
||||||
// store hooks
|
// store hooks
|
||||||
const {
|
|
||||||
issues: { updateIssue, removeIssue },
|
|
||||||
} = useIssues(EIssuesStoreType.PROJECT);
|
|
||||||
const {
|
const {
|
||||||
membership: { currentProjectRole },
|
membership: { currentProjectRole },
|
||||||
} = useUser();
|
} = useUser();
|
||||||
// router
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug, projectId } = router.query;
|
|
||||||
|
|
||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
|
const {
|
||||||
|
subIssues: { subIssuesByIssueId, subIssuesStateDistribution },
|
||||||
|
updateIssue,
|
||||||
|
removeIssue,
|
||||||
|
fetchSubIssues,
|
||||||
|
createSubIssues,
|
||||||
|
} = useIssueDetail();
|
||||||
|
// state
|
||||||
|
const [currentIssue, setCurrentIssue] = useState<TIssue>();
|
||||||
|
|
||||||
const { data: issues, isLoading } = useSWR(
|
console.log("subIssuesByIssueId", subIssuesByIssueId(issueId));
|
||||||
workspaceSlug && projectId && parentIssue && parentIssue?.id ? SUB_ISSUES(parentIssue?.id) : null,
|
|
||||||
workspaceSlug && projectId && parentIssue && parentIssue?.id
|
|
||||||
? () => issueService.subIssues(workspaceSlug.toString(), projectId.toString(), parentIssue.id)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const [issuesLoader, setIssuesLoader] = React.useState<ISubIssuesRootLoaders>({
|
const copyText = (text: string) => {
|
||||||
visibility: [parentIssue?.id],
|
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
||||||
delete: [],
|
copyTextToClipboard(`${originURL}/${text}`).then(() => {
|
||||||
sub_issues: [],
|
setToastAlert({
|
||||||
|
type: "success",
|
||||||
|
title: "Link Copied!",
|
||||||
|
message: "Issue link copied to clipboard.",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
const handleIssuesLoader = ({ key, issueId }: ISubIssuesRootLoadersHandler) => {
|
|
||||||
setIssuesLoader((previousData: ISubIssuesRootLoaders) => ({
|
|
||||||
...previousData,
|
|
||||||
[key]: previousData[key].includes(issueId)
|
|
||||||
? previousData[key].filter((i: string) => i !== issueId)
|
|
||||||
: [...previousData[key], issueId],
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const subIssueOperations = useMemo(
|
||||||
|
() => ({
|
||||||
|
fetchSubIssues: async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||||
|
try {
|
||||||
|
await fetchSubIssues(workspaceSlug, projectId, issueId);
|
||||||
|
} catch (error) {}
|
||||||
|
},
|
||||||
|
update: async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => {
|
||||||
|
try {
|
||||||
|
await updateIssue(workspaceSlug, projectId, issueId, data);
|
||||||
|
setToastAlert({
|
||||||
|
title: "Issue updated successfully",
|
||||||
|
type: "success",
|
||||||
|
message: "Issue updated successfully",
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setToastAlert({
|
||||||
|
title: "Issue update failed",
|
||||||
|
type: "error",
|
||||||
|
message: "Issue update failed",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addSubIssue: async () => {
|
||||||
|
try {
|
||||||
|
} catch (error) {}
|
||||||
|
},
|
||||||
|
removeSubIssue: async () => {
|
||||||
|
try {
|
||||||
|
} catch (error) {}
|
||||||
|
},
|
||||||
|
updateIssue: async () => {
|
||||||
|
try {
|
||||||
|
} catch (error) {}
|
||||||
|
},
|
||||||
|
deleteIssue: async () => {
|
||||||
|
try {
|
||||||
|
} catch (error) {}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const [issueCrudOperation, setIssueCrudOperation] = React.useState<{
|
const [issueCrudOperation, setIssueCrudOperation] = React.useState<{
|
||||||
|
// type: "create" | "edit";
|
||||||
create: { toggle: boolean; issueId: string | null };
|
create: { toggle: boolean; issueId: string | null };
|
||||||
existing: { toggle: boolean; issueId: string | null };
|
existing: { toggle: boolean; issueId: string | null };
|
||||||
edit: { toggle: boolean; issueId: string | null; issue: TIssue | null };
|
|
||||||
delete: { toggle: boolean; issueId: string | null; issue: TIssue | null };
|
|
||||||
}>({
|
}>({
|
||||||
create: {
|
create: {
|
||||||
toggle: false,
|
toggle: false,
|
||||||
@ -92,19 +118,10 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = observer((props) => {
|
|||||||
toggle: false,
|
toggle: false,
|
||||||
issueId: null,
|
issueId: null,
|
||||||
},
|
},
|
||||||
edit: {
|
|
||||||
toggle: false,
|
|
||||||
issueId: null, // parent issue id for mutation
|
|
||||||
issue: null,
|
|
||||||
},
|
|
||||||
delete: {
|
|
||||||
toggle: false,
|
|
||||||
issueId: null, // parent issue id for mutation
|
|
||||||
issue: null,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleIssueCrudOperation = (
|
const handleIssueCrudOperation = (
|
||||||
key: "create" | "existing" | "edit" | "delete",
|
key: "create" | "existing",
|
||||||
issueId: string | null,
|
issueId: string | null,
|
||||||
issue: TIssue | null = null
|
issue: TIssue | null = null
|
||||||
) => {
|
) => {
|
||||||
@ -118,75 +135,14 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = observer((props) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const addAsSubIssueFromExistingIssues = async (data: ISearchIssueResponse[]) => {
|
|
||||||
if (!workspaceSlug || !projectId || !parentIssue || issueCrudOperation?.existing?.issueId === null) return;
|
|
||||||
const issueId = issueCrudOperation?.existing?.issueId;
|
|
||||||
const payload = {
|
|
||||||
sub_issue_ids: data.map((i) => i.id),
|
|
||||||
};
|
|
||||||
await issueService.addSubIssues(workspaceSlug.toString(), projectId.toString(), issueId, payload).finally(() => {
|
|
||||||
if (issueId) mutate(SUB_ISSUES(issueId));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeIssueFromSubIssues = async (parentIssueId: string, issue: TIssue) => {
|
|
||||||
if (!workspaceSlug || !projectId || !parentIssue || !issue?.id) return;
|
|
||||||
issueService
|
|
||||||
.patchIssue(workspaceSlug.toString(), projectId.toString(), issue.id, { parent_id: null })
|
|
||||||
.then(async () => {
|
|
||||||
if (parentIssueId) await mutate(SUB_ISSUES(parentIssueId));
|
|
||||||
handleIssuesLoader({ key: "delete", issueId: issue?.id });
|
|
||||||
setToastAlert({
|
|
||||||
type: "success",
|
|
||||||
title: `Issue removed!`,
|
|
||||||
message: `Issue removed successfully.`,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
handleIssuesLoader({ key: "delete", issueId: issue?.id });
|
|
||||||
setToastAlert({
|
|
||||||
type: "warning",
|
|
||||||
title: `Error!`,
|
|
||||||
message: `Error, Please try again later.`,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyText = (text: string) => {
|
|
||||||
const originURL = typeof window !== "undefined" && window.location.origin ? window.location.origin : "";
|
|
||||||
copyTextToClipboard(`${originURL}/${text}`).then(() => {
|
|
||||||
setToastAlert({
|
|
||||||
type: "success",
|
|
||||||
title: "Link Copied!",
|
|
||||||
message: "Issue link copied to clipboard.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdateIssue = useCallback(
|
|
||||||
(issue: TIssue, data: Partial<TIssue>) => {
|
|
||||||
if (!workspaceSlug || !projectId || !user) return;
|
|
||||||
|
|
||||||
updateIssue(workspaceSlug.toString(), projectId.toString(), issue.id, data);
|
|
||||||
},
|
|
||||||
[projectId, updateIssue, user, workspaceSlug]
|
|
||||||
);
|
|
||||||
|
|
||||||
const isEditable = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
|
||||||
|
|
||||||
const mutateSubIssues = (parentIssueId: string | null) => {
|
|
||||||
if (parentIssueId) mutate(SUB_ISSUES(parentIssueId));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full w-full space-y-2">
|
<div className="h-full w-full space-y-2">
|
||||||
{!issues && isLoading ? (
|
{/* {!issues && isLoading ? (
|
||||||
<div className="py-3 text-center text-sm font-medium text-custom-text-300">Loading...</div>
|
<div className="py-3 text-center text-sm font-medium text-custom-text-300">Loading...</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{issues && issues?.sub_issues && issues?.sub_issues?.length > 0 ? (
|
{issues && issues?.sub_issues && issues?.sub_issues?.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
{/* header */}
|
|
||||||
<div className="relative flex items-center gap-4 text-xs">
|
<div className="relative flex items-center gap-4 text-xs">
|
||||||
<div
|
<div
|
||||||
className="flex cursor-pointer select-none items-center gap-1 rounded border border-custom-border-100 p-1.5 px-2 shadow transition-all hover:bg-custom-background-80"
|
className="flex cursor-pointer select-none items-center gap-1 rounded border border-custom-border-100 p-1.5 px-2 shadow transition-all hover:bg-custom-background-80"
|
||||||
@ -210,7 +166,7 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = observer((props) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isEditable && issuesLoader.visibility.includes(parentIssue?.id) && (
|
{is_editable && issuesLoader.visibility.includes(parentIssue?.id) && (
|
||||||
<div className="ml-auto flex flex-shrink-0 select-none items-center gap-2">
|
<div className="ml-auto flex flex-shrink-0 select-none items-center gap-2">
|
||||||
<div
|
<div
|
||||||
className="cursor-pointer rounded border border-custom-border-100 p-1.5 px-2 shadow transition-all hover:bg-custom-background-80"
|
className="cursor-pointer rounded border border-custom-border-100 p-1.5 px-2 shadow transition-all hover:bg-custom-background-80"
|
||||||
@ -228,7 +184,7 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = observer((props) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* issues */}
|
|
||||||
{issuesLoader.visibility.includes(parentIssue?.id) && workspaceSlug && projectId && (
|
{issuesLoader.visibility.includes(parentIssue?.id) && workspaceSlug && projectId && (
|
||||||
<div className="border border-b-0 border-custom-border-100">
|
<div className="border border-b-0 border-custom-border-100">
|
||||||
<SubIssuesRootList
|
<SubIssuesRootList
|
||||||
@ -236,7 +192,7 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = observer((props) => {
|
|||||||
projectId={projectId.toString()}
|
projectId={projectId.toString()}
|
||||||
parentIssue={parentIssue}
|
parentIssue={parentIssue}
|
||||||
user={undefined}
|
user={undefined}
|
||||||
editable={isEditable}
|
editable={is_editable}
|
||||||
removeIssueFromSubIssues={removeIssueFromSubIssues}
|
removeIssueFromSubIssues={removeIssueFromSubIssues}
|
||||||
issuesLoader={issuesLoader}
|
issuesLoader={issuesLoader}
|
||||||
handleIssuesLoader={handleIssuesLoader}
|
handleIssuesLoader={handleIssuesLoader}
|
||||||
@ -262,7 +218,6 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = observer((props) => {
|
|||||||
>
|
>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateSubIssues(parentIssue?.id);
|
|
||||||
handleIssueCrudOperation("create", parentIssue?.id);
|
handleIssueCrudOperation("create", parentIssue?.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -270,7 +225,6 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = observer((props) => {
|
|||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateSubIssues(parentIssue?.id);
|
|
||||||
handleIssueCrudOperation("existing", parentIssue?.id);
|
handleIssueCrudOperation("existing", parentIssue?.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -280,7 +234,7 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
isEditable && (
|
is_editable && (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="py-2 text-xs italic text-custom-text-300">No Sub-Issues yet</div>
|
<div className="py-2 text-xs italic text-custom-text-300">No Sub-Issues yet</div>
|
||||||
<div>
|
<div>
|
||||||
@ -298,7 +252,6 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = observer((props) => {
|
|||||||
>
|
>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateSubIssues(parentIssue?.id);
|
|
||||||
handleIssueCrudOperation("create", parentIssue?.id);
|
handleIssueCrudOperation("create", parentIssue?.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -306,7 +259,6 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = observer((props) => {
|
|||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
mutateSubIssues(parentIssue?.id);
|
|
||||||
handleIssueCrudOperation("existing", parentIssue?.id);
|
handleIssueCrudOperation("existing", parentIssue?.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -317,19 +269,20 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = observer((props) => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
{isEditable && issueCrudOperation?.create?.toggle && (
|
|
||||||
|
{is_editable && issueCrudOperation?.create?.toggle && (
|
||||||
<CreateUpdateIssueModal
|
<CreateUpdateIssueModal
|
||||||
isOpen={issueCrudOperation?.create?.toggle}
|
isOpen={issueCrudOperation?.create?.toggle}
|
||||||
data={{
|
data={{
|
||||||
parent_id: issueCrudOperation?.create?.issueId,
|
parent_id: issueCrudOperation?.create?.issueId,
|
||||||
}}
|
}}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
mutateSubIssues(issueCrudOperation?.create?.issueId);
|
|
||||||
handleIssueCrudOperation("create", null);
|
handleIssueCrudOperation("create", null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isEditable && issueCrudOperation?.existing?.toggle && issueCrudOperation?.existing?.issueId && (
|
|
||||||
|
{is_editable && issueCrudOperation?.existing?.toggle && issueCrudOperation?.existing?.issueId && (
|
||||||
<ExistingIssuesListModal
|
<ExistingIssuesListModal
|
||||||
isOpen={issueCrudOperation?.existing?.toggle}
|
isOpen={issueCrudOperation?.existing?.toggle}
|
||||||
handleClose={() => handleIssueCrudOperation("existing", null)}
|
handleClose={() => handleIssueCrudOperation("existing", null)}
|
||||||
@ -338,19 +291,20 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = observer((props) => {
|
|||||||
workspaceLevelToggle
|
workspaceLevelToggle
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isEditable && issueCrudOperation?.edit?.toggle && issueCrudOperation?.edit?.issueId && (
|
|
||||||
|
{is_editable && issueCrudOperation?.edit?.toggle && issueCrudOperation?.edit?.issueId && (
|
||||||
<>
|
<>
|
||||||
<CreateUpdateIssueModal
|
<CreateUpdateIssueModal
|
||||||
isOpen={issueCrudOperation?.edit?.toggle}
|
isOpen={issueCrudOperation?.edit?.toggle}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
mutateSubIssues(issueCrudOperation?.edit?.issueId);
|
|
||||||
handleIssueCrudOperation("edit", null, null);
|
handleIssueCrudOperation("edit", null, null);
|
||||||
}}
|
}}
|
||||||
data={issueCrudOperation?.edit?.issue ?? undefined}
|
data={issueCrudOperation?.edit?.issue ?? undefined}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{isEditable &&
|
|
||||||
|
{is_editable &&
|
||||||
workspaceSlug &&
|
workspaceSlug &&
|
||||||
projectId &&
|
projectId &&
|
||||||
issueCrudOperation?.delete?.issueId &&
|
issueCrudOperation?.delete?.issueId &&
|
||||||
@ -358,7 +312,6 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = observer((props) => {
|
|||||||
<DeleteIssueModal
|
<DeleteIssueModal
|
||||||
isOpen={issueCrudOperation?.delete?.toggle}
|
isOpen={issueCrudOperation?.delete?.toggle}
|
||||||
handleClose={() => {
|
handleClose={() => {
|
||||||
mutateSubIssues(issueCrudOperation?.delete?.issueId);
|
|
||||||
handleIssueCrudOperation("delete", null, null);
|
handleIssueCrudOperation("delete", null, null);
|
||||||
}}
|
}}
|
||||||
data={issueCrudOperation?.delete?.issue}
|
data={issueCrudOperation?.delete?.issue}
|
||||||
@ -372,7 +325,7 @@ export const SubIssuesRoot: React.FC<ISubIssuesRoot> = observer((props) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -76,7 +76,7 @@ export const ModuleDetailsSidebar: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const submitChanges = (data: Partial<IModule>) => {
|
const submitChanges = (data: Partial<IModule>) => {
|
||||||
if (!workspaceSlug || !projectId || !moduleId) return;
|
if (!workspaceSlug || !projectId || !moduleId) return;
|
||||||
updateModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId, data);
|
updateModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString(), data);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateLink = async (formData: ModuleLink) => {
|
const handleCreateLink = async (formData: ModuleLink) => {
|
||||||
|
@ -37,90 +37,90 @@ export const CreateUpdatePageModal: FC<Props> = (props) => {
|
|||||||
const createProjectPage = async (payload: IPage) => {
|
const createProjectPage = async (payload: IPage) => {
|
||||||
if (!workspaceSlug) return;
|
if (!workspaceSlug) return;
|
||||||
|
|
||||||
await createPage(workspaceSlug.toString(), projectId, payload)
|
// await createPage(workspaceSlug.toString(), projectId, payload)
|
||||||
.then((res) => {
|
// .then((res) => {
|
||||||
router.push(`/${workspaceSlug}/projects/${projectId}/pages/${res.id}`);
|
// router.push(`/${workspaceSlug}/projects/${projectId}/pages/${res.id}`);
|
||||||
onClose();
|
// onClose();
|
||||||
setToastAlert({
|
// setToastAlert({
|
||||||
type: "success",
|
// type: "success",
|
||||||
title: "Success!",
|
// title: "Success!",
|
||||||
message: "Page created successfully.",
|
// message: "Page created successfully.",
|
||||||
});
|
// });
|
||||||
postHogEventTracker(
|
// postHogEventTracker(
|
||||||
"PAGE_CREATED",
|
// "PAGE_CREATED",
|
||||||
{
|
// {
|
||||||
...res,
|
// ...res,
|
||||||
state: "SUCCESS",
|
// state: "SUCCESS",
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
isGrouping: true,
|
// isGrouping: true,
|
||||||
groupType: "Workspace_metrics",
|
// groupType: "Workspace_metrics",
|
||||||
groupId: currentWorkspace?.id!,
|
// groupId: currentWorkspace?.id!,
|
||||||
}
|
// }
|
||||||
);
|
// );
|
||||||
})
|
// })
|
||||||
.catch((err) => {
|
// .catch((err) => {
|
||||||
setToastAlert({
|
// setToastAlert({
|
||||||
type: "error",
|
// type: "error",
|
||||||
title: "Error!",
|
// title: "Error!",
|
||||||
message: err.detail ?? "Page could not be created. Please try again.",
|
// message: err.detail ?? "Page could not be created. Please try again.",
|
||||||
});
|
// });
|
||||||
postHogEventTracker(
|
// postHogEventTracker(
|
||||||
"PAGE_CREATED",
|
// "PAGE_CREATED",
|
||||||
{
|
// {
|
||||||
state: "FAILED",
|
// state: "FAILED",
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
isGrouping: true,
|
// isGrouping: true,
|
||||||
groupType: "Workspace_metrics",
|
// groupType: "Workspace_metrics",
|
||||||
groupId: currentWorkspace?.id!,
|
// groupId: currentWorkspace?.id!,
|
||||||
}
|
// }
|
||||||
);
|
// );
|
||||||
});
|
// });
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateProjectPage = async (payload: IPage) => {
|
const updateProjectPage = async (payload: IPage) => {
|
||||||
if (!data || !workspaceSlug) return;
|
if (!data || !workspaceSlug) return;
|
||||||
|
|
||||||
await updatePage(workspaceSlug.toString(), projectId, data.id, payload)
|
// await updatePage(workspaceSlug.toString(), projectId, data.id, payload)
|
||||||
.then((res) => {
|
// .then((res) => {
|
||||||
onClose();
|
// onClose();
|
||||||
setToastAlert({
|
// setToastAlert({
|
||||||
type: "success",
|
// type: "success",
|
||||||
title: "Success!",
|
// title: "Success!",
|
||||||
message: "Page updated successfully.",
|
// message: "Page updated successfully.",
|
||||||
});
|
// });
|
||||||
postHogEventTracker(
|
// postHogEventTracker(
|
||||||
"PAGE_UPDATED",
|
// "PAGE_UPDATED",
|
||||||
{
|
// {
|
||||||
...res,
|
// ...res,
|
||||||
state: "SUCCESS",
|
// state: "SUCCESS",
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
isGrouping: true,
|
// isGrouping: true,
|
||||||
groupType: "Workspace_metrics",
|
// groupType: "Workspace_metrics",
|
||||||
groupId: currentWorkspace?.id!,
|
// groupId: currentWorkspace?.id!,
|
||||||
}
|
// }
|
||||||
);
|
// );
|
||||||
})
|
// })
|
||||||
.catch((err) => {
|
// .catch((err) => {
|
||||||
setToastAlert({
|
// setToastAlert({
|
||||||
type: "error",
|
// type: "error",
|
||||||
title: "Error!",
|
// title: "Error!",
|
||||||
message: err.detail ?? "Page could not be updated. Please try again.",
|
// message: err.detail ?? "Page could not be updated. Please try again.",
|
||||||
});
|
// });
|
||||||
postHogEventTracker(
|
// postHogEventTracker(
|
||||||
"PAGE_UPDATED",
|
// "PAGE_UPDATED",
|
||||||
{
|
// {
|
||||||
state: "FAILED",
|
// state: "FAILED",
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
isGrouping: true,
|
// isGrouping: true,
|
||||||
groupType: "Workspace_metrics",
|
// groupType: "Workspace_metrics",
|
||||||
groupId: currentWorkspace?.id!,
|
// groupId: currentWorkspace?.id!,
|
||||||
}
|
// }
|
||||||
);
|
// );
|
||||||
});
|
// });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFormSubmit = async (formData: IPage) => {
|
const handleFormSubmit = async (formData: IPage) => {
|
||||||
|
@ -186,8 +186,9 @@ export const PagesListItem: FC<IPagesListItem> = observer((props) => {
|
|||||||
<div className="flex items-center gap-2 overflow-hidden">
|
<div className="flex items-center gap-2 overflow-hidden">
|
||||||
<FileText className="h-4 w-4 shrink-0" />
|
<FileText className="h-4 w-4 shrink-0" />
|
||||||
<p className="mr-2 truncate text-sm text-custom-text-100">{pageDetails.name}</p>
|
<p className="mr-2 truncate text-sm text-custom-text-100">{pageDetails.name}</p>
|
||||||
|
{/* FIXME: replace any with proper type */}
|
||||||
{pageDetails.label_details.length > 0 &&
|
{pageDetails.label_details.length > 0 &&
|
||||||
pageDetails.label_details.map((label) => (
|
pageDetails.label_details.map((label: any) => (
|
||||||
<div
|
<div
|
||||||
key={label.id}
|
key={label.id}
|
||||||
className="group flex items-center gap-1 rounded-2xl border border-custom-border-200 px-2 py-0.5 text-xs"
|
className="group flex items-center gap-1 rounded-2xl border border-custom-border-200 px-2 py-0.5 text-xs"
|
||||||
|
@ -23,7 +23,8 @@ export const RecentPagesList: FC = observer(() => {
|
|||||||
} = useUser();
|
} = useUser();
|
||||||
const { recentProjectPages } = usePage();
|
const { recentProjectPages } = usePage();
|
||||||
|
|
||||||
const isEmpty = recentProjectPages && Object.values(recentProjectPages).every((value) => value.length === 0);
|
// FIXME: replace any with proper type
|
||||||
|
const isEmpty = recentProjectPages && Object.values(recentProjectPages).every((value: any) => value.length === 0);
|
||||||
|
|
||||||
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||||
|
|
||||||
|
@ -36,6 +36,7 @@ export enum EIssueFilterType {
|
|||||||
FILTERS = "filters",
|
FILTERS = "filters",
|
||||||
DISPLAY_FILTERS = "display_filters",
|
DISPLAY_FILTERS = "display_filters",
|
||||||
DISPLAY_PROPERTIES = "display_properties",
|
DISPLAY_PROPERTIES = "display_properties",
|
||||||
|
KANBAN_FILTERS = "kanban_filters",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ISSUE_PRIORITIES: {
|
export const ISSUE_PRIORITIES: {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
|
import merge from "lodash/merge";
|
||||||
// mobx store
|
// mobx store
|
||||||
import { StoreContext } from "contexts/store-context";
|
import { StoreContext } from "contexts/store-context";
|
||||||
// types
|
// types
|
||||||
@ -14,112 +15,102 @@ import { TIssueMap } from "@plane/types";
|
|||||||
// constants
|
// constants
|
||||||
import { EIssuesStoreType } from "constants/issue";
|
import { EIssuesStoreType } from "constants/issue";
|
||||||
|
|
||||||
export interface IStoreIssues {
|
type defaultIssueStore = {
|
||||||
[EIssuesStoreType.GLOBAL]: {
|
|
||||||
issueMap: TIssueMap;
|
issueMap: TIssueMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TStoreIssues = {
|
||||||
|
[EIssuesStoreType.GLOBAL]: defaultIssueStore & {
|
||||||
issues: IWorkspaceIssues;
|
issues: IWorkspaceIssues;
|
||||||
issuesFilter: IWorkspaceIssuesFilter;
|
issuesFilter: IWorkspaceIssuesFilter;
|
||||||
};
|
};
|
||||||
[EIssuesStoreType.PROFILE]: {
|
[EIssuesStoreType.PROFILE]: defaultIssueStore & {
|
||||||
issueMap: TIssueMap;
|
|
||||||
issues: IProfileIssues;
|
issues: IProfileIssues;
|
||||||
issuesFilter: IProfileIssuesFilter;
|
issuesFilter: IProfileIssuesFilter;
|
||||||
};
|
};
|
||||||
[EIssuesStoreType.PROJECT]: {
|
[EIssuesStoreType.PROJECT]: defaultIssueStore & {
|
||||||
issueMap: TIssueMap;
|
|
||||||
issues: IProjectIssues;
|
issues: IProjectIssues;
|
||||||
issuesFilter: IProjectIssuesFilter;
|
issuesFilter: IProjectIssuesFilter;
|
||||||
};
|
};
|
||||||
[EIssuesStoreType.CYCLE]: {
|
[EIssuesStoreType.CYCLE]: defaultIssueStore & {
|
||||||
issueMap: TIssueMap;
|
|
||||||
issues: ICycleIssues;
|
issues: ICycleIssues;
|
||||||
issuesFilter: ICycleIssuesFilter;
|
issuesFilter: ICycleIssuesFilter;
|
||||||
};
|
};
|
||||||
[EIssuesStoreType.MODULE]: {
|
[EIssuesStoreType.MODULE]: defaultIssueStore & {
|
||||||
issueMap: TIssueMap;
|
|
||||||
issues: IModuleIssues;
|
issues: IModuleIssues;
|
||||||
issuesFilter: IModuleIssuesFilter;
|
issuesFilter: IModuleIssuesFilter;
|
||||||
};
|
};
|
||||||
[EIssuesStoreType.PROJECT_VIEW]: {
|
[EIssuesStoreType.PROJECT_VIEW]: defaultIssueStore & {
|
||||||
issueMap: TIssueMap;
|
|
||||||
issues: IProjectViewIssues;
|
issues: IProjectViewIssues;
|
||||||
issuesFilter: IProjectViewIssuesFilter;
|
issuesFilter: IProjectViewIssuesFilter;
|
||||||
};
|
};
|
||||||
[EIssuesStoreType.ARCHIVED]: {
|
[EIssuesStoreType.ARCHIVED]: defaultIssueStore & {
|
||||||
issueMap: TIssueMap;
|
|
||||||
issues: IArchivedIssues;
|
issues: IArchivedIssues;
|
||||||
issuesFilter: IArchivedIssuesFilter;
|
issuesFilter: IArchivedIssuesFilter;
|
||||||
};
|
};
|
||||||
[EIssuesStoreType.DRAFT]: {
|
[EIssuesStoreType.DRAFT]: defaultIssueStore & {
|
||||||
issueMap: TIssueMap;
|
|
||||||
issues: IDraftIssues;
|
issues: IDraftIssues;
|
||||||
issuesFilter: IDraftIssuesFilter;
|
issuesFilter: IDraftIssuesFilter;
|
||||||
};
|
};
|
||||||
[EIssuesStoreType.DEFAULT]: {
|
[EIssuesStoreType.DEFAULT]: defaultIssueStore & {
|
||||||
issueMap: TIssueMap;
|
|
||||||
issues: undefined;
|
issues: undefined;
|
||||||
issuesFilter: undefined;
|
issuesFilter: undefined;
|
||||||
};
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export const useIssues = <T extends EIssuesStoreType>(storeType?: T): IStoreIssues[T] => {
|
export const useIssues = <T extends EIssuesStoreType>(storeType?: T): TStoreIssues[T] => {
|
||||||
const context = useContext(StoreContext);
|
const context = useContext(StoreContext);
|
||||||
if (context === undefined) throw new Error("useIssues must be used within StoreProvider");
|
if (context === undefined) throw new Error("useIssues must be used within StoreProvider");
|
||||||
|
|
||||||
|
const defaultStore: defaultIssueStore = {
|
||||||
|
issueMap: context.issue.issues.issuesMap,
|
||||||
|
};
|
||||||
|
|
||||||
switch (storeType) {
|
switch (storeType) {
|
||||||
case EIssuesStoreType.GLOBAL:
|
case EIssuesStoreType.GLOBAL:
|
||||||
return {
|
return merge(defaultStore, {
|
||||||
issueMap: context.issue.issues.issuesMap,
|
|
||||||
issues: context.issue.workspaceIssues,
|
issues: context.issue.workspaceIssues,
|
||||||
issuesFilter: context.issue.workspaceIssuesFilter,
|
issuesFilter: context.issue.workspaceIssuesFilter,
|
||||||
} as IStoreIssues[T];
|
}) as TStoreIssues[T];
|
||||||
case EIssuesStoreType.PROFILE:
|
case EIssuesStoreType.PROFILE:
|
||||||
return {
|
return merge(defaultStore, {
|
||||||
issueMap: context.issue.issues.issuesMap,
|
|
||||||
issues: context.issue.profileIssues,
|
issues: context.issue.profileIssues,
|
||||||
issuesFilter: context.issue.profileIssuesFilter,
|
issuesFilter: context.issue.profileIssuesFilter,
|
||||||
} as IStoreIssues[T];
|
}) as TStoreIssues[T];
|
||||||
case EIssuesStoreType.PROJECT:
|
case EIssuesStoreType.PROJECT:
|
||||||
return {
|
return merge(defaultStore, {
|
||||||
issueMap: context.issue.issues.issuesMap,
|
|
||||||
issues: context.issue.projectIssues,
|
issues: context.issue.projectIssues,
|
||||||
issuesFilter: context.issue.projectIssuesFilter,
|
issuesFilter: context.issue.projectIssuesFilter,
|
||||||
} as IStoreIssues[T];
|
}) as TStoreIssues[T];
|
||||||
case EIssuesStoreType.CYCLE:
|
case EIssuesStoreType.CYCLE:
|
||||||
return {
|
return merge(defaultStore, {
|
||||||
issueMap: context.issue.issues.issuesMap,
|
|
||||||
issues: context.issue.cycleIssues,
|
issues: context.issue.cycleIssues,
|
||||||
issuesFilter: context.issue.cycleIssuesFilter,
|
issuesFilter: context.issue.cycleIssuesFilter,
|
||||||
} as IStoreIssues[T];
|
}) as TStoreIssues[T];
|
||||||
case EIssuesStoreType.MODULE:
|
case EIssuesStoreType.MODULE:
|
||||||
return {
|
return merge(defaultStore, {
|
||||||
issueMap: context.issue.issues.issuesMap,
|
|
||||||
issues: context.issue.moduleIssues,
|
issues: context.issue.moduleIssues,
|
||||||
issuesFilter: context.issue.moduleIssuesFilter,
|
issuesFilter: context.issue.moduleIssuesFilter,
|
||||||
} as IStoreIssues[T];
|
}) as TStoreIssues[T];
|
||||||
case EIssuesStoreType.PROJECT_VIEW:
|
case EIssuesStoreType.PROJECT_VIEW:
|
||||||
return {
|
return merge(defaultStore, {
|
||||||
issueMap: context.issue.issues.issuesMap,
|
|
||||||
issues: context.issue.projectViewIssues,
|
issues: context.issue.projectViewIssues,
|
||||||
issuesFilter: context.issue.projectViewIssuesFilter,
|
issuesFilter: context.issue.projectViewIssuesFilter,
|
||||||
} as IStoreIssues[T];
|
}) as TStoreIssues[T];
|
||||||
case EIssuesStoreType.ARCHIVED:
|
case EIssuesStoreType.ARCHIVED:
|
||||||
return {
|
return merge(defaultStore, {
|
||||||
issueMap: context.issue.issues.issuesMap,
|
|
||||||
issues: context.issue.archivedIssues,
|
issues: context.issue.archivedIssues,
|
||||||
issuesFilter: context.issue.archivedIssuesFilter,
|
issuesFilter: context.issue.archivedIssuesFilter,
|
||||||
} as IStoreIssues[T];
|
}) as TStoreIssues[T];
|
||||||
case EIssuesStoreType.DRAFT:
|
case EIssuesStoreType.DRAFT:
|
||||||
return {
|
return merge(defaultStore, {
|
||||||
issueMap: context.issue.issues.issuesMap,
|
|
||||||
issues: context.issue.draftIssues,
|
issues: context.issue.draftIssues,
|
||||||
issuesFilter: context.issue.draftIssuesFilter,
|
issuesFilter: context.issue.draftIssuesFilter,
|
||||||
} as IStoreIssues[T];
|
}) as TStoreIssues[T];
|
||||||
default:
|
default:
|
||||||
return {
|
return merge(defaultStore, {
|
||||||
issueMap: context.issue.issues.issuesMap,
|
|
||||||
issues: undefined,
|
issues: undefined,
|
||||||
issuesFilter: undefined,
|
issuesFilter: undefined,
|
||||||
} as IStoreIssues[T];
|
}) as TStoreIssues[T];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -4,8 +4,8 @@ import { StoreContext } from "contexts/store-context";
|
|||||||
// types
|
// types
|
||||||
import { IPageStore } from "store/page.store";
|
import { IPageStore } from "store/page.store";
|
||||||
|
|
||||||
export const usePage = (): IPageStore => {
|
export const usePage = (): any => {
|
||||||
const context = useContext(StoreContext);
|
const context = useContext(StoreContext);
|
||||||
if (context === undefined) throw new Error("usePage must be used within StoreProvider");
|
if (context === undefined) throw new Error("usePage must be used within StoreProvider");
|
||||||
return context.page;
|
return context as any;
|
||||||
};
|
};
|
||||||
|
@ -1,91 +0,0 @@
|
|||||||
import useSWR from "swr";
|
|
||||||
|
|
||||||
// fetch keys
|
|
||||||
import { ISSUE_REACTION_LIST } from "constants/fetch-keys";
|
|
||||||
// helpers
|
|
||||||
import { groupReactions } from "helpers/emoji.helper";
|
|
||||||
// services
|
|
||||||
import { IssueReactionService } from "services/issue";
|
|
||||||
import { useUser } from "./store";
|
|
||||||
// hooks
|
|
||||||
|
|
||||||
const issueReactionService = new IssueReactionService();
|
|
||||||
|
|
||||||
const useIssueReaction = (
|
|
||||||
workspaceSlug?: string | string[] | null,
|
|
||||||
projectId?: string | string[] | null,
|
|
||||||
issueId?: string | string[] | null
|
|
||||||
) => {
|
|
||||||
const user = useUser();
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: reactions,
|
|
||||||
mutate: mutateReaction,
|
|
||||||
error,
|
|
||||||
} = useSWR(
|
|
||||||
workspaceSlug && projectId && issueId
|
|
||||||
? ISSUE_REACTION_LIST(workspaceSlug.toString(), projectId.toString(), issueId.toString())
|
|
||||||
: null,
|
|
||||||
workspaceSlug && projectId && issueId
|
|
||||||
? () =>
|
|
||||||
issueReactionService.listIssueReactions(workspaceSlug.toString(), projectId.toString(), issueId.toString())
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const groupedReactions = groupReactions(reactions || [], "reaction");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Use this function to create user's reaction to an issue. This function will mutate the reactions state.
|
|
||||||
* @param {string} reaction
|
|
||||||
* @example handleReactionCreate("128077") // hexa-code of the emoji
|
|
||||||
*/
|
|
||||||
|
|
||||||
const handleReactionCreate = async (reaction: string) => {
|
|
||||||
if (!workspaceSlug || !projectId || !issueId) return;
|
|
||||||
|
|
||||||
const data = await issueReactionService.createIssueReaction(
|
|
||||||
workspaceSlug.toString(),
|
|
||||||
projectId.toString(),
|
|
||||||
issueId.toString(),
|
|
||||||
{ reaction }
|
|
||||||
);
|
|
||||||
|
|
||||||
mutateReaction((prev: any) => [...(prev || []), data]);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Use this function to delete user's reaction from an issue. This function will mutate the reactions state.
|
|
||||||
* @param {string} reaction
|
|
||||||
* @example handleReactionDelete("123") // 123 -> is emoji hexa-code
|
|
||||||
*/
|
|
||||||
|
|
||||||
const handleReactionDelete = async (reaction: string) => {
|
|
||||||
if (!workspaceSlug || !projectId || !issueId) return;
|
|
||||||
|
|
||||||
mutateReaction(
|
|
||||||
(prevData: any) =>
|
|
||||||
prevData?.filter((r: any) => r.actor !== user?.currentUser?.id || r.reaction !== reaction) || [],
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
await issueReactionService.deleteIssueReaction(
|
|
||||||
workspaceSlug.toString(),
|
|
||||||
projectId.toString(),
|
|
||||||
issueId.toString(),
|
|
||||||
reaction
|
|
||||||
);
|
|
||||||
|
|
||||||
mutateReaction();
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
isLoading: !reactions && !error,
|
|
||||||
reactions,
|
|
||||||
groupedReactions,
|
|
||||||
handleReactionCreate,
|
|
||||||
handleReactionDelete,
|
|
||||||
mutateReaction,
|
|
||||||
} as const;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default useIssueReaction;
|
|
@ -9,7 +9,8 @@ import useToast from "hooks/use-toast";
|
|||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
// components
|
// components
|
||||||
import { IssueDetailsSidebar, IssueMainContent } from "components/issues";
|
// FIXME: have to replace this once the issue details page is ready --issue-detail--
|
||||||
|
// import { IssueDetailsSidebar, IssueMainContent } from "components/issues";
|
||||||
import { ProjectArchivedIssueDetailsHeader } from "components/headers";
|
import { ProjectArchivedIssueDetailsHeader } from "components/headers";
|
||||||
// ui
|
// ui
|
||||||
import { ArchiveIcon, Loader } from "@plane/ui";
|
import { ArchiveIcon, Loader } from "@plane/ui";
|
||||||
@ -158,11 +159,13 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = () => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="pointer-events-none space-y-5 divide-y-2 divide-custom-border-200 opacity-60">
|
{/* FIXME: have to replace this once the issue details page is ready --issue-detail-- */}
|
||||||
|
{/* <div className="pointer-events-none space-y-5 divide-y-2 divide-custom-border-200 opacity-60">
|
||||||
<IssueMainContent issueDetails={issueDetails} submitChanges={submitChanges} uneditable />
|
<IssueMainContent issueDetails={issueDetails} submitChanges={submitChanges} uneditable />
|
||||||
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/* FIXME: have to replace this once the issue details page is ready --issue-detail-- */}
|
||||||
<div className="h-full w-1/3 space-y-5 overflow-hidden border-l border-custom-border-300 p-5">
|
{/* <div className="h-full w-1/3 space-y-5 overflow-hidden border-l border-custom-border-300 p-5">
|
||||||
<IssueDetailsSidebar
|
<IssueDetailsSidebar
|
||||||
control={control}
|
control={control}
|
||||||
issueDetail={issueDetails}
|
issueDetail={issueDetails}
|
||||||
@ -170,7 +173,7 @@ const ArchivedIssueDetailsPage: NextPageWithLayout = () => {
|
|||||||
watch={watch}
|
watch={watch}
|
||||||
uneditable
|
uneditable
|
||||||
/>
|
/>
|
||||||
</div>
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Loader className="flex h-full gap-5 p-5">
|
<Loader className="flex h-full gap-5 p-5">
|
||||||
|
@ -1,143 +1,42 @@
|
|||||||
import React, { useCallback, useEffect, ReactElement } from "react";
|
import React, { ReactElement } from "react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR from "swr";
|
||||||
import { useForm } from "react-hook-form";
|
import { observer } from "mobx-react-lite";
|
||||||
// services
|
|
||||||
import { IssueService } from "services/issue";
|
|
||||||
// layouts
|
// layouts
|
||||||
import { AppLayout } from "layouts/app-layout";
|
import { AppLayout } from "layouts/app-layout";
|
||||||
// components
|
// components
|
||||||
import { ProjectIssueDetailsHeader } from "components/headers";
|
import { ProjectIssueDetailsHeader } from "components/headers";
|
||||||
import { IssueDetailsSidebar, IssueMainContent } from "components/issues";
|
import { IssueDetailRoot } from "components/issues";
|
||||||
// ui
|
// ui
|
||||||
import { EmptyState } from "components/common";
|
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
// images
|
|
||||||
import emptyIssue from "public/empty-state/issue.svg";
|
|
||||||
// types
|
// types
|
||||||
import { TIssue } from "@plane/types";
|
|
||||||
import { NextPageWithLayout } from "lib/types";
|
import { NextPageWithLayout } from "lib/types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { PROJECT_ISSUES_ACTIVITY, ISSUE_DETAILS } from "constants/fetch-keys";
|
|
||||||
import { observer } from "mobx-react-lite";
|
|
||||||
import { useIssueDetail } from "hooks/store";
|
import { useIssueDetail } from "hooks/store";
|
||||||
|
|
||||||
const defaultValues: Partial<TIssue> = {
|
|
||||||
// description: "",
|
|
||||||
description_html: "",
|
|
||||||
estimate_point: null,
|
|
||||||
cycle_id: null,
|
|
||||||
module_id: null,
|
|
||||||
name: "",
|
|
||||||
priority: "low",
|
|
||||||
start_date: undefined,
|
|
||||||
state_id: "",
|
|
||||||
target_date: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
// services
|
|
||||||
const issueService = new IssueService();
|
|
||||||
|
|
||||||
const IssueDetailsPage: NextPageWithLayout = observer(() => {
|
const IssueDetailsPage: NextPageWithLayout = observer(() => {
|
||||||
// router
|
// router
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, issueId: routeIssueId } = router.query;
|
const { workspaceSlug, projectId, issueId } = router.query;
|
||||||
|
// hooks
|
||||||
const { peekIssue, fetchIssue } = useIssueDetail();
|
|
||||||
useEffect(() => {
|
|
||||||
if (!workspaceSlug || !projectId || !routeIssueId) return;
|
|
||||||
fetchIssue(workspaceSlug as string, projectId as string, routeIssueId as string);
|
|
||||||
}, [workspaceSlug, projectId, routeIssueId, fetchIssue]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: issueDetails,
|
fetchIssue,
|
||||||
mutate: mutateIssueDetails,
|
issue: { getIssueById },
|
||||||
error,
|
} = useIssueDetail();
|
||||||
} = useSWR(
|
|
||||||
workspaceSlug && projectId && peekIssue?.issueId ? ISSUE_DETAILS(peekIssue?.issueId as string) : null,
|
const { isLoading } = useSWR(
|
||||||
workspaceSlug && projectId && peekIssue?.issueId
|
workspaceSlug && projectId && issueId ? `ISSUE_DETAIL_${workspaceSlug}_${projectId}_${issueId}` : null,
|
||||||
? () => issueService.retrieve(workspaceSlug as string, projectId as string, peekIssue?.issueId as string)
|
workspaceSlug && projectId && issueId
|
||||||
|
? () => fetchIssue(workspaceSlug.toString(), projectId.toString(), issueId.toString())
|
||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
const { reset, control, watch } = useForm<TIssue>({
|
const issue = getIssueById(issueId?.toString() || "") || undefined;
|
||||||
defaultValues,
|
const issueLoader = !issue || isLoading ? true : false;
|
||||||
});
|
|
||||||
|
|
||||||
const submitChanges = useCallback(
|
|
||||||
async (formData: Partial<TIssue>) => {
|
|
||||||
if (!workspaceSlug || !projectId || !peekIssue?.issueId) return;
|
|
||||||
|
|
||||||
mutate<TIssue>(
|
|
||||||
ISSUE_DETAILS(peekIssue?.issueId as string),
|
|
||||||
(prevData) => {
|
|
||||||
if (!prevData) return prevData;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...prevData,
|
|
||||||
...formData,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
|
|
||||||
const payload: Partial<TIssue> = {
|
|
||||||
...formData,
|
|
||||||
};
|
|
||||||
|
|
||||||
// delete payload.related_issues;
|
|
||||||
// delete payload.issue_relations;
|
|
||||||
|
|
||||||
await issueService
|
|
||||||
.patchIssue(workspaceSlug as string, projectId as string, peekIssue?.issueId as string, payload)
|
|
||||||
.then(() => {
|
|
||||||
mutateIssueDetails();
|
|
||||||
mutate(PROJECT_ISSUES_ACTIVITY(peekIssue?.issueId as string));
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[workspaceSlug, peekIssue?.issueId, projectId, mutateIssueDetails]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!issueDetails) return;
|
|
||||||
|
|
||||||
mutate(PROJECT_ISSUES_ACTIVITY(peekIssue?.issueId as string));
|
|
||||||
reset({
|
|
||||||
...issueDetails,
|
|
||||||
});
|
|
||||||
}, [issueDetails, reset, peekIssue?.issueId]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{" "}
|
{issueLoader ? (
|
||||||
{error ? (
|
|
||||||
<EmptyState
|
|
||||||
image={emptyIssue}
|
|
||||||
title="Issue does not exist"
|
|
||||||
description="The issue you are looking for does not exist, has been archived, or has been deleted."
|
|
||||||
primaryButton={{
|
|
||||||
text: "View other issues",
|
|
||||||
onClick: () => router.push(`/${workspaceSlug}/projects/${projectId}/issues`),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : issueDetails && projectId && peekIssue?.issueId ? (
|
|
||||||
<div className="flex h-full overflow-hidden">
|
|
||||||
<div className="h-full w-2/3 space-y-5 divide-y-2 divide-custom-border-300 overflow-y-auto p-5">
|
|
||||||
<IssueMainContent issueDetails={issueDetails} submitChanges={submitChanges} />
|
|
||||||
</div>
|
|
||||||
<div className="h-full w-1/3 space-y-5 overflow-hidden border-l border-custom-border-300 py-5">
|
|
||||||
<IssueDetailsSidebar
|
|
||||||
control={control}
|
|
||||||
issueDetail={issueDetails}
|
|
||||||
submitChanges={submitChanges}
|
|
||||||
watch={watch}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Loader className="flex h-full gap-5 p-5">
|
<Loader className="flex h-full gap-5 p-5">
|
||||||
<div className="basis-2/3 space-y-2">
|
<div className="basis-2/3 space-y-2">
|
||||||
<Loader.Item height="30px" width="40%" />
|
<Loader.Item height="30px" width="40%" />
|
||||||
@ -152,6 +51,16 @@ const IssueDetailsPage: NextPageWithLayout = observer(() => {
|
|||||||
<Loader.Item height="30px" />
|
<Loader.Item height="30px" />
|
||||||
</div>
|
</div>
|
||||||
</Loader>
|
</Loader>
|
||||||
|
) : (
|
||||||
|
workspaceSlug &&
|
||||||
|
projectId &&
|
||||||
|
issueId && (
|
||||||
|
<IssueDetailRoot
|
||||||
|
workspaceSlug={workspaceSlug.toString()}
|
||||||
|
projectId={projectId.toString()}
|
||||||
|
issueId={issueId.toString()}
|
||||||
|
/>
|
||||||
|
)
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -38,6 +38,7 @@ const ModuleIssuesPage: NextPageWithLayout = () => {
|
|||||||
setValue(`${!isSidebarCollapsed}`);
|
setValue(`${!isSidebarCollapsed}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!workspaceSlug || !projectId || !moduleId) return <></>;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{error ? (
|
{error ? (
|
||||||
|
@ -49,8 +49,10 @@ export class IssueService extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async retrieve(workspaceSlug: string, projectId: string, issueId: string): Promise<TIssue> {
|
async retrieve(workspaceSlug: string, projectId: string, issueId: string, queries?: any): Promise<TIssue> {
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/`)
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/`, {
|
||||||
|
params: queries,
|
||||||
|
})
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
|
@ -29,7 +29,7 @@ export class IssueFiltersService extends APIService {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
// project issue filters
|
// project issue filters
|
||||||
async fetchProjectIssueFilters(workspaceSlug: string, projectId: string): Promise<any> {
|
async fetchProjectIssueFilters(workspaceSlug: string, projectId: string): Promise<IIssueFiltersResponse> {
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-properties/`)
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/user-properties/`)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -49,7 +49,11 @@ export class IssueFiltersService extends APIService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// cycle issue filters
|
// cycle issue filters
|
||||||
async fetchCycleIssueFilters(workspaceSlug: string, projectId: string, cycleId: string): Promise<any> {
|
async fetchCycleIssueFilters(
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
cycleId: string
|
||||||
|
): Promise<IIssueFiltersResponse> {
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/user-properties/`)
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/cycles/${cycleId}/user-properties/`)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@ -70,7 +74,11 @@ export class IssueFiltersService extends APIService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// module issue filters
|
// module issue filters
|
||||||
async fetchModuleIssueFilters(workspaceSlug: string, projectId: string, moduleId: string): Promise<any> {
|
async fetchModuleIssueFilters(
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
moduleId: string
|
||||||
|
): Promise<IIssueFiltersResponse> {
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/user-properties/`)
|
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/modules/${moduleId}/user-properties/`)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { computed, makeObservable, observable, runInAction } from "mobx";
|
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||||
import isEmpty from "lodash/isEmpty";
|
import isEmpty from "lodash/isEmpty";
|
||||||
import set from "lodash/set";
|
import set from "lodash/set";
|
||||||
// base class
|
// base class
|
||||||
@ -11,6 +11,7 @@ import {
|
|||||||
IIssueFilterOptions,
|
IIssueFilterOptions,
|
||||||
IIssueDisplayFilterOptions,
|
IIssueDisplayFilterOptions,
|
||||||
IIssueDisplayProperties,
|
IIssueDisplayProperties,
|
||||||
|
TIssueKanbanFilters,
|
||||||
IIssueFilters,
|
IIssueFilters,
|
||||||
TIssueParams,
|
TIssueParams,
|
||||||
} from "@plane/types";
|
} from "@plane/types";
|
||||||
@ -31,7 +32,7 @@ export interface IArchivedIssuesFilter {
|
|||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
filterType: EIssueFilterType,
|
filterType: EIssueFilterType,
|
||||||
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties
|
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,6 +52,9 @@ export class ArchivedIssuesFilter extends IssueFilterHelperStore implements IArc
|
|||||||
// computed
|
// computed
|
||||||
issueFilters: computed,
|
issueFilters: computed,
|
||||||
appliedFilters: computed,
|
appliedFilters: computed,
|
||||||
|
// actions
|
||||||
|
fetchFilters: action,
|
||||||
|
updateFilters: action,
|
||||||
});
|
});
|
||||||
// root store
|
// root store
|
||||||
this.rootIssueStore = _rootStore;
|
this.rootIssueStore = _rootStore;
|
||||||
@ -100,22 +104,18 @@ export class ArchivedIssuesFilter extends IssueFilterHelperStore implements IArc
|
|||||||
const filters: IIssueFilterOptions = this.computedFilters(_filters?.filters);
|
const filters: IIssueFilterOptions = this.computedFilters(_filters?.filters);
|
||||||
const displayFilters: IIssueDisplayFilterOptions = this.computedDisplayFilters(_filters?.display_filters);
|
const displayFilters: IIssueDisplayFilterOptions = this.computedDisplayFilters(_filters?.display_filters);
|
||||||
const displayProperties: IIssueDisplayProperties = this.computedDisplayProperties(_filters?.display_properties);
|
const displayProperties: IIssueDisplayProperties = this.computedDisplayProperties(_filters?.display_properties);
|
||||||
|
const kanbanFilters = {
|
||||||
this.handleIssuesLocalFilters.set(
|
group_by: [],
|
||||||
EIssuesStoreType.ARCHIVED,
|
sub_group_by: [],
|
||||||
EIssueFilterType.FILTERS,
|
};
|
||||||
workspaceSlug,
|
kanbanFilters.group_by = _filters?.kanban_filters?.group_by || [];
|
||||||
projectId,
|
kanbanFilters.sub_group_by = _filters?.kanban_filters?.sub_group_by || [];
|
||||||
undefined,
|
|
||||||
{
|
|
||||||
filters: filters,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
set(this.filters, [projectId, "filters"], filters);
|
set(this.filters, [projectId, "filters"], filters);
|
||||||
set(this.filters, [projectId, "displayFilters"], displayFilters);
|
set(this.filters, [projectId, "displayFilters"], displayFilters);
|
||||||
set(this.filters, [projectId, "displayProperties"], displayProperties);
|
set(this.filters, [projectId, "displayProperties"], displayProperties);
|
||||||
|
set(this.filters, [projectId, "kanbanFilters"], kanbanFilters);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
@ -126,7 +126,7 @@ export class ArchivedIssuesFilter extends IssueFilterHelperStore implements IArc
|
|||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
type: EIssueFilterType,
|
type: EIssueFilterType,
|
||||||
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties
|
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
if (isEmpty(this.filters) || isEmpty(this.filters[projectId]) || isEmpty(filters)) return;
|
if (isEmpty(this.filters) || isEmpty(this.filters[projectId]) || isEmpty(filters)) return;
|
||||||
@ -135,6 +135,7 @@ export class ArchivedIssuesFilter extends IssueFilterHelperStore implements IArc
|
|||||||
filters: this.filters[projectId].filters as IIssueFilterOptions,
|
filters: this.filters[projectId].filters as IIssueFilterOptions,
|
||||||
displayFilters: this.filters[projectId].displayFilters as IIssueDisplayFilterOptions,
|
displayFilters: this.filters[projectId].displayFilters as IIssueDisplayFilterOptions,
|
||||||
displayProperties: this.filters[projectId].displayProperties as IIssueDisplayProperties,
|
displayProperties: this.filters[projectId].displayProperties as IIssueDisplayProperties,
|
||||||
|
kanbanFilters: this.filters[projectId].kanbanFilters as TIssueKanbanFilters,
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@ -148,7 +149,7 @@ export class ArchivedIssuesFilter extends IssueFilterHelperStore implements IArc
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.rootIssueStore.projectIssues.fetchIssues(workspaceSlug, projectId, "mutation");
|
this.rootIssueStore.archivedIssues.fetchIssues(workspaceSlug, projectId, "mutation");
|
||||||
this.handleIssuesLocalFilters.set(EIssuesStoreType.ARCHIVED, type, workspaceSlug, projectId, undefined, {
|
this.handleIssuesLocalFilters.set(EIssuesStoreType.ARCHIVED, type, workspaceSlug, projectId, undefined, {
|
||||||
filters: _filters.filters,
|
filters: _filters.filters,
|
||||||
});
|
});
|
||||||
@ -209,6 +210,28 @@ export class ArchivedIssuesFilter extends IssueFilterHelperStore implements IArc
|
|||||||
display_properties: _filters.displayProperties,
|
display_properties: _filters.displayProperties,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case EIssueFilterType.KANBAN_FILTERS:
|
||||||
|
const updatedKanbanFilters = filters as TIssueKanbanFilters;
|
||||||
|
_filters.kanbanFilters = { ..._filters.kanbanFilters, ...updatedKanbanFilters };
|
||||||
|
|
||||||
|
const currentUserId = this.rootIssueStore.currentUserId;
|
||||||
|
if (currentUserId)
|
||||||
|
this.handleIssuesLocalFilters.set(EIssuesStoreType.PROJECT, type, workspaceSlug, projectId, undefined, {
|
||||||
|
kanban_filters: _filters.kanbanFilters,
|
||||||
|
});
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
Object.keys(updatedKanbanFilters).forEach((_key) => {
|
||||||
|
set(
|
||||||
|
this.filters,
|
||||||
|
[projectId, "kanbanFilters", _key],
|
||||||
|
updatedKanbanFilters[_key as keyof TIssueKanbanFilters]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { computed, makeObservable, observable, runInAction } from "mobx";
|
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||||
import isEmpty from "lodash/isEmpty";
|
import isEmpty from "lodash/isEmpty";
|
||||||
import set from "lodash/set";
|
import set from "lodash/set";
|
||||||
// base class
|
// base class
|
||||||
@ -11,11 +11,12 @@ import {
|
|||||||
IIssueFilterOptions,
|
IIssueFilterOptions,
|
||||||
IIssueDisplayFilterOptions,
|
IIssueDisplayFilterOptions,
|
||||||
IIssueDisplayProperties,
|
IIssueDisplayProperties,
|
||||||
|
TIssueKanbanFilters,
|
||||||
IIssueFilters,
|
IIssueFilters,
|
||||||
TIssueParams,
|
TIssueParams,
|
||||||
} from "@plane/types";
|
} from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
import { EIssueFilterType } from "constants/issue";
|
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
||||||
// services
|
// services
|
||||||
import { IssueFiltersService } from "services/issue_filter.service";
|
import { IssueFiltersService } from "services/issue_filter.service";
|
||||||
|
|
||||||
@ -31,7 +32,7 @@ export interface ICycleIssuesFilter {
|
|||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
filterType: EIssueFilterType,
|
filterType: EIssueFilterType,
|
||||||
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties,
|
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters,
|
||||||
cycleId?: string | undefined
|
cycleId?: string | undefined
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
}
|
}
|
||||||
@ -52,6 +53,9 @@ export class CycleIssuesFilter extends IssueFilterHelperStore implements ICycleI
|
|||||||
// computed
|
// computed
|
||||||
issueFilters: computed,
|
issueFilters: computed,
|
||||||
appliedFilters: computed,
|
appliedFilters: computed,
|
||||||
|
// actions
|
||||||
|
fetchFilters: action,
|
||||||
|
updateFilters: action,
|
||||||
});
|
});
|
||||||
// root store
|
// root store
|
||||||
this.rootIssueStore = _rootStore;
|
this.rootIssueStore = _rootStore;
|
||||||
@ -97,10 +101,28 @@ export class CycleIssuesFilter extends IssueFilterHelperStore implements ICycleI
|
|||||||
const displayFilters: IIssueDisplayFilterOptions = this.computedDisplayFilters(_filters?.display_filters);
|
const displayFilters: IIssueDisplayFilterOptions = this.computedDisplayFilters(_filters?.display_filters);
|
||||||
const displayProperties: IIssueDisplayProperties = this.computedDisplayProperties(_filters?.display_properties);
|
const displayProperties: IIssueDisplayProperties = this.computedDisplayProperties(_filters?.display_properties);
|
||||||
|
|
||||||
|
// fetching the kanban toggle helpers in the local storage
|
||||||
|
const kanbanFilters = {
|
||||||
|
group_by: [],
|
||||||
|
sub_group_by: [],
|
||||||
|
};
|
||||||
|
const currentUserId = this.rootIssueStore.currentUserId;
|
||||||
|
if (currentUserId) {
|
||||||
|
const _kanbanFilters = this.handleIssuesLocalFilters.get(
|
||||||
|
EIssuesStoreType.CYCLE,
|
||||||
|
workspaceSlug,
|
||||||
|
cycleId,
|
||||||
|
currentUserId
|
||||||
|
);
|
||||||
|
kanbanFilters.group_by = _kanbanFilters?.kanban_filters?.group_by || [];
|
||||||
|
kanbanFilters.sub_group_by = _kanbanFilters?.kanban_filters?.sub_group_by || [];
|
||||||
|
}
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
set(this.filters, [cycleId, "filters"], filters);
|
set(this.filters, [cycleId, "filters"], filters);
|
||||||
set(this.filters, [cycleId, "displayFilters"], displayFilters);
|
set(this.filters, [cycleId, "displayFilters"], displayFilters);
|
||||||
set(this.filters, [cycleId, "displayProperties"], displayProperties);
|
set(this.filters, [cycleId, "displayProperties"], displayProperties);
|
||||||
|
set(this.filters, [cycleId, "kanbanFilters"], kanbanFilters);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
@ -111,7 +133,7 @@ export class CycleIssuesFilter extends IssueFilterHelperStore implements ICycleI
|
|||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
type: EIssueFilterType,
|
type: EIssueFilterType,
|
||||||
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties,
|
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters,
|
||||||
cycleId: string | undefined = undefined
|
cycleId: string | undefined = undefined
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
@ -119,9 +141,10 @@ export class CycleIssuesFilter extends IssueFilterHelperStore implements ICycleI
|
|||||||
if (isEmpty(this.filters) || isEmpty(this.filters[projectId]) || isEmpty(filters)) return;
|
if (isEmpty(this.filters) || isEmpty(this.filters[projectId]) || isEmpty(filters)) return;
|
||||||
|
|
||||||
const _filters = {
|
const _filters = {
|
||||||
filters: this.filters[projectId].filters as IIssueFilterOptions,
|
filters: this.filters[cycleId].filters as IIssueFilterOptions,
|
||||||
displayFilters: this.filters[projectId].displayFilters as IIssueDisplayFilterOptions,
|
displayFilters: this.filters[cycleId].displayFilters as IIssueDisplayFilterOptions,
|
||||||
displayProperties: this.filters[projectId].displayProperties as IIssueDisplayProperties,
|
displayProperties: this.filters[cycleId].displayProperties as IIssueDisplayProperties,
|
||||||
|
kanbanFilters: this.filters[cycleId].kanbanFilters as TIssueKanbanFilters,
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@ -135,7 +158,7 @@ export class CycleIssuesFilter extends IssueFilterHelperStore implements ICycleI
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.rootIssueStore.projectIssues.fetchIssues(workspaceSlug, projectId, "mutation");
|
this.rootIssueStore.cycleIssues.fetchIssues(workspaceSlug, projectId, "mutation", cycleId);
|
||||||
await this.issueFilterService.patchCycleIssueFilters(workspaceSlug, projectId, cycleId, {
|
await this.issueFilterService.patchCycleIssueFilters(workspaceSlug, projectId, cycleId, {
|
||||||
filters: _filters.filters,
|
filters: _filters.filters,
|
||||||
});
|
});
|
||||||
@ -196,6 +219,28 @@ export class CycleIssuesFilter extends IssueFilterHelperStore implements ICycleI
|
|||||||
display_properties: _filters.displayProperties,
|
display_properties: _filters.displayProperties,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case EIssueFilterType.KANBAN_FILTERS:
|
||||||
|
const updatedKanbanFilters = filters as TIssueKanbanFilters;
|
||||||
|
_filters.kanbanFilters = { ..._filters.kanbanFilters, ...updatedKanbanFilters };
|
||||||
|
|
||||||
|
const currentUserId = this.rootIssueStore.currentUserId;
|
||||||
|
if (currentUserId)
|
||||||
|
this.handleIssuesLocalFilters.set(EIssuesStoreType.PROJECT, type, workspaceSlug, cycleId, currentUserId, {
|
||||||
|
kanban_filters: _filters.kanbanFilters,
|
||||||
|
});
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
Object.keys(updatedKanbanFilters).forEach((_key) => {
|
||||||
|
set(
|
||||||
|
this.filters,
|
||||||
|
[cycleId, "kanbanFilters", _key],
|
||||||
|
updatedKanbanFilters[_key as keyof TIssueKanbanFilters]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import { action, observable, makeObservable, computed, runInAction } from "mobx";
|
import { action, observable, makeObservable, computed, runInAction } from "mobx";
|
||||||
import set from "lodash/set";
|
import set from "lodash/set";
|
||||||
|
import update from "lodash/update";
|
||||||
|
import concat from "lodash/concat";
|
||||||
|
import pull from "lodash/pull";
|
||||||
// base class
|
// base class
|
||||||
import { IssueHelperStore } from "../helpers/issue-helper.store";
|
import { IssueHelperStore } from "../helpers/issue-helper.store";
|
||||||
// services
|
// services
|
||||||
@ -259,13 +262,17 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues {
|
|||||||
addIssueToCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => {
|
addIssueToCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => {
|
||||||
try {
|
try {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.issues[cycleId].push(...issueIds);
|
update(this.issues, cycleId, (cycleIssueIds) => {
|
||||||
|
if (!cycleIssueIds) return issueIds;
|
||||||
|
else return concat(cycleIssueIds, issueIds);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
issueIds.map((issueId) => this.rootStore.issues.updateIssue(issueId, { cycle_id: cycleId }));
|
||||||
|
|
||||||
const issueToCycle = await this.issueService.addIssueToCycle(workspaceSlug, projectId, cycleId, {
|
const issueToCycle = await this.issueService.addIssueToCycle(workspaceSlug, projectId, cycleId, {
|
||||||
issues: issueIds,
|
issues: issueIds,
|
||||||
});
|
});
|
||||||
|
|
||||||
return issueToCycle;
|
return issueToCycle;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
@ -274,14 +281,14 @@ export class CycleIssues extends IssueHelperStore implements ICycleIssues {
|
|||||||
|
|
||||||
removeIssueFromCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => {
|
removeIssueFromCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueId: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await this.issueService.removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId);
|
|
||||||
|
|
||||||
const issueIndex = this.issues[cycleId].findIndex((_issueId) => _issueId === issueId);
|
|
||||||
if (issueIndex >= 0)
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.issues[cycleId].splice(issueIndex, 1);
|
pull(this.issues[cycleId], issueId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.rootStore.issues.updateIssue(issueId, { cycle_id: null });
|
||||||
|
|
||||||
|
const response = await this.issueService.removeIssueFromCycle(workspaceSlug, projectId, cycleId, issueId);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { computed, makeObservable, observable, runInAction } from "mobx";
|
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||||
import isEmpty from "lodash/isEmpty";
|
import isEmpty from "lodash/isEmpty";
|
||||||
import set from "lodash/set";
|
import set from "lodash/set";
|
||||||
// base class
|
// base class
|
||||||
@ -11,6 +11,7 @@ import {
|
|||||||
IIssueFilterOptions,
|
IIssueFilterOptions,
|
||||||
IIssueDisplayFilterOptions,
|
IIssueDisplayFilterOptions,
|
||||||
IIssueDisplayProperties,
|
IIssueDisplayProperties,
|
||||||
|
TIssueKanbanFilters,
|
||||||
IIssueFilters,
|
IIssueFilters,
|
||||||
TIssueParams,
|
TIssueParams,
|
||||||
} from "@plane/types";
|
} from "@plane/types";
|
||||||
@ -31,7 +32,7 @@ export interface IDraftIssuesFilter {
|
|||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
filterType: EIssueFilterType,
|
filterType: EIssueFilterType,
|
||||||
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties
|
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,6 +52,9 @@ export class DraftIssuesFilter extends IssueFilterHelperStore implements IDraftI
|
|||||||
// computed
|
// computed
|
||||||
issueFilters: computed,
|
issueFilters: computed,
|
||||||
appliedFilters: computed,
|
appliedFilters: computed,
|
||||||
|
// actions
|
||||||
|
fetchFilters: action,
|
||||||
|
updateFilters: action,
|
||||||
});
|
});
|
||||||
// root store
|
// root store
|
||||||
this.rootIssueStore = _rootStore;
|
this.rootIssueStore = _rootStore;
|
||||||
@ -95,11 +99,18 @@ export class DraftIssuesFilter extends IssueFilterHelperStore implements IDraftI
|
|||||||
const filters: IIssueFilterOptions = this.computedFilters(_filters?.filters);
|
const filters: IIssueFilterOptions = this.computedFilters(_filters?.filters);
|
||||||
const displayFilters: IIssueDisplayFilterOptions = this.computedDisplayFilters(_filters?.display_filters);
|
const displayFilters: IIssueDisplayFilterOptions = this.computedDisplayFilters(_filters?.display_filters);
|
||||||
const displayProperties: IIssueDisplayProperties = this.computedDisplayProperties(_filters?.display_properties);
|
const displayProperties: IIssueDisplayProperties = this.computedDisplayProperties(_filters?.display_properties);
|
||||||
|
const kanbanFilters = {
|
||||||
|
group_by: [],
|
||||||
|
sub_group_by: [],
|
||||||
|
};
|
||||||
|
kanbanFilters.group_by = _filters?.kanban_filters?.group_by || [];
|
||||||
|
kanbanFilters.sub_group_by = _filters?.kanban_filters?.sub_group_by || [];
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
set(this.filters, [projectId, "filters"], filters);
|
set(this.filters, [projectId, "filters"], filters);
|
||||||
set(this.filters, [projectId, "displayFilters"], displayFilters);
|
set(this.filters, [projectId, "displayFilters"], displayFilters);
|
||||||
set(this.filters, [projectId, "displayProperties"], displayProperties);
|
set(this.filters, [projectId, "displayProperties"], displayProperties);
|
||||||
|
set(this.filters, [projectId, "kanbanFilters"], kanbanFilters);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
@ -110,7 +121,7 @@ export class DraftIssuesFilter extends IssueFilterHelperStore implements IDraftI
|
|||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string,
|
projectId: string,
|
||||||
type: EIssueFilterType,
|
type: EIssueFilterType,
|
||||||
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties
|
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
if (isEmpty(this.filters) || isEmpty(this.filters[projectId]) || isEmpty(filters)) return;
|
if (isEmpty(this.filters) || isEmpty(this.filters[projectId]) || isEmpty(filters)) return;
|
||||||
@ -119,6 +130,7 @@ export class DraftIssuesFilter extends IssueFilterHelperStore implements IDraftI
|
|||||||
filters: this.filters[projectId].filters as IIssueFilterOptions,
|
filters: this.filters[projectId].filters as IIssueFilterOptions,
|
||||||
displayFilters: this.filters[projectId].displayFilters as IIssueDisplayFilterOptions,
|
displayFilters: this.filters[projectId].displayFilters as IIssueDisplayFilterOptions,
|
||||||
displayProperties: this.filters[projectId].displayProperties as IIssueDisplayProperties,
|
displayProperties: this.filters[projectId].displayProperties as IIssueDisplayProperties,
|
||||||
|
kanbanFilters: this.filters[projectId].kanbanFilters as TIssueKanbanFilters,
|
||||||
};
|
};
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
@ -132,7 +144,7 @@ export class DraftIssuesFilter extends IssueFilterHelperStore implements IDraftI
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.rootIssueStore.projectIssues.fetchIssues(workspaceSlug, projectId, "mutation");
|
this.rootIssueStore.draftIssues.fetchIssues(workspaceSlug, projectId, "mutation");
|
||||||
this.handleIssuesLocalFilters.set(EIssuesStoreType.DRAFT, type, workspaceSlug, projectId, undefined, {
|
this.handleIssuesLocalFilters.set(EIssuesStoreType.DRAFT, type, workspaceSlug, projectId, undefined, {
|
||||||
filters: _filters.filters,
|
filters: _filters.filters,
|
||||||
});
|
});
|
||||||
@ -193,6 +205,28 @@ export class DraftIssuesFilter extends IssueFilterHelperStore implements IDraftI
|
|||||||
display_properties: _filters.displayProperties,
|
display_properties: _filters.displayProperties,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case EIssueFilterType.KANBAN_FILTERS:
|
||||||
|
const updatedKanbanFilters = filters as TIssueKanbanFilters;
|
||||||
|
_filters.kanbanFilters = { ..._filters.kanbanFilters, ...updatedKanbanFilters };
|
||||||
|
|
||||||
|
const currentUserId = this.rootIssueStore.currentUserId;
|
||||||
|
if (currentUserId)
|
||||||
|
this.handleIssuesLocalFilters.set(EIssuesStoreType.PROJECT, type, workspaceSlug, projectId, undefined, {
|
||||||
|
kanban_filters: _filters.kanbanFilters,
|
||||||
|
});
|
||||||
|
|
||||||
|
runInAction(() => {
|
||||||
|
Object.keys(updatedKanbanFilters).forEach((_key) => {
|
||||||
|
set(
|
||||||
|
this.filters,
|
||||||
|
[projectId, "kanbanFilters", _key],
|
||||||
|
updatedKanbanFilters[_key as keyof TIssueKanbanFilters]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
IIssueFilterOptions,
|
IIssueFilterOptions,
|
||||||
IIssueFilters,
|
IIssueFilters,
|
||||||
IIssueFiltersResponse,
|
IIssueFiltersResponse,
|
||||||
|
TIssueKanbanFilters,
|
||||||
TIssueParams,
|
TIssueParams,
|
||||||
} from "@plane/types";
|
} from "@plane/types";
|
||||||
// constants
|
// constants
|
||||||
@ -17,7 +18,7 @@ import { storage } from "lib/local-storage";
|
|||||||
interface ILocalStoreIssueFilters {
|
interface ILocalStoreIssueFilters {
|
||||||
key: EIssuesStoreType;
|
key: EIssuesStoreType;
|
||||||
workspaceSlug: string;
|
workspaceSlug: string;
|
||||||
projectId: string | undefined;
|
viewId: string | undefined; // It can be projectId, moduleId, cycleId, projectViewId
|
||||||
userId: string | undefined;
|
userId: string | undefined;
|
||||||
filters: IIssueFilters;
|
filters: IIssueFilters;
|
||||||
}
|
}
|
||||||
@ -46,6 +47,7 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore {
|
|||||||
filters: isEmpty(filters?.filters) ? undefined : filters?.filters,
|
filters: isEmpty(filters?.filters) ? undefined : filters?.filters,
|
||||||
displayFilters: isEmpty(filters?.displayFilters) ? undefined : filters?.displayFilters,
|
displayFilters: isEmpty(filters?.displayFilters) ? undefined : filters?.displayFilters,
|
||||||
displayProperties: isEmpty(filters?.displayProperties) ? undefined : filters?.displayProperties,
|
displayProperties: isEmpty(filters?.displayProperties) ? undefined : filters?.displayProperties,
|
||||||
|
kanbanFilters: isEmpty(filters?.kanbanFilters) ? undefined : filters?.kanbanFilters,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -157,7 +159,7 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore {
|
|||||||
get: (
|
get: (
|
||||||
currentView: EIssuesStoreType,
|
currentView: EIssuesStoreType,
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string | undefined,
|
viewId: string | undefined, // It can be projectId, moduleId, cycleId, projectViewId
|
||||||
userId: string | undefined
|
userId: string | undefined
|
||||||
) => {
|
) => {
|
||||||
const storageFilters = this.handleIssuesLocalFilters.fetchFiltersFromStorage();
|
const storageFilters = this.handleIssuesLocalFilters.fetchFiltersFromStorage();
|
||||||
@ -165,28 +167,28 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore {
|
|||||||
(filter: ILocalStoreIssueFilters) =>
|
(filter: ILocalStoreIssueFilters) =>
|
||||||
filter.key === currentView &&
|
filter.key === currentView &&
|
||||||
filter.workspaceSlug === workspaceSlug &&
|
filter.workspaceSlug === workspaceSlug &&
|
||||||
filter.projectId === projectId &&
|
filter.viewId === viewId &&
|
||||||
filter.userId === userId
|
filter.userId === userId
|
||||||
);
|
);
|
||||||
if (!currentFilterIndex && currentFilterIndex.length < 0) return undefined;
|
if (!currentFilterIndex && currentFilterIndex.length < 0) return undefined;
|
||||||
|
|
||||||
return storageFilters[currentFilterIndex];
|
return storageFilters[currentFilterIndex]?.filters || {};
|
||||||
},
|
},
|
||||||
|
|
||||||
set: (
|
set: (
|
||||||
currentView: EIssuesStoreType,
|
currentView: EIssuesStoreType,
|
||||||
filterType: EIssueFilterType,
|
filterType: EIssueFilterType,
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
projectId: string | undefined,
|
viewId: string | undefined, // It can be projectId, moduleId, cycleId, projectViewId
|
||||||
userId: string | undefined,
|
userId: string | undefined,
|
||||||
filters: Partial<IIssueFiltersResponse>
|
filters: Partial<IIssueFiltersResponse & { kanban_filters: TIssueKanbanFilters }>
|
||||||
) => {
|
) => {
|
||||||
const storageFilters = this.handleIssuesLocalFilters.fetchFiltersFromStorage();
|
const storageFilters = this.handleIssuesLocalFilters.fetchFiltersFromStorage();
|
||||||
const currentFilterIndex = storageFilters.findIndex(
|
const currentFilterIndex = storageFilters.findIndex(
|
||||||
(filter: ILocalStoreIssueFilters) =>
|
(filter: ILocalStoreIssueFilters) =>
|
||||||
filter.key === currentView &&
|
filter.key === currentView &&
|
||||||
filter.workspaceSlug === workspaceSlug &&
|
filter.workspaceSlug === workspaceSlug &&
|
||||||
filter.projectId === projectId &&
|
filter.viewId === viewId &&
|
||||||
filter.userId === userId
|
filter.userId === userId
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -194,14 +196,17 @@ export class IssueFilterHelperStore implements IIssueFilterHelperStore {
|
|||||||
storageFilters.push({
|
storageFilters.push({
|
||||||
key: currentView,
|
key: currentView,
|
||||||
workspaceSlug: workspaceSlug,
|
workspaceSlug: workspaceSlug,
|
||||||
projectId: projectId,
|
viewId: viewId,
|
||||||
userId: userId,
|
userId: userId,
|
||||||
filters: filters,
|
filters: filters,
|
||||||
});
|
});
|
||||||
else
|
else
|
||||||
storageFilters[currentFilterIndex] = {
|
storageFilters[currentFilterIndex] = {
|
||||||
...storageFilters[currentFilterIndex],
|
...storageFilters[currentFilterIndex],
|
||||||
[filterType]: filters,
|
filters: {
|
||||||
|
...storageFilters[currentFilterIndex].filters,
|
||||||
|
[filterType]: filters[filterType],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
storage.set("issue_local_filters", JSON.stringify(storageFilters));
|
storage.set("issue_local_filters", JSON.stringify(storageFilters));
|
||||||
|
@ -49,11 +49,18 @@ export class IssueStore implements IIssueStore {
|
|||||||
// actions
|
// actions
|
||||||
fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
|
||||||
try {
|
try {
|
||||||
const issue = await this.issueService.retrieve(workspaceSlug, projectId, issueId);
|
const query = {
|
||||||
|
expand: "state,assignees,labels,parent",
|
||||||
|
};
|
||||||
|
const issue = (await this.issueService.retrieve(workspaceSlug, projectId, issueId, query)) as any;
|
||||||
if (!issue) throw new Error("Issue not found");
|
if (!issue) throw new Error("Issue not found");
|
||||||
|
|
||||||
this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issue]);
|
this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issue]);
|
||||||
|
|
||||||
|
// store handlers from issue detail
|
||||||
|
if (issue && issue?.parent && issue?.parent?.id)
|
||||||
|
this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issue?.parent]);
|
||||||
|
|
||||||
// issue reactions
|
// issue reactions
|
||||||
this.rootIssueDetailStore.reaction.fetchReactions(workspaceSlug, projectId, issueId);
|
this.rootIssueDetailStore.reaction.fetchReactions(workspaceSlug, projectId, issueId);
|
||||||
|
|
||||||
|
@ -134,10 +134,10 @@ export class IssueLinkStore implements IIssueLinkStore {
|
|||||||
try {
|
try {
|
||||||
const response = await this.issueService.deleteIssueLink(workspaceSlug, projectId, issueId, linkId);
|
const response = await this.issueService.deleteIssueLink(workspaceSlug, projectId, issueId, linkId);
|
||||||
|
|
||||||
const reactionIndex = this.links[issueId].findIndex((_comment) => _comment === linkId);
|
const linkIndex = this.links[issueId].findIndex((_comment) => _comment === linkId);
|
||||||
if (reactionIndex >= 0)
|
if (linkIndex >= 0)
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.links[issueId].splice(reactionIndex, 1);
|
this.links[issueId].splice(linkIndex, 1);
|
||||||
delete this.linkMap[linkId];
|
delete this.linkMap[linkId];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user