[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:
Anmol Singh Bhatia
2024-10-11 15:23:32 +05:30
committed by GitHub
parent 2c96e042c6
commit bf7b3229d1
9 changed files with 97 additions and 63 deletions

View File

@@ -276,6 +276,8 @@ class DraftIssueSerializer(BaseSerializer):
"updated_at",
"created_by",
"updated_by",
"type_id",
"description_html",
]
read_only_fields = fields

View File

@@ -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>

View File

@@ -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)}

View File

@@ -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,

View File

@@ -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 &darr;
</div>
)}
onClick={handleNextIssues}
>
Load More &darr;
</div>
</Fragment>
)}
</div>
);

View File

@@ -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);
}
}

View File

@@ -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