diff --git a/apiserver/plane/app/serializers/draft.py b/apiserver/plane/app/serializers/draft.py index 2128b927d2..10929e453d 100644 --- a/apiserver/plane/app/serializers/draft.py +++ b/apiserver/plane/app/serializers/draft.py @@ -276,6 +276,8 @@ class DraftIssueSerializer(BaseSerializer): "updated_at", "created_by", "updated_by", + "type_id", + "description_html", ] read_only_fields = fields diff --git a/web/app/[workspaceSlug]/(projects)/drafts/header.tsx b/web/app/[workspaceSlug]/(projects)/drafts/header.tsx index dfe9c5fa32..6f0b90717d 100644 --- a/web/app/[workspaceSlug]/(projects)/drafts/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/drafts/header.tsx @@ -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"; @@ -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], @@ -37,12 +37,15 @@ export const WorkspaceDraftHeader: FC = observer(() => { />
- - } />} - /> - +
+ + } />} + /> + + {paginationInfo?.count && paginationInfo?.count > 0 ? : <>} +
diff --git a/web/core/components/issues/issue-modal/base.tsx b/web/core/components/issues/issue-modal/base.tsx index acaea9ebd5..afbebcebad 100644 --- a/web/core/components/issues/issue-modal/base.tsx +++ b/web/core/components/issues/issue-modal/base.tsx @@ -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"; @@ -55,10 +54,6 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( const { handleCreateUpdatePropertyValues } = useIssueModal(); // pathname const pathname = usePathname(); - // local storage - const { storedValue: localStorageDraftIssues, setValue: setLocalStorageDraftIssue } = useLocalStorage< - Record> - >("draftedIssue", {}); // current store details const { createIssue, updateIssue } = useIssuesActions(storeType); // derived values @@ -128,14 +123,9 @@ export const CreateUpdateIssueModalBase: React.FC = 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); @@ -328,7 +318,7 @@ export const CreateUpdateIssueModalBase: React.FC = 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)} diff --git a/web/core/components/issues/issue-modal/draft-issue-layout.tsx b/web/core/components/issues/issue-modal/draft-issue-layout.tsx index 38a230a236..c3a7c18055 100644 --- a/web/core/components/issues/issue-modal/draft-issue-layout.tsx +++ b/web/core/components/issues/issue-modal/draft-issue-layout.tsx @@ -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"; @@ -55,6 +53,7 @@ export const DraftIssueLayout: React.FC = observer((props) => { // store hooks const { captureIssueEvent } = useEventTracker(); const { handleCreateUpdatePropertyValues } = useIssueModal(); + const { createIssue } = useWorkspaceDraftIssues(); const handleClose = () => { if (data?.id) { @@ -96,8 +95,7 @@ export const DraftIssueLayout: React.FC = 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, diff --git a/web/core/components/issues/workspace-draft/root.tsx b/web/core/components/issues/workspace-draft/root.tsx index a61e48aa01..bfb70fc45d 100644 --- a/web/core/components/issues/workspace-draft/root.tsx +++ b/web/core/components/issues/workspace-draft/root.tsx @@ -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"; @@ -21,7 +24,9 @@ type TWorkspaceDraftIssuesRoot = { export const WorkspaceDraftIssuesRoot: FC = 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( @@ -39,6 +44,17 @@ export const WorkspaceDraftIssuesRoot: FC = observer( return ; } + if (workspaceProjectIds?.length === 0) + return ( + { + toggleCreateProjectModal(true); + }} + /> + ); + if (loader === "empty-state" && issueIds.length <= 0) return ; return ( @@ -48,22 +64,26 @@ export const WorkspaceDraftIssuesRoot: FC = observer( ))} - {loader === "pagination" && issueIds.length >= 0 ? ( - - ) : ( -
+ {loader === "pagination" && issueIds.length >= 0 ? ( + + ) : ( +
+ Load More ↓ +
)} - onClick={handleNextIssues} - > - Load More ↓ -
+ )} ); diff --git a/web/core/store/issue/root.store.ts b/web/core/store/issue/root.store.ts index 8fc49ade19..7cf2afda27 100644 --- a/web/core/store/issue/root.store.ts +++ b/web/core/store/issue/root.store.ts @@ -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); @@ -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); } } diff --git a/web/core/store/issue/workspace-draft/issue.store.ts b/web/core/store/issue/workspace-draft/issue.store.ts index 1080b275be..f60c842afc 100644 --- a/web/core/store/issue/workspace-draft/issue.store.ts +++ b/web/core/store/issue/workspace-draft/issue.store.ts @@ -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; - paginationInfo: Omit, "results"> | undefined; loader: TWorkspaceDraftIssueLoader; + paginationInfo: Omit, "results"> | undefined; + issuesMap: Record; // issue_id -> issue; + issueMapIds: Record; // workspace_id -> issue_ids; // computed issueIds: string[]; // computed functions @@ -112,15 +115,17 @@ export class WorkspaceDraftIssues implements IWorkspaceDraftIssues { // local constants paginatedCount = 50; // observables - paginationInfo: Omit, "results"> | undefined = undefined; loader: TWorkspaceDraftIssueLoader = undefined; + paginationInfo: Omit, "results"> | undefined = undefined; issuesMap: Record = {}; + issueMapIds: Record = {}; - constructor() { + constructor(public issueStore: IIssueRootStore) { makeObservable(this, { - paginationInfo: observable, loader: observable.ref, + paginationInfo: observable, issuesMap: observable, + issueMapIds: observable, // computed issueIds: computed, // action @@ -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 @@ -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"; @@ -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; @@ -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; @@ -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); + }); this.loader = undefined; return response; @@ -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); + }); this.loader = undefined; return response; diff --git a/web/public/empty-state/workspace-draft/issue-dark.webp b/web/public/empty-state/workspace-draft/issue-dark.webp index 76306f646e..2765335235 100644 Binary files a/web/public/empty-state/workspace-draft/issue-dark.webp and b/web/public/empty-state/workspace-draft/issue-dark.webp differ diff --git a/web/public/empty-state/workspace-draft/issue-light.webp b/web/public/empty-state/workspace-draft/issue-light.webp index 5ade38de5b..8be274b3c6 100644 Binary files a/web/public/empty-state/workspace-draft/issue-light.webp and b/web/public/empty-state/workspace-draft/issue-light.webp differ