mirror of
https://github.com/makeplane/plane
synced 2025-08-07 19:59:33 +00:00
[WEB-2388] fix: workspace draft issues (#5800)
* fix: create issue modal handle close * fix: workspace level draft issue store update * chore: count added * chore: added description html in list endpoint * fix: workspace draft issue mutation * fix: workspace draft issue empty state and count --------- Co-authored-by: gurusainath <gurusainath007@gmail.com>
This commit is contained in:
committed by
GitHub
parent
2c96e042c6
commit
bf7b3229d1
@@ -276,6 +276,8 @@ class DraftIssueSerializer(BaseSerializer):
|
||||
"updated_at",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"type_id",
|
||||
"description_html",
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
/>
|
||||
<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>
|
||||
|
||||
@@ -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<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
|
||||
@@ -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);
|
||||
@@ -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)}
|
||||
|
||||
@@ -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<DraftIssueProps> = 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<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,
|
||||
|
||||
@@ -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<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(
|
||||
@@ -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);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
if (loader === "empty-state" && issueIds.length <= 0) return <WorkspaceDraftEmptyState />;
|
||||
|
||||
return (
|
||||
@@ -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 ? (
|
||||
<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 ↓
|
||||
</div>
|
||||
)}
|
||||
onClick={handleNextIssues}
|
||||
>
|
||||
Load More ↓
|
||||
</div>
|
||||
</Fragment>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 79 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 90 KiB |
Reference in New Issue
Block a user