[WEB-4828] refactor: remove legacy project-level draft work items components (#7694)

This commit is contained in:
Prateek Shourya 2025-09-02 12:29:00 +05:30 committed by GitHub
parent fd5ba6c7b8
commit d960d7ce88
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 16 additions and 1313 deletions

View file

@ -1,178 +0,0 @@
"use client";
import { FC, useCallback } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane constants
import { EIssueFilterType, ISSUE_DISPLAY_FILTERS_BY_PAGE } from "@plane/constants";
// i18n
import { useTranslation } from "@plane/i18n";
// types
import {
EIssuesStoreType,
IIssueDisplayFilterOptions,
IIssueDisplayProperties,
IIssueFilterOptions,
EIssueLayoutTypes,
} from "@plane/types";
// ui
import { Breadcrumbs, LayersIcon, Tooltip } from "@plane/ui";
// components
import { isIssueFilterActive } from "@plane/utils";
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
import {
DisplayFiltersSelection,
FiltersDropdown,
FilterSelection,
LayoutSelection,
} from "@/components/issues/issue-layouts/filters";
// helpers
// hooks
import { useIssues } from "@/hooks/store/use-issues";
import { useLabel } from "@/hooks/store/use-label";
import { useMember } from "@/hooks/store/use-member";
import { useProject } from "@/hooks/store/use-project";
import { useProjectState } from "@/hooks/store/use-project-state";
import { usePlatformOS } from "@/hooks/use-platform-os";
// plane web
import { ProjectBreadcrumb } from "@/plane-web/components/breadcrumbs/project";
// FIXME: Deprecated. Remove it
export const ProjectDraftIssueHeader: FC = observer(() => {
// i18n
const { t } = useTranslation();
// router
const { workspaceSlug, projectId } = useParams() as { workspaceSlug: string; projectId: string };
// store hooks
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.DRAFT);
const { currentProjectDetails, loader } = useProject();
const { projectStates } = useProjectState();
const { projectLabels } = useLabel();
const {
project: { projectMemberIds },
} = useMember();
const { isMobile } = usePlatformOS();
const activeLayout = issueFilters?.displayFilters?.layout;
const handleFiltersUpdate = useCallback(
(key: keyof IIssueFilterOptions, value: string | string[]) => {
if (!workspaceSlug || !projectId) return;
const newValues = issueFilters?.filters?.[key] ?? [];
if (Array.isArray(value)) {
// this validation is majorly for the filter start_date, target_date custom
value.forEach((val) => {
if (!newValues.includes(val)) newValues.push(val);
else newValues.splice(newValues.indexOf(val), 1);
});
} else {
if (issueFilters?.filters?.[key]?.includes(value)) newValues.splice(newValues.indexOf(value), 1);
else newValues.push(value);
}
updateFilters(workspaceSlug, projectId, EIssueFilterType.FILTERS, { [key]: newValues });
},
[workspaceSlug, projectId, issueFilters, updateFilters]
);
const handleLayoutChange = useCallback(
(layout: EIssueLayoutTypes) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, { layout: layout });
},
[workspaceSlug, projectId, updateFilters]
);
const handleDisplayFilters = useCallback(
(updatedDisplayFilter: Partial<IIssueDisplayFilterOptions>) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_FILTERS, updatedDisplayFilter);
},
[workspaceSlug, projectId, updateFilters]
);
const handleDisplayProperties = useCallback(
(property: Partial<IIssueDisplayProperties>) => {
if (!workspaceSlug || !projectId) return;
updateFilters(workspaceSlug, projectId, EIssueFilterType.DISPLAY_PROPERTIES, property);
},
[workspaceSlug, projectId, updateFilters]
);
const issueCount = undefined;
return (
<div className="relative z-10 flex h-header w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 bg-custom-sidebar-background-100 p-4">
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
<div className="flex items-center gap-2.5">
<Breadcrumbs isLoading={loader === "init-loader"}>
<ProjectBreadcrumb workspaceSlug={workspaceSlug} projectId={projectId} />
<Breadcrumbs.Item
component={
<BreadcrumbLink
label="Draft work items"
icon={<LayersIcon className="h-4 w-4 text-custom-text-300" />}
/>
}
/>
</Breadcrumbs>
{issueCount && issueCount > 0 ? (
<Tooltip
isMobile={isMobile}
tooltipContent={`There are ${issueCount} ${issueCount > 1 ? "work items" : "work item"} in project's draft`}
position="bottom"
>
<span className="cursor-default flex items-center text-center justify-center px-2.5 py-0.5 flex-shrink-0 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-semibold rounded-xl">
{issueCount}
</span>
</Tooltip>
) : null}
</div>
<div className="ml-auto flex items-center gap-2">
<LayoutSelection
layouts={[EIssueLayoutTypes.LIST, EIssueLayoutTypes.KANBAN]}
onChange={(layout) => handleLayoutChange(layout)}
selectedLayout={activeLayout}
/>
<FiltersDropdown
title={t("common.filters")}
placement="bottom-end"
isFiltersApplied={isIssueFilterActive(issueFilters)}
>
<FilterSelection
filters={issueFilters?.filters ?? {}}
handleFiltersUpdate={handleFiltersUpdate}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
}
labels={projectLabels}
memberIds={projectMemberIds ?? undefined}
states={projectStates}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
<FiltersDropdown title={t("common.display")} placement="bottom-end">
<DisplayFiltersSelection
layoutDisplayFiltersOptions={
activeLayout ? ISSUE_DISPLAY_FILTERS_BY_PAGE.issues[activeLayout] : undefined
}
displayFilters={issueFilters?.displayFilters ?? {}}
handleDisplayFiltersUpdate={handleDisplayFilters}
displayProperties={issueFilters?.displayProperties ?? {}}
handleDisplayPropertiesUpdate={handleDisplayProperties}
cycleViewDisabled={!currentProjectDetails?.cycle_view}
moduleViewDisabled={!currentProjectDetails?.module_view}
/>
</FiltersDropdown>
</div>
</div>
</div>
);
});

View file

@ -1,15 +0,0 @@
"use client";
// components
import { AppHeader } from "@/components/core/app-header";
import { ContentWrapper } from "@/components/core/content-wrapper";
import { ProjectDraftIssueHeader } from "./header";
export default function ProjectDraftIssuesLayou({ children }: { children: React.ReactNode }) {
return (
<>
<AppHeader header={<ProjectDraftIssueHeader />} />
<ContentWrapper>{children}</ContentWrapper>
</>
);
}

View file

@ -1,43 +0,0 @@
"use client";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import { X, PenSquare } from "lucide-react";
// components
import { PageHead } from "@/components/core/page-title";
import { DraftIssueLayoutRoot } from "@/components/issues/issue-layouts/roots/draft-issue-layout-root";
// hooks
import { useProject } from "@/hooks/store/use-project";
import { useAppRouter } from "@/hooks/use-app-router";
const ProjectDraftIssuesPage = observer(() => {
const router = useAppRouter();
const { workspaceSlug, projectId } = useParams();
// store
const { getProjectById } = useProject();
// derived values
const project = projectId ? getProjectById(projectId.toString()) : undefined;
const pageTitle = project?.name ? `${project?.name} - Draft work items` : undefined;
return (
<>
<PageHead title={pageTitle} />
<div className="flex h-full w-full flex-col">
<div className="gap-1 flex items-center border-b border-custom-border-200 px-4 py-2.5 shadow-sm bg-custom-background-100 z-[12]">
<button
type="button"
onClick={() => router.push(`/${workspaceSlug}/projects/${projectId}/issues/`)}
className="flex items-center gap-1.5 rounded-full border border-custom-border-200 px-3 py-1.5 text-xs"
>
<PenSquare className="h-4 w-4" />
<span>Draft work items</span>
<X className="h-3 w-3" />
</button>
</div>
<DraftIssueLayoutRoot />
</div>
</>
);
});
export default ProjectDraftIssuesPage;

View file

@ -1,6 +1,6 @@
import { FC } from "react";
import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
import { useParams } from "next/navigation";
// plane imports
import { EIssueServiceType, EIssuesStoreType, TIssue } from "@plane/types";
// components
@ -22,7 +22,6 @@ export type TIssueLevelModalsProps = {
export const IssueLevelModals: FC<TIssueLevelModalsProps> = observer((props) => {
const { projectId, issueId } = props;
// router
const pathname = usePathname();
const { workspaceSlug, cycleId, moduleId } = useParams();
const router = useAppRouter();
// store hooks
@ -45,7 +44,6 @@ export const IssueLevelModals: FC<TIssueLevelModalsProps> = observer((props) =>
} = useCommandPalette();
// derived values
const issueDetails = issueId ? getIssueById(issueId) : undefined;
const isDraftIssue = pathname?.includes("draft-issues") || false;
const { fetchSubIssues: fetchSubWorkItems } = useIssueDetail();
const { fetchSubIssues: fetchEpicSubWorkItems } = useIssueDetail(EIssueServiceType.EPICS);
@ -81,7 +79,6 @@ export const IssueLevelModals: FC<TIssueLevelModalsProps> = observer((props) =>
isOpen={isCreateIssueModalOpen}
onClose={() => toggleCreateIssueModal(false)}
data={getCreateIssueModalData()}
isDraft={isDraftIssue}
onSubmit={handleCreateIssueSubmit}
allowedProjectIds={createWorkItemAllowedProjectIds}
/>

View file

@ -1,6 +0,0 @@
import { observer } from "mobx-react";
// FIXME: Project drafts is deprecated. Remove this component and all the related code.
export const ProjectDraftEmptyState: React.FC = observer(() => (
<div className="relative h-full w-full overflow-y-auto" />
));

View file

@ -5,7 +5,6 @@ import { TeamProjectWorkItemEmptyState } from "@/plane-web/components/issues/iss
// components
import { ProjectArchivedEmptyState } from "./archived-issues";
import { CycleEmptyState } from "./cycle";
import { ProjectDraftEmptyState } from "./draft-issues";
import { GlobalViewEmptyState } from "./global-view";
import { ModuleEmptyState } from "./module";
import { ProfileViewEmptyState } from "./profile-view";
@ -29,8 +28,6 @@ export const IssueLayoutEmptyState = (props: Props) => {
return <CycleEmptyState />;
case EIssuesStoreType.MODULE:
return <ModuleEmptyState />;
case EIssuesStoreType.DRAFT:
return <ProjectDraftEmptyState />;
case EIssuesStoreType.GLOBAL:
return <GlobalViewEmptyState />;
case EIssuesStoreType.PROFILE:

View file

@ -1,77 +0,0 @@
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// plane imports
import { EIssueFilterType } from "@plane/constants";
import { EIssuesStoreType, IIssueFilterOptions } from "@plane/types";
// hooks
import { useIssues } from "@/hooks/store/use-issues";
import { useLabel } from "@/hooks/store/use-label";
import { useProjectState } from "@/hooks/store/use-project-state";
// local imports
import { AppliedFiltersList } from "../filters-list";
export const DraftIssueAppliedFiltersRoot: React.FC = observer(() => {
// router
const { workspaceSlug, projectId } = useParams() as { workspaceSlug: string; projectId: string };
// store hooks
const {
issuesFilter: { issueFilters, updateFilters },
} = useIssues(EIssuesStoreType.DRAFT);
const { projectLabels } = useLabel();
const { projectStates } = useProjectState();
// derived values
const userFilters = issueFilters?.filters;
// filters whose value not null or empty array
const appliedFilters: IIssueFilterOptions = {};
Object.entries(userFilters ?? {}).forEach(([key, value]) => {
if (!value) return;
if (Array.isArray(value) && value.length === 0) return;
appliedFilters[key as keyof IIssueFilterOptions] = value;
});
const handleRemoveFilter = (key: keyof IIssueFilterOptions, value: string | null) => {
if (!workspaceSlug || !projectId) return;
// remove all values of the key if value is null
if (!value) {
updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, {
[key]: null,
});
return;
}
// remove the passed value from the key
let newValues = issueFilters?.filters?.[key] ?? [];
newValues = newValues.filter((val) => val !== value);
updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, {
[key]: newValues,
});
};
const handleClearAllFilters = () => {
if (!workspaceSlug || !projectId) return;
const newFilters: IIssueFilterOptions = {};
Object.keys(userFilters ?? {}).forEach((key) => {
newFilters[key as keyof IIssueFilterOptions] = [];
});
updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { ...newFilters });
};
// return if no filters are applied
if (Object.keys(appliedFilters).length === 0) return null;
return (
<div className="flex justify-between p-4 gap-2.5">
<AppliedFiltersList
appliedFilters={appliedFilters}
handleClearAllFilters={handleClearAllFilters}
handleRemoveFilter={handleRemoveFilter}
labels={projectLabels ?? []}
states={projectStates}
/>
</div>
);
});

View file

@ -34,7 +34,6 @@ export type KanbanStoreType =
| EIssuesStoreType.MODULE
| EIssuesStoreType.CYCLE
| EIssuesStoreType.PROJECT_VIEW
| EIssuesStoreType.DRAFT
| EIssuesStoreType.PROFILE
| EIssuesStoreType.TEAM
| EIssuesStoreType.TEAM_VIEW

View file

@ -2,7 +2,7 @@
import React, { FC } from "react";
import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
import { useParams } from "next/navigation";
// lucide icons
import { Minimize2, Maximize2, Circle, Plus } from "lucide-react";
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
@ -58,9 +58,6 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
const storeType = useIssueStoreType();
// router
const { workspaceSlug, projectId, moduleId, cycleId } = useParams();
const pathname = usePathname();
const isDraftIssue = pathname.includes("draft-issue");
const renderExistingIssueModal = moduleId || cycleId;
const ExistingIssuesListModalPayload = moduleId ? { module: moduleId.toString() } : { cycle: true };
@ -97,7 +94,6 @@ export const HeaderGroupByCard: FC<IHeaderGroupByCard> = observer((props) => {
onClose={() => setIsOpen(false)}
data={issuePayload}
storeType={storeType}
isDraft={isDraftIssue}
/>
)}

View file

@ -1,6 +0,0 @@
import { observer } from "mobx-react";
// local imports
import { DraftIssueQuickActions } from "../../quick-action-dropdowns";
import { BaseKanBanRoot } from "../base-kanban-root";
export const DraftKanBanLayout: React.FC = observer(() => <BaseKanBanRoot QuickActions={DraftIssueQuickActions} />);

View file

@ -30,7 +30,6 @@ type ListStoreType =
| EIssuesStoreType.MODULE
| EIssuesStoreType.CYCLE
| EIssuesStoreType.PROJECT_VIEW
| EIssuesStoreType.DRAFT
| EIssuesStoreType.PROFILE
| EIssuesStoreType.ARCHIVED
| EIssuesStoreType.WORKSPACE_DRAFT

View file

@ -2,7 +2,7 @@
import { useState } from "react";
import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
import { useParams } from "next/navigation";
import { CircleDashed, Plus } from "lucide-react";
// types
import { WORK_ITEM_TRACKER_EVENTS } from "@plane/constants";
@ -58,10 +58,8 @@ export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => {
const [openExistingIssueListModal, setOpenExistingIssueListModal] = useState(false);
// router
const { workspaceSlug, projectId, moduleId, cycleId } = useParams();
const pathname = usePathname();
const storeType = useIssueStoreType();
// derived values
const isDraftIssue = pathname.includes("draft-issue");
const renderExistingIssueModal = moduleId || cycleId;
const existingIssuesListModalPayload = moduleId ? { module: moduleId.toString() } : { cycle: true };
const isGroupSelectionEmpty = selectionHelpers.isGroupSelected(groupID) === "empty";
@ -167,7 +165,6 @@ export const HeaderGroupByCard = observer((props: IHeaderGroupByCard) => {
onClose={() => setIsOpen(false)}
data={issuePayload}
storeType={storeType}
isDraft={isDraftIssue}
/>
)}

View file

@ -1,14 +0,0 @@
import { FC } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// local imports
import { DraftIssueQuickActions } from "../../quick-action-dropdowns";
import { BaseListRoot } from "../base-list-root";
export const DraftIssueListLayout: FC = observer(() => {
const { workspaceSlug, projectId } = useParams();
if (!workspaceSlug || !projectId) return null;
return <BaseListRoot QuickActions={DraftIssueQuickActions} />;
});

View file

@ -118,7 +118,6 @@ export const AllIssueQuickActions: React.FC<IQuickActionProps> = observer((props
if (issueToEdit && handleUpdate) await handleUpdate(data);
}}
storeType={EIssuesStoreType.GLOBAL}
isDraft={false}
/>
{issue.project_id && workspaceSlug && (
<DuplicateWorkItemModal

View file

@ -135,7 +135,6 @@ export const CycleIssueQuickActions: React.FC<IQuickActionProps> = observer((pro
if (issueToEdit && handleUpdate) await handleUpdate(data);
}}
storeType={EIssuesStoreType.CYCLE}
isDraft={false}
/>
{issue.project_id && workspaceSlug && (
<DuplicateWorkItemModal

View file

@ -1,163 +0,0 @@
"use client";
import { useState } from "react";
import omit from "lodash/omit";
import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
// plane imports
import { EUserPermissions, EUserPermissionsLevel, WORK_ITEM_TRACKER_ELEMENTS } from "@plane/constants";
import { EIssuesStoreType, TIssue } from "@plane/types";
import { ContextMenu, CustomMenu, TContextMenuItem } from "@plane/ui";
import { cn } from "@plane/utils";
// hooks
import { captureClick } from "@/helpers/event-tracker.helper";
import { useUserPermissions } from "@/hooks/store/user";
// local imports
import { DeleteIssueModal } from "../../delete-issue-modal";
import { CreateUpdateIssueModal } from "../../issue-modal/modal";
import { IQuickActionProps } from "../list/list-view-types";
import { useDraftIssueMenuItems, MenuItemFactoryProps } from "./helper";
export const DraftIssueQuickActions: React.FC<IQuickActionProps> = observer((props) => {
const {
issue,
handleDelete,
handleUpdate,
customActionButton,
portalElement,
readOnly = false,
placements = "bottom-end",
parentRef,
} = props;
// router
const { workspaceSlug } = useParams();
const pathname = usePathname();
// states
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
const [issueToEdit, setIssueToEdit] = useState<TIssue | undefined>(undefined);
const [deleteIssueModal, setDeleteIssueModal] = useState(false);
// store hooks
const { allowPermissions } = useUserPermissions();
// derived values
const activeLayout = "Draft Issues";
// auth
const isEditingAllowed =
allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
EUserPermissionsLevel.PROJECT,
workspaceSlug?.toString(),
issue.project_id ?? undefined
) && !readOnly;
const isDeletingAllowed = isEditingAllowed;
const isDraftIssue = pathname?.includes("draft-issues") || false;
const duplicateIssuePayload = omit(
{
...issue,
name: `${issue.name} (copy)`,
is_draft: isDraftIssue ? false : issue.is_draft,
sourceIssueId: issue.id,
},
["id"]
);
// Menu items and modals using helper
const menuItemProps: MenuItemFactoryProps = {
issue,
workspaceSlug: workspaceSlug?.toString(),
activeLayout,
isEditingAllowed,
isDeletingAllowed,
isDraftIssue,
setIssueToEdit,
setCreateUpdateIssueModal,
setDeleteIssueModal,
handleDelete,
handleUpdate,
storeType: EIssuesStoreType.DRAFT,
};
const MENU_ITEMS = useDraftIssueMenuItems(menuItemProps);
const CONTEXT_MENU_ITEMS: TContextMenuItem[] = MENU_ITEMS.map((item) => ({
...item,
onClick: () => {
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.QUICK_ACTIONS.DRAFT });
item.action();
},
}));
return (
<>
{/* Modals */}
<DeleteIssueModal
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);
}}
storeType={EIssuesStoreType.DRAFT}
isDraft={isDraftIssue}
/>
<ContextMenu parentRef={parentRef} items={CONTEXT_MENU_ITEMS} />
<CustomMenu
ellipsis
placement={placements}
customButton={customActionButton}
portalElement={portalElement}
menuItemsClassName="z-[14]"
maxHeight="lg"
closeOnSelect
>
{MENU_ITEMS.map((item) => {
if (item.shouldRender === false) return null;
return (
<CustomMenu.MenuItem
key={item.key}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
captureClick({ elementName: WORK_ITEM_TRACKER_ELEMENTS.QUICK_ACTIONS.DRAFT });
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>
</>
);
});

View file

@ -54,7 +54,6 @@ export interface MenuItemFactoryProps {
isRestoringAllowed?: boolean;
isInArchivableGroup?: boolean;
issueTypeDetail?: { is_active?: boolean };
isDraftIssue?: boolean;
// Action handlers
setIssueToEdit: (issue: TIssue | undefined) => void;
setCreateUpdateIssueModal: (open: boolean) => void;
@ -369,9 +368,3 @@ export const useArchivedIssueMenuItems = (props: MenuItemFactoryProps): TContext
[factory]
);
};
export const useDraftIssueMenuItems = (props: MenuItemFactoryProps): TContextMenuItem[] => {
const factory = useMenuItemFactory(props);
return useMemo(() => [factory.createEditMenuItem(), factory.createDeleteMenuItem()], [factory]);
};

View file

@ -1,7 +1,6 @@
export * from "./all-issue";
export * from "./archived-issue";
export * from "./cycle-issue";
export * from "./draft-issue";
export * from "./module-issue";
export * from "./project-issue";
export * from "./helper";

View file

@ -88,13 +88,10 @@ export const WorkItemDetailQuickActions: React.FC<TWorkItemDetailQuickActionProp
const isDeletingAllowed = isEditingAllowed;
const isDraftIssue = pathname?.includes("draft-issues") || false;
const duplicateIssuePayload = omit(
{
...issue,
name: `${issue.name} (copy)`,
is_draft: isDraftIssue ? false : issue.is_draft,
sourceIssueId: issue.id,
},
["id"]
@ -137,7 +134,6 @@ export const WorkItemDetailQuickActions: React.FC<TWorkItemDetailQuickActionProp
isRestoringAllowed,
isDeletingAllowed,
isInArchivableGroup,
isDraftIssue,
setIssueToEdit,
setCreateUpdateIssueModal: customEditAction,
setDeleteIssueModal: customDeleteAction,
@ -220,7 +216,6 @@ export const WorkItemDetailQuickActions: React.FC<TWorkItemDetailQuickActionProp
if (issueToEdit && handleUpdate) await handleUpdate(data);
}}
storeType={EIssuesStoreType.PROJECT}
isDraft={isDraftIssue}
fetchIssueDetails={false}
/>
{issue.project_id && workspaceSlug && (

View file

@ -134,7 +134,6 @@ export const ModuleIssueQuickActions: React.FC<IQuickActionProps> = observer((pr
if (issueToEdit && handleUpdate) await handleUpdate(data);
}}
storeType={EIssuesStoreType.MODULE}
isDraft={false}
/>
{issue.project_id && workspaceSlug && (
<DuplicateWorkItemModal

View file

@ -3,7 +3,7 @@
import { useState } from "react";
import omit from "lodash/omit";
import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
import { useParams } from "next/navigation";
// plane imports
import {
ARCHIVABLE_STATE_GROUPS,
@ -43,7 +43,6 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = observer((p
} = props;
// router
const { workspaceSlug } = useParams();
const pathname = usePathname();
// states
const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false);
const [issueToEdit, setIssueToEdit] = useState<TIssue | undefined>(undefined);
@ -71,13 +70,10 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = observer((p
const isInArchivableGroup = !!stateDetails && ARCHIVABLE_STATE_GROUPS.includes(stateDetails?.group);
const isDeletingAllowed = isEditingAllowed;
const isDraftIssue = pathname?.includes("draft-issues") || false;
const duplicateIssuePayload = omit(
{
...issue,
name: `${issue.name} (copy)`,
is_draft: isDraftIssue ? false : issue.is_draft,
sourceIssueId: issue.id,
},
["id"]
@ -93,7 +89,6 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = observer((p
isArchivingAllowed,
isDeletingAllowed,
isInArchivableGroup,
isDraftIssue,
setIssueToEdit,
setCreateUpdateIssueModal,
setDeleteIssueModal,
@ -141,7 +136,6 @@ export const ProjectIssueQuickActions: React.FC<IQuickActionProps> = observer((p
if (issueToEdit && handleUpdate) await handleUpdate(data);
}}
storeType={EIssuesStoreType.PROJECT}
isDraft={isDraftIssue}
/>
{issue.project_id && workspaceSlug && (
<DuplicateWorkItemModal

View file

@ -1,70 +0,0 @@
import React from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
import useSWR from "swr";
// plane constants
import { EIssuesStoreType, EIssueLayoutTypes } from "@plane/types";
// components
import { LogoSpinner } from "@/components/common/logo-spinner";
import { IssuePeekOverview } from "@/components/issues/peek-overview";
// hooks
import { useIssues } from "@/hooks/store/use-issues";
import { IssuesStoreContext } from "@/hooks/use-issue-layout-store";
// components
import { DraftIssueAppliedFiltersRoot } from "../filters/applied-filters/roots/draft-issue";
import { DraftKanBanLayout } from "../kanban/roots/draft-issue-root";
import { DraftIssueListLayout } from "../list/roots/draft-issue-root";
// ui
// constants
const DraftIssueLayout = (props: { activeLayout: EIssueLayoutTypes | undefined }) => {
switch (props.activeLayout) {
case EIssueLayoutTypes.LIST:
return <DraftIssueListLayout />;
case EIssueLayoutTypes.KANBAN:
return <DraftKanBanLayout />;
default:
return null;
}
};
export const DraftIssueLayoutRoot: React.FC = observer(() => {
// router
const { workspaceSlug, projectId } = useParams();
// hooks
const { issuesFilter } = useIssues(EIssuesStoreType.DRAFT);
const { isLoading } = useSWR(
workspaceSlug && projectId ? `DRAFT_ISSUES_${workspaceSlug.toString()}_${projectId.toString()}` : null,
async () => {
if (workspaceSlug && projectId) {
await issuesFilter?.fetchFilters(workspaceSlug.toString(), projectId.toString());
}
},
{ revalidateIfStale: false, revalidateOnFocus: false }
);
const issueFilters = issuesFilter?.getIssueFilters(projectId?.toString());
const activeLayout = issueFilters?.displayFilters?.layout || undefined;
if (!workspaceSlug || !projectId) return <></>;
if (isLoading && !issueFilters)
return (
<div className="h-full w-full flex items-center justify-center">
<LogoSpinner />
</div>
);
return (
<IssuesStoreContext.Provider value={EIssuesStoreType.DRAFT}>
<div className="relative flex h-full w-full flex-col overflow-hidden">
<DraftIssueAppliedFiltersRoot />
<div className="relative h-full w-full overflow-auto">
<DraftIssueLayout activeLayout={activeLayout} />
{/* issue peek overview */}
<IssuePeekOverview is_draft />
</div>
</div>
</IssuesStoreContext.Provider>
);
});

View file

@ -86,12 +86,7 @@ export const CreateUpdateIssueModalBase: React.FC<IssuesModalProps> = observer((
setDescription(data?.description_html || "<p></p>");
return;
}
const response = await fetchIssue(
workspaceSlug.toString(),
projectId.toString(),
issueId,
isDraft ? "DRAFT" : "DEFAULT"
);
const response = await fetchIssue(workspaceSlug.toString(), projectId.toString(), issueId);
if (response) setDescription(response?.description_html || "<p></p>");
};

View file

@ -64,7 +64,7 @@ export const IssuePeekOverview: FC<IWorkItemPeekOverview> = observer((props) =>
fetch: async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
setError(false);
await fetchIssue(workspaceSlug, projectId, issueId, is_draft ? "DRAFT" : "DEFAULT");
await fetchIssue(workspaceSlug, projectId, issueId);
} catch (error) {
setError(true);
console.error("Error fetching the parent issue", error);

View file

@ -70,6 +70,7 @@ export const SidebarQuickActions = observer(() => {
onClose={() => setIsDraftIssueModalOpen(false)}
data={workspaceDraftIssue ?? {}}
onSubmit={() => removeWorkspaceDraftIssue()}
fetchIssueDetails={false}
isDraft
/>
<div className={cn("flex items-center justify-between gap-1 cursor-pointer", {})}>

View file

@ -11,7 +11,6 @@ import type { ITeamViewIssues, ITeamViewIssuesFilter } from "@/plane-web/store/i
import type { IWorkspaceIssues } from "@/plane-web/store/issue/workspace/issue.store";
import type { IArchivedIssues, IArchivedIssuesFilter } from "@/store/issue/archived";
import type { ICycleIssues, ICycleIssuesFilter } from "@/store/issue/cycle";
import type { IDraftIssues, IDraftIssuesFilter } from "@/store/issue/draft";
import type { IModuleIssues, IModuleIssuesFilter } from "@/store/issue/module";
import type { IProfileIssues, IProfileIssuesFilter } from "@/store/issue/profile";
import type { IProjectIssues, IProjectIssuesFilter } from "@/store/issue/project";
@ -65,10 +64,6 @@ export type TStoreIssues = {
issues: IArchivedIssues;
issuesFilter: IArchivedIssuesFilter;
};
[EIssuesStoreType.DRAFT]: defaultIssueStore & {
issues: IDraftIssues;
issuesFilter: IDraftIssuesFilter;
};
[EIssuesStoreType.DEFAULT]: defaultIssueStore & {
issues: IProjectIssues;
issuesFilter: IProjectIssuesFilter;
@ -142,11 +137,6 @@ export const useIssues = <T extends EIssuesStoreType>(storeType?: T): TStoreIssu
issues: context.issue.archivedIssues,
issuesFilter: context.issue.archivedIssuesFilter,
}) as TStoreIssues[T];
case EIssuesStoreType.DRAFT:
return merge(defaultStore, {
issues: context.issue.draftIssues,
issuesFilter: context.issue.draftIssuesFilter,
}) as TStoreIssues[T];
case EIssuesStoreType.EPIC:
return merge(defaultStore, {
issues: context.issue.projectEpics,

View file

@ -14,7 +14,6 @@ type DNDStoreType =
| EIssuesStoreType.MODULE
| EIssuesStoreType.CYCLE
| EIssuesStoreType.PROJECT_VIEW
| EIssuesStoreType.DRAFT
| EIssuesStoreType.PROFILE
| EIssuesStoreType.ARCHIVED
| EIssuesStoreType.WORKSPACE_DRAFT

View file

@ -52,7 +52,6 @@ export const useIssuesActions = (storeType: EIssuesStoreType): IssueActions => {
const projectViewIssueActions = useProjectViewIssueActions();
const globalIssueActions = useGlobalIssueActions();
const profileIssueActions = useProfileIssueActions();
const draftIssueActions = useDraftIssueActions();
const archivedIssueActions = useArchivedIssueActions();
const workspaceDraftIssueActions = useWorkspaceDraftIssueActions();
const teamProjectWorkItemsActions = useTeamProjectWorkItemsActions();
@ -68,8 +67,6 @@ export const useIssuesActions = (storeType: EIssuesStoreType): IssueActions => {
return teamIssueActions;
case EIssuesStoreType.ARCHIVED:
return archivedIssueActions;
case EIssuesStoreType.DRAFT:
return draftIssueActions;
case EIssuesStoreType.CYCLE:
return cycleIssueActions;
case EIssuesStoreType.MODULE:
@ -644,76 +641,6 @@ const useProjectViewIssueActions = () => {
);
};
const useDraftIssueActions = () => {
// router
const { workspaceSlug: routerWorkspaceSlug, projectId: routerProjectId } = useParams();
const workspaceSlug = routerWorkspaceSlug?.toString();
const projectId = routerProjectId?.toString();
// store hooks
const { issues, issuesFilter } = useIssues(EIssuesStoreType.DRAFT);
const fetchIssues = useCallback(
async (loadType: TLoader, options: IssuePaginationOptions) => {
if (!workspaceSlug || !projectId) return;
return issues.fetchIssues(workspaceSlug.toString(), projectId.toString(), loadType, options);
},
[issues.fetchIssues, workspaceSlug, projectId]
);
const fetchNextIssues = useCallback(
async (groupId?: string, subGroupId?: string) => {
if (!workspaceSlug || !projectId) return;
return issues.fetchNextIssues(workspaceSlug.toString(), projectId.toString(), groupId, subGroupId);
},
[issues.fetchIssues, workspaceSlug, projectId]
);
const createIssue = useCallback(
async (projectId: string | undefined | null, data: Partial<TIssue>) => {
if (!workspaceSlug || !projectId) return;
return await issues.createIssue(workspaceSlug, projectId, data);
},
[issues.createIssue, workspaceSlug]
);
const updateIssue = useCallback(
async (projectId: string | undefined | null, issueId: string, data: Partial<TIssue>) => {
if (!workspaceSlug || !projectId) return;
return await issues.updateIssue(workspaceSlug, projectId, issueId, data);
},
[issues.updateIssue, workspaceSlug]
);
const removeIssue = useCallback(
async (projectId: string | undefined | null, issueId: string) => {
if (!workspaceSlug || !projectId) return;
return await issues.removeIssue(workspaceSlug, projectId, issueId);
},
[issues.removeIssue, workspaceSlug]
);
const updateFilters = useCallback(
async (
projectId: string,
filterType: EIssueFilterType,
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters
) => {
if (!workspaceSlug) return;
return await issuesFilter.updateFilters(workspaceSlug, projectId, filterType, filters);
},
[issuesFilter.updateFilters]
);
return useMemo(
() => ({
fetchIssues,
fetchNextIssues,
createIssue,
updateIssue,
removeIssue,
updateFilters,
}),
[fetchIssues, createIssue, updateIssue, removeIssue, updateFilters]
);
};
const useArchivedIssueActions = () => {
// router
const { workspaceSlug: routerWorkspaceSlug, projectId: routerProjectId } = useParams();

View file

@ -1,6 +1,5 @@
export * from "./issue_archive.service";
export * from "./issue.service";
export * from "./issue_draft.service";
export * from "./issue_reaction.service";
export * from "./issue_label.service";
export * from "./issue_attachment.service";

View file

@ -1,58 +0,0 @@
import { API_BASE_URL } from "@plane/constants";
import { TIssue, TIssuesResponse } from "@plane/types";
import { APIService } from "@/services/api.service";
// helpers
export class IssueDraftService extends APIService {
constructor() {
super(API_BASE_URL);
}
async getDraftIssues(workspaceSlug: string, projectId: string, query?: any, config = {}): Promise<TIssuesResponse> {
return this.get(
`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/`,
{
params: { ...query },
},
config
)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async createDraftIssue(workspaceSlug: string, projectId: string, data: any): Promise<TIssue> {
return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/`, data)
.then((response) => response?.data)
.catch((error) => {
throw error?.response;
});
}
async updateDraftIssue(workspaceSlug: string, projectId: string, issueId: string, data: any): Promise<void> {
return this.patch(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/`, data)
.then((response) => response?.data)
.catch((error) => {
throw error?.response;
});
}
async deleteDraftIssue(workspaceSlug: string, projectId: string, issueId: string): Promise<void> {
return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/`)
.then((response) => response?.data)
.catch((error) => {
throw error?.response;
});
}
async getDraftIssueById(workspaceSlug: string, projectId: string, issueId: string, queries?: any): Promise<TIssue> {
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issue-drafts/${issueId}/`, {
params: queries,
})
.then((response) => response?.data)
.catch((error) => {
throw error?.response;
});
}
}

View file

@ -1,279 +0,0 @@
import isArray from "lodash/isArray";
import isEmpty from "lodash/isEmpty";
import pickBy from "lodash/pickBy";
import set from "lodash/set";
import { action, computed, makeObservable, observable, runInAction } from "mobx";
// base class
import { computedFn } from "mobx-utils";
import { EIssueFilterType } from "@plane/constants";
import {
EIssuesStoreType,
IIssueFilterOptions,
IIssueDisplayFilterOptions,
IIssueDisplayProperties,
TIssueKanbanFilters,
IIssueFilters,
TIssueParams,
IssuePaginationOptions,
} from "@plane/types";
import { handleIssueQueryParamsByLayout } from "@plane/utils";
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 IDraftIssuesFilter extends IBaseIssueFilterStore {
//helper actions
getFilterParams: (
options: IssuePaginationOptions,
projectId: string,
cursor: string | undefined,
groupId: string | undefined,
subGroupId: string | undefined
) => Partial<Record<TIssueParams, string | boolean>>;
getIssueFilters(projectId: string): IIssueFilters | undefined;
// action
fetchFilters: (workspaceSlug: string, projectId: string) => Promise<void>;
updateFilters: (
workspaceSlug: string,
projectId: string,
filterType: EIssueFilterType,
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters
) => Promise<void>;
}
export class DraftIssuesFilter extends IssueFilterHelperStore implements IDraftIssuesFilter {
// observables
filters: { [projectId: string]: IIssueFilters } = {};
// root store
rootIssueStore: IIssueRootStore;
// services
issueFilterService;
constructor(_rootStore: IIssueRootStore) {
super();
makeObservable(this, {
// observables
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 projectId = this.rootIssueStore.projectId;
if (!projectId) return undefined;
return this.getIssueFilters(projectId);
}
get appliedFilters() {
const projectId = this.rootIssueStore.projectId;
if (!projectId) return undefined;
return this.getAppliedFilters(projectId);
}
getIssueFilters(projectId: string) {
const displayFilters = this.filters[projectId] || undefined;
if (!projectId || isEmpty(displayFilters)) return undefined;
const _filters: IIssueFilters = this.computedIssueFilters(displayFilters);
return _filters;
}
getAppliedFilters(projectId: string) {
const userFilters = this.getIssueFilters(projectId);
if (!userFilters) return undefined;
const filteredParams = handleIssueQueryParamsByLayout(userFilters?.displayFilters?.layout, "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,
projectId: string,
cursor: string | undefined,
groupId: string | undefined,
subGroupId: string | undefined
) => {
const filterParams = this.getAppliedFilters(projectId);
const paginationParams = this.getPaginationParams(filterParams, options, cursor, groupId, subGroupId);
return paginationParams;
}
);
fetchFilters = async (workspaceSlug: string, projectId: string) => {
const _filters = this.handleIssuesLocalFilters.get(EIssuesStoreType.DRAFT, workspaceSlug, projectId, 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: [],
sub_group_by: [],
};
kanbanFilters.group_by = _filters?.kanban_filters?.group_by || [];
kanbanFilters.sub_group_by = _filters?.kanban_filters?.sub_group_by || [];
runInAction(() => {
set(this.filters, [projectId, "filters"], filters);
set(this.filters, [projectId, "displayFilters"], displayFilters);
set(this.filters, [projectId, "displayProperties"], displayProperties);
set(this.filters, [projectId, "kanbanFilters"], kanbanFilters);
});
};
updateFilters = async (
workspaceSlug: string,
projectId: string,
type: EIssueFilterType,
filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters
) => {
try {
if (isEmpty(this.filters) || isEmpty(this.filters[projectId]) || isEmpty(filters)) return;
const _filters = {
filters: this.filters[projectId].filters as IIssueFilterOptions,
displayFilters: this.filters[projectId].displayFilters as IIssueDisplayFilterOptions,
displayProperties: this.filters[projectId].displayProperties as IIssueDisplayProperties,
kanbanFilters: this.filters[projectId].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, [projectId, "filters", _key], updatedFilters[_key as keyof IIssueFilterOptions]);
});
});
const appliedFilters = _filters.filters || {};
const filteredFilters = pickBy(appliedFilters, (value) => value && isArray(value) && value.length > 0);
this.rootIssueStore.draftIssues.fetchIssuesWithExistingPagination(
workspaceSlug,
projectId,
isEmpty(filteredFilters) ? "init-loader" : "mutation"
);
this.handleIssuesLocalFilters.set(EIssuesStoreType.DRAFT, type, workspaceSlug, projectId, 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 state 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 = "state";
updatedDisplayFilters.group_by = "state";
}
runInAction(() => {
Object.keys(updatedDisplayFilters).forEach((_key) => {
set(
this.filters,
[projectId, "displayFilters", _key],
updatedDisplayFilters[_key as keyof IIssueDisplayFilterOptions]
);
});
});
if (this.getShouldReFetchIssues(updatedDisplayFilters)) {
this.rootIssueStore.draftIssues.fetchIssuesWithExistingPagination(workspaceSlug, projectId, "mutation");
}
this.handleIssuesLocalFilters.set(EIssuesStoreType.DRAFT, type, workspaceSlug, projectId, 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,
[projectId, "displayProperties", _key],
updatedDisplayProperties[_key as keyof IIssueDisplayProperties]
);
});
});
this.handleIssuesLocalFilters.set(EIssuesStoreType.DRAFT, type, workspaceSlug, projectId, undefined, {
display_properties: _filters.displayProperties,
});
break;
}
case EIssueFilterType.KANBAN_FILTERS: {
const updatedKanbanFilters = filters as TIssueKanbanFilters;
_filters.kanbanFilters = { ..._filters.kanbanFilters, ...updatedKanbanFilters };
const currentUserId = this.rootIssueStore.currentUserId;
if (currentUserId)
this.handleIssuesLocalFilters.set(EIssuesStoreType.DRAFT, type, workspaceSlug, projectId, undefined, {
kanban_filters: _filters.kanbanFilters,
});
runInAction(() => {
Object.keys(updatedKanbanFilters).forEach((_key) => {
set(
this.filters,
[projectId, "kanbanFilters", _key],
updatedKanbanFilters[_key as keyof TIssueKanbanFilters]
);
});
});
break;
}
default:
break;
}
} catch (error) {
this.fetchFilters(workspaceSlug, projectId);
throw error;
}
};
}

View file

@ -1,2 +0,0 @@
export * from "./filter.store";
export * from "./issue.store";

View file

@ -1,186 +0,0 @@
import { action, makeObservable, runInAction } from "mobx";
// base class
// services
// types
import {
TIssue,
TLoader,
ViewFlags,
IssuePaginationOptions,
TIssuesResponse,
TBulkOperationsPayload,
} from "@plane/types";
import { BaseIssuesStore, IBaseIssuesStore } from "../helpers/base-issues.store";
import { IIssueRootStore } from "../root.store";
import { IDraftIssuesFilter } from "./filter.store";
export interface IDraftIssues extends IBaseIssuesStore {
// observable
viewFlags: ViewFlags;
// actions
fetchIssues: (
workspaceSlug: string,
projectId: string,
loadType: TLoader,
option: IssuePaginationOptions
) => Promise<TIssuesResponse | undefined>;
fetchIssuesWithExistingPagination: (
workspaceSlug: string,
projectId: string,
loadType: TLoader
) => Promise<TIssuesResponse | undefined>;
fetchNextIssues: (
workspaceSlug: string,
projectId: string,
groupId?: string,
subGroupId?: string
) => Promise<TIssuesResponse | undefined>;
createIssue: (workspaceSlug: string, projectId: string, data: Partial<TIssue>) => Promise<TIssue>;
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
removeBulkIssues: (workspaceSlug: string, projectId: string, issueIds: string[]) => Promise<void>;
bulkUpdateProperties: (workspaceSlug: string, projectId: string, data: TBulkOperationsPayload) => Promise<void>;
archiveBulkIssues: undefined;
quickAddIssue: undefined;
archiveIssue: undefined;
}
export class DraftIssues extends BaseIssuesStore implements IDraftIssues {
viewFlags = {
enableQuickAdd: false,
enableIssueCreation: true,
enableInlineEditing: true,
};
// filter store
issueFilterStore: IDraftIssuesFilter;
constructor(_rootStore: IIssueRootStore, issueFilterStore: IDraftIssuesFilter) {
super(_rootStore, issueFilterStore);
makeObservable(this, {
// action
fetchIssues: action,
fetchNextIssues: action,
fetchIssuesWithExistingPagination: action,
});
// filter store
this.issueFilterStore = issueFilterStore;
}
/**
* Fetches the project details
* @param workspaceSlug
* @param projectId
*/
fetchParentStats = async (workspaceSlug: string, projectId?: string) => {
projectId && this.rootIssueStore.rootStore.projectRoot.project.fetchProjectDetails(workspaceSlug, projectId);
};
/** */
updateParentStats = () => {};
/**
* This method is called to fetch the first issues of pagination
* @param workspaceSlug
* @param projectId
* @param loadType
* @param options
* @returns
*/
fetchIssues = async (
workspaceSlug: string,
projectId: string,
loadType: TLoader = "init-loader",
options: IssuePaginationOptions,
isExistingPaginationOptions: boolean = false
) => {
try {
// set loader and clear store
runInAction(() => {
this.setLoader(loadType);
});
this.clear(!isExistingPaginationOptions);
// get params from pagination options
const params = this.issueFilterStore?.getFilterParams(options, projectId, undefined, undefined, undefined);
// call the fetch issues API with the params
const response = await this.issueDraftService.getDraftIssues(workspaceSlug, projectId, params, {
signal: this.controller.signal,
});
// after fetching issues, call the base method to process the response further
this.onfetchIssues(response, options, workspaceSlug, projectId, undefined, !isExistingPaginationOptions);
return response;
} catch (error) {
// set loader to undefined if errored out
this.setLoader(undefined);
throw error;
}
};
/**
* This method is called subsequent pages of pagination
* if groupId/subgroupId is provided, only that specific group's next page is fetched
* else all the groups' next page is fetched
* @param workspaceSlug
* @param projectId
* @param groupId
* @param subGroupId
* @returns
*/
fetchNextIssues = async (workspaceSlug: string, projectId: string, groupId?: string, subGroupId?: string) => {
const cursorObject = this.getPaginationData(groupId, subGroupId);
// if there are no pagination options and the next page results do not exist the return
if (!this.paginationOptions || (cursorObject && !cursorObject?.nextPageResults)) return;
try {
// set Loader
this.setLoader("pagination", groupId, subGroupId);
// get params from stored pagination options
const params = this.issueFilterStore?.getFilterParams(
this.paginationOptions,
projectId,
this.getNextCursor(groupId, subGroupId),
groupId,
subGroupId
);
// call the fetch issues API with the params for next page in issues
const response = await this.issueDraftService.getDraftIssues(workspaceSlug, projectId, params);
// after the next page of issues are fetched, call the base method to process the response
this.onfetchNexIssues(response, groupId, subGroupId);
return response;
} catch (error) {
// set Loader as undefined if errored out
this.setLoader(undefined, groupId, subGroupId);
throw error;
}
};
/**
* This Method exists to fetch the first page of the issues with the existing stored pagination
* This is useful for refetching when filters, groupBy, orderBy etc changes
* @param workspaceSlug
* @param projectId
* @param loadType
* @returns
*/
fetchIssuesWithExistingPagination = async (
workspaceSlug: string,
projectId: string,
loadType: TLoader = "mutation"
) => {
if (!this.paginationOptions) return;
return await this.fetchIssues(workspaceSlug, projectId, loadType, this.paginationOptions, true);
};
// Using aliased names as they cannot be overridden in other stores
createIssue = this.createDraftIssue;
updateIssue = this.updateDraftIssue;
// Setting them as undefined as they can not performed on draft issues
archiveBulkIssues = undefined;
quickAddIssue = undefined;
archiveIssue = undefined;
}

View file

@ -41,7 +41,7 @@ import { updatePersistentLayer } from "@/local-db/utils/utils";
import { workItemSortWithOrderByExtended } from "@/plane-web/store/issue/helpers/base-issue.store";
// services
import { CycleService } from "@/services/cycle.service";
import { IssueArchiveService, IssueDraftService, IssueService } from "@/services/issue";
import { IssueArchiveService, IssueService } from "@/services/issue";
import { ModuleService } from "@/services/module.service";
//
import { IIssueRootStore } from "../root.store";
@ -194,7 +194,6 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
// services
issueService;
issueArchiveService;
issueDraftService;
moduleService;
cycleService;
// root store
@ -238,8 +237,6 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
createIssue: action,
issueUpdate: action,
createDraftIssue: action,
updateDraftIssue: action,
updateIssueDates: action,
issueQuickAdd: action.bound,
removeIssue: action.bound,
@ -264,7 +261,6 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
this.issueService = new IssueService(serviceType);
this.issueArchiveService = new IssueArchiveService();
this.issueDraftService = new IssueDraftService();
this.moduleService = new ModuleService();
this.cycleService = new CycleService();
@ -613,54 +609,6 @@ export abstract class BaseIssuesStore implements IBaseIssuesStore {
}
}
/**
* Similar to Create Issue but for creating Draft issues
* @param workspaceSlug
* @param projectId
* @param data draft issue data
* @returns
*/
async createDraftIssue(workspaceSlug: string, projectId: string, data: Partial<TIssue>) {
// call API to create a Draft issue
const response = await this.issueDraftService.createDraftIssue(workspaceSlug, projectId, data);
// call Fetch parent stats
this.fetchParentStats(workspaceSlug, projectId);
// Add issue to store
this.addIssue(response);
return response;
}
/**
* Similar to update issue but for draft issues.
* @param workspaceSlug
* @param projectId
* @param issueId
* @param data Partial Issue Data to be updated
*/
async updateDraftIssue(workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) {
// Store Before state of the issue
const issueBeforeUpdate = clone(this.rootIssueStore.issues.getIssueById(issueId));
try {
// Update the Respective Stores
this.rootIssueStore.issues.updateIssue(issueId, data);
this.updateIssueList({ ...issueBeforeUpdate, ...data } as TIssue, issueBeforeUpdate);
// call API to update the issue
await this.issueDraftService.updateDraftIssue(workspaceSlug, projectId, issueId, data);
// call Fetch parent stats
this.fetchParentStats(workspaceSlug, projectId);
// If the issue is updated to not a draft issue anymore remove from the store list
if (!isNil(data.is_draft) && !data.is_draft) this.removeIssueFromList(issueId);
} catch (error) {
// If errored out update store again to revert the change
this.rootIssueStore.issues.updateIssue(issueId, issueBeforeUpdate ?? {});
this.updateIssueList(issueBeforeUpdate, { ...issueBeforeUpdate, ...data } as TIssue);
throw error;
}
}
/**
* This method is called to delete an issue
* @param workspaceSlug

View file

@ -5,18 +5,13 @@ import { EIssueServiceType, TIssue, TIssueServiceType } from "@plane/types";
// local
import { persistence } from "@/local-db/storage.sqlite";
// services
import { IssueArchiveService, IssueDraftService, IssueService } from "@/services/issue";
import { IssueArchiveService, WorkspaceDraftService, IssueService } from "@/services/issue";
// types
import { IIssueDetail } from "./root.store";
export interface IIssueStoreActions {
// actions
fetchIssue: (
workspaceSlug: string,
projectId: string,
issueId: string,
issueStatus?: "DEFAULT" | "DRAFT"
) => Promise<TIssue>;
fetchIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<TIssue>;
updateIssue: (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) => Promise<void>;
removeIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
archiveIssue: (workspaceSlug: string, projectId: string, issueId: string) => Promise<void>;
@ -52,7 +47,7 @@ export class IssueStore implements IIssueStore {
issueService;
epicService;
issueArchiveService;
issueDraftService;
draftWorkItemService;
constructor(rootStore: IIssueDetail, serviceType: TIssueServiceType) {
makeObservable(this, {
@ -66,7 +61,7 @@ export class IssueStore implements IIssueStore {
this.issueService = new IssueService(serviceType);
this.epicService = new IssueService(EIssueServiceType.EPICS);
this.issueArchiveService = new IssueArchiveService(serviceType);
this.issueDraftService = new IssueDraftService();
this.draftWorkItemService = new WorkspaceDraftService();
}
getIsFetchingIssueDetails = computedFn((issueId: string | undefined) => {
@ -93,7 +88,7 @@ export class IssueStore implements IIssueStore {
});
// actions
fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string, issueStatus = "DEFAULT") => {
fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string) => {
const query = {
expand: "issue_reactions,issue_attachments,issue_link,parent",
};
@ -112,9 +107,7 @@ export class IssueStore implements IIssueStore {
this.localDBIssueDescription = issueId;
}
if (issueStatus === "DRAFT")
issue = await this.issueDraftService.getDraftIssueById(workspaceSlug, projectId, issueId, query);
else issue = await this.issueService.retrieve(workspaceSlug, projectId, issueId, query);
await this.issueService.retrieve(workspaceSlug, projectId, issueId, query);
if (!issue) throw new Error("Work item not found");

View file

@ -272,12 +272,8 @@ export abstract class IssueDetail implements IIssueDetail {
setIssueLinkData = (issueLinkData: TIssueLink | null) => (this.issueLinkData = issueLinkData);
// issue
fetchIssue = async (
workspaceSlug: string,
projectId: string,
issueId: string,
issueStatus: "DEFAULT" | "DRAFT" = "DEFAULT"
) => this.issue.fetchIssue(workspaceSlug, projectId, issueId, issueStatus);
fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string) =>
this.issue.fetchIssue(workspaceSlug, projectId, issueId);
fetchIssueWithIdentifier = async (workspaceSlug: string, projectIdentifier: string, sequenceId: string) =>
this.issue.fetchIssueWithIdentifier(workspaceSlug, projectIdentifier, sequenceId);
updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial<TIssue>) =>

View file

@ -33,7 +33,6 @@ import { IWorkspaceMembership } from "@/store/member/workspace-member.store";
// issues data store
import { IArchivedIssuesFilter, ArchivedIssuesFilter, IArchivedIssues, ArchivedIssues } from "./archived";
import { ICycleIssuesFilter, CycleIssuesFilter, ICycleIssues, CycleIssues } from "./cycle";
import { IDraftIssuesFilter, DraftIssuesFilter, IDraftIssues, DraftIssues } from "./draft";
import { IIssueStore, IssueStore } from "./issue.store";
import { ICalendarStore, CalendarStore } from "./issue_calendar_view.store";
import { IIssueKanBanViewStore, IssueKanBanViewStore } from "./issue_kanban_view.store";
@ -115,9 +114,6 @@ export interface IIssueRootStore {
archivedIssuesFilter: IArchivedIssuesFilter;
archivedIssues: IArchivedIssues;
draftIssuesFilter: IDraftIssuesFilter;
draftIssues: IDraftIssues;
issueKanBanView: IIssueKanBanViewStore;
issueCalendarView: ICalendarStore;
@ -186,9 +182,6 @@ export class IssueRootStore implements IIssueRootStore {
archivedIssuesFilter: IArchivedIssuesFilter;
archivedIssues: IArchivedIssues;
draftIssuesFilter: IDraftIssuesFilter;
draftIssues: IDraftIssues;
issueKanBanView: IIssueKanBanViewStore;
issueCalendarView: ICalendarStore;
@ -280,9 +273,6 @@ export class IssueRootStore implements IIssueRootStore {
this.archivedIssuesFilter = new ArchivedIssuesFilter(this);
this.archivedIssues = new ArchivedIssues(this, this.archivedIssuesFilter);
this.draftIssuesFilter = new DraftIssuesFilter(this);
this.draftIssues = new DraftIssues(this, this.draftIssuesFilter);
this.issueKanBanView = new IssueKanBanViewStore(this);
this.issueCalendarView = new CalendarStore();