Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apiserver/plane/app/serializers/draft.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@ class Meta:
"updated_at",
"created_by",
"updated_by",
"type_id",
"description_html",
]
read_only_fields = fields

Expand Down
21 changes: 12 additions & 9 deletions web/app/[workspaceSlug]/(projects)/drafts/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import { PenSquare } from "lucide-react";
// ui
import { Breadcrumbs, Button, Header } from "@plane/ui";
// components
import { BreadcrumbLink } from "@/components/common";
import { BreadcrumbLink, CountChip } from "@/components/common";
import { CreateUpdateIssueModal } from "@/components/issues";
// constants
import { EIssuesStoreType } from "@/constants/issue";
// hooks
import { useUserPermissions } from "@/hooks/store";
import { useUserPermissions, useWorkspaceDraftIssues } from "@/hooks/store";
// plane-web
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";

Expand All @@ -20,7 +20,7 @@ export const WorkspaceDraftHeader: FC = observer(() => {
const [isDraftIssueModalOpen, setIsDraftIssueModalOpen] = useState(false);
// store hooks
const { allowPermissions } = useUserPermissions();

const { paginationInfo } = useWorkspaceDraftIssues();
// check if user is authorized to create draft issue
const isAuthorizedUser = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
Expand All @@ -37,12 +37,15 @@ export const WorkspaceDraftHeader: FC = observer(() => {
/>
<Header>
<Header.LeftItem>
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"
link={<BreadcrumbLink label={`Draft`} icon={<PenSquare className="h-4 w-4 text-custom-text-300" />} />}
/>
</Breadcrumbs>
<div className="flex items-center gap-2.5">
<Breadcrumbs>
<Breadcrumbs.BreadcrumbItem
type="text"
link={<BreadcrumbLink label={`Draft`} icon={<PenSquare className="h-4 w-4 text-custom-text-300" />} />}
/>
</Breadcrumbs>
{paginationInfo?.count && paginationInfo?.count > 0 ? <CountChip count={paginationInfo?.count} /> : <></>}
</div>
</Header.LeftItem>

<Header.RightItem>
Expand Down
18 changes: 4 additions & 14 deletions web/core/components/issues/issue-modal/base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import { useIssueModal } from "@/hooks/context/use-issue-modal";
import { useEventTracker, useCycle, useIssues, useModule, useIssueDetail, useUser } from "@/hooks/store";
import { useIssueStoreType } from "@/hooks/use-issue-layout-store";
import { useIssuesActions } from "@/hooks/use-issues-actions";
import useLocalStorage from "@/hooks/use-local-storage";
// local components
import { DraftIssueLayout } from "./draft-issue-layout";
import { IssueFormRoot } from "./form";
Expand Down Expand Up @@ -55,10 +54,6 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
const { handleCreateUpdatePropertyValues } = useIssueModal();
// pathname
const pathname = usePathname();
// local storage
const { storedValue: localStorageDraftIssues, setValue: setLocalStorageDraftIssue } = useLocalStorage<
Record<string, Partial<TIssue>>
>("draftedIssue", {});
// current store details
const { createIssue, updateIssue } = useIssuesActions(storeType);
// derived values
Expand Down Expand Up @@ -128,14 +123,9 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
setCreateMore(value);
};

const handleClose = (saveDraftIssueInLocalStorage?: boolean) => {
if (changesMade && saveDraftIssueInLocalStorage) {
// updating the current edited issue data in the local storage
let draftIssues = localStorageDraftIssues ? localStorageDraftIssues : {};
if (workspaceSlug) {
draftIssues = { ...draftIssues, [workspaceSlug.toString()]: changesMade };
setLocalStorageDraftIssue(draftIssues);
}
const handleClose = (saveAsDraft?: boolean) => {
if (changesMade && saveAsDraft && !data) {
handleCreateIssue(changesMade, true);
}

setActiveProjectId(null);
Expand Down Expand Up @@ -328,7 +318,7 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
cycle_id: data?.cycle_id ? data?.cycle_id : cycleId ? cycleId.toString() : null,
module_ids: data?.module_ids ? data?.module_ids : moduleId ? [moduleId.toString()] : null,
}}
onClose={() => handleClose(false)}
onClose={handleClose}
isCreateMoreToggleEnabled={createMore}
onCreateMoreToggleChange={handleCreateMoreToggleChange}
onSubmit={(payload) => handleFormSubmit(payload, isDraft)}
Expand Down
8 changes: 3 additions & 5 deletions web/core/components/issues/issue-modal/draft-issue-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@ import { ConfirmIssueDiscard } from "@/components/issues";
import { isEmptyHtmlString } from "@/helpers/string.helper";
// hooks
import { useIssueModal } from "@/hooks/context/use-issue-modal";
import { useEventTracker } from "@/hooks/store";
// services
import workspaceDraftService from "@/services/issue/workspace_draft.service";
import { useEventTracker, useWorkspaceDraftIssues } from "@/hooks/store";
// local components
import { IssueFormRoot } from "./form";

Expand Down Expand Up @@ -55,6 +53,7 @@ export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
// store hooks
const { captureIssueEvent } = useEventTracker();
const { handleCreateUpdatePropertyValues } = useIssueModal();
const { createIssue } = useWorkspaceDraftIssues();

const handleClose = () => {
if (data?.id) {
Expand Down Expand Up @@ -96,8 +95,7 @@ export const DraftIssueLayout: React.FC<DraftIssueProps> = observer((props) => {
project_id: projectId,
};

const response = await workspaceDraftService
.createIssue(workspaceSlug.toString(), payload)
const response = await createIssue(workspaceSlug.toString(), payload)
.then((res) => {
setToast({
type: TOAST_TYPE.SUCCESS,
Expand Down
56 changes: 38 additions & 18 deletions web/core/components/issues/workspace-draft/root.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
"use client";

import { FC } from "react";
import { FC, Fragment } from "react";
import { observer } from "mobx-react";
import useSWR from "swr";
// components
import { EmptyState } from "@/components/empty-state";
// constants
import { EmptyStateType } from "@/constants/empty-state";
import { EDraftIssuePaginationType } from "@/constants/workspace-drafts";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useWorkspaceDraftIssues } from "@/hooks/store";
import { useCommandPalette, useProject, useWorkspaceDraftIssues } from "@/hooks/store";
// components
import { DraftIssueBlock } from "./draft-issue-block";
import { WorkspaceDraftEmptyState } from "./empty-state";
Expand All @@ -21,7 +24,9 @@ type TWorkspaceDraftIssuesRoot = {
export const WorkspaceDraftIssuesRoot: FC<TWorkspaceDraftIssuesRoot> = observer((props) => {
const { workspaceSlug } = props;
// hooks
const { loader, paginationInfo, fetchIssues, issuesMap, issueIds } = useWorkspaceDraftIssues();
const { loader, paginationInfo, fetchIssues, issueIds } = useWorkspaceDraftIssues();
const { workspaceProjectIds } = useProject();
const { toggleCreateProjectModal } = useCommandPalette();

// fetching issues
useSWR(
Expand All @@ -39,6 +44,17 @@ export const WorkspaceDraftIssuesRoot: FC<TWorkspaceDraftIssuesRoot> = observer(
return <WorkspaceDraftIssuesLoader items={14} />;
}

if (workspaceProjectIds?.length === 0)
return (
<EmptyState
type={EmptyStateType.WORKSPACE_NO_PROJECTS}
size="sm"
primaryButtonOnClick={() => {
toggleCreateProjectModal(true);
}}
/>
);

Comment on lines +47 to +57
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure proper handling when workspaceProjectIds is undefined

At line 47, the condition workspaceProjectIds?.length === 0 checks for an empty array. However, if workspaceProjectIds is undefined, workspaceProjectIds?.length will be undefined, and the condition will evaluate to false. If you intend to show the EmptyState when workspaceProjectIds is undefined or empty, consider updating the condition.

Apply this diff to handle both undefined and empty arrays:

-  if (workspaceProjectIds?.length === 0)
+  if (!workspaceProjectIds || workspaceProjectIds.length === 0)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (workspaceProjectIds?.length === 0)
return (
<EmptyState
type={EmptyStateType.WORKSPACE_NO_PROJECTS}
size="sm"
primaryButtonOnClick={() => {
toggleCreateProjectModal(true);
}}
/>
);
if (!workspaceProjectIds || workspaceProjectIds.length === 0)
return (
<EmptyState
type={EmptyStateType.WORKSPACE_NO_PROJECTS}
size="sm"
primaryButtonOnClick={() => {
toggleCreateProjectModal(true);
}}
/>
);

if (loader === "empty-state" && issueIds.length <= 0) return <WorkspaceDraftEmptyState />;

return (
Expand All @@ -48,22 +64,26 @@ export const WorkspaceDraftIssuesRoot: FC<TWorkspaceDraftIssuesRoot> = observer(
<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,
}

{paginationInfo?.next_page_results && (
<Fragment>
{loader === "pagination" && issueIds.length >= 0 ? (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Simplify conditional rendering by removing redundant check

The condition issueIds.length >= 0 at line 70 is always true because an array's length is always greater than or equal to zero. You can simplify the conditional rendering by removing this redundant check.

Apply this diff to simplify the condition:

-          {loader === "pagination" && issueIds.length >= 0 ? (
+          {loader === "pagination" ? (
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{loader === "pagination" && issueIds.length >= 0 ? (
{loader === "pagination" ? (

<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,
}
)}
onClick={handleNextIssues}
>
Load More &darr;
</div>
)}
onClick={handleNextIssues}
>
Load More &darr;
</div>
</Fragment>
Comment on lines +69 to +86
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider removing unnecessary Fragment wrapper

The <Fragment> wrapper introduced at lines 69 to 86 is unnecessary since it only contains a single child element. You can remove the Fragment to simplify the code.

Apply this diff to remove the Fragment:

{paginationInfo?.next_page_results && (
-  <Fragment>
    {loader === "pagination" ? (
      <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,
          }
        )}
        onClick={handleNextIssues}
      >
        Load More &darr;
      </div>
    )}
-  </Fragment>
)}

Since Fragment is no longer used elsewhere, you can also remove the import statement at line 3:

-import { FC, Fragment } from "react";
+import { FC } from "react";

Committable suggestion was skipped due to low confidence.

)}
</div>
);
Expand Down
4 changes: 2 additions & 2 deletions web/core/store/issue/root.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ export class IssueRootStore implements IIssueRootStore {
this.profileIssues = new ProfileIssues(this, this.profileIssuesFilter);

this.workspaceDraftIssuesFilter = new WorkspaceDraftIssuesFilter(this);
this.workspaceDraftIssues = new WorkspaceDraftIssues();
this.workspaceDraftIssues = new WorkspaceDraftIssues(this);

this.projectIssuesFilter = new ProjectIssuesFilter(this);
this.projectIssues = new ProjectIssues(this, this.projectIssuesFilter);
Expand All @@ -224,6 +224,6 @@ export class IssueRootStore implements IIssueRootStore {
this.draftIssues = new DraftIssues(this, this.draftIssuesFilter);

this.issueKanBanView = new IssueKanBanViewStore(this);
this.issueCalendarView = new CalendarStore();
this.issueCalendarView = new CalendarStore(this);
}
}
51 changes: 36 additions & 15 deletions web/core/store/issue/workspace-draft/issue.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,17 @@ import { EDraftIssuePaginationType } from "@/constants/workspace-drafts";
import { getCurrentDateTimeInISO, convertToISODateString } from "@/helpers/date-time.helper";
// services
import workspaceDraftService from "@/services/issue/workspace_draft.service";
// types
import { IIssueRootStore } from "../root.store";

export type TDraftIssuePaginationType = EDraftIssuePaginationType;

export interface IWorkspaceDraftIssues {
// observables
issuesMap: Record<string, TWorkspaceDraftIssue>;
paginationInfo: Omit<TWorkspaceDraftPaginationInfo<TWorkspaceDraftIssue>, "results"> | undefined;
loader: TWorkspaceDraftIssueLoader;
paginationInfo: Omit<TWorkspaceDraftPaginationInfo<TWorkspaceDraftIssue>, "results"> | undefined;
issuesMap: Record<string, TWorkspaceDraftIssue>; // issue_id -> issue;
issueMapIds: Record<string, string[]>; // workspace_id -> issue_ids;
// computed
issueIds: string[];
// computed functions
Expand Down Expand Up @@ -112,15 +115,17 @@ export class WorkspaceDraftIssues implements IWorkspaceDraftIssues {
// local constants
paginatedCount = 50;
// observables
paginationInfo: Omit<TWorkspaceDraftPaginationInfo<TWorkspaceDraftIssue>, "results"> | undefined = undefined;
loader: TWorkspaceDraftIssueLoader = undefined;
paginationInfo: Omit<TWorkspaceDraftPaginationInfo<TWorkspaceDraftIssue>, "results"> | undefined = undefined;
issuesMap: Record<string, TWorkspaceDraftIssue> = {};
issueMapIds: Record<string, string[]> = {};

constructor() {
constructor(public issueStore: IIssueRootStore) {
makeObservable(this, {
paginationInfo: observable,
loader: observable.ref,
paginationInfo: observable,
issuesMap: observable,
issueMapIds: observable,
// computed
issueIds: computed,
// action
Expand All @@ -136,10 +141,11 @@ export class WorkspaceDraftIssues implements IWorkspaceDraftIssues {

// 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
);
const workspaceSlug = this.issueStore.workspaceSlug;
if (!workspaceSlug) return [];
if (!this.issueMapIds[workspaceSlug]) return [];
const issueIds = this.issueMapIds[workspaceSlug];
return orderBy(issueIds, (issueId) => convertToISODateString(this.issuesMap[issueId]?.created_at), ["desc"]);
}

// computed functions
Expand Down Expand Up @@ -216,7 +222,10 @@ export class WorkspaceDraftIssues implements IWorkspaceDraftIssues {
const { results, ...paginationInfo } = draftIssuesResponse;
runInAction(() => {
if (results && results.length > 0) {
this.addIssue(results as TWorkspaceDraftIssue[]);
// adding issueIds
const issueIds = results.map((issue) => issue.id);
this.addIssue(results);
update(this.issueMapIds, [workspaceSlug], (existingIssueIds = []) => [...issueIds, ...existingIssueIds]);
this.loader = undefined;
} else {
this.loader = "empty-state";
Expand All @@ -240,7 +249,10 @@ export class WorkspaceDraftIssues implements IWorkspaceDraftIssues {

const response = await workspaceDraftService.createIssue(workspaceSlug, payload);
if (response) {
runInAction(() => set(this.issuesMap, response.id, response));
runInAction(() => {
this.addIssue([response]);
update(this.issueMapIds, [workspaceSlug], (existingIssueIds = []) => [response.id, ...existingIssueIds]);
});
}

this.loader = undefined;
Expand All @@ -256,8 +268,11 @@ export class WorkspaceDraftIssues implements IWorkspaceDraftIssues {
try {
this.loader = "update";
runInAction(() => {
set(this.issuesMap, [issueId, "updated_at"], getCurrentDateTimeInISO());
set(this.issuesMap, [issueId], { ...issueBeforeUpdate, ...payload });
set(this.issuesMap, [issueId], {
...issueBeforeUpdate,
...payload,
...{ updated_at: getCurrentDateTimeInISO() },
});
});
const response = await workspaceDraftService.updateIssue(workspaceSlug, issueId, payload);
this.loader = undefined;
Expand All @@ -276,7 +291,10 @@ export class WorkspaceDraftIssues implements IWorkspaceDraftIssues {
this.loader = "delete";

const response = await workspaceDraftService.deleteIssue(workspaceSlug, issueId);
runInAction(() => unset(this.issuesMap, issueId));
runInAction(() => {
unset(this.issueMapIds[workspaceSlug], issueId);
unset(this.issuesMap, issueId);
});
Comment on lines +294 to +297
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix issue removal: incorrect use of unset on arrays

The deleteIssue method uses unset on an array, which does not remove the issueId as intended. Since issueMapIds[workspaceSlug] is an array, you should remove the issueId using array manipulation methods.

Suggested fix:

runInAction(() => {
-  unset(this.issueMapIds[workspaceSlug], issueId);
+  update(this.issueMapIds, [workspaceSlug], (issueIds = []) => issueIds.filter(id => id !== issueId));
   unset(this.issuesMap, issueId);
});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
runInAction(() => {
unset(this.issueMapIds[workspaceSlug], issueId);
unset(this.issuesMap, issueId);
});
runInAction(() => {
update(this.issueMapIds, [workspaceSlug], (issueIds = []) => issueIds.filter(id => id !== issueId));
unset(this.issuesMap, issueId);
});


this.loader = undefined;
return response;
Expand All @@ -291,7 +309,10 @@ export class WorkspaceDraftIssues implements IWorkspaceDraftIssues {
this.loader = "move";

const response = await workspaceDraftService.moveIssue(workspaceSlug, issueId, payload);
runInAction(() => unset(this.issuesMap, issueId));
runInAction(() => {
unset(this.issueMapIds[workspaceSlug], issueId);
unset(this.issuesMap, issueId);
});
Comment on lines +312 to +315
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix issue removal in moveIssue: incorrect use of unset on arrays

Similar to deleteIssue, the moveIssue method incorrectly uses unset on an array to remove an issueId. This does not effectively remove the issue ID from the list.

Suggested fix:

runInAction(() => {
-  unset(this.issueMapIds[workspaceSlug], issueId);
+  update(this.issueMapIds, [workspaceSlug], (issueIds = []) => issueIds.filter(id => id !== issueId));
   unset(this.issuesMap, issueId);
});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
runInAction(() => {
unset(this.issueMapIds[workspaceSlug], issueId);
unset(this.issuesMap, issueId);
});
runInAction(() => {
update(this.issueMapIds, [workspaceSlug], (issueIds = []) => issueIds.filter(id => id !== issueId));
unset(this.issuesMap, issueId);
});


this.loader = undefined;
return response;
Expand Down
Binary file modified web/public/empty-state/workspace-draft/issue-dark.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified web/public/empty-state/workspace-draft/issue-light.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.