mirror of
https://github.com/makeplane/plane.git
synced 2026-07-12 13:29:56 +02:00
Merge pull request #605 from makeplane/sync/ce-ee
sync: community changes
This commit is contained in:
@@ -481,7 +481,38 @@ class IssueViewSet(BaseViewSet):
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
|
||||
def partial_update(self, request, slug, project_id, pk=None):
|
||||
issue = self.get_queryset().filter(pk=pk).first()
|
||||
issue = (
|
||||
self.get_queryset()
|
||||
.annotate(
|
||||
label_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"labels__id",
|
||||
distinct=True,
|
||||
filter=~Q(labels__id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
assignee_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"assignees__id",
|
||||
distinct=True,
|
||||
filter=~Q(assignees__id__isnull=True)
|
||||
& Q(assignees__member_project__is_active=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
module_ids=Coalesce(
|
||||
ArrayAgg(
|
||||
"issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=~Q(issue_module__module_id__isnull=True),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
)
|
||||
.filter(pk=pk)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not issue:
|
||||
return Response(
|
||||
|
||||
@@ -30,7 +30,7 @@ def page_version(
|
||||
workspace_id=page.workspace_id,
|
||||
description_html=page.description_html,
|
||||
description_binary=page.description_binary,
|
||||
ownned_by_id=user_id,
|
||||
owned_by_id=user_id,
|
||||
last_saved_at=page.updated_at,
|
||||
)
|
||||
|
||||
|
||||
6
packages/types/src/issues/issue.d.ts
vendored
6
packages/types/src/issues/issue.d.ts
vendored
@@ -97,3 +97,9 @@ export type TBulkOperationsPayload = {
|
||||
issue_ids: string[];
|
||||
properties: Partial<TBulkIssueProperties>;
|
||||
};
|
||||
|
||||
export type TIssueDetailWidget =
|
||||
| "sub-issues"
|
||||
| "relations"
|
||||
| "links"
|
||||
| "attachments";
|
||||
|
||||
@@ -13,7 +13,7 @@ type Props = {
|
||||
export const CollapsibleButton: FC<Props> = (props) => {
|
||||
const { isOpen, title, hideChevron = false, indicatorElement, actionItemElement } = props;
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-3 h-12 px-2.5 py-3 border-b border-custom-border-100">
|
||||
<div className="flex items-center justify-between gap-3 h-12 px-2.5 py-3 border-b border-custom-border-200">
|
||||
<div className="flex items-center gap-3.5">
|
||||
<div className="flex items-center gap-3">
|
||||
{!hideChevron && (
|
||||
|
||||
@@ -30,7 +30,7 @@ export const AuthView = observer(() => {
|
||||
/>
|
||||
</div>
|
||||
<div className="relative z-10 w-screen h-screen overflow-hidden overflow-y-auto flex flex-col">
|
||||
<div className="container mx-auto px-10 lg:px-0 flex-shrink-0 relative flex items-center justify-between pb-4 transition-all">
|
||||
<div className="container min-w-full px-10 lg:px-20 xl:px-36 flex-shrink-0 relative flex items-center justify-between pb-4 transition-all">
|
||||
<div className="flex items-center gap-x-2 py-10">
|
||||
<Link href={`${SPACE_BASE_PATH}/`} className="h-[30px] w-[133px]">
|
||||
<Image src={logo} alt="Plane logo" />
|
||||
|
||||
@@ -6,9 +6,11 @@ import useSWR from "swr";
|
||||
// components
|
||||
import { LogoSpinner } from "@/components/common";
|
||||
import { PageHead } from "@/components/core";
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
import { InboxContentRoot } from "@/components/inbox";
|
||||
import { IssuePeekOverview } from "@/components/issues";
|
||||
// constants
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
import { ENotificationLoader, ENotificationQueryParamType } from "@/constants/notification";
|
||||
// hooks
|
||||
import { useIssueDetail, useUser, useWorkspace, useWorkspaceNotifications } from "@/hooks/store";
|
||||
@@ -62,36 +64,44 @@ const WorkspaceDashboardPage = observer(() => {
|
||||
setCurrentSelectedNotificationId(undefined);
|
||||
setPeekIssue(undefined);
|
||||
},
|
||||
[]
|
||||
[setCurrentSelectedNotificationId, setPeekIssue]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="w-full h-full overflow-hidden overflow-y-auto">
|
||||
{is_inbox_issue === true && workspace_slug && project_id && issue_id ? (
|
||||
{!currentSelectedNotificationId ? (
|
||||
<div className="w-full h-screen flex justify-center items-center">
|
||||
<EmptyState type={EmptyStateType.NOTIFICATION_DETAIL_EMPTY_STATE} layout="screen-simple" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{projectMemberInfoLoader ? (
|
||||
<div className="w-full h-full flex justify-center items-center">
|
||||
<LogoSpinner />
|
||||
</div>
|
||||
{is_inbox_issue === true && workspace_slug && project_id && issue_id ? (
|
||||
<>
|
||||
{projectMemberInfoLoader ? (
|
||||
<div className="w-full h-full flex justify-center items-center">
|
||||
<LogoSpinner />
|
||||
</div>
|
||||
) : (
|
||||
<InboxContentRoot
|
||||
setIsMobileSidebar={() => {}}
|
||||
isMobileSidebar={false}
|
||||
workspaceSlug={workspace_slug}
|
||||
projectId={project_id}
|
||||
inboxIssueId={issue_id}
|
||||
isNotificationEmbed
|
||||
embedRemoveCurrentNotification={() => setCurrentSelectedNotificationId(undefined)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<InboxContentRoot
|
||||
setIsMobileSidebar={() => {}}
|
||||
isMobileSidebar={false}
|
||||
workspaceSlug={workspace_slug}
|
||||
projectId={project_id}
|
||||
inboxIssueId={issue_id}
|
||||
isNotificationEmbed
|
||||
<IssuePeekOverview
|
||||
embedIssue
|
||||
embedRemoveCurrentNotification={() => setCurrentSelectedNotificationId(undefined)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<IssuePeekOverview
|
||||
embedIssue
|
||||
embedRemoveCurrentNotification={() => setCurrentSelectedNotificationId(undefined)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -104,7 +104,7 @@ export default function ForgotPasswordPage() {
|
||||
/>
|
||||
</div>
|
||||
<div className="relative z-10 w-screen h-screen overflow-hidden overflow-y-auto flex flex-col">
|
||||
<div className="container mx-auto px-10 lg:px-0 flex-shrink-0 relative flex items-center justify-between pb-4 transition-all">
|
||||
<div className="container min-w-full px-10 lg:px-20 xl:px-36 flex-shrink-0 relative flex items-center justify-between pb-4 transition-all">
|
||||
<div className="flex items-center gap-x-2 py-10">
|
||||
<Link href={`/`} className="h-[30px] w-[133px]">
|
||||
<Image src={logo} alt="Plane logo" />
|
||||
|
||||
@@ -116,7 +116,7 @@ export default function ResetPasswordPage() {
|
||||
/>
|
||||
</div>
|
||||
<div className="relative z-10 w-screen h-screen overflow-hidden overflow-y-auto flex flex-col">
|
||||
<div className="container mx-auto px-10 lg:px-0 flex-shrink-0 relative flex items-center justify-between pb-4 transition-all">
|
||||
<div className="container min-w-full px-10 lg:px-20 xl:px-36 flex-shrink-0 relative flex items-center justify-between pb-4 transition-all">
|
||||
<div className="flex items-center gap-x-2 py-10">
|
||||
<Link href={`/`} className="h-[30px] w-[133px]">
|
||||
<Image src={logo} alt="Plane logo" />
|
||||
|
||||
@@ -118,7 +118,7 @@ const SetPasswordPage = observer(() => {
|
||||
/>
|
||||
</div>
|
||||
<div className="relative z-10 w-screen h-screen overflow-hidden overflow-y-auto flex flex-col">
|
||||
<div className="container mx-auto px-10 lg:px-0 flex-shrink-0 relative flex items-center justify-between pb-4 transition-all">
|
||||
<div className="container min-w-full px-10 lg:px-20 xl:px-36 flex-shrink-0 relative flex items-center justify-between pb-4 transition-all">
|
||||
<div className="flex items-center gap-x-2 py-10">
|
||||
<Link href={`/`} className="h-[30px] w-[133px]">
|
||||
<Image src={logo} alt="Plane logo" />
|
||||
|
||||
@@ -46,7 +46,7 @@ const HomePage = observer(() => {
|
||||
/>
|
||||
</div>
|
||||
<div className="relative z-10 w-screen h-screen overflow-hidden overflow-y-auto flex flex-col">
|
||||
<div className="container mx-auto px-10 lg:px-0 flex-shrink-0 relative flex items-center justify-between pb-4 transition-all">
|
||||
<div className="container min-w-full px-10 lg:px-20 xl:px-36 flex-shrink-0 relative flex items-center justify-between pb-4 transition-all">
|
||||
<div className="flex items-center gap-x-2 py-10">
|
||||
<Link href={`/`} className="h-[30px] w-[133px]">
|
||||
<Image src={logo} alt="Plane logo" />
|
||||
|
||||
@@ -41,7 +41,7 @@ const SignInPage = observer(() => {
|
||||
/>
|
||||
</div>
|
||||
<div className="relative z-10 w-screen h-screen overflow-hidden overflow-y-auto flex flex-col">
|
||||
<div className="container mx-auto px-10 lg:px-0 flex-shrink-0 relative flex items-center justify-between pb-4 transition-all">
|
||||
<div className="container min-w-full px-10 lg:px-20 xl:px-36 flex-shrink-0 relative flex items-center justify-between pb-4 transition-all">
|
||||
<div className="flex items-center gap-x-2 py-10">
|
||||
<Link href={`/`} className="h-[30px] w-[133px]">
|
||||
<Image src={logo} alt="Plane logo" />
|
||||
|
||||
@@ -23,7 +23,7 @@ export const SelectProject: React.FC<Props> = observer((props) => {
|
||||
value: projectDetails?.id,
|
||||
query: `${projectDetails?.name} ${projectDetails?.identifier}`,
|
||||
content: (
|
||||
<div className="flex items-center gap-2 truncate">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[0.65rem] text-custom-text-200 flex-shrink-0">{projectDetails?.identifier}</span>
|
||||
<span className="flex-grow truncate">{projectDetails?.name}</span>
|
||||
</div>
|
||||
@@ -44,6 +44,7 @@ export const SelectProject: React.FC<Props> = observer((props) => {
|
||||
.join(", ")
|
||||
: "All projects"
|
||||
}
|
||||
optionsClassName={"w-48"}
|
||||
multiple
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { MAX_FILE_SIZE } from "@/constants/common";
|
||||
// helper
|
||||
import { generateFileName } from "@/helpers/attachment.helper";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
import { useInstance, useIssueDetail } from "@/hooks/store";
|
||||
|
||||
import { useAttachmentOperations } from "./helper";
|
||||
|
||||
@@ -22,11 +22,16 @@ type Props = {
|
||||
|
||||
export const IssueAttachmentActionButton: FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, customButton, disabled = false } = props;
|
||||
// helper
|
||||
// state
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
// store hooks
|
||||
const { config } = useInstance();
|
||||
const { setLastWidgetAction } = useIssueDetail();
|
||||
|
||||
// operations
|
||||
const handleAttachmentOperations = useAttachmentOperations(workspaceSlug, projectId, issueId);
|
||||
|
||||
// handlers
|
||||
const onDrop = useCallback(
|
||||
(acceptedFiles: File[]) => {
|
||||
const currentFile: File = acceptedFiles[0];
|
||||
@@ -45,7 +50,10 @@ export const IssueAttachmentActionButton: FC<Props> = observer((props) => {
|
||||
})
|
||||
);
|
||||
setIsLoading(true);
|
||||
handleAttachmentOperations.create(formData).finally(() => setIsLoading(false));
|
||||
handleAttachmentOperations.create(formData).finally(() => {
|
||||
setLastWidgetAction("attachments");
|
||||
setIsLoading(false);
|
||||
});
|
||||
},
|
||||
[handleAttachmentOperations, workspaceSlug]
|
||||
);
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
"use client";
|
||||
import React, { FC, useState } from "react";
|
||||
import React, { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Collapsible } from "@plane/ui";
|
||||
// components
|
||||
import {
|
||||
IssueAttachmentsCollapsibleContent,
|
||||
IssueAttachmentsCollapsibleTitle,
|
||||
} from "@/components/issues/issue-detail-widgets";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
@@ -14,17 +17,21 @@ type Props = {
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const AttachmentsCollapsible: FC<Props> = (props) => {
|
||||
export const AttachmentsCollapsible: FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, disabled = false } = props;
|
||||
// state
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
// store hooks
|
||||
const { activeIssueDetailWidgets, toggleActiveIssueDetailWidget } = useIssueDetail();
|
||||
|
||||
// derived values
|
||||
const isCollapsibleOpen = activeIssueDetailWidgets.includes("attachments");
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
isOpen={isOpen}
|
||||
onToggle={() => setIsOpen((prev) => !prev)}
|
||||
isOpen={isCollapsibleOpen}
|
||||
onToggle={() => toggleActiveIssueDetailWidget("attachments")}
|
||||
title={
|
||||
<IssueAttachmentsCollapsibleTitle
|
||||
isOpen={isOpen}
|
||||
isOpen={isCollapsibleOpen}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
@@ -40,4 +47,4 @@ export const AttachmentsCollapsible: FC<Props> = (props) => {
|
||||
/>
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ export const IssueLinksActionButton: FC<Props> = observer((props) => {
|
||||
const [isIssueLinkModal, setIsIssueLinkModal] = useState(false);
|
||||
|
||||
// store hooks
|
||||
const { toggleIssueLinkModal: toggleIssueLinkModalStore } = useIssueDetail();
|
||||
const { toggleIssueLinkModal: toggleIssueLinkModalStore, setLastWidgetAction } = useIssueDetail();
|
||||
|
||||
// helper
|
||||
const handleLinkOperations = useLinkOperations(workspaceSlug, projectId, issueId);
|
||||
@@ -43,11 +43,16 @@ export const IssueLinksActionButton: FC<Props> = observer((props) => {
|
||||
toggleIssueLinkModal(true);
|
||||
};
|
||||
|
||||
const handleOnClose = () => {
|
||||
toggleIssueLinkModal(false);
|
||||
setLastWidgetAction("links");
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IssueLinkCreateUpdateModal
|
||||
isModalOpen={isIssueLinkModal}
|
||||
handleModal={toggleIssueLinkModal}
|
||||
handleOnClose={handleOnClose}
|
||||
linkOperations={handleLinkOperations}
|
||||
/>
|
||||
<button type="button" onClick={handleOnClick} disabled={disabled}>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
"use client";
|
||||
import React, { FC, useState } from "react";
|
||||
import React, { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Collapsible } from "@plane/ui";
|
||||
// components
|
||||
import { IssueLinksCollapsibleContent, IssueLinksCollapsibleTitle } from "@/components/issues/issue-detail-widgets";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
@@ -11,17 +14,21 @@ type Props = {
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const LinksCollapsible: FC<Props> = (props) => {
|
||||
export const LinksCollapsible: FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, disabled = false } = props;
|
||||
// state
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
// store hooks
|
||||
const { activeIssueDetailWidgets, toggleActiveIssueDetailWidget } = useIssueDetail();
|
||||
|
||||
// derived values
|
||||
const isCollapsibleOpen = activeIssueDetailWidgets.includes("links");
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
isOpen={isOpen}
|
||||
onToggle={() => setIsOpen((prev) => !prev)}
|
||||
isOpen={isCollapsibleOpen}
|
||||
onToggle={() => toggleActiveIssueDetailWidget("links")}
|
||||
title={
|
||||
<IssueLinksCollapsibleTitle
|
||||
isOpen={isOpen}
|
||||
isOpen={isCollapsibleOpen}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
@@ -37,4 +44,4 @@ export const LinksCollapsible: FC<Props> = (props) => {
|
||||
/>
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -23,7 +23,8 @@ export const RelationActionButton: FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, customButton, issueId, disabled = false } = props;
|
||||
// state
|
||||
const [relationKey, setRelationKey] = useState<TIssueRelationTypes | null>(null);
|
||||
const { createRelation, isRelationModalOpen, toggleRelationModal } = useIssueDetail();
|
||||
// store hooks
|
||||
const { createRelation, isRelationModalOpen, toggleRelationModal, setLastWidgetAction } = useIssueDetail();
|
||||
|
||||
// handlers
|
||||
const handleOnClick = (relationKey: TIssueRelationTypes) => {
|
||||
@@ -57,6 +58,7 @@ export const RelationActionButton: FC<Props> = observer((props) => {
|
||||
const handleOnClose = () => {
|
||||
setRelationKey(null);
|
||||
toggleRelationModal(null, null);
|
||||
setLastWidgetAction("relations");
|
||||
};
|
||||
|
||||
// button element
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
"use client";
|
||||
import React, { FC, useState } from "react";
|
||||
import React, { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Collapsible } from "@plane/ui";
|
||||
// components
|
||||
import { RelationsCollapsibleContent, RelationsCollapsibleTitle } from "@/components/issues/issue-detail-widgets";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
@@ -11,17 +14,21 @@ type Props = {
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const RelationsCollapsible: FC<Props> = (props) => {
|
||||
export const RelationsCollapsible: FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, disabled = false } = props;
|
||||
// state
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
// store hooks
|
||||
const { activeIssueDetailWidgets, toggleActiveIssueDetailWidget } = useIssueDetail();
|
||||
|
||||
// derived values
|
||||
const isCollapsibleOpen = activeIssueDetailWidgets.includes("relations");
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
isOpen={isOpen}
|
||||
onToggle={() => setIsOpen((prev) => !prev)}
|
||||
isOpen={isCollapsibleOpen}
|
||||
onToggle={() => toggleActiveIssueDetailWidget("relations")}
|
||||
title={
|
||||
<RelationsCollapsibleTitle
|
||||
isOpen={isOpen}
|
||||
isOpen={isCollapsibleOpen}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
@@ -37,4 +44,4 @@ export const RelationsCollapsible: FC<Props> = (props) => {
|
||||
/>
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -47,6 +47,7 @@ export const SubIssuesActionButton: FC<Props> = observer((props) => {
|
||||
toggleCreateIssueModal,
|
||||
isSubIssuesModalOpen,
|
||||
toggleSubIssuesModal,
|
||||
setLastWidgetAction,
|
||||
} = useIssueDetail();
|
||||
const { setTrackElement } = useEventTracker();
|
||||
|
||||
@@ -88,6 +89,7 @@ export const SubIssuesActionButton: FC<Props> = observer((props) => {
|
||||
|
||||
const handleExistingIssuesModalClose = () => {
|
||||
handleIssueCrudState("existing", null, null);
|
||||
setLastWidgetAction("sub-issues");
|
||||
toggleSubIssuesModal(null);
|
||||
};
|
||||
|
||||
@@ -102,6 +104,7 @@ export const SubIssuesActionButton: FC<Props> = observer((props) => {
|
||||
const handleCreateUpdateModalClose = () => {
|
||||
handleIssueCrudState("create", null, null);
|
||||
toggleCreateIssueModal(false);
|
||||
setLastWidgetAction("sub-issues");
|
||||
};
|
||||
|
||||
const handleCreateUpdateModalOnSubmit = async (_issue: TIssue) => {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
"use client";
|
||||
import React, { FC, useState } from "react";
|
||||
import React, { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Collapsible } from "@plane/ui";
|
||||
// components
|
||||
import { SubIssuesCollapsibleContent, SubIssuesCollapsibleTitle } from "@/components/issues/issue-detail-widgets";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
@@ -11,17 +14,22 @@ type Props = {
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export const SubIssuesCollapsible: FC<Props> = (props) => {
|
||||
export const SubIssuesCollapsible: FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, disabled = false } = props;
|
||||
// state
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
|
||||
// store hooks
|
||||
const { activeIssueDetailWidgets, toggleActiveIssueDetailWidget } = useIssueDetail();
|
||||
|
||||
// derived state
|
||||
const isCollapsibleOpen = activeIssueDetailWidgets.includes("sub-issues");
|
||||
|
||||
return (
|
||||
<Collapsible
|
||||
isOpen={isOpen}
|
||||
onToggle={() => setIsOpen((prev) => !prev)}
|
||||
isOpen={isCollapsibleOpen}
|
||||
onToggle={() => toggleActiveIssueDetailWidget("sub-issues")}
|
||||
title={
|
||||
<SubIssuesCollapsibleTitle
|
||||
isOpen={isOpen}
|
||||
isOpen={isCollapsibleOpen}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
parentIssueId={issueId}
|
||||
@@ -37,4 +45,4 @@ export const SubIssuesCollapsible: FC<Props> = (props) => {
|
||||
/>
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@ export type TIssueLinkCreateFormFieldOptions = TIssueLinkEditableFields & {
|
||||
|
||||
export type TIssueLinkCreateEditModal = {
|
||||
isModalOpen: boolean;
|
||||
handleModal: (modalToggle: boolean) => void;
|
||||
handleOnClose?: () => void;
|
||||
linkOperations: TLinkOperationsModal;
|
||||
preloadedData?: TIssueLinkCreateFormFieldOptions | null;
|
||||
};
|
||||
@@ -29,7 +29,7 @@ const defaultValues: TIssueLinkCreateFormFieldOptions = {
|
||||
|
||||
export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = (props) => {
|
||||
// props
|
||||
const { isModalOpen, handleModal, linkOperations, preloadedData } = props;
|
||||
const { isModalOpen, handleOnClose, linkOperations, preloadedData } = props;
|
||||
|
||||
// react hook form
|
||||
const {
|
||||
@@ -42,7 +42,7 @@ export const IssueLinkCreateUpdateModal: FC<TIssueLinkCreateEditModal> = (props)
|
||||
});
|
||||
|
||||
const onClose = () => {
|
||||
handleModal(false);
|
||||
if (handleOnClose) handleOnClose();
|
||||
const timeout = setTimeout(() => {
|
||||
reset(preloadedData ? preloadedData : defaultValues);
|
||||
clearTimeout(timeout);
|
||||
|
||||
@@ -42,11 +42,15 @@ export const IssueLinkDetail: FC<TIssueLinkDetail> = (props) => {
|
||||
|
||||
const createdByDetails = getUserDetails(linkDetail.created_by_id);
|
||||
|
||||
const handleOnClose = () => {
|
||||
toggleIssueLinkModal(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={linkId}>
|
||||
<IssueLinkCreateUpdateModal
|
||||
isModalOpen={isIssueLinkModalOpen}
|
||||
handleModal={toggleIssueLinkModal}
|
||||
handleOnClose={handleOnClose}
|
||||
linkOperations={linkOperations}
|
||||
preloadedData={linkDetail}
|
||||
/>
|
||||
|
||||
@@ -37,11 +37,14 @@ export const IssueLinkItem: FC<TIssueLinkItem> = (props) => {
|
||||
const linkDetail = getLinkById(linkId);
|
||||
if (!linkDetail) return <></>;
|
||||
|
||||
const handleOnClose = () => {
|
||||
toggleIssueLinkModal(false);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<IssueLinkCreateUpdateModal
|
||||
isModalOpen={isIssueLinkModalOpen}
|
||||
handleModal={toggleIssueLinkModal}
|
||||
handleOnClose={handleOnClose}
|
||||
linkOperations={linkOperations}
|
||||
preloadedData={linkDetail}
|
||||
/>
|
||||
|
||||
@@ -100,11 +100,15 @@ export const IssueLinkRoot: FC<TIssueLinkRoot> = (props) => {
|
||||
[workspaceSlug, projectId, issueId, createLink, updateLink, removeLink, toggleIssueLinkModal]
|
||||
);
|
||||
|
||||
const handleOnClose = () => {
|
||||
toggleIssueLinkModal(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<IssueLinkCreateUpdateModal
|
||||
isModalOpen={isIssueLinkModal}
|
||||
handleModal={toggleIssueLinkModal}
|
||||
handleOnClose={handleOnClose}
|
||||
linkOperations={handleLinkOperations}
|
||||
/>
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ export const ProjectViewsList = observer(() => {
|
||||
|
||||
if (loader || !projectViews || !filteredProjectViews) return <ViewListLoader />;
|
||||
|
||||
if (filteredProjectViews.length === 0 && projectViews) {
|
||||
if (filteredProjectViews.length === 0 && projectViews.length > 0) {
|
||||
return (
|
||||
<div className="h-full w-full grid place-items-center">
|
||||
<div className="text-center">
|
||||
|
||||
@@ -38,7 +38,7 @@ export const NotificationAppSidebarOption: FC<TNotificationAppSidebarOption> = o
|
||||
|
||||
return (
|
||||
<div className="ml-auto">
|
||||
<CountChip count={`${isMentionsEnabled ? `@` : ``}${getNumberCount(totalNotifications)}`} />
|
||||
<CountChip count={`${isMentionsEnabled ? `@ ` : ``}${getNumberCount(totalNotifications)}`} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from "./root";
|
||||
export * from "./menu";
|
||||
export * from "./applied-filter";
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./root";
|
||||
export * from "./menu-option-item";
|
||||
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Check } from "lucide-react";
|
||||
// constants
|
||||
import { ENotificationFilterType } from "@/constants/notification";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useWorkspaceNotifications } from "@/hooks/store";
|
||||
|
||||
export const NotificationFilterOptionItem: FC<{ label: string; value: ENotificationFilterType }> = observer((props) => {
|
||||
const { value, label } = props;
|
||||
// hooks
|
||||
const { filters, updateFilters } = useWorkspaceNotifications();
|
||||
|
||||
const handleFilterTypeChange = (filterType: ENotificationFilterType, filterValue: boolean) =>
|
||||
updateFilters("type", {
|
||||
...filters.type,
|
||||
[filterType]: filterValue,
|
||||
});
|
||||
|
||||
// derived values
|
||||
const isSelected = filters?.type?.[value] || false;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={value}
|
||||
className="flex items-center gap-2 cursor-pointer px-2 p-1 transition-all hover:bg-custom-background-80 rounded-sm"
|
||||
onClick={() => handleFilterTypeChange(value, !isSelected)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex-shrink-0 w-3 h-3 flex justify-center items-center rounded-sm transition-all",
|
||||
isSelected ? "bg-custom-primary-100" : "bg-custom-background-90"
|
||||
)}
|
||||
>
|
||||
{isSelected && <Check className="h-2 w-2" />}
|
||||
</div>
|
||||
<div className={cn("whitespace-nowrap text-sm", isSelected ? "text-custom-text-100" : "text-custom-text-200")}>
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { ListFilter } from "lucide-react";
|
||||
import { PopoverMenu, Tooltip } from "@plane/ui";
|
||||
// components
|
||||
import { NotificationFilterOptionItem } from "@/components/workspace-notifications";
|
||||
// constants
|
||||
import { ENotificationFilterType, FILTER_TYPE_OPTIONS } from "@/constants/notification";
|
||||
// hooks
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
|
||||
export const NotificationFilter: FC = observer(() => {
|
||||
// hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
return (
|
||||
<PopoverMenu
|
||||
data={FILTER_TYPE_OPTIONS}
|
||||
button={
|
||||
<Tooltip tooltipContent="Notification Filters" isMobile={isMobile} position="bottom">
|
||||
<div className="flex-shrink-0 w-5 h-5 flex justify-center items-center overflow-hidden cursor-pointer transition-all hover:bg-custom-background-80 rounded-sm outline-none">
|
||||
<ListFilter className="h-3 w-3" />
|
||||
</div>
|
||||
</Tooltip>
|
||||
}
|
||||
keyExtractor={(item: { label: string; value: ENotificationFilterType }) => item.value}
|
||||
render={(item) => <NotificationFilterOptionItem {...item} />}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -1,80 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { FC, Fragment } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Check, ListFilter } from "lucide-react";
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// constants
|
||||
import { ENotificationFilterType, FILTER_TYPE_OPTIONS } from "@/constants/notification";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useWorkspaceNotifications } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
|
||||
export const NotificationFilter: FC = observer(() => {
|
||||
// hooks
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { filters, updateFilters } = useWorkspaceNotifications();
|
||||
|
||||
const handleFilterTypeChange = (filterType: ENotificationFilterType, filterValue: boolean) =>
|
||||
updateFilters("type", {
|
||||
...filters.type,
|
||||
[filterType]: filterValue,
|
||||
});
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
<Tooltip tooltipContent="Notification Filters" isMobile={isMobile} position="bottom">
|
||||
<Popover.Button
|
||||
className={cn(
|
||||
"flex-shrink-0 w-5 h-5 flex justify-center items-center overflow-hidden cursor-pointer transition-all hover:bg-custom-background-80 rounded-sm outline-none",
|
||||
({ open }: { open: boolean }) => (open ? "bg-custom-background-80" : "")
|
||||
)}
|
||||
>
|
||||
<ListFilter className="h-3 w-3" />
|
||||
</Popover.Button>
|
||||
</Tooltip>
|
||||
|
||||
<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 mt-2 right-0 z-10 min-w-44">
|
||||
<div className="p-2 rounded-md border border-custom-border-200 bg-custom-background-100">
|
||||
{FILTER_TYPE_OPTIONS.map((filter) => {
|
||||
const isSelected = filters?.type?.[filter?.value] || false;
|
||||
return (
|
||||
<div
|
||||
key={filter.value}
|
||||
className="flex items-center gap-2 cursor-pointer px-2 p-1 transition-all hover:bg-custom-background-80 rounded-sm"
|
||||
onClick={() => handleFilterTypeChange(filter?.value, !isSelected)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex-shrink-0 w-3 h-3 flex justify-center items-center rounded-sm transition-all",
|
||||
isSelected ? "bg-custom-primary-100" : "bg-custom-background-90"
|
||||
)}
|
||||
>
|
||||
{isSelected && <Check className="h-2 w-2" />}
|
||||
</div>
|
||||
<div
|
||||
className={cn("whitespace-nowrap", isSelected ? "text-custom-text-100" : "text-custom-text-200")}
|
||||
>
|
||||
{filter.label}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
);
|
||||
});
|
||||
@@ -1,160 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { FC, Fragment } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Check, CheckCheck, CheckCircle, Clock, MoreVertical } from "lucide-react";
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
import { TNotificationFilter } from "@plane/types";
|
||||
import { ArchiveIcon, Spinner, Tooltip } from "@plane/ui";
|
||||
// constants
|
||||
import { NOTIFICATIONS_READ } from "@/constants/event-tracker";
|
||||
import { ENotificationLoader } from "@/constants/notification";
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useEventTracker, useWorkspaceNotifications } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
|
||||
type TNotificationHeaderMenuOption = {
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const NotificationHeaderMenuOption: FC<TNotificationHeaderMenuOption> = observer((props) => {
|
||||
const { workspaceSlug } = props;
|
||||
// hooks
|
||||
const { captureEvent } = useEventTracker();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const { loader, filters, updateFilters, updateBulkFilters, markAllNotificationsAsRead } = useWorkspaceNotifications();
|
||||
|
||||
const handleFilterChange = (filterType: keyof TNotificationFilter, filterValue: boolean) =>
|
||||
updateFilters(filterType, filterValue);
|
||||
|
||||
const handleBulkFilterChange = (filter: Partial<TNotificationFilter>) => updateBulkFilters(filter);
|
||||
|
||||
const handleMarkAllNotificationsAsRead = async () => {
|
||||
// NOTE: We are using loader to prevent continues request when we are making all the notification to read
|
||||
if (loader) return;
|
||||
try {
|
||||
await markAllNotificationsAsRead(workspaceSlug);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover className="relative">
|
||||
<Tooltip tooltipContent="Notification Filters" isMobile={isMobile} position="bottom">
|
||||
<Popover.Button
|
||||
className={cn(
|
||||
"flex-shrink-0 w-5 h-5 flex justify-center items-center overflow-hidden cursor-pointer transition-all hover:bg-custom-background-80 rounded-sm outline-none",
|
||||
({ open }: { open: boolean }) => (open ? "bg-custom-background-80" : "")
|
||||
)}
|
||||
>
|
||||
<MoreVertical className="h-3 w-3" />
|
||||
</Popover.Button>
|
||||
</Tooltip>
|
||||
|
||||
<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 mt-2 right-0 z-10 min-w-44 select-none">
|
||||
<div className="py-2 rounded-md border border-custom-border-200 bg-custom-background-100 space-y-1">
|
||||
<div className="px-2">
|
||||
<div
|
||||
className="flex items-center gap-2 cursor-pointer px-2 p-1 transition-all hover:bg-custom-background-80 rounded-sm"
|
||||
onClick={() => {
|
||||
handleMarkAllNotificationsAsRead();
|
||||
captureEvent(NOTIFICATIONS_READ);
|
||||
}}
|
||||
>
|
||||
<CheckCheck className="h-3 w-3" />
|
||||
<div>Mark all as read</div>
|
||||
{loader === ENotificationLoader.MARK_ALL_AS_READY && (
|
||||
<div className="ml-auto">
|
||||
<Spinner height="14px" width="14px" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-b border-custom-border-200 " />
|
||||
|
||||
<div className="px-2">
|
||||
<div
|
||||
className="flex items-center gap-2 cursor-pointer px-2 p-1 transition-all hover:bg-custom-background-80 rounded-sm"
|
||||
onClick={() => handleFilterChange("read", !filters?.read)}
|
||||
>
|
||||
<CheckCircle className="flex-shrink-0 h-3 w-3" />
|
||||
<div
|
||||
className={cn("whitespace-nowrap", filters?.read ? "text-custom-text-100" : "text-custom-text-200")}
|
||||
>
|
||||
Show unread
|
||||
</div>
|
||||
{filters?.read && (
|
||||
<div className="ml-auto">
|
||||
<Check className="w-3 h-3" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex items-center gap-2 cursor-pointer px-2 p-1 transition-all hover:bg-custom-background-80 rounded-sm"
|
||||
onClick={() =>
|
||||
handleBulkFilterChange({
|
||||
archived: !filters?.archived,
|
||||
snoozed: false,
|
||||
})
|
||||
}
|
||||
>
|
||||
<ArchiveIcon className="flex-shrink-0 h-3 w-3" />
|
||||
<div
|
||||
className={cn(
|
||||
"whitespace-nowrap",
|
||||
filters?.archived ? "text-custom-text-100" : "text-custom-text-200"
|
||||
)}
|
||||
>
|
||||
Show Archived
|
||||
</div>
|
||||
{filters?.archived && (
|
||||
<div className="ml-auto">
|
||||
<Check className="w-3 h-3" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="flex items-center gap-2 cursor-pointer px-2 p-1 transition-all hover:bg-custom-background-80 rounded-sm"
|
||||
onClick={() =>
|
||||
handleBulkFilterChange({
|
||||
snoozed: !filters?.snoozed,
|
||||
archived: false,
|
||||
})
|
||||
}
|
||||
>
|
||||
<Clock className="flex-shrink-0 h-3 w-3" />
|
||||
<div
|
||||
className={cn(
|
||||
"whitespace-nowrap",
|
||||
filters?.snoozed ? "text-custom-text-100" : "text-custom-text-200"
|
||||
)}
|
||||
>
|
||||
Show Snoozed
|
||||
</div>
|
||||
{filters?.snoozed && (
|
||||
<div className="ml-auto">
|
||||
<Check className="w-3 h-3" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</Popover>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./root";
|
||||
export * from "./menu-item";
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// components
|
||||
import type { TPopoverMenuOptions } from "@/components/workspace-notifications";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
|
||||
export const NotificationMenuOptionItem: FC<TPopoverMenuOptions> = observer((props) => {
|
||||
const { type, label = "", isActive, prependIcon, appendIcon, onClick } = props;
|
||||
|
||||
if (type === "menu-item")
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-2 cursor-pointer mx-2 px-2 p-1 transition-all hover:bg-custom-background-80 rounded-sm"
|
||||
onClick={() => onClick && onClick()}
|
||||
>
|
||||
{prependIcon && prependIcon}
|
||||
<div className={cn("whitespace-nowrap text-sm", isActive ? "text-custom-text-100" : "text-custom-text-200")}>
|
||||
{label}
|
||||
</div>
|
||||
{appendIcon && <div className="ml-auto">{appendIcon}</div>}
|
||||
</div>
|
||||
);
|
||||
|
||||
return <div className="border-b border-custom-border-200" />;
|
||||
});
|
||||
@@ -0,0 +1,114 @@
|
||||
"use client";
|
||||
|
||||
import { FC, ReactNode } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Check, CheckCheck, CheckCircle, Clock } from "lucide-react";
|
||||
import { TNotificationFilter } from "@plane/types";
|
||||
import { ArchiveIcon, PopoverMenu, Spinner } from "@plane/ui";
|
||||
// components
|
||||
import { NotificationMenuOptionItem } from "@/components/workspace-notifications";
|
||||
// constants
|
||||
import { NOTIFICATIONS_READ } from "@/constants/event-tracker";
|
||||
import { ENotificationLoader } from "@/constants/notification";
|
||||
// hooks
|
||||
import { useEventTracker, useWorkspaceNotifications } from "@/hooks/store";
|
||||
|
||||
type TNotificationHeaderMenuOption = {
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export type TPopoverMenuOptions = {
|
||||
key: string;
|
||||
type: string;
|
||||
label?: string | undefined;
|
||||
isActive?: boolean | undefined;
|
||||
prependIcon?: ReactNode | undefined;
|
||||
appendIcon?: ReactNode | undefined;
|
||||
onClick?: (() => void) | undefined;
|
||||
};
|
||||
|
||||
export const NotificationHeaderMenuOption: FC<TNotificationHeaderMenuOption> = observer((props) => {
|
||||
const { workspaceSlug } = props;
|
||||
// hooks
|
||||
const { captureEvent } = useEventTracker();
|
||||
const { loader, filters, updateFilters, updateBulkFilters, markAllNotificationsAsRead } = useWorkspaceNotifications();
|
||||
|
||||
const handleFilterChange = (filterType: keyof TNotificationFilter, filterValue: boolean) =>
|
||||
updateFilters(filterType, filterValue);
|
||||
|
||||
const handleBulkFilterChange = (filter: Partial<TNotificationFilter>) => updateBulkFilters(filter);
|
||||
|
||||
const handleMarkAllNotificationsAsRead = async () => {
|
||||
// NOTE: We are using loader to prevent continues request when we are making all the notification to read
|
||||
if (loader) return;
|
||||
try {
|
||||
await markAllNotificationsAsRead(workspaceSlug);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const popoverMenuOptions: TPopoverMenuOptions[] = [
|
||||
{
|
||||
key: "menu-mark-all-read",
|
||||
type: "menu-item",
|
||||
label: "Mark all as read",
|
||||
isActive: true,
|
||||
prependIcon: <CheckCheck className="h-3 w-3" />,
|
||||
appendIcon: loader === ENotificationLoader.MARK_ALL_AS_READY ? <Spinner height="14px" width="14px" /> : undefined,
|
||||
onClick: () => {
|
||||
captureEvent(NOTIFICATIONS_READ);
|
||||
handleMarkAllNotificationsAsRead();
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "menu-divider",
|
||||
type: "divider",
|
||||
},
|
||||
{
|
||||
key: "menu-unread",
|
||||
type: "menu-item",
|
||||
label: "Show unread",
|
||||
isActive: filters?.read,
|
||||
prependIcon: <CheckCircle className="flex-shrink-0 h-3 w-3" />,
|
||||
appendIcon: filters?.read ? <Check className="w-3 h-3" /> : undefined,
|
||||
onClick: () => handleFilterChange("read", !filters?.read),
|
||||
},
|
||||
{
|
||||
key: "menu-archived",
|
||||
type: "menu-item",
|
||||
label: "Show archived",
|
||||
isActive: filters?.archived,
|
||||
prependIcon: <ArchiveIcon className="flex-shrink-0 h-3 w-3" />,
|
||||
appendIcon: filters?.archived ? <Check className="w-3 h-3" /> : undefined,
|
||||
onClick: () =>
|
||||
handleBulkFilterChange({
|
||||
archived: !filters?.archived,
|
||||
snoozed: false,
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: "menu-snoozed",
|
||||
type: "menu-item",
|
||||
label: "Show snoozed",
|
||||
isActive: filters?.snoozed,
|
||||
prependIcon: <Clock className="flex-shrink-0 h-3 w-3" />,
|
||||
appendIcon: filters?.snoozed ? <Check className="w-3 h-3" /> : undefined,
|
||||
onClick: () =>
|
||||
handleBulkFilterChange({
|
||||
snoozed: !filters?.snoozed,
|
||||
archived: false,
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<PopoverMenu
|
||||
data={popoverMenuOptions}
|
||||
buttonClassName="flex-shrink-0 w-5 h-5 flex justify-center items-center overflow-hidden cursor-pointer transition-all hover:bg-custom-background-80 rounded-sm outline-none"
|
||||
keyExtractor={(item: TPopoverMenuOptions) => item.key}
|
||||
panelClassName="p-0 py-2 rounded-md border border-custom-border-200 bg-custom-background-100 space-y-1"
|
||||
render={(item: TPopoverMenuOptions) => <NotificationMenuOptionItem {...item} />}
|
||||
/>
|
||||
);
|
||||
});
|
||||
@@ -66,12 +66,9 @@ export const NotificationsSidebar: FC = observer(() => {
|
||||
)}
|
||||
>
|
||||
<div className="font-medium">{tab.label}</div>
|
||||
<CountChip
|
||||
count={getNumberCount(tab.count(unreadNotificationsCount))}
|
||||
className={
|
||||
currentNotificationTab === tab.value ? `bg-custom-primary-100/20` : `bg-custom-background-80/50`
|
||||
}
|
||||
/>
|
||||
{tab.count(unreadNotificationsCount) > 0 && (
|
||||
<CountChip count={getNumberCount(tab.count(unreadNotificationsCount))} />
|
||||
)}
|
||||
</div>
|
||||
{currentNotificationTab === tab.value && (
|
||||
<div className="border absolute bottom-0 right-0 left-0 rounded-t-md border-custom-primary-100" />
|
||||
|
||||
@@ -80,6 +80,7 @@ export enum EmptyStateType {
|
||||
ISSUE_RELATION_EMPTY_STATE = "issue-relation-empty-state",
|
||||
ISSUE_COMMENT_EMPTY_STATE = "issue-comment-empty-state",
|
||||
|
||||
NOTIFICATION_DETAIL_EMPTY_STATE = "notification-detail-empty-state",
|
||||
NOTIFICATION_ALL_EMPTY_STATE = "notification-all-empty-state",
|
||||
NOTIFICATION_MENTIONS_EMPTY_STATE = "notification-mentions-empty-state",
|
||||
NOTIFICATION_MY_ISSUE_EMPTY_STATE = "notification-my-issues-empty-state",
|
||||
@@ -596,6 +597,11 @@ const emptyStateDetails = {
|
||||
path: "/empty-state/search/comments",
|
||||
},
|
||||
|
||||
[EmptyStateType.NOTIFICATION_DETAIL_EMPTY_STATE]: {
|
||||
key: EmptyStateType.INBOX_DETAIL_EMPTY_STATE,
|
||||
title: "Select to view details.",
|
||||
path: "/empty-state/inbox/issue-detail",
|
||||
},
|
||||
[EmptyStateType.NOTIFICATION_ALL_EMPTY_STATE]: {
|
||||
key: EmptyStateType.NOTIFICATION_ALL_EMPTY_STATE,
|
||||
title: "No issues assigned",
|
||||
|
||||
@@ -133,7 +133,10 @@ export class ArchivedIssuesFilter extends IssueFilterHelperStore implements IArc
|
||||
);
|
||||
|
||||
const filters: IIssueFilterOptions = this.computedFilters(_filters?.filters);
|
||||
const displayFilters: IIssueDisplayFilterOptions = this.computedDisplayFilters(_filters?.display_filters);
|
||||
const displayFilters: IIssueDisplayFilterOptions = this.computedDisplayFilters({
|
||||
..._filters?.display_filters,
|
||||
sub_issue: true,
|
||||
});
|
||||
const displayProperties: IIssueDisplayProperties = this.computedDisplayProperties(_filters?.display_properties);
|
||||
const kanbanFilters = {
|
||||
group_by: [],
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
TIssueLink,
|
||||
TIssueReaction,
|
||||
TIssueRelationTypes,
|
||||
TIssueDetailWidget,
|
||||
} from "@plane/types";
|
||||
import { IIssueRootStore } from "../root.store";
|
||||
import { IIssueActivityStore, IssueActivityStore, IIssueActivityStoreActions, TActivityLoader } from "./activity.store";
|
||||
@@ -50,6 +51,8 @@ export interface IIssueDetail
|
||||
IIssueCommentReactionStoreActions {
|
||||
// observables
|
||||
peekIssue: TPeekIssue | undefined;
|
||||
activeIssueDetailWidgets: TIssueDetailWidget[];
|
||||
lastWidgetAction: TIssueDetailWidget | null;
|
||||
isCreateIssueModalOpen: boolean;
|
||||
isIssueLinkModalOpen: boolean;
|
||||
isParentIssueModalOpen: string | null;
|
||||
@@ -72,6 +75,9 @@ export interface IIssueDetail
|
||||
toggleRelationModal: (issueId: string | null, relationType: TIssueRelationTypes | null) => void;
|
||||
toggleSubIssuesModal: (value: string | null) => void;
|
||||
toggleDeleteAttachmentModal: (attachmentId: string | null) => void;
|
||||
setActiveIssueDetailWidgets: (state: TIssueDetailWidget[]) => void;
|
||||
setLastWidgetAction: (action: TIssueDetailWidget) => void;
|
||||
toggleActiveIssueDetailWidget: (state: TIssueDetailWidget) => void;
|
||||
// store
|
||||
rootIssueStore: IIssueRootStore;
|
||||
issue: IIssueStore;
|
||||
@@ -89,6 +95,8 @@ export interface IIssueDetail
|
||||
export class IssueDetail implements IIssueDetail {
|
||||
// observables
|
||||
peekIssue: TPeekIssue | undefined = undefined;
|
||||
activeIssueDetailWidgets: TIssueDetailWidget[] = ["sub-issues"];
|
||||
lastWidgetAction: TIssueDetailWidget | null = null;
|
||||
isCreateIssueModalOpen: boolean = false;
|
||||
isIssueLinkModalOpen: boolean = false;
|
||||
isParentIssueModalOpen: string | null = null;
|
||||
@@ -122,6 +130,8 @@ export class IssueDetail implements IIssueDetail {
|
||||
isRelationModalOpen: observable.ref,
|
||||
isSubIssuesModalOpen: observable.ref,
|
||||
attachmentDeleteModalId: observable.ref,
|
||||
activeIssueDetailWidgets: observable.ref,
|
||||
lastWidgetAction: observable.ref,
|
||||
// computed
|
||||
isAnyModalOpen: computed,
|
||||
// action
|
||||
@@ -134,6 +144,9 @@ export class IssueDetail implements IIssueDetail {
|
||||
toggleRelationModal: action,
|
||||
toggleSubIssuesModal: action,
|
||||
toggleDeleteAttachmentModal: action,
|
||||
setActiveIssueDetailWidgets: action,
|
||||
setLastWidgetAction: action,
|
||||
toggleActiveIssueDetailWidget: action,
|
||||
});
|
||||
|
||||
// store
|
||||
@@ -178,6 +191,18 @@ export class IssueDetail implements IIssueDetail {
|
||||
(this.isRelationModalOpen = { issueId, relationType });
|
||||
toggleSubIssuesModal = (issueId: string | null) => (this.isSubIssuesModalOpen = issueId);
|
||||
toggleDeleteAttachmentModal = (attachmentId: string | null) => (this.attachmentDeleteModalId = attachmentId);
|
||||
setActiveIssueDetailWidgets = (state: TIssueDetailWidget[]) => {
|
||||
this.activeIssueDetailWidgets = state;
|
||||
if (this.lastWidgetAction) this.lastWidgetAction = null;
|
||||
};
|
||||
setLastWidgetAction = (action: TIssueDetailWidget) => {
|
||||
this.activeIssueDetailWidgets = [action];
|
||||
};
|
||||
toggleActiveIssueDetailWidget = (state: TIssueDetailWidget) => {
|
||||
if (this.activeIssueDetailWidgets && this.activeIssueDetailWidgets.includes(state))
|
||||
this.activeIssueDetailWidgets = this.activeIssueDetailWidgets.filter((s) => s !== state);
|
||||
else this.activeIssueDetailWidgets = [state, ...this.activeIssueDetailWidgets];
|
||||
};
|
||||
|
||||
// issue
|
||||
fetchIssue = async (
|
||||
|
||||
Reference in New Issue
Block a user