Merge pull request #605 from makeplane/sync/ce-ee

sync: community changes
This commit is contained in:
sriram veeraghanta
2024-07-11 14:43:42 +05:30
committed by GitHub
39 changed files with 443 additions and 322 deletions

View File

@@ -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(

View File

@@ -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,
)

View File

@@ -97,3 +97,9 @@ export type TBulkOperationsPayload = {
issue_ids: string[];
properties: Partial<TBulkIssueProperties>;
};
export type TIssueDetailWidget =
| "sub-issues"
| "relations"
| "links"
| "attachments";

View File

@@ -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 && (

View File

@@ -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" />

View File

@@ -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>
</>

View File

@@ -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" />

View File

@@ -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" />

View File

@@ -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" />

View File

@@ -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" />

View File

@@ -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" />

View File

@@ -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
/>
);

View File

@@ -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]
);

View File

@@ -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>
);
};
});

View File

@@ -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}>

View File

@@ -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>
);
};
});

View File

@@ -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

View File

@@ -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>
);
};
});

View File

@@ -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) => {

View File

@@ -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>
);
};
});

View File

@@ -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);

View File

@@ -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}
/>

View File

@@ -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}
/>

View File

@@ -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}
/>

View File

@@ -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">

View File

@@ -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>
);
});

View File

@@ -1,2 +1,2 @@
export * from "./root";
export * from "./menu";
export * from "./applied-filter";

View File

@@ -0,0 +1,2 @@
export * from "./root";
export * from "./menu-option-item";

View File

@@ -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>
);
});

View File

@@ -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} />}
/>
);
});

View File

@@ -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>
);
});

View File

@@ -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>
);
});

View File

@@ -0,0 +1,2 @@
export * from "./root";
export * from "./menu-item";

View File

@@ -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" />;
});

View File

@@ -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} />}
/>
);
});

View File

@@ -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" />

View File

@@ -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",

View File

@@ -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: [],

View File

@@ -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 (