[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):
|
class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||||
|
|
||||||
model = DraftIssue
|
model = DraftIssue
|
||||||
|
|
||||||
@method_decorator(gzip_page)
|
def get_queryset(self):
|
||||||
@allow_permission(
|
return (
|
||||||
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
|
DraftIssue.objects.filter(workspace__slug=self.kwargs.get("slug"))
|
||||||
)
|
|
||||||
def list(self, request, slug):
|
|
||||||
filters = issue_filters(request.query_params, "GET")
|
|
||||||
issues = (
|
|
||||||
DraftIssue.objects.filter(workspace__slug=slug)
|
|
||||||
.filter(created_by=request.user)
|
|
||||||
.select_related("workspace", "project", "state", "parent")
|
.select_related("workspace", "project", "state", "parent")
|
||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
"assignees", "labels", "draft_issue_module__module"
|
"assignees", "labels", "draft_issue_module__module"
|
||||||
|
|
@ -91,6 +84,17 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||||
Value([], output_field=ArrayField(UUIDField())),
|
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")
|
.order_by("-created_at")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -120,7 +124,34 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||||
)
|
)
|
||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
serializer.save()
|
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)
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
@allow_permission(
|
@allow_permission(
|
||||||
|
|
@ -131,45 +162,7 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||||
)
|
)
|
||||||
def partial_update(self, request, slug, pk):
|
def partial_update(self, request, slug, pk):
|
||||||
issue = (
|
issue = (
|
||||||
DraftIssue.objects.filter(workspace__slug=slug)
|
self.get_queryset().filter(pk=pk, created_by=request.user).first()
|
||||||
.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()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if not issue:
|
if not issue:
|
||||||
|
|
@ -202,46 +195,8 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||||
)
|
)
|
||||||
def retrieve(self, request, slug, pk=None):
|
def retrieve(self, request, slug, pk=None):
|
||||||
issue = (
|
issue = (
|
||||||
DraftIssue.objects.filter(workspace__slug=slug)
|
self.get_queryset().filter(pk=pk, created_by=request.user).first()
|
||||||
.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"))
|
|
||||||
.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:
|
if not issue:
|
||||||
return Response(
|
return Response(
|
||||||
|
|
@ -268,42 +223,7 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||||
level="WORKSPACE",
|
level="WORKSPACE",
|
||||||
)
|
)
|
||||||
def create_draft_to_issue(self, request, slug, draft_id):
|
def create_draft_to_issue(self, request, slug, draft_id):
|
||||||
draft_issue = (
|
draft_issue = self.get_queryset().filter(pk=draft_id).first()
|
||||||
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()
|
|
||||||
)
|
|
||||||
|
|
||||||
if not draft_issue.project_id:
|
if not draft_issue.project_id:
|
||||||
return Response(
|
return Response(
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ class OffsetPaginator:
|
||||||
raise BadPaginationError("Pagination offset cannot be negative")
|
raise BadPaginationError("Pagination offset cannot be negative")
|
||||||
|
|
||||||
results = queryset[offset:stop]
|
results = queryset[offset:stop]
|
||||||
|
print(limit, "limit")
|
||||||
if cursor.value != limit:
|
if cursor.value != limit:
|
||||||
results = results[-(limit + 1) :]
|
results = results[-(limit + 1) :]
|
||||||
|
|
||||||
|
|
@ -761,7 +761,7 @@ class BasePaginator:
|
||||||
):
|
):
|
||||||
"""Paginate the request"""
|
"""Paginate the request"""
|
||||||
per_page = self.get_per_page(request, default_per_page, max_per_page)
|
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
|
# Convert the cursor value to integer and float from string
|
||||||
input_cursor = None
|
input_cursor = None
|
||||||
try:
|
try:
|
||||||
|
|
@ -788,6 +788,7 @@ class BasePaginator:
|
||||||
paginator = paginator_cls(**paginator_kwargs)
|
paginator = paginator_cls(**paginator_kwargs)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
print(per_page, "per_page 2")
|
||||||
cursor_result = paginator.get_result(
|
cursor_result = paginator.get_result(
|
||||||
limit=per_page, cursor=input_cursor
|
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 "./publish";
|
||||||
export * from "./workspace-notifications";
|
export * from "./workspace-notifications";
|
||||||
export * from "./favorite";
|
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 "./issue_sub_issues";
|
||||||
export * from "./activity/base";
|
export * from "./activity/base";
|
||||||
|
|
||||||
|
|
||||||
export type TLoader =
|
export type TLoader =
|
||||||
| "init-loader"
|
| "init-loader"
|
||||||
| "mutation"
|
| "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.PROJECT_VIEW
|
||||||
| EIssuesStoreType.DRAFT
|
| EIssuesStoreType.DRAFT
|
||||||
| EIssuesStoreType.PROFILE
|
| EIssuesStoreType.PROFILE
|
||||||
| EIssuesStoreType.ARCHIVED;
|
| EIssuesStoreType.ARCHIVED
|
||||||
|
| EIssuesStoreType.WORKSPACE_DRAFT;
|
||||||
interface IBaseListRoot {
|
interface IBaseListRoot {
|
||||||
QuickActions: FC<IQuickActionProps>;
|
QuickActions: FC<IQuickActionProps>;
|
||||||
addIssuesToView?: (issueIds: string[]) => Promise<any>;
|
addIssuesToView?: (issueIds: string[]) => Promise<any>;
|
||||||
|
|
@ -61,8 +62,9 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
|
||||||
const showEmptyGroup = displayFilters?.show_empty_groups ?? false;
|
const showEmptyGroup = displayFilters?.show_empty_groups ?? false;
|
||||||
|
|
||||||
const { workspaceSlug, projectId } = useParams();
|
const { workspaceSlug, projectId } = useParams();
|
||||||
const {updateFilters} = useIssuesActions(storeType);
|
const { updateFilters } = useIssuesActions(storeType);
|
||||||
const collapsedGroups = issuesFilter?.issueFilters?.kanbanFilters || { group_by: [], sub_group_by: [] } as TIssueKanbanFilters;
|
const collapsedGroups =
|
||||||
|
issuesFilter?.issueFilters?.kanbanFilters || ({ group_by: [], sub_group_by: [] } as TIssueKanbanFilters);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchIssues("init-loader", { canGroup: true, perPageCount: group_by ? 50 : 100 }, viewId);
|
fetchIssues("init-loader", { canGroup: true, perPageCount: group_by ? 50 : 100 }, viewId);
|
||||||
|
|
@ -122,15 +124,14 @@ export const BaseListRoot = observer((props: IBaseListRoot) => {
|
||||||
} else {
|
} else {
|
||||||
collapsedGroups.push(value);
|
collapsedGroups.push(value);
|
||||||
}
|
}
|
||||||
updateFilters(projectId?.toString() ?? "", EIssueFilterType.KANBAN_FILTERS,
|
updateFilters(projectId?.toString() ?? "", EIssueFilterType.KANBAN_FILTERS, {
|
||||||
{ group_by: collapsedGroups } as TIssueKanbanFilters
|
group_by: collapsedGroups,
|
||||||
);
|
} as TIssueKanbanFilters);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[workspaceSlug, issuesFilter, projectId, updateFilters]
|
[workspaceSlug, issuesFilter, projectId, updateFilters]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IssueLayoutHOC layout={EIssueLayoutTypes.LIST}>
|
<IssueLayoutHOC layout={EIssueLayoutTypes.LIST}>
|
||||||
<div className={`relative size-full bg-custom-background-90`}>
|
<div className={`relative size-full bg-custom-background-90`}>
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ export interface IList {
|
||||||
isCompletedCycle?: boolean;
|
isCompletedCycle?: boolean;
|
||||||
loadMoreIssues: (groupId?: string) => void;
|
loadMoreIssues: (groupId?: string) => void;
|
||||||
handleCollapsedGroups: (value: string) => void;
|
handleCollapsedGroups: (value: string) => void;
|
||||||
collapsedGroups : TIssueKanbanFilters;
|
collapsedGroups: TIssueKanbanFilters;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const List: React.FC<IList> = observer((props) => {
|
export const List: React.FC<IList> = observer((props) => {
|
||||||
|
|
@ -71,7 +71,7 @@ export const List: React.FC<IList> = observer((props) => {
|
||||||
isCompletedCycle = false,
|
isCompletedCycle = false,
|
||||||
loadMoreIssues,
|
loadMoreIssues,
|
||||||
handleCollapsedGroups,
|
handleCollapsedGroups,
|
||||||
collapsedGroups
|
collapsedGroups,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const storeType = useIssueStoreType();
|
const storeType = useIssueStoreType();
|
||||||
|
|
@ -133,7 +133,6 @@ export const List: React.FC<IList> = observer((props) => {
|
||||||
} else {
|
} else {
|
||||||
entities = orderedGroups;
|
entities = orderedGroups;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative size-full flex flex-col">
|
<div className="relative size-full flex flex-col">
|
||||||
{groups && (
|
{groups && (
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ export interface IQuickActionProps {
|
||||||
handleRemoveFromView?: () => Promise<void>;
|
handleRemoveFromView?: () => Promise<void>;
|
||||||
handleArchive?: () => Promise<void>;
|
handleArchive?: () => Promise<void>;
|
||||||
handleRestore?: () => Promise<void>;
|
handleRestore?: () => Promise<void>;
|
||||||
|
handleMoveToIssues?: () => Promise<void>;
|
||||||
customActionButton?: React.ReactElement;
|
customActionButton?: React.ReactElement;
|
||||||
portalElement?: HTMLDivElement | null;
|
portalElement?: HTMLDivElement | null;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,7 @@ import { WithDisplayPropertiesHOC } from "./with-display-properties-HOC";
|
||||||
|
|
||||||
export interface IIssueProperties {
|
export interface IIssueProperties {
|
||||||
issue: TIssue;
|
issue: TIssue;
|
||||||
updateIssue:
|
updateIssue: ((projectId: string | null, issueId: string, data: Partial<TIssue>) => Promise<void>) | undefined;
|
||||||
| ((projectId: string | null, issueId: string, data: Partial<TIssue>) => Promise<void>)
|
|
||||||
| undefined;
|
|
||||||
displayProperties: IIssueDisplayProperties | undefined;
|
displayProperties: IIssueDisplayProperties | undefined;
|
||||||
isReadOnly: boolean;
|
isReadOnly: boolean;
|
||||||
className: string;
|
className: string;
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,4 @@ export * from "./project-issue";
|
||||||
export * from "./archived-issue";
|
export * from "./archived-issue";
|
||||||
export * from "./draft-issue";
|
export * from "./draft-issue";
|
||||||
export * from "./all-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 { fetchModuleDetails } = useModule();
|
||||||
const { issues } = useIssues(storeType);
|
const { issues } = useIssues(storeType);
|
||||||
const { issues: projectIssues } = useIssues(EIssuesStoreType.PROJECT);
|
const { issues: projectIssues } = useIssues(EIssuesStoreType.PROJECT);
|
||||||
const { issues: draftIssues } = useIssues(EIssuesStoreType.DRAFT);
|
const { issues: draftIssues } = useIssues(EIssuesStoreType.WORKSPACE_DRAFT);
|
||||||
const { fetchIssue } = useIssueDetail();
|
const { fetchIssue } = useIssueDetail();
|
||||||
const { handleCreateUpdatePropertyValues } = useIssueModal();
|
const { handleCreateUpdatePropertyValues } = useIssueModal();
|
||||||
// pathname
|
// pathname
|
||||||
|
|
@ -70,7 +70,7 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
||||||
if (!workspaceSlug) return;
|
if (!workspaceSlug) return;
|
||||||
|
|
||||||
if (!projectId || issueId === undefined || !fetchIssueDetails) {
|
if (!projectId || issueId === undefined || !fetchIssueDetails) {
|
||||||
// Set description to the issue description from the props if available
|
// Set description to the issue description from the props if available
|
||||||
setDescription(data?.description_html || "<p></p>");
|
setDescription(data?.description_html || "<p></p>");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -151,10 +151,9 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let response;
|
let response;
|
||||||
|
|
||||||
// if draft issue, use draft issue store to create issue
|
// if draft issue, use draft issue store to create issue
|
||||||
if (is_draft_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
|
// 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
|
// 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" },
|
payload: { ...response, state: "SUCCESS" },
|
||||||
path: pathname,
|
path: pathname,
|
||||||
});
|
});
|
||||||
!createMore && handleClose();
|
if (!createMore) handleClose();
|
||||||
if (createMore) issueTitleRef && issueTitleRef?.current?.focus();
|
if (createMore && issueTitleRef) issueTitleRef?.current?.focus();
|
||||||
setDescription("<p></p>");
|
setDescription("<p></p>");
|
||||||
setChangesMade(null);
|
setChangesMade(null);
|
||||||
return response;
|
return response;
|
||||||
|
|
@ -237,9 +236,8 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
||||||
if (!workspaceSlug || !payload.project_id || !data?.id) return;
|
if (!workspaceSlug || !payload.project_id || !data?.id) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
isDraft
|
if (isDraft) await draftIssues.updateIssue(workspaceSlug.toString(), data.id, payload);
|
||||||
? await draftIssues.updateIssue(workspaceSlug.toString(), payload.project_id, data.id, payload)
|
else if (updateIssue) await updateIssue(payload.project_id, data.id, payload);
|
||||||
: updateIssue && (await updateIssue(payload.project_id, data.id, payload));
|
|
||||||
|
|
||||||
// add other property values
|
// add other property values
|
||||||
await handleCreateUpdatePropertyValues({
|
await handleCreateUpdatePropertyValues({
|
||||||
|
|
@ -260,6 +258,7 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
||||||
});
|
});
|
||||||
handleClose();
|
handleClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.ERROR,
|
type: TOAST_TYPE.ERROR,
|
||||||
title: "Error!",
|
title: "Error!",
|
||||||
|
|
@ -314,7 +313,7 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
||||||
issueTitleRef={issueTitleRef}
|
issueTitleRef={issueTitleRef}
|
||||||
onChange={handleFormChange}
|
onChange={handleFormChange}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
onSubmit={handleFormSubmit}
|
onSubmit={(payload) => handleFormSubmit(payload, isDraft)}
|
||||||
projectId={activeProjectId}
|
projectId={activeProjectId}
|
||||||
isCreateMoreToggleEnabled={createMore}
|
isCreateMoreToggleEnabled={createMore}
|
||||||
onCreateMoreToggleChange={handleCreateMoreToggleChange}
|
onCreateMoreToggleChange={handleCreateMoreToggleChange}
|
||||||
|
|
@ -332,7 +331,7 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
|
||||||
onClose={() => handleClose(false)}
|
onClose={() => handleClose(false)}
|
||||||
isCreateMoreToggleEnabled={createMore}
|
isCreateMoreToggleEnabled={createMore}
|
||||||
onCreateMoreToggleChange={handleCreateMoreToggleChange}
|
onCreateMoreToggleChange={handleCreateMoreToggleChange}
|
||||||
onSubmit={handleFormSubmit}
|
onSubmit={(payload) => handleFormSubmit(payload, isDraft)}
|
||||||
projectId={activeProjectId}
|
projectId={activeProjectId}
|
||||||
isDraft={isDraft}
|
isDraft={isDraft}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import { isEmptyHtmlString } from "@/helpers/string.helper";
|
||||||
import { useIssueModal } from "@/hooks/context/use-issue-modal";
|
import { useIssueModal } from "@/hooks/context/use-issue-modal";
|
||||||
import { useEventTracker } from "@/hooks/store";
|
import { useEventTracker } from "@/hooks/store";
|
||||||
// services
|
// services
|
||||||
import { IssueDraftService } from "@/services/issue";
|
import workspaceDraftService from "@/services/issue/workspace_draft.service";
|
||||||
// local components
|
// local components
|
||||||
import { IssueFormRoot } from "./form";
|
import { IssueFormRoot } from "./form";
|
||||||
|
|
||||||
|
|
@ -33,8 +33,6 @@ export interface DraftIssueProps {
|
||||||
isDraft: boolean;
|
isDraft: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const issueDraftService = new IssueDraftService();
|
|
||||||
|
|
||||||
export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
|
export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
|
||||||
const {
|
const {
|
||||||
changesMade,
|
changesMade,
|
||||||
|
|
@ -95,10 +93,11 @@ export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
|
||||||
const payload = {
|
const payload = {
|
||||||
...changesMade,
|
...changesMade,
|
||||||
name: changesMade?.name && changesMade?.name?.trim() !== "" ? changesMade.name?.trim() : "Untitled",
|
name: changesMade?.name && changesMade?.name?.trim() !== "" ? changesMade.name?.trim() : "Untitled",
|
||||||
|
project_id: projectId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await issueDraftService
|
const response = await workspaceDraftService
|
||||||
.createDraftIssue(workspaceSlug.toString(), projectId.toString(), payload)
|
.createIssue(workspaceSlug.toString(), payload)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.SUCCESS,
|
type: TOAST_TYPE.SUCCESS,
|
||||||
|
|
|
||||||
|
|
@ -266,7 +266,9 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||||
)}
|
)}
|
||||||
<form onSubmit={handleSubmit((data) => handleFormSubmit(data))}>
|
<form onSubmit={handleSubmit((data) => handleFormSubmit(data))}>
|
||||||
<div className="p-5">
|
<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 */}
|
{/* Disable project selection if editing an issue */}
|
||||||
<div className="flex items-center pt-2 pb-4 gap-x-1">
|
<div className="flex items-center pt-2 pb-4 gap-x-1">
|
||||||
<IssueProjectSelect
|
<IssueProjectSelect
|
||||||
|
|
@ -397,31 +399,6 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||||
>
|
>
|
||||||
Discard
|
Discard
|
||||||
</Button>
|
</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
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|
@ -430,7 +407,15 @@ export const IssueFormRoot: FC<IssueFormProps> = observer((props) => {
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
tabIndex={isDraft ? getIndex("submit_button") : getIndex("draft_button")}
|
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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
)}
|
)}
|
||||||
{isAuthorized && (
|
{/* {isAuthorized && (
|
||||||
<CustomMenu.MenuItem>
|
<CustomMenu.MenuItem>
|
||||||
<Link href={`/${workspaceSlug}/projects/${project?.id}/draft-issues/`}>
|
<Link href={`/${workspaceSlug}/projects/${project?.id}/draft-issues/`}>
|
||||||
<div className="flex items-center justify-start gap-2">
|
<div className="flex items-center justify-start gap-2">
|
||||||
|
|
@ -437,7 +437,7 @@ export const SidebarProjectsListItem: React.FC<Props> = observer((props) => {
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
)}
|
)} */}
|
||||||
<CustomMenu.MenuItem onClick={handleCopyText}>
|
<CustomMenu.MenuItem onClick={handleCopyText}>
|
||||||
<span className="flex items-center justify-start gap-2">
|
<span className="flex items-center justify-start gap-2">
|
||||||
<LinkIcon className="h-3.5 w-3.5 stroke-[1.5]" />
|
<LinkIcon className="h-3.5 w-3.5 stroke-[1.5]" />
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ export const SidebarQuickActions = observer(() => {
|
||||||
<PenSquare className="size-4" />
|
<PenSquare className="size-4" />
|
||||||
{!isSidebarCollapsed && <span className="text-sm font-medium">New issue</span>}
|
{!isSidebarCollapsed && <span className="text-sm font-medium">New issue</span>}
|
||||||
</button>
|
</button>
|
||||||
{!disabled && workspaceDraftIssue && (
|
{/* {!disabled && workspaceDraftIssue && (
|
||||||
<>
|
<>
|
||||||
{!isSidebarCollapsed && (
|
{!isSidebarCollapsed && (
|
||||||
<button type="button" className="grid place-items-center">
|
<button type="button" className="grid place-items-center">
|
||||||
|
|
@ -127,7 +127,7 @@ export const SidebarQuickActions = observer(() => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { linearGradientDef } from "@nivo/core";
|
import { linearGradientDef } from "@nivo/core";
|
||||||
// icons
|
// icons
|
||||||
import { BarChart2, Briefcase, Home, Inbox, Layers } from "lucide-react";
|
import { BarChart2, Briefcase, Home, Inbox, Layers, PenSquare } from "lucide-react";
|
||||||
// types
|
// types
|
||||||
import { TIssuesListTypes, TStateGroups } from "@plane/types";
|
import { TIssuesListTypes, TStateGroups } from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
|
|
@ -329,4 +329,12 @@ export const SIDEBAR_USER_MENU_ITEMS: {
|
||||||
highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/notifications/`),
|
highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/notifications/`),
|
||||||
Icon: Inbox,
|
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_CLOSED_TAB = "inbox-sidebar-closed-tab",
|
||||||
INBOX_SIDEBAR_FILTER_EMPTY_STATE = "inbox-sidebar-filter-empty-state",
|
INBOX_SIDEBAR_FILTER_EMPTY_STATE = "inbox-sidebar-filter-empty-state",
|
||||||
INBOX_DETAIL_EMPTY_STATE = "inbox-detail-empty-state",
|
INBOX_DETAIL_EMPTY_STATE = "inbox-detail-empty-state",
|
||||||
|
|
||||||
|
WORKSPACE_DRAFT_ISSUES = "workspace-draft-issues",
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyStateDetails = {
|
const emptyStateDetails = {
|
||||||
|
|
@ -757,6 +759,17 @@ const emptyStateDetails = {
|
||||||
title: "Select an issue to view its details.",
|
title: "Select an issue to view its details.",
|
||||||
path: "/empty-state/intake/issue-detail",
|
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;
|
} as const;
|
||||||
|
|
||||||
export const EMPTY_STATE_DETAILS: Record<EmptyStateType, EmptyStateDetails> = emptyStateDetails;
|
export const EMPTY_STATE_DETAILS: Record<EmptyStateType, EmptyStateDetails> = emptyStateDetails;
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ export enum EIssuesStoreType {
|
||||||
ARCHIVED = "ARCHIVED",
|
ARCHIVED = "ARCHIVED",
|
||||||
DRAFT = "DRAFT",
|
DRAFT = "DRAFT",
|
||||||
DEFAULT = "DEFAULT",
|
DEFAULT = "DEFAULT",
|
||||||
|
WORKSPACE_DRAFT = "WORKSPACE_DRAFT",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum EIssueLayoutTypes {
|
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 "./use-workspace";
|
||||||
export * from "./user";
|
export * from "./user";
|
||||||
export * from "./use-transient";
|
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 { IProjectIssues, IProjectIssuesFilter } from "@/store/issue/project";
|
||||||
import { IProjectViewIssues, IProjectViewIssuesFilter } from "@/store/issue/project-views";
|
import { IProjectViewIssues, IProjectViewIssuesFilter } from "@/store/issue/project-views";
|
||||||
import { IWorkspaceIssues, IWorkspaceIssuesFilter } from "@/store/issue/workspace";
|
import { IWorkspaceIssues, IWorkspaceIssuesFilter } from "@/store/issue/workspace";
|
||||||
|
import { IWorkspaceDraftIssues, IWorkspaceDraftIssuesFilter } from "@/store/issue/workspace-draft";
|
||||||
// constants
|
// constants
|
||||||
|
|
||||||
type defaultIssueStore = {
|
type defaultIssueStore = {
|
||||||
|
|
@ -24,6 +25,10 @@ export type TStoreIssues = {
|
||||||
issues: IWorkspaceIssues;
|
issues: IWorkspaceIssues;
|
||||||
issuesFilter: IWorkspaceIssuesFilter;
|
issuesFilter: IWorkspaceIssuesFilter;
|
||||||
};
|
};
|
||||||
|
[EIssuesStoreType.WORKSPACE_DRAFT]: defaultIssueStore & {
|
||||||
|
issues: IWorkspaceDraftIssues;
|
||||||
|
issuesFilter: IWorkspaceDraftIssuesFilter;
|
||||||
|
};
|
||||||
[EIssuesStoreType.PROFILE]: defaultIssueStore & {
|
[EIssuesStoreType.PROFILE]: defaultIssueStore & {
|
||||||
issues: IProfileIssues;
|
issues: IProfileIssues;
|
||||||
issuesFilter: IProfileIssuesFilter;
|
issuesFilter: IProfileIssuesFilter;
|
||||||
|
|
@ -72,6 +77,16 @@ export const useIssues = <T extends EIssuesStoreType>(storeType?: T): TStoreIssu
|
||||||
issues: context.issue.workspaceIssues,
|
issues: context.issue.workspaceIssues,
|
||||||
issuesFilter: context.issue.workspaceIssuesFilter,
|
issuesFilter: context.issue.workspaceIssuesFilter,
|
||||||
}) as TStoreIssues[T];
|
}) 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:
|
case EIssuesStoreType.PROFILE:
|
||||||
return merge(defaultStore, {
|
return merge(defaultStore, {
|
||||||
issues: context.issue.profileIssues,
|
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 profileIssueActions = useProfileIssueActions();
|
||||||
const draftIssueActions = useDraftIssueActions();
|
const draftIssueActions = useDraftIssueActions();
|
||||||
const archivedIssueActions = useArchivedIssueActions();
|
const archivedIssueActions = useArchivedIssueActions();
|
||||||
|
const workspaceDraftIssueActions = useWorkspaceDraftIssueActions();
|
||||||
|
|
||||||
switch (storeType) {
|
switch (storeType) {
|
||||||
case EIssuesStoreType.PROJECT_VIEW:
|
case EIssuesStoreType.PROJECT_VIEW:
|
||||||
|
|
@ -61,6 +62,8 @@ export const useIssuesActions = (storeType: EIssuesStoreType): IssueActions => {
|
||||||
return moduleIssueActions;
|
return moduleIssueActions;
|
||||||
case EIssuesStoreType.GLOBAL:
|
case EIssuesStoreType.GLOBAL:
|
||||||
return globalIssueActions;
|
return globalIssueActions;
|
||||||
|
case EIssuesStoreType.WORKSPACE_DRAFT:
|
||||||
|
return workspaceDraftIssueActions;
|
||||||
case EIssuesStoreType.PROJECT:
|
case EIssuesStoreType.PROJECT:
|
||||||
default:
|
default:
|
||||||
return projectIssueActions;
|
return projectIssueActions;
|
||||||
|
|
@ -737,3 +740,80 @@ const useGlobalIssueActions = () => {
|
||||||
[createIssue, updateIssue, removeIssue, updateFilters]
|
[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_activity.service";
|
||||||
export * from "./issue_comment.service";
|
export * from "./issue_comment.service";
|
||||||
export * from "./issue_relation.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,
|
IProjectViewIssues,
|
||||||
ProjectViewIssues,
|
ProjectViewIssues,
|
||||||
} from "./project-views";
|
} 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 {
|
export interface IIssueRootStore {
|
||||||
currentUserId: string | undefined;
|
currentUserId: string | undefined;
|
||||||
|
|
@ -55,6 +56,9 @@ export interface IIssueRootStore {
|
||||||
workspaceIssuesFilter: IWorkspaceIssuesFilter;
|
workspaceIssuesFilter: IWorkspaceIssuesFilter;
|
||||||
workspaceIssues: IWorkspaceIssues;
|
workspaceIssues: IWorkspaceIssues;
|
||||||
|
|
||||||
|
workspaceDraftIssuesFilter: IWorkspaceDraftIssuesFilter;
|
||||||
|
workspaceDraftIssues: IWorkspaceDraftIssues;
|
||||||
|
|
||||||
profileIssuesFilter: IProfileIssuesFilter;
|
profileIssuesFilter: IProfileIssuesFilter;
|
||||||
profileIssues: IProfileIssues;
|
profileIssues: IProfileIssues;
|
||||||
|
|
||||||
|
|
@ -110,6 +114,9 @@ export class IssueRootStore implements IIssueRootStore {
|
||||||
workspaceIssuesFilter: IWorkspaceIssuesFilter;
|
workspaceIssuesFilter: IWorkspaceIssuesFilter;
|
||||||
workspaceIssues: IWorkspaceIssues;
|
workspaceIssues: IWorkspaceIssues;
|
||||||
|
|
||||||
|
workspaceDraftIssuesFilter: IWorkspaceDraftIssuesFilter;
|
||||||
|
workspaceDraftIssues: IWorkspaceDraftIssues;
|
||||||
|
|
||||||
profileIssuesFilter: IProfileIssuesFilter;
|
profileIssuesFilter: IProfileIssuesFilter;
|
||||||
profileIssues: IProfileIssues;
|
profileIssues: IProfileIssues;
|
||||||
|
|
||||||
|
|
@ -190,6 +197,9 @@ export class IssueRootStore implements IIssueRootStore {
|
||||||
this.profileIssuesFilter = new ProfileIssuesFilter(this);
|
this.profileIssuesFilter = new ProfileIssuesFilter(this);
|
||||||
this.profileIssues = new ProfileIssues(this, this.profileIssuesFilter);
|
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.projectIssuesFilter = new ProjectIssuesFilter(this);
|
||||||
this.projectIssues = new ProjectIssues(this, this.projectIssuesFilter);
|
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