[WEB-2388] dev: workspace draft issues (#5772)
* chore: workspace draft page added * chore: workspace draft issues services added * chore: workspace draft issue store added * chore: workspace draft issue filter store added * chore: issue rendering * conflicts: resolved merge conflicts * conflicts: handled draft issue store * chore: draft issue modal * chore: code optimisation * chore: ui changes * chore: workspace draft store and modal updated * chore: workspace draft issue component added * chore: updated store and workflow in draft issues * chore: updated issue draft store * chore: updated issue type cleanup in components * chore: code refactor * fix: build error * fix: quick actions * fix: update mutation * fix: create update modal * chore: commented project draft issue code --------- Co-authored-by: gurusainath <gurusainath007@gmail.com> Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
This commit is contained in:
parent
e9158f820f
commit
332d2d5c68
45 changed files with 1895 additions and 190 deletions
|
|
@ -44,18 +44,11 @@ from plane.utils.issue_filters import issue_filters
|
|||
|
||||
|
||||
class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||
|
||||
model = DraftIssue
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
)
|
||||
def list(self, request, slug):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
issues = (
|
||||
DraftIssue.objects.filter(workspace__slug=slug)
|
||||
.filter(created_by=request.user)
|
||||
def get_queryset(self):
|
||||
return (
|
||||
DraftIssue.objects.filter(workspace__slug=self.kwargs.get("slug"))
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related(
|
||||
"assignees", "labels", "draft_issue_module__module"
|
||||
|
|
@ -91,6 +84,17 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
|||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
)
|
||||
).distinct()
|
||||
|
||||
@method_decorator(gzip_page)
|
||||
@allow_permission(
|
||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
||||
)
|
||||
def list(self, request, slug):
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
issues = (
|
||||
self.get_queryset()
|
||||
.filter(created_by=request.user)
|
||||
.order_by("-created_at")
|
||||
)
|
||||
|
||||
|
|
@ -120,7 +124,34 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
|||
)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||||
issue = (
|
||||
self.get_queryset()
|
||||
.filter(pk=serializer.data.get("id"))
|
||||
.values(
|
||||
"id",
|
||||
"name",
|
||||
"state_id",
|
||||
"sort_order",
|
||||
"completed_at",
|
||||
"estimate_point",
|
||||
"priority",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"project_id",
|
||||
"parent_id",
|
||||
"cycle_id",
|
||||
"module_ids",
|
||||
"label_ids",
|
||||
"assignee_ids",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
return Response(issue, status=status.HTTP_201_CREATED)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@allow_permission(
|
||||
|
|
@ -131,45 +162,7 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
|||
)
|
||||
def partial_update(self, request, slug, pk):
|
||||
issue = (
|
||||
DraftIssue.objects.filter(workspace__slug=slug)
|
||||
.filter(pk=pk)
|
||||
.filter(created_by=request.user)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related(
|
||||
"assignees", "labels", "draft_issue_module__module"
|
||||
)
|
||||
.annotate(cycle_id=F("draft_issue_cycle__cycle_id"))
|
||||
.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(
|
||||
"draft_issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=~Q(draft_issue_module__module_id__isnull=True)
|
||||
& Q(
|
||||
draft_issue_module__module__archived_at__isnull=True
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
)
|
||||
.first()
|
||||
self.get_queryset().filter(pk=pk, created_by=request.user).first()
|
||||
)
|
||||
|
||||
if not issue:
|
||||
|
|
@ -202,46 +195,8 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
|||
)
|
||||
def retrieve(self, request, slug, pk=None):
|
||||
issue = (
|
||||
DraftIssue.objects.filter(workspace__slug=slug)
|
||||
.filter(pk=pk)
|
||||
.filter(created_by=request.user)
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related(
|
||||
"assignees", "labels", "draft_issue_module__module"
|
||||
self.get_queryset().filter(pk=pk, created_by=request.user).first()
|
||||
)
|
||||
.annotate(cycle_id=F("draft_issue_cycle__cycle_id"))
|
||||
.filter(pk=pk)
|
||||
.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(
|
||||
"draft_issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=~Q(draft_issue_module__module_id__isnull=True)
|
||||
& Q(
|
||||
draft_issue_module__module__archived_at__isnull=True
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
)
|
||||
).first()
|
||||
|
||||
if not issue:
|
||||
return Response(
|
||||
|
|
@ -268,42 +223,7 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
|||
level="WORKSPACE",
|
||||
)
|
||||
def create_draft_to_issue(self, request, slug, draft_id):
|
||||
draft_issue = (
|
||||
DraftIssue.objects.filter(workspace__slug=slug, pk=draft_id)
|
||||
.annotate(cycle_id=F("draft_issue_cycle__cycle_id"))
|
||||
.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(
|
||||
"draft_issue_module__module_id",
|
||||
distinct=True,
|
||||
filter=~Q(draft_issue_module__module_id__isnull=True)
|
||||
& Q(
|
||||
draft_issue_module__module__archived_at__isnull=True
|
||||
),
|
||||
),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
),
|
||||
)
|
||||
.select_related("project", "workspace")
|
||||
.first()
|
||||
)
|
||||
draft_issue = self.get_queryset().filter(pk=draft_id).first()
|
||||
|
||||
if not draft_issue.project_id:
|
||||
return Response(
|
||||
|
|
|
|||
|
|
@ -150,7 +150,7 @@ class OffsetPaginator:
|
|||
raise BadPaginationError("Pagination offset cannot be negative")
|
||||
|
||||
results = queryset[offset:stop]
|
||||
|
||||
print(limit, "limit")
|
||||
if cursor.value != limit:
|
||||
results = results[-(limit + 1) :]
|
||||
|
||||
|
|
@ -761,7 +761,7 @@ class BasePaginator:
|
|||
):
|
||||
"""Paginate the request"""
|
||||
per_page = self.get_per_page(request, default_per_page, max_per_page)
|
||||
|
||||
print(per_page, "per_page")
|
||||
# Convert the cursor value to integer and float from string
|
||||
input_cursor = None
|
||||
try:
|
||||
|
|
@ -788,6 +788,7 @@ class BasePaginator:
|
|||
paginator = paginator_cls(**paginator_kwargs)
|
||||
|
||||
try:
|
||||
print(per_page, "per_page 2")
|
||||
cursor_result = paginator.get_result(
|
||||
limit=per_page, cursor=input_cursor
|
||||
)
|
||||
|
|
|
|||
1
packages/types/src/index.d.ts
vendored
1
packages/types/src/index.d.ts
vendored
|
|
@ -29,3 +29,4 @@ export * from "./pragmatic";
|
|||
export * from "./publish";
|
||||
export * from "./workspace-notifications";
|
||||
export * from "./favorite";
|
||||
export * from "./workspace-draft-issues/base";
|
||||
|
|
|
|||
1
packages/types/src/issues/base.d.ts
vendored
1
packages/types/src/issues/base.d.ts
vendored
|
|
@ -10,6 +10,7 @@ export * from "./issue_relation";
|
|||
export * from "./issue_sub_issues";
|
||||
export * from "./activity/base";
|
||||
|
||||
|
||||
export type TLoader =
|
||||
| "init-loader"
|
||||
| "mutation"
|
||||
|
|
|
|||
61
packages/types/src/workspace-draft-issues/base.d.ts
vendored
Normal file
61
packages/types/src/workspace-draft-issues/base.d.ts
vendored
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import { TIssuePriorities } from "../issues";
|
||||
|
||||
export type TWorkspaceDraftIssue = {
|
||||
id: string;
|
||||
name: string;
|
||||
sort_order: number;
|
||||
|
||||
state_id: string | undefined;
|
||||
priority: TIssuePriorities | undefined;
|
||||
label_ids: string[];
|
||||
assignee_ids: string[];
|
||||
estimate_point: string | undefined;
|
||||
|
||||
project_id: string | undefined;
|
||||
parent_id: string | undefined;
|
||||
cycle_id: string | undefined;
|
||||
module_ids: string[] | undefined;
|
||||
|
||||
start_date: string | undefined;
|
||||
target_date: string | undefined;
|
||||
completed_at: string | undefined;
|
||||
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
created_by: string;
|
||||
updated_by: string;
|
||||
|
||||
is_draft: boolean;
|
||||
};
|
||||
|
||||
export type TWorkspaceDraftPaginationInfo<T> = {
|
||||
next_cursor: string | undefined;
|
||||
prev_cursor: string | undefined;
|
||||
next_page_results: boolean | undefined;
|
||||
prev_page_results: boolean | undefined;
|
||||
total_pages: number | undefined;
|
||||
count: number | undefined; // current paginated results count
|
||||
total_count: number | undefined; // total available results count
|
||||
total_results: number | undefined;
|
||||
results: T[] | undefined;
|
||||
extra_stats: string | undefined;
|
||||
grouped_by: string | undefined;
|
||||
sub_grouped_by: string | undefined;
|
||||
};
|
||||
|
||||
export type TWorkspaceDraftQueryParams = {
|
||||
per_page: number;
|
||||
cursor: string;
|
||||
};
|
||||
|
||||
export type TWorkspaceDraftIssueLoader =
|
||||
| "init-loader"
|
||||
| "empty-state"
|
||||
| "mutation"
|
||||
| "pagination"
|
||||
| "loaded"
|
||||
| "create"
|
||||
| "update"
|
||||
| "delete"
|
||||
| "move"
|
||||
| undefined;
|
||||
62
web/app/[workspaceSlug]/(projects)/drafts/header.tsx
Normal file
62
web/app/[workspaceSlug]/(projects)/drafts/header.tsx
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
"use client";
|
||||
|
||||
import { FC, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { PenSquare } from "lucide-react";
|
||||
// ui
|
||||
import { Breadcrumbs, Button, Header } from "@plane/ui";
|
||||
// components
|
||||
import { BreadcrumbLink } from "@/components/common";
|
||||
import { CreateUpdateIssueModal } from "@/components/issues";
|
||||
// constants
|
||||
import { EIssuesStoreType } from "@/constants/issue";
|
||||
// hooks
|
||||
import { useUserPermissions } from "@/hooks/store";
|
||||
// plane-web
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||
|
||||
export const WorkspaceDraftHeader: FC = observer(() => {
|
||||
// state
|
||||
const [isDraftIssueModalOpen, setIsDraftIssueModalOpen] = useState(false);
|
||||
// store hooks
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
|
||||
// check if user is authorized to create draft issue
|
||||
const isAuthorizedUser = allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
EUserPermissionsLevel.WORKSPACE
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={isDraftIssueModalOpen}
|
||||
storeType={EIssuesStoreType.WORKSPACE_DRAFT}
|
||||
onClose={() => setIsDraftIssueModalOpen(false)}
|
||||
isDraft
|
||||
/>
|
||||
<Header>
|
||||
<Header.LeftItem>
|
||||
<Breadcrumbs>
|
||||
<Breadcrumbs.BreadcrumbItem
|
||||
type="text"
|
||||
link={<BreadcrumbLink label={`Draft`} icon={<PenSquare className="h-4 w-4 text-custom-text-300" />} />}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
</Header.LeftItem>
|
||||
|
||||
<Header.RightItem>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="items-center gap-1"
|
||||
onClick={() => setIsDraftIssueModalOpen(true)}
|
||||
disabled={!isAuthorizedUser}
|
||||
>
|
||||
Draft <span className="hidden sm:inline-block">issue</span>
|
||||
</Button>
|
||||
</Header.RightItem>
|
||||
</Header>
|
||||
</>
|
||||
);
|
||||
});
|
||||
13
web/app/[workspaceSlug]/(projects)/drafts/layout.tsx
Normal file
13
web/app/[workspaceSlug]/(projects)/drafts/layout.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
"use client";
|
||||
|
||||
import { AppHeader, ContentWrapper } from "@/components/core";
|
||||
import { WorkspaceDraftHeader } from "./header";
|
||||
|
||||
export default function WorkspaceDraftLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<AppHeader header={<WorkspaceDraftHeader />} />
|
||||
<ContentWrapper>{children}</ContentWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
27
web/app/[workspaceSlug]/(projects)/drafts/page.tsx
Normal file
27
web/app/[workspaceSlug]/(projects)/drafts/page.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
"use client";
|
||||
|
||||
import { useParams } from "next/navigation";
|
||||
// components
|
||||
import { PageHead } from "@/components/core";
|
||||
import { WorkspaceDraftIssuesRoot } from "@/components/issues/workspace-draft";
|
||||
|
||||
const WorkspaceDraftPage = () => {
|
||||
// router
|
||||
const { workspaceSlug: routeWorkspaceSlug } = useParams();
|
||||
const pageTitle = "Workspace Draft";
|
||||
|
||||
// derived values
|
||||
const workspaceSlug = (routeWorkspaceSlug as string) || undefined;
|
||||
|
||||
if (!workspaceSlug) return null;
|
||||
return (
|
||||
<>
|
||||
<PageHead title={pageTitle} />
|
||||
<div className="relative h-full w-full overflow-hidden overflow-y-auto">
|
||||
<WorkspaceDraftIssuesRoot workspaceSlug={workspaceSlug} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default WorkspaceDraftPage;
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import React, { useCallback } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams } from "next/navigation";
|
||||
import { WorkspaceDraftIssueQuickActions } from "@/components/issues/issue-layouts/quick-action-dropdowns";
|
||||
import { IssuePeekOverview } from "@/components/issues/peek-overview";
|
||||
import { EIssuesStoreType } from "@/constants/issue";
|
||||
import { useUserPermissions } from "@/hooks/store";
|
||||
import { IssuesStoreContext } from "@/hooks/use-issue-layout-store";
|
||||
import { useWorkspaceIssueProperties } from "@/hooks/use-workspace-issue-properties";
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||
import { BaseListRoot } from "../../../list/base-list-root";
|
||||
|
||||
export const WorkspaceDraftIssueLayoutRoot = observer(() => {
|
||||
// router
|
||||
const { workspaceSlug } = useParams();
|
||||
|
||||
//swr hook for fetching issue properties
|
||||
useWorkspaceIssueProperties(workspaceSlug);
|
||||
// store
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
|
||||
const canEditProperties = useCallback(
|
||||
(projectId: string | undefined) => {
|
||||
if (!projectId) return false;
|
||||
return allowPermissions(
|
||||
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
EUserPermissionsLevel.PROJECT,
|
||||
workspaceSlug.toString(),
|
||||
projectId
|
||||
);
|
||||
},
|
||||
[workspaceSlug, allowPermissions]
|
||||
);
|
||||
|
||||
return (
|
||||
<IssuesStoreContext.Provider value={EIssuesStoreType.WORKSPACE_DRAFT}>
|
||||
<div className="relative flex h-full w-full flex-col overflow-hidden">
|
||||
<div className="relative h-full w-full overflow-auto">
|
||||
<BaseListRoot
|
||||
QuickActions={WorkspaceDraftIssueQuickActions}
|
||||
canEditPropertiesBasedOnProject={canEditProperties}
|
||||
/>
|
||||
<IssuePeekOverview is_draft />
|
||||
</div>
|
||||
</div>
|
||||
</IssuesStoreContext.Provider>
|
||||
);
|
||||
});
|
||||
|
|
@ -25,7 +25,8 @@ type ListStoreType =
|
|||
| EIssuesStoreType.PROJECT_VIEW
|
||||
| EIssuesStoreType.DRAFT
|
||||
| EIssuesStoreType.PROFILE
|
||||
| EIssuesStoreType.ARCHIVED;
|
||||
| EIssuesStoreType.ARCHIVED
|
||||
| EIssuesStoreType.WORKSPACE_DRAFT;
|
||||
interface IBaseListRoot {
|
||||
QuickActions: FC<IQuickActionProps>;
|
||||
addIssuesToView?: (issueIds: string[]) => Promise<any>;
|
||||
|
|
@ -61,8 +62,9 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
|
|||
const showEmptyGroup = displayFilters?.show_empty_groups ?? false;
|
||||
|
||||
const { workspaceSlug, projectId } = useParams();
|
||||
const {updateFilters} = useIssuesActions(storeType);
|
||||
const collapsedGroups = issuesFilter?.issueFilters?.kanbanFilters || { group_by: [], sub_group_by: [] } as TIssueKanbanFilters;
|
||||
const { updateFilters } = useIssuesActions(storeType);
|
||||
const collapsedGroups =
|
||||
issuesFilter?.issueFilters?.kanbanFilters || ({ group_by: [], sub_group_by: [] } as TIssueKanbanFilters);
|
||||
|
||||
useEffect(() => {
|
||||
fetchIssues("init-loader", { canGroup: true, perPageCount: group_by ? 50 : 100 }, viewId);
|
||||
|
|
@ -122,15 +124,14 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
|
|||
} else {
|
||||
collapsedGroups.push(value);
|
||||
}
|
||||
updateFilters(projectId?.toString() ?? "", EIssueFilterType.KANBAN_FILTERS,
|
||||
{ group_by: collapsedGroups } as TIssueKanbanFilters
|
||||
);
|
||||
updateFilters(projectId?.toString() ?? "", EIssueFilterType.KANBAN_FILTERS, {
|
||||
group_by: collapsedGroups,
|
||||
} as TIssueKanbanFilters);
|
||||
}
|
||||
},
|
||||
[workspaceSlug, issuesFilter, projectId, updateFilters]
|
||||
);
|
||||
|
||||
|
||||
return (
|
||||
<IssueLayoutHOC layout={EIssueLayoutTypes.LIST}>
|
||||
<div className={`relative size-full bg-custom-background-90`}>
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export interface IList {
|
|||
isCompletedCycle?: boolean;
|
||||
loadMoreIssues: (groupId?: string) => void;
|
||||
handleCollapsedGroups: (value: string) => void;
|
||||
collapsedGroups : TIssueKanbanFilters;
|
||||
collapsedGroups: TIssueKanbanFilters;
|
||||
}
|
||||
|
||||
export const List: React.FC<IList> = observer((props) => {
|
||||
|
|
@ -71,7 +71,7 @@ export const List: React.FC<IList> = observer((props) => {
|
|||
isCompletedCycle = false,
|
||||
loadMoreIssues,
|
||||
handleCollapsedGroups,
|
||||
collapsedGroups
|
||||
collapsedGroups,
|
||||
} = props;
|
||||
|
||||
const storeType = useIssueStoreType();
|
||||
|
|
@ -133,7 +133,6 @@ export const List: React.FC<IList> = observer((props) => {
|
|||
} else {
|
||||
entities = orderedGroups;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative size-full flex flex-col">
|
||||
{groups && (
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export interface IQuickActionProps {
|
|||
handleRemoveFromView?: () => Promise<void>;
|
||||
handleArchive?: () => Promise<void>;
|
||||
handleRestore?: () => Promise<void>;
|
||||
handleMoveToIssues?: () => Promise<void>;
|
||||
customActionButton?: React.ReactElement;
|
||||
portalElement?: HTMLDivElement | null;
|
||||
readOnly?: boolean;
|
||||
|
|
|
|||
|
|
@ -37,9 +37,7 @@ import { WithDisplayPropertiesHOC } from "./with-display-properties-HOC";
|
|||
|
||||
export interface IIssueProperties {
|
||||
issue: TIssue;
|
||||
updateIssue:
|
||||
| ((projectId: string | null, issueId: string, data: Partial<TIssue>) => Promise<void>)
|
||||
| undefined;
|
||||
updateIssue: ((projectId: string | null, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
|
||||
displayProperties: IIssueDisplayProperties | undefined;
|
||||
isReadOnly: boolean;
|
||||
className: string;
|
||||
|
|
|
|||
|
|
@ -4,3 +4,4 @@ export * from "./project-issue";
|
|||
export * from "./archived-issue";
|
||||
export * from "./draft-issue";
|
||||
export * from "./all-issue";
|
||||
export * from "../../workspace-draft/quick-action";
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
|||
const { fetchModuleDetails } = useModule();
|
||||
const { issues } = useIssues(storeType);
|
||||
const { issues: projectIssues } = useIssues(EIssuesStoreType.PROJECT);
|
||||
const { issues: draftIssues } = useIssues(EIssuesStoreType.DRAFT);
|
||||
const { issues: draftIssues } = useIssues(EIssuesStoreType.WORKSPACE_DRAFT);
|
||||
const { fetchIssue } = useIssueDetail();
|
||||
const { handleCreateUpdatePropertyValues } = useIssueModal();
|
||||
// pathname
|
||||
|
|
@ -151,10 +151,9 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
|||
|
||||
try {
|
||||
let response;
|
||||
|
||||
// if draft issue, use draft issue store to create issue
|
||||
if (is_draft_issue) {
|
||||
response = await draftIssues.createIssue(workspaceSlug.toString(), payload.project_id, payload);
|
||||
response = await draftIssues.createIssue(workspaceSlug.toString(), payload);
|
||||
}
|
||||
// if cycle id in payload does not match the cycleId in url
|
||||
// or if the moduleIds in Payload does not match the moduleId in url
|
||||
|
|
@ -213,8 +212,8 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
|||
payload: { ...response, state: "SUCCESS" },
|
||||
path: pathname,
|
||||
});
|
||||
!createMore && handleClose();
|
||||
if (createMore) issueTitleRef && issueTitleRef?.current?.focus();
|
||||
if (!createMore) handleClose();
|
||||
if (createMore && issueTitleRef) issueTitleRef?.current?.focus();
|
||||
setDescription("<p></p>");
|
||||
setChangesMade(null);
|
||||
return response;
|
||||
|
|
@ -237,9 +236,8 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
|||
if (!workspaceSlug || !payload.project_id || !data?.id) return;
|
||||
|
||||
try {
|
||||
isDraft
|
||||
? await draftIssues.updateIssue(workspaceSlug.toString(), payload.project_id, data.id, payload)
|
||||
: updateIssue && (await updateIssue(payload.project_id, data.id, payload));
|
||||
if (isDraft) await draftIssues.updateIssue(workspaceSlug.toString(), data.id, payload);
|
||||
else if (updateIssue) await updateIssue(payload.project_id, data.id, payload);
|
||||
|
||||
// add other property values
|
||||
await handleCreateUpdatePropertyValues({
|
||||
|
|
@ -260,6 +258,7 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
|||
});
|
||||
handleClose();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
|
|
@ -314,7 +313,7 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
|||
issueTitleRef={issueTitleRef}
|
||||
onChange={handleFormChange}
|
||||
onClose={handleClose}
|
||||
onSubmit={handleFormSubmit}
|
||||
onSubmit={(payload) => handleFormSubmit(payload, isDraft)}
|
||||
projectId={activeProjectId}
|
||||
isCreateMoreToggleEnabled={createMore}
|
||||
onCreateMoreToggleChange={handleCreateMoreToggleChange}
|
||||
|
|
@ -332,7 +331,7 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
|||
onClose={() => handleClose(false)}
|
||||
isCreateMoreToggleEnabled={createMore}
|
||||
onCreateMoreToggleChange={handleCreateMoreToggleChange}
|
||||
onSubmit={handleFormSubmit}
|
||||
onSubmit={(payload) => handleFormSubmit(payload, isDraft)}
|
||||
projectId={activeProjectId}
|
||||
isDraft={isDraft}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { isEmptyHtmlString } from "@/helpers/string.helper";
|
|||
import { useIssueModal } from "@/hooks/context/use-issue-modal";
|
||||
import { useEventTracker } from "@/hooks/store";
|
||||
// services
|
||||
import { IssueDraftService } from "@/services/issue";
|
||||
import workspaceDraftService from "@/services/issue/workspace_draft.service";
|
||||
// local components
|
||||
import { IssueFormRoot } from "./form";
|
||||
|
||||
|
|
@ -33,8 +33,6 @@ export interface DraftIssueProps {
|
|||
isDraft: boolean;
|
||||
}
|
||||
|
||||
const issueDraftService = new IssueDraftService();
|
||||
|
||||
export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
|
||||
const {
|
||||
changesMade,
|
||||
|
|
@ -95,10 +93,11 @@ export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
|
|||
const payload = {
|
||||
...changesMade,
|
||||
name: changesMade?.name && changesMade?.name?.trim() !== "" ? changesMade.name?.trim() : "Untitled",
|
||||
project_id: projectId,
|
||||
};
|
||||
|
||||
const response = await issueDraftService
|
||||
.createDraftIssue(workspaceSlug.toString(), projectId.toString(), payload)
|
||||
const response = await workspaceDraftService
|
||||
.createIssue(workspaceSlug.toString(), payload)
|
||||
.then((res) => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
|
|
|
|||
|
|
@ -266,7 +266,9 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
|||
)}
|
||||
<form onSubmit={handleSubmit((data) => handleFormSubmit(data))}>
|
||||
<div className="p-5">
|
||||
<h3 className="text-xl font-medium text-custom-text-200 pb-2">{data?.id ? "Update" : "Create new"} issue</h3>
|
||||
<h3 className="text-xl font-medium text-custom-text-200 pb-2">
|
||||
{data?.id ? "Update" : isDraft ? "Create draft" : "Create new"} issue
|
||||
</h3>
|
||||
{/* Disable project selection if editing an issue */}
|
||||
<div className="flex items-center pt-2 pb-4 gap-x-1">
|
||||
<IssueProjectSelect
|
||||
|
|
@ -397,31 +399,6 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
|||
>
|
||||
Discard
|
||||
</Button>
|
||||
{isDraft && (
|
||||
<>
|
||||
{data?.id ? (
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
size="sm"
|
||||
loading={isSubmitting}
|
||||
onClick={handleSubmit((data) => handleFormSubmit({ ...data, is_draft: false }))}
|
||||
tabIndex={getIndex("draft_button")}
|
||||
>
|
||||
{isSubmitting ? "Moving" : "Move from draft"}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="neutral-primary"
|
||||
size="sm"
|
||||
loading={isSubmitting}
|
||||
onClick={handleSubmit((data) => handleFormSubmit(data, true))}
|
||||
tabIndex={getIndex("draft_button")}
|
||||
>
|
||||
{isSubmitting ? "Saving" : "Save as draft"}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
|
|
@ -430,7 +407,15 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
|||
loading={isSubmitting}
|
||||
tabIndex={isDraft ? getIndex("submit_button") : getIndex("draft_button")}
|
||||
>
|
||||
{data?.id ? (isSubmitting ? "Updating" : "Update") : isSubmitting ? "Creating" : "Create"}
|
||||
{data?.id
|
||||
? isSubmitting
|
||||
? "Updating"
|
||||
: "Update"
|
||||
: isSubmitting
|
||||
? "Creating"
|
||||
: isDraft
|
||||
? "Create draft issue"
|
||||
: "Create"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
105
web/core/components/issues/workspace-draft/delete-modal.tsx
Normal file
105
web/core/components/issues/workspace-draft/delete-modal.tsx
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
// types
|
||||
import { TWorkspaceDraftIssue } from "@plane/types";
|
||||
// ui
|
||||
import { AlertModalCore, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// constants
|
||||
import { PROJECT_ERROR_MESSAGES } from "@/constants/project";
|
||||
// hooks
|
||||
import { useIssues, useProject, useUser, useUserPermissions } from "@/hooks/store";
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
dataId?: string | null | undefined;
|
||||
data?: TWorkspaceDraftIssue;
|
||||
isSubIssue?: boolean;
|
||||
onSubmit?: () => Promise<void>;
|
||||
};
|
||||
|
||||
export const WorkspaceDraftIssueDeleteIssueModal: React.FC<Props> = (props) => {
|
||||
const { dataId, data, isOpen, handleClose, isSubIssue = false, onSubmit } = props;
|
||||
// states
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
// store hooks
|
||||
const { issueMap } = useIssues();
|
||||
const { getProjectById } = useProject();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
|
||||
const { data: currentUser } = useUser();
|
||||
|
||||
// derived values
|
||||
const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT);
|
||||
|
||||
useEffect(() => {
|
||||
setIsDeleting(false);
|
||||
}, [isOpen]);
|
||||
|
||||
if (!dataId && !data) return null;
|
||||
|
||||
// derived values
|
||||
const issue = data ? data : issueMap[dataId!];
|
||||
const projectDetails = getProjectById(issue?.project_id);
|
||||
const isIssueCreator = issue?.created_by === currentUser?.id;
|
||||
const authorized = isIssueCreator || canPerformProjectAdminActions;
|
||||
|
||||
const onClose = () => {
|
||||
setIsDeleting(false);
|
||||
handleClose();
|
||||
};
|
||||
|
||||
const handleIssueDelete = async () => {
|
||||
setIsDeleting(true);
|
||||
|
||||
if (!authorized) {
|
||||
setToast({
|
||||
title: PROJECT_ERROR_MESSAGES.permissionError.title,
|
||||
type: TOAST_TYPE.ERROR,
|
||||
message: PROJECT_ERROR_MESSAGES.permissionError.message,
|
||||
});
|
||||
onClose();
|
||||
return;
|
||||
}
|
||||
if (onSubmit)
|
||||
await onSubmit()
|
||||
.then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Success!",
|
||||
message: `${isSubIssue ? "Sub-issue" : "Issue"} deleted successfully`,
|
||||
});
|
||||
onClose();
|
||||
})
|
||||
.catch((errors) => {
|
||||
const isPermissionError = errors?.error === "Only admin or creator can delete the issue";
|
||||
const currentError = isPermissionError
|
||||
? PROJECT_ERROR_MESSAGES.permissionError
|
||||
: PROJECT_ERROR_MESSAGES.issueDeleteError;
|
||||
setToast({
|
||||
title: currentError.title,
|
||||
type: TOAST_TYPE.ERROR,
|
||||
message: currentError.message,
|
||||
});
|
||||
})
|
||||
.finally(() => onClose());
|
||||
};
|
||||
|
||||
return (
|
||||
<AlertModalCore
|
||||
handleClose={onClose}
|
||||
handleSubmit={handleIssueDelete}
|
||||
isSubmitting={isDeleting}
|
||||
isOpen={isOpen}
|
||||
title="Delete issue"
|
||||
content={
|
||||
<>
|
||||
Are you sure you want to delete issue{" "}
|
||||
<span className="break-words font-medium text-custom-text-100">{projectDetails?.identifier}</span>
|
||||
{""}? All of the data related to the issue will be permanently removed. This action cannot be undone.
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
127
web/core/components/issues/workspace-draft/draft-issue-block.tsx
Normal file
127
web/core/components/issues/workspace-draft/draft-issue-block.tsx
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
"use client";
|
||||
import React, { FC, useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// ui
|
||||
import { Row, Tooltip } from "@plane/ui";
|
||||
// helper
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useAppTheme, useProject, useWorkspaceDraftIssues } from "@/hooks/store";
|
||||
// plane-web components
|
||||
import { IdentifierText } from "@/plane-web/components/issues";
|
||||
// local components
|
||||
import { WorkspaceDraftIssueQuickActions } from "../issue-layouts";
|
||||
import { DraftIssueProperties } from "./draft-issue-properties";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
issueId: string;
|
||||
};
|
||||
|
||||
export const DraftIssueBlock: FC<Props> = observer((props) => {
|
||||
// props
|
||||
const { workspaceSlug, issueId } = props;
|
||||
// hooks
|
||||
const { getIssueById, updateIssue, deleteIssue, moveIssue } = useWorkspaceDraftIssues();
|
||||
const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme();
|
||||
const { getProjectIdentifierById } = useProject();
|
||||
// ref
|
||||
const issueRef = useRef<HTMLDivElement | null>(null);
|
||||
// derived values
|
||||
const issue = getIssueById(issueId);
|
||||
const projectIdentifier = (issue && issue.project_id && getProjectIdentifierById(issue.project_id)) || undefined;
|
||||
if (!issue || !projectIdentifier) return null;
|
||||
|
||||
return (
|
||||
<div id={`issue-${issue.id}`} className=" relative border-b border-b-custom-border-200 w-full cursor-pointer">
|
||||
<Row
|
||||
ref={issueRef}
|
||||
className={cn(
|
||||
"group/list-block min-h-11 relative flex flex-col gap-3 bg-custom-background-100 hover:bg-custom-background-90 py-3 text-sm transition-colors border border-transparent last:border-b-transparent",
|
||||
{
|
||||
"md:flex-row md:items-center": isSidebarCollapsed,
|
||||
"lg:flex-row lg:items-center": !isSidebarCollapsed,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full truncate">
|
||||
<div className="flex flex-grow items-center gap-0.5 truncate">
|
||||
<div className="flex items-center gap-1">
|
||||
{/* {displayProperties && (displayProperties.key || displayProperties.issue_type) && ( */}
|
||||
<div className="flex-shrink-0">
|
||||
{issue.project_id && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<IdentifierText
|
||||
identifier={projectIdentifier}
|
||||
enableClickToCopyIdentifier
|
||||
textContainerClassName="text-xs font-medium text-custom-text-300"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* )} */}
|
||||
|
||||
{/* sub-issues chevron */}
|
||||
<div className="size-4 grid place-items-center flex-shrink-0" />
|
||||
</div>
|
||||
|
||||
<Tooltip
|
||||
tooltipContent={issue.name}
|
||||
// isMobile={isMobile}
|
||||
position="top-left"
|
||||
// disabled={isCurrentBlockDragging}
|
||||
renderByDefault={false}
|
||||
>
|
||||
<p className="w-full truncate cursor-pointer text-sm text-custom-text-100">{issue.name}</p>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{/* quick actions */}
|
||||
<div
|
||||
className={cn("block border border-custom-border-300 rounded", {
|
||||
"md:hidden": isSidebarCollapsed,
|
||||
"lg:hidden": !isSidebarCollapsed,
|
||||
})}
|
||||
>
|
||||
<WorkspaceDraftIssueQuickActions
|
||||
parentRef={issueRef}
|
||||
issue={issue}
|
||||
handleUpdate={async (data) => updateIssue(workspaceSlug, issueId, data)}
|
||||
handleDelete={async () => deleteIssue(workspaceSlug, issueId)}
|
||||
handleMoveToIssues={async () => moveIssue(workspaceSlug, issueId, issue)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-shrink-0 items-center gap-2">
|
||||
<DraftIssueProperties
|
||||
className={`relative flex flex-wrap ${isSidebarCollapsed ? "md:flex-grow md:flex-shrink-0" : "lg:flex-grow lg:flex-shrink-0"} items-center gap-2 whitespace-nowrap`}
|
||||
issue={issue}
|
||||
updateIssue={async (projectId, issueId, data) => {
|
||||
await updateIssue(workspaceSlug, issueId, data);
|
||||
}}
|
||||
activeLayout="List"
|
||||
/>
|
||||
<div
|
||||
className={cn("hidden", {
|
||||
"md:flex": isSidebarCollapsed,
|
||||
"lg:flex": !isSidebarCollapsed,
|
||||
})}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<WorkspaceDraftIssueQuickActions
|
||||
parentRef={issueRef}
|
||||
issue={issue}
|
||||
handleUpdate={async (data) => updateIssue(workspaceSlug, issueId, data)}
|
||||
handleDelete={async () => deleteIssue(workspaceSlug, issueId)}
|
||||
handleMoveToIssues={async () => moveIssue(workspaceSlug, issueId, issue)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,299 @@
|
|||
"use client";
|
||||
|
||||
import { useCallback, useMemo } from "react";
|
||||
import xor from "lodash/xor";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
// icons
|
||||
import { CalendarCheck2, CalendarClock } from "lucide-react";
|
||||
// types
|
||||
import { TIssue, TIssuePriorities, TWorkspaceDraftIssue } from "@plane/types";
|
||||
// components
|
||||
import {
|
||||
DateDropdown,
|
||||
EstimateDropdown,
|
||||
PriorityDropdown,
|
||||
MemberDropdown,
|
||||
ModuleDropdown,
|
||||
CycleDropdown,
|
||||
StateDropdown,
|
||||
} from "@/components/dropdowns";
|
||||
// constants
|
||||
import { ISSUE_UPDATED } from "@/constants/event-tracker";
|
||||
// helpers
|
||||
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
|
||||
import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper";
|
||||
// hooks
|
||||
import {
|
||||
useEventTracker,
|
||||
useLabel,
|
||||
useProjectState,
|
||||
useProject,
|
||||
useProjectEstimates,
|
||||
useWorkspaceDraftIssues,
|
||||
} from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// local components
|
||||
import { IssuePropertyLabels } from "../issue-layouts";
|
||||
|
||||
export interface IIssueProperties {
|
||||
issue: TWorkspaceDraftIssue;
|
||||
updateIssue:
|
||||
| ((projectId: string | null, issueId: string, data: Partial<TWorkspaceDraftIssue>) => Promise<void>)
|
||||
| undefined;
|
||||
className: string;
|
||||
activeLayout: string;
|
||||
}
|
||||
|
||||
export const DraftIssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
||||
const { issue, updateIssue, activeLayout, className } = props;
|
||||
// store hooks
|
||||
const { getProjectById } = useProject();
|
||||
const { labelMap } = useLabel();
|
||||
const { captureIssueEvent } = useEventTracker();
|
||||
const { addCycleToIssue, addModulesToIssue } = useWorkspaceDraftIssues();
|
||||
const { areEstimateEnabledByProjectId } = useProjectEstimates();
|
||||
const { getStateById } = useProjectState();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const projectDetails = getProjectById(issue.project_id);
|
||||
|
||||
// router
|
||||
const { workspaceSlug } = useParams();
|
||||
const pathname = usePathname();
|
||||
|
||||
const currentLayout = `${activeLayout} layout`;
|
||||
// derived values
|
||||
const stateDetails = getStateById(issue.state_id);
|
||||
|
||||
const issueOperations = useMemo(
|
||||
() => ({
|
||||
addModulesToIssue: async (moduleIds: string[]) => {
|
||||
if (!workspaceSlug || !issue.id) return;
|
||||
await addModulesToIssue(workspaceSlug.toString(), issue.id, moduleIds);
|
||||
},
|
||||
removeModulesFromIssue: async (moduleIds: string[]) => {
|
||||
if (!workspaceSlug || !issue.id) return;
|
||||
await addModulesToIssue(workspaceSlug.toString(), issue.id, moduleIds);
|
||||
},
|
||||
addIssueToCycle: async (cycleId: string) => {
|
||||
if (!workspaceSlug || !issue.id) return;
|
||||
await addCycleToIssue(workspaceSlug.toString(), issue.id, cycleId);
|
||||
},
|
||||
removeIssueFromCycle: async () => {
|
||||
if (!workspaceSlug || !issue.id) return;
|
||||
// TODO: To be checked
|
||||
await addCycleToIssue(workspaceSlug.toString(), issue.id, "");
|
||||
},
|
||||
}),
|
||||
[workspaceSlug, issue, addCycleToIssue, addModulesToIssue]
|
||||
);
|
||||
|
||||
const handleState = (stateId: string) =>
|
||||
issue?.project_id && updateIssue && updateIssue(issue.project_id, issue.id, { state_id: stateId });
|
||||
|
||||
const handlePriority = (value: TIssuePriorities) =>
|
||||
issue?.project_id && updateIssue && updateIssue(issue.project_id, issue.id, { priority: value });
|
||||
|
||||
const handleLabel = (ids: string[]) =>
|
||||
issue?.project_id && updateIssue && updateIssue(issue.project_id, issue.id, { label_ids: ids });
|
||||
|
||||
const handleAssignee = (ids: string[]) =>
|
||||
issue?.project_id && updateIssue && updateIssue(issue.project_id, issue.id, { assignee_ids: ids });
|
||||
|
||||
const handleModule = useCallback(
|
||||
(moduleIds: string[] | null) => {
|
||||
if (!issue || !issue.module_ids || !moduleIds) return;
|
||||
|
||||
const updatedModuleIds = xor(issue.module_ids, moduleIds);
|
||||
const modulesToAdd: string[] = [];
|
||||
const modulesToRemove: string[] = [];
|
||||
for (const moduleId of updatedModuleIds)
|
||||
if (issue.module_ids.includes(moduleId)) modulesToRemove.push(moduleId);
|
||||
else modulesToAdd.push(moduleId);
|
||||
if (modulesToAdd.length > 0) issueOperations.addModulesToIssue(modulesToAdd);
|
||||
if (modulesToRemove.length > 0) issueOperations.removeModulesFromIssue(modulesToRemove);
|
||||
},
|
||||
[issueOperations, currentLayout, pathname, issue]
|
||||
);
|
||||
|
||||
const handleCycle = useCallback(
|
||||
(cycleId: string | null) => {
|
||||
if (!issue || issue.cycle_id === cycleId) return;
|
||||
if (cycleId) issueOperations.addIssueToCycle?.(cycleId);
|
||||
else issueOperations.removeIssueFromCycle?.();
|
||||
},
|
||||
[issue, issueOperations, currentLayout, pathname]
|
||||
);
|
||||
|
||||
const handleStartDate = (date: Date | null) =>
|
||||
issue?.project_id &&
|
||||
updateIssue &&
|
||||
updateIssue(issue.project_id, issue.id, {
|
||||
start_date: date ? (renderFormattedPayloadDate(date) ?? undefined) : undefined,
|
||||
});
|
||||
|
||||
const handleTargetDate = (date: Date | null) =>
|
||||
issue?.project_id &&
|
||||
updateIssue &&
|
||||
updateIssue(issue.project_id, issue.id, {
|
||||
target_date: date ? (renderFormattedPayloadDate(date) ?? undefined) : undefined,
|
||||
});
|
||||
|
||||
const handleEstimate = (value: string | undefined) =>
|
||||
issue?.project_id && updateIssue && updateIssue(issue.project_id, issue.id, { estimate_point: value });
|
||||
|
||||
if (!issue.project_id) return null;
|
||||
|
||||
const defaultLabelOptions = issue?.label_ids?.map((id) => labelMap[id]) || [];
|
||||
|
||||
const minDate = getDate(issue.start_date);
|
||||
minDate?.setDate(minDate.getDate());
|
||||
|
||||
const maxDate = getDate(issue.target_date);
|
||||
maxDate?.setDate(maxDate.getDate());
|
||||
|
||||
const handleEventPropagation = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{/* basic properties */}
|
||||
{/* state */}
|
||||
<div className="h-5" onClick={handleEventPropagation}>
|
||||
<StateDropdown
|
||||
buttonContainerClassName="truncate max-w-40"
|
||||
value={issue.state_id}
|
||||
onChange={handleState}
|
||||
projectId={issue.project_id}
|
||||
buttonVariant="border-with-text"
|
||||
renderByDefault={isMobile}
|
||||
showTooltip
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* priority */}
|
||||
<div className="h-5" onClick={handleEventPropagation}>
|
||||
<PriorityDropdown
|
||||
value={issue?.priority}
|
||||
onChange={handlePriority}
|
||||
buttonVariant="border-without-text"
|
||||
buttonClassName="border"
|
||||
renderByDefault={isMobile}
|
||||
showTooltip
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* label */}
|
||||
|
||||
<div className="h-5" onClick={handleEventPropagation}>
|
||||
<IssuePropertyLabels
|
||||
projectId={issue?.project_id || null}
|
||||
value={issue?.label_ids || null}
|
||||
defaultOptions={defaultLabelOptions}
|
||||
onChange={handleLabel}
|
||||
renderByDefault={isMobile}
|
||||
hideDropdownArrow
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* start date */}
|
||||
<div className="h-5" onClick={handleEventPropagation}>
|
||||
<DateDropdown
|
||||
value={issue.start_date ?? null}
|
||||
onChange={handleStartDate}
|
||||
maxDate={maxDate}
|
||||
placeholder="Start date"
|
||||
icon={<CalendarClock className="h-3 w-3 flex-shrink-0" />}
|
||||
buttonVariant={issue.start_date ? "border-with-text" : "border-without-text"}
|
||||
optionsClassName="z-10"
|
||||
renderByDefault={isMobile}
|
||||
showTooltip
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* target/due date */}
|
||||
<div className="h-5" onClick={handleEventPropagation}>
|
||||
<DateDropdown
|
||||
value={issue?.target_date ?? null}
|
||||
onChange={handleTargetDate}
|
||||
minDate={minDate}
|
||||
placeholder="Due date"
|
||||
icon={<CalendarCheck2 className="h-3 w-3 flex-shrink-0" />}
|
||||
buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"}
|
||||
buttonClassName={
|
||||
shouldHighlightIssueDueDate(issue?.target_date || null, stateDetails?.group) ? "text-red-500" : ""
|
||||
}
|
||||
clearIconClassName="!text-custom-text-100"
|
||||
optionsClassName="z-10"
|
||||
renderByDefault={isMobile}
|
||||
showTooltip
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* assignee */}
|
||||
<div className="h-5" onClick={handleEventPropagation}>
|
||||
<MemberDropdown
|
||||
projectId={issue?.project_id}
|
||||
value={issue?.assignee_ids}
|
||||
onChange={handleAssignee}
|
||||
multiple
|
||||
buttonVariant={issue.assignee_ids?.length > 0 ? "transparent-without-text" : "border-without-text"}
|
||||
buttonClassName={issue.assignee_ids?.length > 0 ? "hover:bg-transparent px-0" : ""}
|
||||
showTooltip={issue?.assignee_ids?.length === 0}
|
||||
placeholder="Assignees"
|
||||
optionsClassName="z-10"
|
||||
tooltipContent=""
|
||||
renderByDefault={isMobile}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* modules */}
|
||||
{projectDetails?.module_view && (
|
||||
<div className="h-5" onClick={handleEventPropagation}>
|
||||
<ModuleDropdown
|
||||
buttonContainerClassName="truncate max-w-40"
|
||||
projectId={issue?.project_id}
|
||||
value={issue?.module_ids ?? []}
|
||||
onChange={handleModule}
|
||||
renderByDefault={isMobile}
|
||||
multiple
|
||||
buttonVariant="border-with-text"
|
||||
showCount
|
||||
showTooltip
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* cycles */}
|
||||
{projectDetails?.cycle_view && (
|
||||
<div className="h-5" onClick={handleEventPropagation}>
|
||||
<CycleDropdown
|
||||
buttonContainerClassName="truncate max-w-40"
|
||||
projectId={issue?.project_id}
|
||||
value={issue?.cycle_id || null}
|
||||
onChange={handleCycle}
|
||||
buttonVariant="border-with-text"
|
||||
renderByDefault={isMobile}
|
||||
showTooltip
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* estimates */}
|
||||
{issue.project_id && areEstimateEnabledByProjectId(issue.project_id?.toString()) && (
|
||||
<div className="h-5" onClick={handleEventPropagation}>
|
||||
<EstimateDropdown
|
||||
value={issue.estimate_point ?? undefined}
|
||||
onChange={handleEstimate}
|
||||
projectId={issue.project_id}
|
||||
buttonVariant="border-with-text"
|
||||
renderByDefault={isMobile}
|
||||
showTooltip
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
33
web/core/components/issues/workspace-draft/empty-state.tsx
Normal file
33
web/core/components/issues/workspace-draft/empty-state.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
"use client";
|
||||
|
||||
import { FC, Fragment, useState } from "react";
|
||||
// components
|
||||
import { EmptyState } from "@/components/empty-state";
|
||||
import { CreateUpdateIssueModal } from "@/components/issues";
|
||||
// constants
|
||||
import { EmptyStateType } from "@/constants/empty-state";
|
||||
import { EIssuesStoreType } from "@/constants/issue";
|
||||
|
||||
export const WorkspaceDraftEmptyState: FC = () => {
|
||||
// state
|
||||
const [isDraftIssueModalOpen, setIsDraftIssueModalOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={isDraftIssueModalOpen}
|
||||
storeType={EIssuesStoreType.WORKSPACE_DRAFT}
|
||||
onClose={() => setIsDraftIssueModalOpen(false)}
|
||||
isDraft
|
||||
/>
|
||||
<div className="relative h-full w-full overflow-y-auto">
|
||||
<EmptyState
|
||||
type={EmptyStateType.WORKSPACE_DRAFT_ISSUES}
|
||||
primaryButtonOnClick={() => {
|
||||
setIsDraftIssueModalOpen(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
4
web/core/components/issues/workspace-draft/index.ts
Normal file
4
web/core/components/issues/workspace-draft/index.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export * from "./draft-issue-block";
|
||||
export * from "./draft-issue-properties";
|
||||
export * from "./delete-modal";
|
||||
export * from "./root";
|
||||
20
web/core/components/issues/workspace-draft/loader.tsx
Normal file
20
web/core/components/issues/workspace-draft/loader.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
// components
|
||||
import { ListLoaderItemRow } from "@/components/ui";
|
||||
|
||||
type TWorkspaceDraftIssuesLoader = {
|
||||
items?: number;
|
||||
};
|
||||
|
||||
export const WorkspaceDraftIssuesLoader: FC<TWorkspaceDraftIssuesLoader> = (props) => {
|
||||
const { items = 14 } = props;
|
||||
return (
|
||||
<div className="relative h-full w-full">
|
||||
{[...Array(items)].map((_, index) => (
|
||||
<ListLoaderItemRow key={index} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
160
web/core/components/issues/workspace-draft/quick-action.tsx
Normal file
160
web/core/components/issues/workspace-draft/quick-action.tsx
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Placement } from "@popperjs/core";
|
||||
import omit from "lodash/omit";
|
||||
import { observer } from "mobx-react";
|
||||
// icons
|
||||
import { Copy, Pencil, SquareStackIcon, Trash2 } from "lucide-react";
|
||||
// types
|
||||
import { TWorkspaceDraftIssue } from "@plane/types";
|
||||
// ui
|
||||
import { ContextMenu, CustomMenu, TContextMenuItem } from "@plane/ui";
|
||||
// components
|
||||
import { CreateUpdateIssueModal } from "@/components/issues";
|
||||
// constant
|
||||
import { EIssuesStoreType } from "@/constants/issue";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// local components
|
||||
import { WorkspaceDraftIssueDeleteIssueModal } from "./delete-modal";
|
||||
|
||||
export interface IQuickActionProps {
|
||||
issue: TWorkspaceDraftIssue;
|
||||
handleDelete: () => Promise<void>;
|
||||
handleUpdate: (payload: Partial<TWorkspaceDraftIssue>) => Promise<TWorkspaceDraftIssue | undefined>;
|
||||
handleMoveToIssues?: () => Promise<void>;
|
||||
customActionButton?: React.ReactElement;
|
||||
portalElement?: HTMLDivElement | null;
|
||||
placements?: Placement;
|
||||
parentRef: React.RefObject<HTMLElement>;
|
||||
}
|
||||
|
||||
export const WorkspaceDraftIssueQuickActions: React.FC<IQuickActionProps> = observer((props) => {
|
||||
const {
|
||||
issue,
|
||||
handleDelete,
|
||||
handleUpdate,
|
||||
handleMoveToIssues,
|
||||
customActionButton,
|
||||
portalElement,
|
||||
placements = "bottom-end",
|
||||
parentRef,
|
||||
} = props;
|
||||
// states
|
||||
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
|
||||
const [issueToEdit, setIssueToEdit] = useState<TWorkspaceDraftIssue | undefined>(undefined);
|
||||
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
|
||||
|
||||
const duplicateIssuePayload = omit(
|
||||
{
|
||||
...issue,
|
||||
name: `${issue.name} (copy)`,
|
||||
is_draft: true,
|
||||
},
|
||||
["id"]
|
||||
);
|
||||
|
||||
const MENU_ITEMS: TContextMenuItem[] = [
|
||||
{
|
||||
key: "edit",
|
||||
title: "Edit",
|
||||
icon: Pencil,
|
||||
action: () => {
|
||||
setIssueToEdit(issue);
|
||||
setCreateUpdateIssueModal(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "make-a-copy",
|
||||
title: "Make a copy",
|
||||
icon: Copy,
|
||||
action: () => {
|
||||
setCreateUpdateIssueModal(true);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "move-to-issues",
|
||||
title: "Move to issues",
|
||||
icon: SquareStackIcon,
|
||||
action: () => handleMoveToIssues && handleMoveToIssues(),
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
title: "Delete",
|
||||
icon: Trash2,
|
||||
action: () => {
|
||||
setDeleteIssueModal(true);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<WorkspaceDraftIssueDeleteIssueModal
|
||||
data={issue}
|
||||
isOpen={deleteIssueModal}
|
||||
handleClose={() => setDeleteIssueModal(false)}
|
||||
onSubmit={handleDelete}
|
||||
/>
|
||||
<CreateUpdateIssueModal
|
||||
isOpen={createUpdateIssueModal}
|
||||
onClose={() => {
|
||||
setCreateUpdateIssueModal(false);
|
||||
setIssueToEdit(undefined);
|
||||
}}
|
||||
data={issueToEdit ?? duplicateIssuePayload}
|
||||
onSubmit={async (data) => {
|
||||
if (issueToEdit && handleUpdate) await handleUpdate(data as TWorkspaceDraftIssue);
|
||||
}}
|
||||
storeType={EIssuesStoreType.WORKSPACE_DRAFT}
|
||||
fetchIssueDetails={false}
|
||||
isDraft
|
||||
/>
|
||||
<ContextMenu parentRef={parentRef} items={MENU_ITEMS} />
|
||||
<CustomMenu
|
||||
ellipsis
|
||||
customButton={customActionButton}
|
||||
portalElement={portalElement}
|
||||
placement={placements}
|
||||
menuItemsClassName="z-[14]"
|
||||
maxHeight="lg"
|
||||
useCaptureForOutsideClick
|
||||
closeOnSelect
|
||||
>
|
||||
{MENU_ITEMS.map((item) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={item.key}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
item.action();
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center gap-2",
|
||||
{
|
||||
"text-custom-text-400": item.disabled,
|
||||
},
|
||||
item.className
|
||||
)}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
||||
<div>
|
||||
<h5>{item.title}</h5>
|
||||
{item.description && (
|
||||
<p
|
||||
className={cn("text-custom-text-300 whitespace-pre-line", {
|
||||
"text-custom-text-400": item.disabled,
|
||||
})}
|
||||
>
|
||||
{item.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
</>
|
||||
);
|
||||
});
|
||||
70
web/core/components/issues/workspace-draft/root.tsx
Normal file
70
web/core/components/issues/workspace-draft/root.tsx
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import useSWR from "swr";
|
||||
// constants
|
||||
import { EDraftIssuePaginationType } from "@/constants/workspace-drafts";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
// hooks
|
||||
import { useWorkspaceDraftIssues } from "@/hooks/store";
|
||||
// components
|
||||
import { DraftIssueBlock } from "./draft-issue-block";
|
||||
import { WorkspaceDraftEmptyState } from "./empty-state";
|
||||
import { WorkspaceDraftIssuesLoader } from "./loader";
|
||||
|
||||
type TWorkspaceDraftIssuesRoot = {
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const WorkspaceDraftIssuesRoot: FC<TWorkspaceDraftIssuesRoot> = observer((props) => {
|
||||
const { workspaceSlug } = props;
|
||||
// hooks
|
||||
const { loader, paginationInfo, fetchIssues, issuesMap, issueIds } = useWorkspaceDraftIssues();
|
||||
|
||||
// fetching issues
|
||||
useSWR(
|
||||
workspaceSlug && issueIds.length <= 0 ? `WORKSPACE_DRAFT_ISSUES_${workspaceSlug}` : null,
|
||||
workspaceSlug && issueIds.length <= 0 ? async () => await fetchIssues(workspaceSlug, "init-loader") : null
|
||||
);
|
||||
|
||||
// handle nest issues
|
||||
const handleNextIssues = async () => {
|
||||
if (!paginationInfo?.next_page_results) return;
|
||||
await fetchIssues(workspaceSlug, "pagination", EDraftIssuePaginationType.NEXT);
|
||||
};
|
||||
|
||||
if (loader === "init-loader" && issueIds.length <= 0) {
|
||||
return <WorkspaceDraftIssuesLoader items={14} />;
|
||||
}
|
||||
|
||||
if (loader === "empty-state" && issueIds.length <= 0) return <WorkspaceDraftEmptyState />;
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="relative">
|
||||
{issueIds.map((issueId: string) => (
|
||||
<DraftIssueBlock key={issueId} workspaceSlug={workspaceSlug} issueId={issueId} />
|
||||
))}
|
||||
</div>
|
||||
{loader === "pagination" && issueIds.length >= 0 ? (
|
||||
<WorkspaceDraftIssuesLoader items={1} />
|
||||
) : (
|
||||
<div
|
||||
className={cn(
|
||||
"h-11 pl-6 p-3 text-sm font-medium bg-custom-background-100 border-b border-custom-border-200 transition-all",
|
||||
{
|
||||
"text-custom-primary-100 hover:text-custom-primary-200 cursor-pointer underline-offset-2 hover:underline":
|
||||
paginationInfo?.next_page_results,
|
||||
"text-custom-text-300 cursor-not-allowed": !paginationInfo?.next_page_results,
|
||||
}
|
||||
)}
|
||||
onClick={handleNextIssues}
|
||||
>
|
||||
Load More ↓
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -428,7 +428,7 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
{isAuthorized && (
|
||||
{/* {isAuthorized && (
|
||||
<CustomMenu.MenuItem>
|
||||
<Link href={`/${workspaceSlug}/projects/${project?.id}/draft-issues/`}>
|
||||
<div className="flex items-center justify-start gap-2">
|
||||
|
|
@ -437,7 +437,7 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
|||
</div>
|
||||
</Link>
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
)} */}
|
||||
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<LinkIcon className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ export const SidebarQuickActions = observer(() => {
|
|||
<PenSquare className="size-4" />
|
||||
{!isSidebarCollapsed && <span className="text-sm font-medium">New issue</span>}
|
||||
</button>
|
||||
{!disabled && workspaceDraftIssue && (
|
||||
{/* {!disabled && workspaceDraftIssue && (
|
||||
<>
|
||||
{!isSidebarCollapsed && (
|
||||
<button type="button" className="grid place-items-center">
|
||||
|
|
@ -127,7 +127,7 @@ export const SidebarQuickActions = observer(() => {
|
|||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
)} */}
|
||||
</div>
|
||||
<button
|
||||
className={cn(
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { linearGradientDef } from "@nivo/core";
|
||||
// icons
|
||||
import { BarChart2, Briefcase, Home, Inbox, Layers } from "lucide-react";
|
||||
import { BarChart2, Briefcase, Home, Inbox, Layers, PenSquare } from "lucide-react";
|
||||
// types
|
||||
import { TIssuesListTypes, TStateGroups } from "@plane/types";
|
||||
// ui
|
||||
|
|
@ -329,4 +329,12 @@ export const SIDEBAR_USER_MENU_ITEMS: {
|
|||
highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/notifications/`),
|
||||
Icon: Inbox,
|
||||
},
|
||||
{
|
||||
key: "drafts",
|
||||
label: "Drafts",
|
||||
href: `/drafts`,
|
||||
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/drafts/`),
|
||||
Icon: PenSquare,
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -105,6 +105,8 @@ export enum EmptyStateType {
|
|||
INBOX_SIDEBAR_CLOSED_TAB = "inbox-sidebar-closed-tab",
|
||||
INBOX_SIDEBAR_FILTER_EMPTY_STATE = "inbox-sidebar-filter-empty-state",
|
||||
INBOX_DETAIL_EMPTY_STATE = "inbox-detail-empty-state",
|
||||
|
||||
WORKSPACE_DRAFT_ISSUES = "workspace-draft-issues",
|
||||
}
|
||||
|
||||
const emptyStateDetails = {
|
||||
|
|
@ -757,6 +759,17 @@ const emptyStateDetails = {
|
|||
title: "Select an issue to view its details.",
|
||||
path: "/empty-state/intake/issue-detail",
|
||||
},
|
||||
[EmptyStateType.WORKSPACE_DRAFT_ISSUES]: {
|
||||
key: EmptyStateType.WORKSPACE_DRAFT_ISSUES,
|
||||
title: "No Draft Issues Yet",
|
||||
description: "There are no draft issues in your workspace right now. Begin by adding your first one.",
|
||||
path: "/empty-state/workspace-draft/issue",
|
||||
primaryButton: {
|
||||
text: "Create draft issue",
|
||||
},
|
||||
accessType: "workspace",
|
||||
access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER],
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const EMPTY_STATE_DETAILS: Record<EmptyStateType, EmptyStateDetails> = emptyStateDetails;
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export enum EIssuesStoreType {
|
|||
ARCHIVED = "ARCHIVED",
|
||||
DRAFT = "DRAFT",
|
||||
DEFAULT = "DEFAULT",
|
||||
WORKSPACE_DRAFT = "WORKSPACE_DRAFT",
|
||||
}
|
||||
|
||||
export enum EIssueLayoutTypes {
|
||||
|
|
|
|||
6
web/core/constants/workspace-drafts.ts
Normal file
6
web/core/constants/workspace-drafts.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export enum EDraftIssuePaginationType {
|
||||
INIT = "INIT",
|
||||
NEXT = "NEXT",
|
||||
PREV = "PREV",
|
||||
CURRENT = "CURRENT",
|
||||
}
|
||||
|
|
@ -31,3 +31,4 @@ export * from "./use-webhook";
|
|||
export * from "./use-workspace";
|
||||
export * from "./user";
|
||||
export * from "./use-transient";
|
||||
export * from "./workspace-draft";
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { IProfileIssues, IProfileIssuesFilter } from "@/store/issue/profile";
|
|||
import { IProjectIssues, IProjectIssuesFilter } from "@/store/issue/project";
|
||||
import { IProjectViewIssues, IProjectViewIssuesFilter } from "@/store/issue/project-views";
|
||||
import { IWorkspaceIssues, IWorkspaceIssuesFilter } from "@/store/issue/workspace";
|
||||
import { IWorkspaceDraftIssues, IWorkspaceDraftIssuesFilter } from "@/store/issue/workspace-draft";
|
||||
// constants
|
||||
|
||||
type defaultIssueStore = {
|
||||
|
|
@ -24,6 +25,10 @@ export type TStoreIssues = {
|
|||
issues: IWorkspaceIssues;
|
||||
issuesFilter: IWorkspaceIssuesFilter;
|
||||
};
|
||||
[EIssuesStoreType.WORKSPACE_DRAFT]: defaultIssueStore & {
|
||||
issues: IWorkspaceDraftIssues;
|
||||
issuesFilter: IWorkspaceDraftIssuesFilter;
|
||||
};
|
||||
[EIssuesStoreType.PROFILE]: defaultIssueStore & {
|
||||
issues: IProfileIssues;
|
||||
issuesFilter: IProfileIssuesFilter;
|
||||
|
|
@ -72,6 +77,16 @@ export const useIssues = <T extends EIssuesStoreType>(storeType?: T): TStoreIssu
|
|||
issues: context.issue.workspaceIssues,
|
||||
issuesFilter: context.issue.workspaceIssuesFilter,
|
||||
}) as TStoreIssues[T];
|
||||
case EIssuesStoreType.WORKSPACE_DRAFT:
|
||||
return merge(defaultStore, {
|
||||
issues: context.issue.workspaceDraftIssues,
|
||||
issuesFilter: context.issue.workspaceDraftIssuesFilter,
|
||||
}) as TStoreIssues[T];
|
||||
case EIssuesStoreType.WORKSPACE_DRAFT:
|
||||
return merge(defaultStore, {
|
||||
issues: context.issue.workspaceDraftIssues,
|
||||
issuesFilter: context.issue.workspaceDraftIssuesFilter,
|
||||
}) as TStoreIssues[T];
|
||||
case EIssuesStoreType.PROFILE:
|
||||
return merge(defaultStore, {
|
||||
issues: context.issue.profileIssues,
|
||||
|
|
|
|||
2
web/core/hooks/store/workspace-draft/index.ts
Normal file
2
web/core/hooks/store/workspace-draft/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./use-workspace-draft-issue";
|
||||
export * from "./use-workspace-draft-issue-filters";
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// types
|
||||
import { IWorkspaceDraftIssues } from "@/store/issue/workspace-draft";
|
||||
|
||||
export const useWorkspaceDraftIssueFilters = (): IWorkspaceDraftIssues => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useWorkspaceDraftIssueFilters must be used within StoreProvider");
|
||||
|
||||
return context.issue.workspaceDraftIssues;
|
||||
};
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { useContext } from "react";
|
||||
// mobx store
|
||||
import { StoreContext } from "@/lib/store-context";
|
||||
// types
|
||||
import { IWorkspaceDraftIssues } from "@/store/issue/workspace-draft";
|
||||
|
||||
export const useWorkspaceDraftIssues = (): IWorkspaceDraftIssues => {
|
||||
const context = useContext(StoreContext);
|
||||
if (context === undefined) throw new Error("useWorkspaceDraftIssues must be used within StoreProvider");
|
||||
|
||||
return context.issue.workspaceDraftIssues;
|
||||
};
|
||||
|
|
@ -45,6 +45,7 @@ export const useIssuesActions = (storeType: EIssuesStoreType): IssueActions => {
|
|||
const profileIssueActions = useProfileIssueActions();
|
||||
const draftIssueActions = useDraftIssueActions();
|
||||
const archivedIssueActions = useArchivedIssueActions();
|
||||
const workspaceDraftIssueActions = useWorkspaceDraftIssueActions();
|
||||
|
||||
switch (storeType) {
|
||||
case EIssuesStoreType.PROJECT_VIEW:
|
||||
|
|
@ -61,6 +62,8 @@ export const useIssuesActions = (storeType: EIssuesStoreType): IssueActions => {
|
|||
return moduleIssueActions;
|
||||
case EIssuesStoreType.GLOBAL:
|
||||
return globalIssueActions;
|
||||
case EIssuesStoreType.WORKSPACE_DRAFT:
|
||||
return workspaceDraftIssueActions;
|
||||
case EIssuesStoreType.PROJECT:
|
||||
default:
|
||||
return projectIssueActions;
|
||||
|
|
@ -737,3 +740,80 @@ const useGlobalIssueActions = () => {
|
|||
[createIssue, updateIssue, removeIssue, updateFilters]
|
||||
);
|
||||
};
|
||||
|
||||
const useWorkspaceDraftIssueActions = () => {
|
||||
// router
|
||||
const { workspaceSlug: routerWorkspaceSlug, globalViewId: routerGlobalViewId } = useParams();
|
||||
const workspaceSlug = routerWorkspaceSlug?.toString();
|
||||
const globalViewId = routerGlobalViewId?.toString();
|
||||
// store hooks
|
||||
const { issues, issuesFilter } = useIssues(EIssuesStoreType.WORKSPACE_DRAFT);
|
||||
const fetchIssues = useCallback(
|
||||
async (loadType: TLoader, options: IssuePaginationOptions) => {
|
||||
if (!workspaceSlug) return;
|
||||
return issues.fetchIssues(workspaceSlug.toString(), loadType, options);
|
||||
},
|
||||
[workspaceSlug, issues]
|
||||
);
|
||||
|
||||
const fetchNextIssues = useCallback(async () => {
|
||||
if (!workspaceSlug) return;
|
||||
return issues.fetchNextIssues(workspaceSlug.toString());
|
||||
}, [workspaceSlug, issues]);
|
||||
|
||||
const createIssue = useCallback(
|
||||
async (projectId: string | undefined | null, data: Partial<TIssue>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.createWorkspaceDraftIssue(workspaceSlug, data);
|
||||
},
|
||||
[issues, workspaceSlug]
|
||||
);
|
||||
const updateIssue = useCallback(
|
||||
async (projectId: string | undefined | null, issueId: string, data: Partial<TIssue>) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.updateWorkspaceDraftIssue(workspaceSlug, issueId, data);
|
||||
},
|
||||
[issues, workspaceSlug]
|
||||
);
|
||||
const removeIssue = useCallback(
|
||||
async (projectId: string | undefined | null, issueId: string) => {
|
||||
if (!workspaceSlug || !projectId) return;
|
||||
return await issues.deleteWorkspaceDraftIssue(workspaceSlug, issueId);
|
||||
},
|
||||
[issues, workspaceSlug]
|
||||
);
|
||||
|
||||
const moveToIssue = useCallback(
|
||||
async (workspaceSlug: string, issueId: string, data: Partial<TIssue>) => {
|
||||
if (!workspaceSlug || !issueId || !data) return;
|
||||
return await issues.moveToIssues(workspaceSlug, issueId, data);
|
||||
},
|
||||
[issues]
|
||||
);
|
||||
|
||||
const updateFilters = useCallback(
|
||||
async (
|
||||
projectId: string,
|
||||
filterType: EIssueFilterType,
|
||||
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters
|
||||
) => {
|
||||
filters = filters as IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties;
|
||||
if (!globalViewId || !workspaceSlug) return;
|
||||
return await issuesFilter.updateFilters(workspaceSlug, filterType, filters);
|
||||
},
|
||||
[globalViewId, workspaceSlug, issuesFilter]
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
fetchIssues,
|
||||
fetchNextIssues,
|
||||
createIssue,
|
||||
updateIssue,
|
||||
removeIssue,
|
||||
updateFilters,
|
||||
moveToIssue,
|
||||
}),
|
||||
[fetchIssues, fetchNextIssues, createIssue, updateIssue, removeIssue, updateFilters, moveToIssue]
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -7,3 +7,4 @@ export * from "./issue_attachment.service";
|
|||
export * from "./issue_activity.service";
|
||||
export * from "./issue_comment.service";
|
||||
export * from "./issue_relation.service";
|
||||
export * from "./workspace_draft.service";
|
||||
|
|
|
|||
73
web/core/services/issue/workspace_draft.service.ts
Normal file
73
web/core/services/issue/workspace_draft.service.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { TWorkspaceDraftIssue, TWorkspaceDraftPaginationInfo } from "@plane/types";
|
||||
// helpers
|
||||
import { API_BASE_URL } from "@/helpers/common.helper";
|
||||
// services
|
||||
import { APIService } from "@/services/api.service";
|
||||
|
||||
export class WorkspaceDraftService extends APIService {
|
||||
constructor() {
|
||||
super(API_BASE_URL);
|
||||
}
|
||||
|
||||
async getIssues(
|
||||
workspaceSlug: string,
|
||||
query: object = {}
|
||||
): Promise<TWorkspaceDraftPaginationInfo<TWorkspaceDraftIssue> | undefined> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/draft-issues/`, { params: { ...query } })
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
}
|
||||
|
||||
async getIssueById(workspaceSlug: string, issueId: string): Promise<TWorkspaceDraftIssue | undefined> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/draft-issues/${issueId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async createIssue(
|
||||
workspaceSlug: string,
|
||||
payload: Partial<TWorkspaceDraftIssue>
|
||||
): Promise<TWorkspaceDraftIssue | undefined> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/draft-issues/`, payload)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async updateIssue(
|
||||
workspaceSlug: string,
|
||||
issueId: string,
|
||||
payload: Partial<TWorkspaceDraftIssue>
|
||||
): Promise<TWorkspaceDraftIssue | undefined> {
|
||||
return this.patch(`/api/workspaces/${workspaceSlug}/draft-issues/${issueId}/`, payload)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async deleteIssue(workspaceSlug: string, issueId: string): Promise<void> {
|
||||
return this.delete(`/api/workspaces/${workspaceSlug}/draft-issues/${issueId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
|
||||
async moveIssue(workspaceSlug: string, issueId: string, payload: Partial<TWorkspaceDraftIssue>): Promise<void> {
|
||||
return this.post(`/api/workspaces/${workspaceSlug}/draft-to-issue/${issueId}/`, payload)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const workspaceDraftService = new WorkspaceDraftService();
|
||||
|
||||
export default workspaceDraftService;
|
||||
|
|
@ -23,7 +23,8 @@ import {
|
|||
IProjectViewIssues,
|
||||
ProjectViewIssues,
|
||||
} from "./project-views";
|
||||
import { IWorkspaceIssuesFilter, WorkspaceIssuesFilter, IWorkspaceIssues, WorkspaceIssues } from "./workspace";
|
||||
import { WorkspaceIssuesFilter, IWorkspaceIssues, WorkspaceIssues, IWorkspaceIssuesFilter } from "./workspace";
|
||||
import { IWorkspaceDraftIssues, IWorkspaceDraftIssuesFilter, WorkspaceDraftIssues, WorkspaceDraftIssuesFilter } from "./workspace-draft";
|
||||
|
||||
export interface IIssueRootStore {
|
||||
currentUserId: string | undefined;
|
||||
|
|
@ -55,6 +56,9 @@ export interface IIssueRootStore {
|
|||
workspaceIssuesFilter: IWorkspaceIssuesFilter;
|
||||
workspaceIssues: IWorkspaceIssues;
|
||||
|
||||
workspaceDraftIssuesFilter: IWorkspaceDraftIssuesFilter;
|
||||
workspaceDraftIssues: IWorkspaceDraftIssues;
|
||||
|
||||
profileIssuesFilter: IProfileIssuesFilter;
|
||||
profileIssues: IProfileIssues;
|
||||
|
||||
|
|
@ -110,6 +114,9 @@ export class IssueRootStore implements IIssueRootStore {
|
|||
workspaceIssuesFilter: IWorkspaceIssuesFilter;
|
||||
workspaceIssues: IWorkspaceIssues;
|
||||
|
||||
workspaceDraftIssuesFilter: IWorkspaceDraftIssuesFilter;
|
||||
workspaceDraftIssues: IWorkspaceDraftIssues;
|
||||
|
||||
profileIssuesFilter: IProfileIssuesFilter;
|
||||
profileIssues: IProfileIssues;
|
||||
|
||||
|
|
@ -190,6 +197,9 @@ export class IssueRootStore implements IIssueRootStore {
|
|||
this.profileIssuesFilter = new ProfileIssuesFilter(this);
|
||||
this.profileIssues = new ProfileIssues(this, this.profileIssuesFilter);
|
||||
|
||||
this.workspaceDraftIssuesFilter = new WorkspaceDraftIssuesFilter(this);
|
||||
this.workspaceDraftIssues = new WorkspaceDraftIssues(this, this.workspaceDraftIssuesFilter);
|
||||
|
||||
this.projectIssuesFilter = new ProjectIssuesFilter(this);
|
||||
this.projectIssues = new ProjectIssues(this, this.projectIssuesFilter);
|
||||
|
||||
|
|
|
|||
254
web/core/store/issue/workspace-draft/filter.store.ts
Normal file
254
web/core/store/issue/workspace-draft/filter.store.ts
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
import isEmpty from "lodash/isEmpty";
|
||||
import set from "lodash/set";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
// base class
|
||||
import { computedFn } from "mobx-utils";
|
||||
import {
|
||||
IIssueFilterOptions,
|
||||
IIssueDisplayFilterOptions,
|
||||
IIssueDisplayProperties,
|
||||
TIssueKanbanFilters,
|
||||
IIssueFilters,
|
||||
TIssueParams,
|
||||
IssuePaginationOptions,
|
||||
} from "@plane/types";
|
||||
import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue";
|
||||
import { handleIssueQueryParamsByLayout } from "@/helpers/issue.helper";
|
||||
import { IssueFiltersService } from "@/services/issue_filter.service";
|
||||
import { IBaseIssueFilterStore, IssueFilterHelperStore } from "../helpers/issue-filter-helper.store";
|
||||
// helpers
|
||||
// types
|
||||
import { IIssueRootStore } from "../root.store";
|
||||
// constants
|
||||
// services
|
||||
|
||||
export interface IWorkspaceDraftIssuesFilter extends IBaseIssueFilterStore {
|
||||
// observables
|
||||
workspaceSlug: string;
|
||||
//helper actions
|
||||
getFilterParams: (
|
||||
options: IssuePaginationOptions,
|
||||
userId: string,
|
||||
cursor: string | undefined,
|
||||
groupId: string | undefined,
|
||||
subGroupId: string | undefined
|
||||
) => Partial<Record<TIssueParams, string | boolean>>;
|
||||
// action
|
||||
fetchFilters: (workspaceSlug: string) => Promise<void>;
|
||||
updateFilters: (
|
||||
workspaceSlug: string,
|
||||
filterType: EIssueFilterType,
|
||||
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
export class WorkspaceDraftIssuesFilter extends IssueFilterHelperStore implements IWorkspaceDraftIssuesFilter {
|
||||
// observables
|
||||
workspaceSlug: string = "";
|
||||
filters: { [userId: string]: IIssueFilters } = {};
|
||||
// root store
|
||||
rootIssueStore: IIssueRootStore;
|
||||
// services
|
||||
issueFilterService;
|
||||
|
||||
constructor(_rootStore: IIssueRootStore) {
|
||||
super();
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
workspaceSlug: observable.ref,
|
||||
filters: observable,
|
||||
// computed
|
||||
issueFilters: computed,
|
||||
appliedFilters: computed,
|
||||
// actions
|
||||
fetchFilters: action,
|
||||
updateFilters: action,
|
||||
});
|
||||
// root store
|
||||
this.rootIssueStore = _rootStore;
|
||||
// services
|
||||
this.issueFilterService = new IssueFiltersService();
|
||||
}
|
||||
|
||||
get issueFilters() {
|
||||
const workspaceSlug = this.rootIssueStore.workspaceSlug;
|
||||
if (!workspaceSlug) return undefined;
|
||||
|
||||
return this.getIssueFilters(workspaceSlug);
|
||||
}
|
||||
|
||||
get appliedFilters() {
|
||||
const workspaceSlug = this.rootIssueStore.workspaceSlug;
|
||||
if (!workspaceSlug) return undefined;
|
||||
|
||||
return this.getAppliedFilters(workspaceSlug);
|
||||
}
|
||||
|
||||
getIssueFilters(workspaceSlug: string) {
|
||||
const displayFilters = this.filters[workspaceSlug] || undefined;
|
||||
if (isEmpty(displayFilters)) return undefined;
|
||||
|
||||
const _filters: IIssueFilters = this.computedIssueFilters(displayFilters);
|
||||
|
||||
return _filters;
|
||||
}
|
||||
|
||||
getAppliedFilters(workspaceSlug: string) {
|
||||
const userFilters = this.getIssueFilters(workspaceSlug);
|
||||
if (!userFilters) return undefined;
|
||||
|
||||
const filteredParams = handleIssueQueryParamsByLayout(userFilters?.displayFilters?.layout, "profile_issues");
|
||||
if (!filteredParams) return undefined;
|
||||
|
||||
const filteredRouteParams: Partial<Record<TIssueParams, string | boolean>> = this.computedFilteredParams(
|
||||
userFilters?.filters as IIssueFilterOptions,
|
||||
userFilters?.displayFilters as IIssueDisplayFilterOptions,
|
||||
filteredParams
|
||||
);
|
||||
|
||||
return filteredRouteParams;
|
||||
}
|
||||
|
||||
getFilterParams = computedFn(
|
||||
(
|
||||
options: IssuePaginationOptions,
|
||||
userId: string,
|
||||
cursor: string | undefined,
|
||||
groupId: string | undefined,
|
||||
subGroupId: string | undefined
|
||||
) => {
|
||||
const filterParams = this.getAppliedFilters(this.workspaceSlug);
|
||||
|
||||
const paginationParams = this.getPaginationParams(filterParams, options, cursor, groupId, subGroupId);
|
||||
return paginationParams;
|
||||
}
|
||||
);
|
||||
|
||||
fetchFilters = async (workspaceSlug: string) => {
|
||||
this.workspaceSlug = workspaceSlug;
|
||||
const _filters = this.handleIssuesLocalFilters.get(
|
||||
EIssuesStoreType.PROFILE,
|
||||
workspaceSlug,
|
||||
workspaceSlug,
|
||||
undefined
|
||||
);
|
||||
|
||||
const filters: IIssueFilterOptions = this.computedFilters(_filters?.filters);
|
||||
const displayFilters: IIssueDisplayFilterOptions = this.computedDisplayFilters(_filters?.display_filters);
|
||||
const displayProperties: IIssueDisplayProperties = this.computedDisplayProperties(_filters?.display_properties);
|
||||
const kanbanFilters = {
|
||||
group_by: _filters?.kanban_filters?.group_by || [],
|
||||
sub_group_by: _filters?.kanban_filters?.sub_group_by || [],
|
||||
};
|
||||
|
||||
runInAction(() => {
|
||||
set(this.filters, [workspaceSlug, "filters"], filters);
|
||||
set(this.filters, [workspaceSlug, "displayFilters"], displayFilters);
|
||||
set(this.filters, [workspaceSlug, "displayProperties"], displayProperties);
|
||||
set(this.filters, [workspaceSlug, "kanbanFilters"], kanbanFilters);
|
||||
});
|
||||
};
|
||||
|
||||
updateFilters = async (
|
||||
workspaceSlug: string,
|
||||
type: EIssueFilterType,
|
||||
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters
|
||||
) => {
|
||||
try {
|
||||
if (isEmpty(this.filters) || isEmpty(this.filters[workspaceSlug]) || isEmpty(filters)) return;
|
||||
|
||||
const _filters = {
|
||||
filters: this.filters[workspaceSlug].filters as IIssueFilterOptions,
|
||||
displayFilters: this.filters[workspaceSlug].displayFilters as IIssueDisplayFilterOptions,
|
||||
displayProperties: this.filters[workspaceSlug].displayProperties as IIssueDisplayProperties,
|
||||
kanbanFilters: this.filters[workspaceSlug].kanbanFilters as TIssueKanbanFilters,
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case EIssueFilterType.FILTERS: {
|
||||
const updatedFilters = filters as IIssueFilterOptions;
|
||||
_filters.filters = { ..._filters.filters, ...updatedFilters };
|
||||
|
||||
runInAction(() => {
|
||||
Object.keys(updatedFilters).forEach((_key) => {
|
||||
set(this.filters, [workspaceSlug, "filters", _key], updatedFilters[_key as keyof IIssueFilterOptions]);
|
||||
});
|
||||
});
|
||||
|
||||
this.rootIssueStore.profileIssues.fetchIssuesWithExistingPagination(workspaceSlug, workspaceSlug, "mutation");
|
||||
|
||||
this.handleIssuesLocalFilters.set(EIssuesStoreType.PROFILE, type, workspaceSlug, workspaceSlug, undefined, {
|
||||
filters: _filters.filters,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case EIssueFilterType.DISPLAY_FILTERS: {
|
||||
const updatedDisplayFilters = filters as IIssueDisplayFilterOptions;
|
||||
_filters.displayFilters = { ..._filters.displayFilters, ...updatedDisplayFilters };
|
||||
|
||||
// set sub_group_by to null if group_by is set to null
|
||||
if (_filters.displayFilters.group_by === null) {
|
||||
_filters.displayFilters.sub_group_by = null;
|
||||
updatedDisplayFilters.sub_group_by = null;
|
||||
}
|
||||
// set sub_group_by to null if layout is switched to kanban group_by and sub_group_by are same
|
||||
if (
|
||||
_filters.displayFilters.layout === "kanban" &&
|
||||
_filters.displayFilters.group_by === _filters.displayFilters.sub_group_by
|
||||
) {
|
||||
_filters.displayFilters.sub_group_by = null;
|
||||
updatedDisplayFilters.sub_group_by = null;
|
||||
}
|
||||
// set group_by to priority if layout is switched to kanban and group_by is null
|
||||
if (_filters.displayFilters.layout === "kanban" && _filters.displayFilters.group_by === null) {
|
||||
_filters.displayFilters.group_by = "priority";
|
||||
updatedDisplayFilters.group_by = "priority";
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
Object.keys(updatedDisplayFilters).forEach((_key) => {
|
||||
set(
|
||||
this.filters,
|
||||
[workspaceSlug, "displayFilters", _key],
|
||||
updatedDisplayFilters[_key as keyof IIssueDisplayFilterOptions]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
this.rootIssueStore.profileIssues.fetchIssuesWithExistingPagination(workspaceSlug, workspaceSlug, "mutation");
|
||||
|
||||
this.handleIssuesLocalFilters.set(EIssuesStoreType.PROFILE, type, workspaceSlug, workspaceSlug, undefined, {
|
||||
display_filters: _filters.displayFilters,
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
case EIssueFilterType.DISPLAY_PROPERTIES: {
|
||||
const updatedDisplayProperties = filters as IIssueDisplayProperties;
|
||||
_filters.displayProperties = { ..._filters.displayProperties, ...updatedDisplayProperties };
|
||||
|
||||
runInAction(() => {
|
||||
Object.keys(updatedDisplayProperties).forEach((_key) => {
|
||||
set(
|
||||
this.filters,
|
||||
[workspaceSlug, "displayProperties", _key],
|
||||
updatedDisplayProperties[_key as keyof IIssueDisplayProperties]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
this.handleIssuesLocalFilters.set(EIssuesStoreType.PROFILE, type, workspaceSlug, workspaceSlug, undefined, {
|
||||
display_properties: _filters.displayProperties,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
if (workspaceSlug) this.fetchFilters(workspaceSlug);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
2
web/core/store/issue/workspace-draft/index.ts
Normal file
2
web/core/store/issue/workspace-draft/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from "./issue.store";
|
||||
export * from "./filter.store";
|
||||
280
web/core/store/issue/workspace-draft/issue.store.ts
Normal file
280
web/core/store/issue/workspace-draft/issue.store.ts
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
import orderBy from "lodash/orderBy";
|
||||
import set from "lodash/set";
|
||||
import unset from "lodash/unset";
|
||||
import update from "lodash/update";
|
||||
import { action, computed, makeObservable, observable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
import {
|
||||
TWorkspaceDraftIssue,
|
||||
TWorkspaceDraftPaginationInfo,
|
||||
TWorkspaceDraftIssueLoader,
|
||||
TWorkspaceDraftQueryParams,
|
||||
} from "@plane/types";
|
||||
// constants
|
||||
import { EDraftIssuePaginationType } from "@/constants/workspace-drafts";
|
||||
// helpers
|
||||
import { getCurrentDateTimeInISO, convertToISODateString } from "@/helpers/date-time.helper";
|
||||
// services
|
||||
import workspaceDraftService from "@/services/issue/workspace_draft.service";
|
||||
import { IIssueDetail } from "../issue-details/root.store";
|
||||
import { clone } from "lodash";
|
||||
|
||||
export type TDraftIssuePaginationType = EDraftIssuePaginationType;
|
||||
|
||||
export interface IWorkspaceDraftIssues {
|
||||
// observables
|
||||
issuesMap: Record<string, TWorkspaceDraftIssue>;
|
||||
paginationInfo: Omit<TWorkspaceDraftPaginationInfo<TWorkspaceDraftIssue>, "results"> | undefined;
|
||||
loader: TWorkspaceDraftIssueLoader;
|
||||
// computed
|
||||
issueIds: string[];
|
||||
// computed functions
|
||||
getIssueById: (issueId: string) => TWorkspaceDraftIssue | undefined;
|
||||
// helper actions
|
||||
addIssue: (issues: TWorkspaceDraftIssue[]) => void;
|
||||
mutateIssue: (issueId: string, data: Partial<TWorkspaceDraftIssue>) => void;
|
||||
removeIssue: (issueId: string) => void;
|
||||
// actions
|
||||
fetchIssues: (
|
||||
workspaceSlug: string,
|
||||
loadType: TWorkspaceDraftIssueLoader,
|
||||
paginationType?: TDraftIssuePaginationType
|
||||
) => Promise<TWorkspaceDraftPaginationInfo<TWorkspaceDraftIssue> | undefined>;
|
||||
createIssue: (
|
||||
workspaceSlug: string,
|
||||
payload: Partial<TWorkspaceDraftIssue>
|
||||
) => Promise<TWorkspaceDraftIssue | undefined>;
|
||||
updateIssue: (
|
||||
workspaceSlug: string,
|
||||
issueId: string,
|
||||
payload: Partial<TWorkspaceDraftIssue>
|
||||
) => Promise<TWorkspaceDraftIssue | undefined>;
|
||||
deleteIssue: (workspaceSlug: string, issueId: string) => Promise<void>;
|
||||
moveIssue: (workspaceSlug: string, issueId: string, payload: Partial<TWorkspaceDraftIssue>) => Promise<void>;
|
||||
addCycleToIssue: (
|
||||
workspaceSlug: string,
|
||||
issueId: string,
|
||||
cycleId: string
|
||||
) => Promise<TWorkspaceDraftIssue | undefined>;
|
||||
addModulesToIssue: (
|
||||
workspaceSlug: string,
|
||||
issueId: string,
|
||||
moduleIds: string[]
|
||||
) => Promise<TWorkspaceDraftIssue | undefined>;
|
||||
}
|
||||
|
||||
export class WorkspaceDraftIssues implements IWorkspaceDraftIssues {
|
||||
// local constants
|
||||
paginatedCount = 50;
|
||||
// observables
|
||||
paginationInfo: Omit<TWorkspaceDraftPaginationInfo<TWorkspaceDraftIssue>, "results"> | undefined = undefined;
|
||||
loader: TWorkspaceDraftIssueLoader = undefined;
|
||||
issuesMap: Record<string, TWorkspaceDraftIssue> = {};
|
||||
|
||||
constructor(private store: IIssueDetail) {
|
||||
makeObservable(this, {
|
||||
paginationInfo: observable,
|
||||
loader: observable.ref,
|
||||
issuesMap: observable,
|
||||
// computed
|
||||
issueIds: computed,
|
||||
// action
|
||||
fetchIssues: action,
|
||||
createIssue: action,
|
||||
updateIssue: action,
|
||||
deleteIssue: action,
|
||||
moveIssue: action,
|
||||
addCycleToIssue: action,
|
||||
addModulesToIssue: action,
|
||||
});
|
||||
}
|
||||
|
||||
// computed
|
||||
get issueIds() {
|
||||
if (Object.keys(this.issuesMap).length <= 0) return [];
|
||||
return orderBy(Object.values(this.issuesMap), (issue) => convertToISODateString(issue["created_at"]), ["asc"]).map(
|
||||
(issue) => issue?.id
|
||||
);
|
||||
}
|
||||
|
||||
// computed functions
|
||||
getIssueById = computedFn((issueId: string) => {
|
||||
if (!issueId || !this.issuesMap[issueId]) return undefined;
|
||||
return this.issuesMap[issueId];
|
||||
});
|
||||
|
||||
// helper actions
|
||||
addIssue = (issues: TWorkspaceDraftIssue[]) => {
|
||||
if (issues && issues.length <= 0) return;
|
||||
runInAction(() => {
|
||||
issues.forEach((issue) => {
|
||||
if (!this.issuesMap[issue.id]) set(this.issuesMap, issue.id, issue);
|
||||
else update(this.issuesMap, issue.id, (prevIssue) => ({ ...prevIssue, ...issue }));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
mutateIssue = (issueId: string, issue: Partial<TWorkspaceDraftIssue>) => {
|
||||
if (!issue || !issueId || !this.issuesMap[issueId]) return;
|
||||
runInAction(() => {
|
||||
set(this.issuesMap, [issueId, "updated_at"], getCurrentDateTimeInISO());
|
||||
Object.keys(issue).forEach((key) => {
|
||||
set(this.issuesMap, [issueId, key], issue[key as keyof TWorkspaceDraftIssue]);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
removeIssue = (issueId: string) => {
|
||||
if (!issueId || !this.issuesMap[issueId]) return;
|
||||
runInAction(() => unset(this.issuesMap, issueId));
|
||||
};
|
||||
|
||||
generateNotificationQueryParams = (
|
||||
paramType: TDraftIssuePaginationType,
|
||||
filterParams = {}
|
||||
): TWorkspaceDraftQueryParams => {
|
||||
const queryCursorNext: string =
|
||||
paramType === EDraftIssuePaginationType.INIT
|
||||
? `${this.paginatedCount}:0:0`
|
||||
: paramType === EDraftIssuePaginationType.CURRENT
|
||||
? `${this.paginatedCount}:${0}:0`
|
||||
: paramType === EDraftIssuePaginationType.NEXT && this.paginationInfo
|
||||
? (this.paginationInfo?.next_cursor ?? `${this.paginatedCount}:${0}:0`)
|
||||
: `${this.paginatedCount}:${0}:0`;
|
||||
|
||||
const queryParams: TWorkspaceDraftQueryParams = {
|
||||
per_page: this.paginatedCount,
|
||||
cursor: queryCursorNext,
|
||||
...filterParams,
|
||||
};
|
||||
|
||||
return queryParams;
|
||||
};
|
||||
|
||||
// actions
|
||||
fetchIssues = async (
|
||||
workspaceSlug: string,
|
||||
loadType: TWorkspaceDraftIssueLoader,
|
||||
paginationType: TDraftIssuePaginationType = EDraftIssuePaginationType.INIT
|
||||
) => {
|
||||
try {
|
||||
this.loader = loadType;
|
||||
|
||||
// filter params and pagination params
|
||||
const filterParams = {};
|
||||
const params = this.generateNotificationQueryParams(paginationType, filterParams);
|
||||
|
||||
// fetching the paginated workspace draft issues
|
||||
const draftIssuesResponse = await workspaceDraftService.getIssues(workspaceSlug, { ...params });
|
||||
if (!draftIssuesResponse) return undefined;
|
||||
|
||||
const { results, ...paginationInfo } = draftIssuesResponse;
|
||||
runInAction(() => {
|
||||
if (results && results.length > 0) {
|
||||
this.addIssue(results as TWorkspaceDraftIssue[]);
|
||||
this.loader = undefined;
|
||||
} else {
|
||||
this.loader = "empty-state";
|
||||
}
|
||||
set(this, "paginationInfo", paginationInfo);
|
||||
});
|
||||
return draftIssuesResponse;
|
||||
} catch (error) {
|
||||
// set loader to undefined if errored out
|
||||
this.loader = undefined;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
createIssue = async (
|
||||
workspaceSlug: string,
|
||||
payload: Partial<TWorkspaceDraftIssue>
|
||||
): Promise<TWorkspaceDraftIssue | undefined> => {
|
||||
try {
|
||||
this.loader = "create";
|
||||
|
||||
const response = await workspaceDraftService.createIssue(workspaceSlug, payload);
|
||||
if (response) {
|
||||
runInAction(() => set(this.issuesMap, response.id, response));
|
||||
}
|
||||
|
||||
this.loader = undefined;
|
||||
return response;
|
||||
} catch (error) {
|
||||
this.loader = undefined;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
updateIssue = async (workspaceSlug: string, issueId: string, payload: Partial<TWorkspaceDraftIssue>) => {
|
||||
const issueBeforeUpdate = clone(this.getIssueById(issueId));
|
||||
try {
|
||||
this.loader = "update";
|
||||
runInAction(() => {
|
||||
set(this.issuesMap, [issueId, "updated_at"], getCurrentDateTimeInISO());
|
||||
set(this.issuesMap, [issueId], { ...issueBeforeUpdate, ...payload });
|
||||
});
|
||||
const response = await workspaceDraftService.updateIssue(workspaceSlug, issueId, payload);
|
||||
this.loader = undefined;
|
||||
return response;
|
||||
} catch (error) {
|
||||
this.loader = undefined;
|
||||
runInAction(() => {
|
||||
set(this.issuesMap, [issueId], issueBeforeUpdate);
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
deleteIssue = async (workspaceSlug: string, issueId: string) => {
|
||||
try {
|
||||
this.loader = "delete";
|
||||
|
||||
const response = await workspaceDraftService.deleteIssue(workspaceSlug, issueId);
|
||||
runInAction(() => unset(this.issuesMap, issueId));
|
||||
|
||||
this.loader = undefined;
|
||||
return response;
|
||||
} catch (error) {
|
||||
this.loader = undefined;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
moveIssue = async (workspaceSlug: string, issueId: string, payload: Partial<TWorkspaceDraftIssue>) => {
|
||||
try {
|
||||
this.loader = "move";
|
||||
|
||||
const response = await workspaceDraftService.moveIssue(workspaceSlug, issueId, payload);
|
||||
runInAction(() => unset(this.issuesMap, issueId));
|
||||
|
||||
this.loader = undefined;
|
||||
return response;
|
||||
} catch (error) {
|
||||
this.loader = undefined;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
addCycleToIssue = async (workspaceSlug: string, issueId: string, cycleId: string) => {
|
||||
try {
|
||||
this.loader = "update";
|
||||
const response = await this.updateIssue(workspaceSlug, issueId, { cycle_id: cycleId });
|
||||
return response;
|
||||
} catch (error) {
|
||||
this.loader = undefined;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
addModulesToIssue = async (workspaceSlug: string, issueId: string, moduleIds: string[]) => {
|
||||
try {
|
||||
this.loader = "update";
|
||||
const response = this.updateIssue(workspaceSlug, issueId, { module_ids: moduleIds });
|
||||
return response;
|
||||
} catch (error) {
|
||||
this.loader = undefined;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
BIN
web/public/empty-state/workspace-draft/issue-dark.webp
Normal file
BIN
web/public/empty-state/workspace-draft/issue-dark.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
web/public/empty-state/workspace-draft/issue-light.webp
Normal file
BIN
web/public/empty-state/workspace-draft/issue-light.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
Loading…
Add table
Add a link
Reference in a new issue