From 9f61c309e00b135253f8b40103e90139ffb10d09 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Tue, 8 Oct 2024 16:19:52 +0530 Subject: [PATCH 01/21] chore: workspace draft page added --- .../(projects)/drafts/header.tsx | 61 +++++++++++++++++++ .../(projects)/drafts/layout.tsx | 13 ++++ .../(projects)/drafts/page.tsx | 17 ++++++ web/core/constants/dashboard.ts | 10 ++- 4 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 web/app/[workspaceSlug]/(projects)/drafts/header.tsx create mode 100644 web/app/[workspaceSlug]/(projects)/drafts/layout.tsx create mode 100644 web/app/[workspaceSlug]/(projects)/drafts/page.tsx diff --git a/web/app/[workspaceSlug]/(projects)/drafts/header.tsx b/web/app/[workspaceSlug]/(projects)/drafts/header.tsx new file mode 100644 index 00000000000..c4c27e7b040 --- /dev/null +++ b/web/app/[workspaceSlug]/(projects)/drafts/header.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { FC, useState } from "react"; +import { EUserPermissions, EUserPermissionsLevel } from "ee/constants/user-permissions"; +import { observer } from "mobx-react"; +import { PenSquare } from "lucide-react"; +// ui +import { Breadcrumbs, Button, Header } from "@plane/ui"; +// components +import { BreadcrumbLink } from "@/components/common"; +import { CreateUpdateIssueModal } from "@/components/issues"; +// constants +import { EIssuesStoreType } from "@/constants/issue"; +// hooks +import { useUserPermissions } from "@/hooks/store"; + +export const WorkspaceDraftHeader: FC = observer(() => { + // state + const [isDraftIssueModalOpen, setIsDraftIssueModalOpen] = useState(false); + // store hooks + const { allowPermissions } = useUserPermissions(); + + // check if user is authorized to create draft issue + const isAuthorizedUser = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); + + return ( + <> + setIsDraftIssueModalOpen(false)} + storeType={EIssuesStoreType.WORKSPACE_DRAFT} + isDraft + /> +
+ + + } />} + /> + + + + + + +
+ + ); +}); diff --git a/web/app/[workspaceSlug]/(projects)/drafts/layout.tsx b/web/app/[workspaceSlug]/(projects)/drafts/layout.tsx new file mode 100644 index 00000000000..a5a647bfdba --- /dev/null +++ b/web/app/[workspaceSlug]/(projects)/drafts/layout.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { AppHeader, ContentWrapper } from "@/components/core"; +import { WorkspaceDraftHeader } from "./header"; + +export default function WorkspaceDraftLayout({ children }: { children: React.ReactNode }) { + return ( + <> + } /> + {children} + + ); +} diff --git a/web/app/[workspaceSlug]/(projects)/drafts/page.tsx b/web/app/[workspaceSlug]/(projects)/drafts/page.tsx new file mode 100644 index 00000000000..633696e309b --- /dev/null +++ b/web/app/[workspaceSlug]/(projects)/drafts/page.tsx @@ -0,0 +1,17 @@ +"use client"; + +// components +import { PageHead } from "@/components/core"; + +const WorkspaceDraftPage = () => { + const pageTitle = "Workspace Draft"; + + return ( + <> + +
Root
+ + ); +}; + +export default WorkspaceDraftPage; diff --git a/web/core/constants/dashboard.ts b/web/core/constants/dashboard.ts index 00a3fd52601..3cfdec0e287 100644 --- a/web/core/constants/dashboard.ts +++ b/web/core/constants/dashboard.ts @@ -2,7 +2,7 @@ import { linearGradientDef } from "@nivo/core"; // icons -import { BarChart2, Briefcase, Home, Inbox, Layers } from "lucide-react"; +import { BarChart2, Briefcase, Home, Inbox, Layers, PenSquare } from "lucide-react"; // types import { TIssuesListTypes, TStateGroups } from "@plane/types"; // ui @@ -329,4 +329,12 @@ export const SIDEBAR_USER_MENU_ITEMS: { highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/notifications/`), Icon: Inbox, }, + { + key: "drafts", + label: "Drafts", + href: `/drafts`, + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + highlight: (pathname: string, baseUrl: string) => pathname.includes(`${baseUrl}/drafts/`), + Icon: PenSquare, + }, ]; From 76b23084e6c821681d52e247526f4b42b5509b7d Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Tue, 8 Oct 2024 16:22:14 +0530 Subject: [PATCH 02/21] chore: workspace draft issues services added --- web/core/services/issue/index.ts | 1 + .../services/issue/workspace_draft.service.ts | 65 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 web/core/services/issue/workspace_draft.service.ts diff --git a/web/core/services/issue/index.ts b/web/core/services/issue/index.ts index 4d8a4623ba7..ad809eae5c2 100644 --- a/web/core/services/issue/index.ts +++ b/web/core/services/issue/index.ts @@ -7,3 +7,4 @@ export * from "./issue_attachment.service"; export * from "./issue_activity.service"; export * from "./issue_comment.service"; export * from "./issue_relation.service"; +export * from "./workspace_draft.service"; diff --git a/web/core/services/issue/workspace_draft.service.ts b/web/core/services/issue/workspace_draft.service.ts new file mode 100644 index 00000000000..56a8ffaa111 --- /dev/null +++ b/web/core/services/issue/workspace_draft.service.ts @@ -0,0 +1,65 @@ +import { TIssue, TIssuesResponse } from "@plane/types"; +import { API_BASE_URL } from "@/helpers/common.helper"; +import { APIService } from "../api.service"; + +export class WorkspaceDraftService extends APIService { + constructor() { + super(API_BASE_URL); + } + + async getIssues(workspaceSlug: string, query?: any, config = {}): Promise { + return this.get( + `/api/workspaces/${workspaceSlug}/draft-issues/`, + { + params: { ...query }, + }, + config + ) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } + + async getIssueById(workspaceSlug: string, issueId: string, queries?: any): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/draft-issues/${issueId}/`, { + params: queries, + }) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async createIssue(workspaceSlug: string, data: any): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/draft-issues/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async updateIssue(workspaceSlug: string, issueId: string, data: any): Promise { + return this.patch(`/api/workspaces/${workspaceSlug}/draft-issues/${issueId}/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async deleteIssue(workspaceSlug: string, issueId: string): Promise { + return this.delete(`/api/workspaces/${workspaceSlug}/draft-issues/${issueId}/`) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } + + async moveToIssues(workspaceSlug: string, issueId: string, data: Partial): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/draft-to-issue/${issueId}/`, data) + .then((response) => response?.data) + .catch((error) => { + throw error?.response; + }); + } +} From 2359b5caca63a42f03490eaee209274df9138ffd Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Tue, 8 Oct 2024 17:05:11 +0530 Subject: [PATCH 03/21] chore: workspace draft issue store added --- web/core/store/issue/root.store.ts | 12 +- .../issue/workspace-draft/filter.store.ts | 257 ++++++++++++++++++ web/core/store/issue/workspace-draft/index.ts | 2 + .../issue/workspace-draft/issue.store.ts | 206 ++++++++++++++ 4 files changed, 476 insertions(+), 1 deletion(-) create mode 100644 web/core/store/issue/workspace-draft/filter.store.ts create mode 100644 web/core/store/issue/workspace-draft/index.ts create mode 100644 web/core/store/issue/workspace-draft/issue.store.ts diff --git a/web/core/store/issue/root.store.ts b/web/core/store/issue/root.store.ts index 10415c0d2e3..f49cb2cae86 100644 --- a/web/core/store/issue/root.store.ts +++ b/web/core/store/issue/root.store.ts @@ -23,7 +23,8 @@ import { IProjectViewIssues, ProjectViewIssues, } from "./project-views"; -import { IWorkspaceIssuesFilter, WorkspaceIssuesFilter, IWorkspaceIssues, WorkspaceIssues } from "./workspace"; +import { WorkspaceIssuesFilter, IWorkspaceIssues, WorkspaceIssues, IWorkspaceIssuesFilter } from "./workspace"; +import { IWorkspaceDraftIssues, IWorkspaceDraftIssuesFilter, WorkspaceDraftIssues, WorkspaceDraftIssuesFilter } from "./workspace-draft"; export interface IIssueRootStore { currentUserId: string | undefined; @@ -55,6 +56,9 @@ export interface IIssueRootStore { workspaceIssuesFilter: IWorkspaceIssuesFilter; workspaceIssues: IWorkspaceIssues; + workspaceDraftIssuesFilter: IWorkspaceDraftIssuesFilter; + workspaceDraftIssues: IWorkspaceDraftIssues; + profileIssuesFilter: IProfileIssuesFilter; profileIssues: IProfileIssues; @@ -110,6 +114,9 @@ export class IssueRootStore implements IIssueRootStore { workspaceIssuesFilter: IWorkspaceIssuesFilter; workspaceIssues: IWorkspaceIssues; + workspaceDraftIssuesFilter: IWorkspaceDraftIssuesFilter; + workspaceDraftIssues: IWorkspaceDraftIssues; + profileIssuesFilter: IProfileIssuesFilter; profileIssues: IProfileIssues; @@ -190,6 +197,9 @@ export class IssueRootStore implements IIssueRootStore { this.profileIssuesFilter = new ProfileIssuesFilter(this); this.profileIssues = new ProfileIssues(this, this.profileIssuesFilter); + this.workspaceDraftIssuesFilter = new WorkspaceDraftIssuesFilter(this); + this.workspaceDraftIssues = new WorkspaceDraftIssues(this, this.workspaceDraftIssuesFilter); + this.projectIssuesFilter = new ProjectIssuesFilter(this); this.projectIssues = new ProjectIssues(this, this.projectIssuesFilter); diff --git a/web/core/store/issue/workspace-draft/filter.store.ts b/web/core/store/issue/workspace-draft/filter.store.ts new file mode 100644 index 00000000000..add7ecc018b --- /dev/null +++ b/web/core/store/issue/workspace-draft/filter.store.ts @@ -0,0 +1,257 @@ +import isEmpty from "lodash/isEmpty"; +import set from "lodash/set"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; +// base class +import { computedFn } from "mobx-utils"; +import { + IIssueFilterOptions, + IIssueDisplayFilterOptions, + IIssueDisplayProperties, + TIssueKanbanFilters, + IIssueFilters, + TIssueParams, + IssuePaginationOptions, +} from "@plane/types"; +import { EIssueFilterType, EIssuesStoreType } from "@/constants/issue"; +import { handleIssueQueryParamsByLayout } from "@/helpers/issue.helper"; +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 IWorkspaceDraftIssuesFilter extends IBaseIssueFilterStore { + // observables + workspaceSlug: string; + //helper actions + getFilterParams: ( + options: IssuePaginationOptions, + userId: string, + cursor: string | undefined, + groupId: string | undefined, + subGroupId: string | undefined + ) => Partial>; + // action + fetchFilters: (workspaceSlug: string, userId: string) => Promise; + updateFilters: ( + workspaceSlug: string, + projectId: string | undefined, + filterType: EIssueFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters, + userId: string + ) => Promise; +} + +export class WorkspaceDraftIssuesFilter extends IssueFilterHelperStore implements IWorkspaceDraftIssuesFilter { + // observables + workspaceSlug: string = ""; + filters: { [userId: string]: IIssueFilters } = {}; + // root store + rootIssueStore: IIssueRootStore; + // services + issueFilterService; + + constructor(_rootStore: IIssueRootStore) { + super(); + makeObservable(this, { + // observables + workspaceSlug: observable.ref, + 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 workspaceSlug = this.rootIssueStore.workspaceSlug; + if (!workspaceSlug) return undefined; + + return this.getIssueFilters(workspaceSlug); + } + + get appliedFilters() { + const workspaceSlug = this.rootIssueStore.workspaceSlug; + if (!workspaceSlug) return undefined; + + return this.getAppliedFilters(workspaceSlug); + } + + getIssueFilters(workspaceSlug: string) { + const displayFilters = this.filters[workspaceSlug] || undefined; + if (isEmpty(displayFilters)) return undefined; + + const _filters: IIssueFilters = this.computedIssueFilters(displayFilters); + + return _filters; + } + + getAppliedFilters(workspaceSlug: string) { + const userFilters = this.getIssueFilters(workspaceSlug); + if (!userFilters) return undefined; + + const filteredParams = handleIssueQueryParamsByLayout(userFilters?.displayFilters?.layout, "profile_issues"); + if (!filteredParams) return undefined; + + const filteredRouteParams: Partial> = this.computedFilteredParams( + userFilters?.filters as IIssueFilterOptions, + userFilters?.displayFilters as IIssueDisplayFilterOptions, + filteredParams + ); + + return filteredRouteParams; + } + + getFilterParams = computedFn( + ( + options: IssuePaginationOptions, + userId: string, + cursor: string | undefined, + groupId: string | undefined, + subGroupId: string | undefined + ) => { + const filterParams = this.getAppliedFilters(this.workspaceSlug); + + const paginationParams = this.getPaginationParams(filterParams, options, cursor, groupId, subGroupId); + return paginationParams; + } + ); + + fetchFilters = async (workspaceSlug: string) => { + this.workspaceSlug = workspaceSlug; + const _filters = this.handleIssuesLocalFilters.get( + EIssuesStoreType.PROFILE, + workspaceSlug, + workspaceSlug, + 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: _filters?.kanban_filters?.group_by || [], + sub_group_by: _filters?.kanban_filters?.sub_group_by || [], + }; + + runInAction(() => { + set(this.filters, [workspaceSlug, "filters"], filters); + set(this.filters, [workspaceSlug, "displayFilters"], displayFilters); + set(this.filters, [workspaceSlug, "displayProperties"], displayProperties); + set(this.filters, [workspaceSlug, "kanbanFilters"], kanbanFilters); + }); + }; + + updateFilters = async ( + workspaceSlug: string, + projectId: string | undefined, + type: EIssueFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters + ) => { + try { + if (isEmpty(this.filters) || isEmpty(this.filters[workspaceSlug]) || isEmpty(filters)) return; + + const _filters = { + filters: this.filters[workspaceSlug].filters as IIssueFilterOptions, + displayFilters: this.filters[workspaceSlug].displayFilters as IIssueDisplayFilterOptions, + displayProperties: this.filters[workspaceSlug].displayProperties as IIssueDisplayProperties, + kanbanFilters: this.filters[workspaceSlug].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, [workspaceSlug, "filters", _key], updatedFilters[_key as keyof IIssueFilterOptions]); + }); + }); + + this.rootIssueStore.profileIssues.fetchIssuesWithExistingPagination(workspaceSlug, workspaceSlug, "mutation"); + + this.handleIssuesLocalFilters.set(EIssuesStoreType.PROFILE, type, workspaceSlug, workspaceSlug, 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 priority 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 = "priority"; + updatedDisplayFilters.group_by = "priority"; + } + + runInAction(() => { + Object.keys(updatedDisplayFilters).forEach((_key) => { + set( + this.filters, + [workspaceSlug, "displayFilters", _key], + updatedDisplayFilters[_key as keyof IIssueDisplayFilterOptions] + ); + }); + }); + + this.rootIssueStore.profileIssues.fetchIssuesWithExistingPagination(workspaceSlug, workspaceSlug, "mutation"); + + this.handleIssuesLocalFilters.set(EIssuesStoreType.PROFILE, type, workspaceSlug, workspaceSlug, 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, + [workspaceSlug, "displayProperties", _key], + updatedDisplayProperties[_key as keyof IIssueDisplayProperties] + ); + }); + }); + + this.handleIssuesLocalFilters.set(EIssuesStoreType.PROFILE, type, workspaceSlug, workspaceSlug, undefined, { + display_properties: _filters.displayProperties, + }); + break; + } + + default: + break; + } + } catch (error) { + if (workspaceSlug) this.fetchFilters(workspaceSlug); + throw error; + } + }; +} diff --git a/web/core/store/issue/workspace-draft/index.ts b/web/core/store/issue/workspace-draft/index.ts new file mode 100644 index 00000000000..53c19d3a802 --- /dev/null +++ b/web/core/store/issue/workspace-draft/index.ts @@ -0,0 +1,2 @@ +export * from "./issue.store"; +export * from "./filter.store"; diff --git a/web/core/store/issue/workspace-draft/issue.store.ts b/web/core/store/issue/workspace-draft/issue.store.ts new file mode 100644 index 00000000000..89404b8857a --- /dev/null +++ b/web/core/store/issue/workspace-draft/issue.store.ts @@ -0,0 +1,206 @@ +import { clone } from "lodash"; +import { action, makeObservable, runInAction } from "mobx"; +// base class +import { IssuePaginationOptions, TIssue, TIssuesResponse, TLoader, ViewFlags } from "@plane/types"; +// services +import { WorkspaceDraftService } from "@/services/issue"; +// types +import { BaseIssuesStore, IBaseIssuesStore } from "../helpers/base-issues.store"; +import { IIssueRootStore } from "../root.store"; +import { IWorkspaceDraftIssuesFilter } from "./filter.store"; + +export interface IWorkspaceDraftIssues extends IBaseIssuesStore { + // observable + viewFlags: ViewFlags; + // actions + fetchIssues: ( + workspaceSlug: string, + viewId: string, + loadType: TLoader, + options: IssuePaginationOptions + ) => Promise; + fetchIssuesWithExistingPagination: ( + workspaceSlug: string, + viewId: string, + loadType: TLoader + ) => Promise; + fetchNextIssues: ( + workspaceSlug: string, + viewId: string, + groupId?: string, + subGroupId?: string + ) => Promise; + + createWorkspaceDraftIssue: (workspaceSlug: string, data: Partial) => Promise; + updateWorkspaceDraftIssue: (workspaceSlug: string, issueId: string, data: Partial) => Promise; + deleteWorkspaceDraftIssue: (workspaceSlug: string, issueId: string) => Promise; + moveToIssues: (workspaceSlug: string, issueId: string, data: Partial) => Promise; +} + +export class WorkspaceDraftIssues extends BaseIssuesStore implements IWorkspaceDraftIssues { + viewFlags = { + enableQuickAdd: true, + enableIssueCreation: true, + enableInlineEditing: true, + }; + // service + workspaceDraftService; + // filterStore + issueFilterStore; + + constructor(_rootStore: IIssueRootStore, issueFilterStore: IWorkspaceDraftIssuesFilter) { + super(_rootStore, issueFilterStore); + + makeObservable(this, { + // action + fetchIssues: action, + fetchNextIssues: action, + fetchIssuesWithExistingPagination: action, + createWorkspaceDraftIssue: action, + updateWorkspaceDraftIssue: action, + deleteWorkspaceDraftIssue: action, + }); + // services + this.workspaceDraftService = new WorkspaceDraftService(); + // filter store + this.issueFilterStore = issueFilterStore; + } + + fetchIssues = async ( + workspaceSlug: string, + viewId: string, + loadType: TLoader, + 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, viewId, undefined, undefined, undefined); + // call the fetch issues API with the params + const response = await this.workspaceDraftService.getIssues(workspaceSlug, params, { + signal: this.controller.signal, + }); + + // after fetching issues, call the base method to process the response further + this.onfetchIssues(response, options, workspaceSlug, undefined, 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 viewId + * @param groupId + * @param subGroupId + * @returns + */ + fetchNextIssues = async (workspaceSlug: string, viewId: 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, + viewId, + this.getNextCursor(groupId, subGroupId), + groupId, + subGroupId + ); + // call the fetch issues API with the params for next page in issues + const response = await this.workspaceDraftService.getIssues(workspaceSlug, 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 viewId + * @param loadType + * @returns + */ + fetchIssuesWithExistingPagination = async (workspaceSlug: string, viewId: string, loadType: TLoader) => { + if (!this.paginationOptions) return; + return await this.fetchIssues(workspaceSlug, viewId, loadType, this.paginationOptions, true); + }; + + createWorkspaceDraftIssue = async (workspaceSlug: string, data: Partial) => { + const response = await this.workspaceDraftService.createIssue(workspaceSlug, data); + this.addIssue(response); + return response; + }; + + updateWorkspaceDraftIssue = async (workspaceSlug: string, issueId: string, data: Partial) => { + // 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.workspaceDraftService.updateIssue(workspaceSlug, issueId, data); + } 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; + } + }; + + deleteWorkspaceDraftIssue = async (workspaceSlug: string, issueId: string) => { + // Male API call + await this.workspaceDraftService.deleteIssue(workspaceSlug, issueId); + // Remove from Respective issue Id list + runInAction(() => { + this.removeIssueFromList(issueId); + }); + // Remove issue from main issue Map store + this.rootIssueStore.issues.removeIssue(issueId); + }; + + moveToIssues = async (workspaceSlug: string, issueId: string, data: Partial) => { + // Make API call + await this.workspaceDraftService.moveToIssues(workspaceSlug, issueId, data); + // Remove from Respective issue Id list + runInAction(() => { + this.removeIssueFromList(issueId); + }); + // Remove issue from main issue Map store + this.rootIssueStore.issues.removeIssue(issueId); + }; + + fetchParentStats = (workspaceSlug: string, projectId?: string, id?: string) => { + // Implement the method logic here + console.log(`Fetching parent stats for workspace: ${workspaceSlug}, project: ${projectId}, id: ${id}`); + }; + updateParentStats = (prevIssueState?: TIssue, nextIssueState?: TIssue, id?: string) => { + // Implement the method logic here + console.log(`Updating parent stats for issue: ${id}`); + }; +} From 3bba8393ac90bc3385abb16b4a59aaf3bca2c576 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Tue, 8 Oct 2024 17:30:30 +0530 Subject: [PATCH 04/21] chore: workspace draft issue filter store added --- web/core/constants/issue.ts | 1 + web/core/hooks/store/use-issues.ts | 10 +++ web/core/hooks/use-issues-actions.tsx | 81 +++++++++++++++++++ .../issue/workspace-draft/filter.store.ts | 7 +- .../issue/workspace-draft/issue.store.ts | 19 ++--- 5 files changed, 100 insertions(+), 18 deletions(-) diff --git a/web/core/constants/issue.ts b/web/core/constants/issue.ts index 22dc8817a96..8629c05a253 100644 --- a/web/core/constants/issue.ts +++ b/web/core/constants/issue.ts @@ -30,6 +30,7 @@ export enum EIssuesStoreType { ARCHIVED = "ARCHIVED", DRAFT = "DRAFT", DEFAULT = "DEFAULT", + WORKSPACE_DRAFT = "WORKSPACE_DRAFT", } export enum EIssueLayoutTypes { diff --git a/web/core/hooks/store/use-issues.ts b/web/core/hooks/store/use-issues.ts index 22356777e7f..9fd68d48c21 100644 --- a/web/core/hooks/store/use-issues.ts +++ b/web/core/hooks/store/use-issues.ts @@ -13,6 +13,7 @@ import { IProfileIssues, IProfileIssuesFilter } from "@/store/issue/profile"; import { IProjectIssues, IProjectIssuesFilter } from "@/store/issue/project"; import { IProjectViewIssues, IProjectViewIssuesFilter } from "@/store/issue/project-views"; import { IWorkspaceIssues, IWorkspaceIssuesFilter } from "@/store/issue/workspace"; +import { IWorkspaceDraftIssues, IWorkspaceDraftIssuesFilter } from "@/store/issue/workspace-draft"; // constants type defaultIssueStore = { @@ -24,6 +25,10 @@ export type TStoreIssues = { issues: IWorkspaceIssues; issuesFilter: IWorkspaceIssuesFilter; }; + [EIssuesStoreType.WORKSPACE_DRAFT]: defaultIssueStore & { + issues: IWorkspaceDraftIssues; + issuesFilter: IWorkspaceDraftIssuesFilter; + }; [EIssuesStoreType.PROFILE]: defaultIssueStore & { issues: IProfileIssues; issuesFilter: IProfileIssuesFilter; @@ -72,6 +77,11 @@ export const useIssues = (storeType?: T): TStoreIssu issues: context.issue.workspaceIssues, issuesFilter: context.issue.workspaceIssuesFilter, }) as TStoreIssues[T]; + case EIssuesStoreType.WORKSPACE_DRAFT: + return merge(defaultStore, { + issues: context.issue.workspaceDraftIssues, + issuesFilter: context.issue.workspaceDraftIssuesFilter, + }) as TStoreIssues[T]; case EIssuesStoreType.PROFILE: return merge(defaultStore, { issues: context.issue.profileIssues, diff --git a/web/core/hooks/use-issues-actions.tsx b/web/core/hooks/use-issues-actions.tsx index 482618697b9..453ee1adc3d 100644 --- a/web/core/hooks/use-issues-actions.tsx +++ b/web/core/hooks/use-issues-actions.tsx @@ -45,6 +45,7 @@ export const useIssuesActions = (storeType: EIssuesStoreType): IssueActions => { const profileIssueActions = useProfileIssueActions(); const draftIssueActions = useDraftIssueActions(); const archivedIssueActions = useArchivedIssueActions(); + const workspaceDraftIssueActions = useWorkspaceDraftIssueActions(); switch (storeType) { case EIssuesStoreType.PROJECT_VIEW: @@ -61,6 +62,8 @@ export const useIssuesActions = (storeType: EIssuesStoreType): IssueActions => { return moduleIssueActions; case EIssuesStoreType.GLOBAL: return globalIssueActions; + case EIssuesStoreType.WORKSPACE_DRAFT: + return workspaceDraftIssueActions; case EIssuesStoreType.PROJECT: default: return projectIssueActions; @@ -737,3 +740,81 @@ const useGlobalIssueActions = () => { [createIssue, updateIssue, removeIssue, updateFilters] ); }; + +const useWorkspaceDraftIssueActions = () => { + // router + const { workspaceSlug: routerWorkspaceSlug, globalViewId: routerGlobalViewId } = useParams(); + const workspaceSlug = routerWorkspaceSlug?.toString(); + const globalViewId = routerGlobalViewId?.toString(); + // store hooks + const { issues, issuesFilter } = useIssues(EIssuesStoreType.WORKSPACE_DRAFT); + + const fetchIssues = useCallback( + async (loadType: TLoader, options: IssuePaginationOptions) => { + if (!workspaceSlug) return; + return issues.fetchIssues(workspaceSlug.toString(), loadType, options); + }, + [workspaceSlug, issues] + ); + + const fetchNextIssues = useCallback(async () => { + if (!workspaceSlug) return; + return issues.fetchNextIssues(workspaceSlug.toString()); + }, [workspaceSlug, issues]); + + const createIssue = useCallback( + async (projectId: string | undefined | null, data: Partial) => { + if (!workspaceSlug || !projectId) return; + return await issues.createWorkspaceDraftIssue(workspaceSlug, data); + }, + [issues, workspaceSlug] + ); + const updateIssue = useCallback( + async (projectId: string | undefined | null, issueId: string, data: Partial) => { + if (!workspaceSlug || !projectId) return; + return await issues.updateWorkspaceDraftIssue(workspaceSlug, issueId, data); + }, + [issues, workspaceSlug] + ); + const removeIssue = useCallback( + async (projectId: string | undefined | null, issueId: string) => { + if (!workspaceSlug || !projectId) return; + return await issues.deleteWorkspaceDraftIssue(workspaceSlug, issueId); + }, + [issues, workspaceSlug] + ); + + const moveToIssue = useCallback( + async (workspaceSlug: string, issueId: string, data: Partial) => { + if (!workspaceSlug || !issueId || !data) return; + return await issues.moveToIssues(workspaceSlug, issueId, data); + }, + [issues] + ); + + const updateFilters = useCallback( + async ( + projectId: string, + filterType: EIssueFilterType, + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters + ) => { + filters = filters as IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties; + if (!globalViewId || !workspaceSlug) return; + return await issuesFilter.updateFilters(workspaceSlug, filterType, filters); + }, + [globalViewId, workspaceSlug, issuesFilter] + ); + + return useMemo( + () => ({ + fetchIssues, + fetchNextIssues, + createIssue, + updateIssue, + removeIssue, + updateFilters, + moveToIssue, + }), + [fetchIssues, fetchNextIssues, createIssue, updateIssue, removeIssue, updateFilters, moveToIssue] + ); +}; diff --git a/web/core/store/issue/workspace-draft/filter.store.ts b/web/core/store/issue/workspace-draft/filter.store.ts index add7ecc018b..f43d0a068e9 100644 --- a/web/core/store/issue/workspace-draft/filter.store.ts +++ b/web/core/store/issue/workspace-draft/filter.store.ts @@ -34,13 +34,11 @@ export interface IWorkspaceDraftIssuesFilter extends IBaseIssueFilterStore { subGroupId: string | undefined ) => Partial>; // action - fetchFilters: (workspaceSlug: string, userId: string) => Promise; + fetchFilters: (workspaceSlug: string) => Promise; updateFilters: ( workspaceSlug: string, - projectId: string | undefined, filterType: EIssueFilterType, - filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters, - userId: string + filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters ) => Promise; } @@ -153,7 +151,6 @@ export class WorkspaceDraftIssuesFilter extends IssueFilterHelperStore implement updateFilters = async ( workspaceSlug: string, - projectId: string | undefined, type: EIssueFilterType, filters: IIssueFilterOptions | IIssueDisplayFilterOptions | IIssueDisplayProperties | TIssueKanbanFilters ) => { diff --git a/web/core/store/issue/workspace-draft/issue.store.ts b/web/core/store/issue/workspace-draft/issue.store.ts index 89404b8857a..6c38a88bfbb 100644 --- a/web/core/store/issue/workspace-draft/issue.store.ts +++ b/web/core/store/issue/workspace-draft/issue.store.ts @@ -15,18 +15,12 @@ export interface IWorkspaceDraftIssues extends IBaseIssuesStore { // actions fetchIssues: ( workspaceSlug: string, - viewId: string, loadType: TLoader, options: IssuePaginationOptions ) => Promise; - fetchIssuesWithExistingPagination: ( - workspaceSlug: string, - viewId: string, - loadType: TLoader - ) => Promise; + fetchIssuesWithExistingPagination: (workspaceSlug: string, loadType: TLoader) => Promise; fetchNextIssues: ( workspaceSlug: string, - viewId: string, groupId?: string, subGroupId?: string ) => Promise; @@ -68,7 +62,6 @@ export class WorkspaceDraftIssues extends BaseIssuesStore implements IWorkspaceD fetchIssues = async ( workspaceSlug: string, - viewId: string, loadType: TLoader, options: IssuePaginationOptions, isExistingPaginationOptions: boolean = false @@ -81,7 +74,7 @@ export class WorkspaceDraftIssues extends BaseIssuesStore implements IWorkspaceD this.clear(!isExistingPaginationOptions); // get params from pagination options - const params = this.issueFilterStore?.getFilterParams(options, viewId, undefined, undefined, undefined); + const params = this.issueFilterStore?.getFilterParams(options, workspaceSlug, undefined, undefined, undefined); // call the fetch issues API with the params const response = await this.workspaceDraftService.getIssues(workspaceSlug, params, { signal: this.controller.signal, @@ -107,7 +100,7 @@ export class WorkspaceDraftIssues extends BaseIssuesStore implements IWorkspaceD * @param subGroupId * @returns */ - fetchNextIssues = async (workspaceSlug: string, viewId: string, groupId?: string, subGroupId?: string) => { + fetchNextIssues = async (workspaceSlug: 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; @@ -118,7 +111,7 @@ export class WorkspaceDraftIssues extends BaseIssuesStore implements IWorkspaceD // get params from stored pagination options const params = this.issueFilterStore?.getFilterParams( this.paginationOptions, - viewId, + workspaceSlug, this.getNextCursor(groupId, subGroupId), groupId, subGroupId @@ -144,9 +137,9 @@ export class WorkspaceDraftIssues extends BaseIssuesStore implements IWorkspaceD * @param loadType * @returns */ - fetchIssuesWithExistingPagination = async (workspaceSlug: string, viewId: string, loadType: TLoader) => { + fetchIssuesWithExistingPagination = async (workspaceSlug: string, loadType: TLoader) => { if (!this.paginationOptions) return; - return await this.fetchIssues(workspaceSlug, viewId, loadType, this.paginationOptions, true); + return await this.fetchIssues(workspaceSlug, loadType, this.paginationOptions, true); }; createWorkspaceDraftIssue = async (workspaceSlug: string, data: Partial) => { From bdbc7635b5e7ee2b50900b0c80c1715f624122b9 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Tue, 8 Oct 2024 19:19:39 +0530 Subject: [PATCH 05/21] chore: issue rendering --- .../(projects)/drafts/page.tsx | 5 +- .../roots/workspace-draft-root.tsx | 48 +++++ .../issue-layouts/list/base-list-root.tsx | 15 +- .../issues/issue-layouts/list/default.tsx | 5 +- .../issue-layouts/list/list-view-types.d.ts | 1 + .../quick-action-dropdowns/index.ts | 1 + .../workspace-draft.tsx | 176 ++++++++++++++++++ web/core/hooks/use-issues-actions.tsx | 1 - .../issue/workspace-draft/issue.store.ts | 3 + 9 files changed, 243 insertions(+), 12 deletions(-) create mode 100644 web/core/components/issues/issue-layouts/filters/applied-filters/roots/workspace-draft-root.tsx create mode 100644 web/core/components/issues/issue-layouts/quick-action-dropdowns/workspace-draft.tsx diff --git a/web/app/[workspaceSlug]/(projects)/drafts/page.tsx b/web/app/[workspaceSlug]/(projects)/drafts/page.tsx index 633696e309b..c00e38400b7 100644 --- a/web/app/[workspaceSlug]/(projects)/drafts/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/drafts/page.tsx @@ -2,6 +2,7 @@ // components import { PageHead } from "@/components/core"; +import { WorkspaceDraftIssueLayoutRoot } from "@/components/issues/issue-layouts/filters/applied-filters/roots/workspace-draft-root"; const WorkspaceDraftPage = () => { const pageTitle = "Workspace Draft"; @@ -9,7 +10,9 @@ const WorkspaceDraftPage = () => { return ( <> -
Root
+
+ +
); }; diff --git a/web/core/components/issues/issue-layouts/filters/applied-filters/roots/workspace-draft-root.tsx b/web/core/components/issues/issue-layouts/filters/applied-filters/roots/workspace-draft-root.tsx new file mode 100644 index 00000000000..b97786839fe --- /dev/null +++ b/web/core/components/issues/issue-layouts/filters/applied-filters/roots/workspace-draft-root.tsx @@ -0,0 +1,48 @@ +import React, { useCallback } from "react"; +import { observer } from "mobx-react"; +import { useParams } from "next/navigation"; +import { WorkspaceDraftIssueQuickActions } from "@/components/issues/issue-layouts/quick-action-dropdowns"; +import { IssuePeekOverview } from "@/components/issues/peek-overview"; +import { EIssuesStoreType } from "@/constants/issue"; +import { useUserPermissions } from "@/hooks/store"; +import { IssuesStoreContext } from "@/hooks/use-issue-layout-store"; +import { useWorkspaceIssueProperties } from "@/hooks/use-workspace-issue-properties"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; +import { BaseListRoot } from "../../../list/base-list-root"; + +export const WorkspaceDraftIssueLayoutRoot = observer(() => { + // router + const { workspaceSlug } = useParams(); + + //swr hook for fetching issue properties + useWorkspaceIssueProperties(workspaceSlug); + // store + const { allowPermissions } = useUserPermissions(); + + const canEditProperties = useCallback( + (projectId: string | undefined) => { + if (!projectId) return false; + return allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.PROJECT, + workspaceSlug.toString(), + projectId + ); + }, + [workspaceSlug, allowPermissions] + ); + + return ( + +
+
+ + +
+
+
+ ); +}); diff --git a/web/core/components/issues/issue-layouts/list/base-list-root.tsx b/web/core/components/issues/issue-layouts/list/base-list-root.tsx index a97ad0a8e05..d73ca6ff7be 100644 --- a/web/core/components/issues/issue-layouts/list/base-list-root.tsx +++ b/web/core/components/issues/issue-layouts/list/base-list-root.tsx @@ -25,7 +25,8 @@ type ListStoreType = | EIssuesStoreType.PROJECT_VIEW | EIssuesStoreType.DRAFT | EIssuesStoreType.PROFILE - | EIssuesStoreType.ARCHIVED; + | EIssuesStoreType.ARCHIVED + | EIssuesStoreType.WORKSPACE_DRAFT; interface IBaseListRoot { QuickActions: FC; addIssuesToView?: (issueIds: string[]) => Promise; @@ -61,8 +62,9 @@ export const BaseListRoot = observer((props: IBaseListRoot) => { const showEmptyGroup = displayFilters?.show_empty_groups ?? false; const { workspaceSlug, projectId } = useParams(); - const {updateFilters} = useIssuesActions(storeType); - const collapsedGroups = issuesFilter?.issueFilters?.kanbanFilters || { group_by: [], sub_group_by: [] } as TIssueKanbanFilters; + const { updateFilters } = useIssuesActions(storeType); + const collapsedGroups = + issuesFilter?.issueFilters?.kanbanFilters || ({ group_by: [], sub_group_by: [] } as TIssueKanbanFilters); useEffect(() => { fetchIssues("init-loader", { canGroup: true, perPageCount: group_by ? 50 : 100 }, viewId); @@ -122,15 +124,14 @@ export const BaseListRoot = observer((props: IBaseListRoot) => { } else { collapsedGroups.push(value); } - updateFilters(projectId?.toString() ?? "", EIssueFilterType.KANBAN_FILTERS, - { group_by: collapsedGroups } as TIssueKanbanFilters - ); + updateFilters(projectId?.toString() ?? "", EIssueFilterType.KANBAN_FILTERS, { + group_by: collapsedGroups, + } as TIssueKanbanFilters); } }, [workspaceSlug, issuesFilter, projectId, updateFilters] ); - return (
diff --git a/web/core/components/issues/issue-layouts/list/default.tsx b/web/core/components/issues/issue-layouts/list/default.tsx index befa1f8fdbc..fca8d68eb25 100644 --- a/web/core/components/issues/issue-layouts/list/default.tsx +++ b/web/core/components/issues/issue-layouts/list/default.tsx @@ -49,7 +49,7 @@ export interface IList { isCompletedCycle?: boolean; loadMoreIssues: (groupId?: string) => void; handleCollapsedGroups: (value: string) => void; - collapsedGroups : TIssueKanbanFilters; + collapsedGroups: TIssueKanbanFilters; } export const List: React.FC = observer((props) => { @@ -71,7 +71,7 @@ export const List: React.FC = observer((props) => { isCompletedCycle = false, loadMoreIssues, handleCollapsedGroups, - collapsedGroups + collapsedGroups, } = props; const storeType = useIssueStoreType(); @@ -133,7 +133,6 @@ export const List: React.FC = observer((props) => { } else { entities = orderedGroups; } - return (
{groups && ( diff --git a/web/core/components/issues/issue-layouts/list/list-view-types.d.ts b/web/core/components/issues/issue-layouts/list/list-view-types.d.ts index 6597855f6f8..089623ed931 100644 --- a/web/core/components/issues/issue-layouts/list/list-view-types.d.ts +++ b/web/core/components/issues/issue-layouts/list/list-view-types.d.ts @@ -9,6 +9,7 @@ export interface IQuickActionProps { handleRemoveFromView?: () => Promise; handleArchive?: () => Promise; handleRestore?: () => Promise; + handleMoveToIssues?: () => Promise; customActionButton?: React.ReactElement; portalElement?: HTMLDivElement | null; readOnly?: boolean; diff --git a/web/core/components/issues/issue-layouts/quick-action-dropdowns/index.ts b/web/core/components/issues/issue-layouts/quick-action-dropdowns/index.ts index 212a43f91c3..d283cf3ec6d 100644 --- a/web/core/components/issues/issue-layouts/quick-action-dropdowns/index.ts +++ b/web/core/components/issues/issue-layouts/quick-action-dropdowns/index.ts @@ -4,3 +4,4 @@ export * from "./project-issue"; export * from "./archived-issue"; export * from "./draft-issue"; export * from "./all-issue"; +export * from "./workspace-draft"; diff --git a/web/core/components/issues/issue-layouts/quick-action-dropdowns/workspace-draft.tsx b/web/core/components/issues/issue-layouts/quick-action-dropdowns/workspace-draft.tsx new file mode 100644 index 00000000000..92755f07cc5 --- /dev/null +++ b/web/core/components/issues/issue-layouts/quick-action-dropdowns/workspace-draft.tsx @@ -0,0 +1,176 @@ +"use client"; + +import { useState } from "react"; +import omit from "lodash/omit"; +import { observer } from "mobx-react"; +// icons +import { Copy, Pencil, SquareStackIcon, Trash2 } from "lucide-react"; +// types +import { TIssue } from "@plane/types"; +// ui +import { ContextMenu, CustomMenu, TContextMenuItem } from "@plane/ui"; +// components +import { CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues"; +// constant +import { EIssuesStoreType } from "@/constants/issue"; +// helpers +import { cn } from "@/helpers/common.helper"; +// hooks +import { useEventTracker, useIssues, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; +// types +import { IQuickActionProps } from "../list/list-view-types"; + +export const WorkspaceDraftIssueQuickActions: React.FC = observer((props) => { + const { + issue, + handleDelete, + handleUpdate, + handleMoveToIssues, + customActionButton, + portalElement, + readOnly = false, + placements = "bottom-end", + parentRef, + } = props; + // states + const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false); + const [issueToEdit, setIssueToEdit] = useState(undefined); + const [deleteIssueModal, setDeleteIssueModal] = useState(false); + // store hooks + const { allowPermissions } = useUserPermissions(); + const { setTrackElement } = useEventTracker(); + const { issuesFilter } = useIssues(EIssuesStoreType.PROJECT); + // derived values + const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`; + // auth + const isEditingAllowed = + allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT) && !readOnly; + const isDeletingAllowed = isEditingAllowed; + + const duplicateIssuePayload = omit( + { + ...issue, + name: `${issue.name} (copy)`, + is_draft: true, + }, + ["id"] + ); + + const MENU_ITEMS: TContextMenuItem[] = [ + { + key: "edit", + title: "Edit", + icon: Pencil, + action: () => { + setTrackElement(activeLayout); + setIssueToEdit(issue); + setCreateUpdateIssueModal(true); + }, + shouldRender: isEditingAllowed, + }, + { + key: "make-a-copy", + title: "Make a copy", + icon: Copy, + action: () => { + setTrackElement(activeLayout); + setCreateUpdateIssueModal(true); + }, + shouldRender: isEditingAllowed, + }, + { + key: "move-to-issues", + title: "Move to issues", + icon: SquareStackIcon, + action: () => handleMoveToIssues && handleMoveToIssues(), + shouldRender: isEditingAllowed, + }, + { + key: "delete", + title: "Delete", + icon: Trash2, + action: () => { + setTrackElement(activeLayout); + setDeleteIssueModal(true); + }, + shouldRender: isDeletingAllowed, + }, + ]; + + // check if any of the menu items should render + const shouldRenderQuickAction = MENU_ITEMS.some((item) => item.shouldRender); + + if (!shouldRenderQuickAction) return <>; + + return ( + <> + setDeleteIssueModal(false)} + onSubmit={handleDelete} + /> + { + setCreateUpdateIssueModal(false); + setIssueToEdit(undefined); + }} + data={issueToEdit ?? duplicateIssuePayload} + onSubmit={async (data) => { + if (issueToEdit && handleUpdate) await handleUpdate(data); + }} + storeType={EIssuesStoreType.DRAFT} + isDraft + /> + + + {MENU_ITEMS.map((item) => { + if (item.shouldRender === false) return null; + return ( + { + e.preventDefault(); + e.stopPropagation(); + item.action(); + }} + className={cn( + "flex items-center gap-2", + { + "text-custom-text-400": item.disabled, + }, + item.className + )} + disabled={item.disabled} + > + {item.icon && } +
+
{item.title}
+ {item.description && ( +

+ {item.description} +

+ )} +
+
+ ); + })} +
+ + ); +}); diff --git a/web/core/hooks/use-issues-actions.tsx b/web/core/hooks/use-issues-actions.tsx index 453ee1adc3d..a4604a06152 100644 --- a/web/core/hooks/use-issues-actions.tsx +++ b/web/core/hooks/use-issues-actions.tsx @@ -748,7 +748,6 @@ const useWorkspaceDraftIssueActions = () => { const globalViewId = routerGlobalViewId?.toString(); // store hooks const { issues, issuesFilter } = useIssues(EIssuesStoreType.WORKSPACE_DRAFT); - const fetchIssues = useCallback( async (loadType: TLoader, options: IssuePaginationOptions) => { if (!workspaceSlug) return; diff --git a/web/core/store/issue/workspace-draft/issue.store.ts b/web/core/store/issue/workspace-draft/issue.store.ts index 6c38a88bfbb..c0bd3cfed44 100644 --- a/web/core/store/issue/workspace-draft/issue.store.ts +++ b/web/core/store/issue/workspace-draft/issue.store.ts @@ -68,6 +68,7 @@ export class WorkspaceDraftIssues extends BaseIssuesStore implements IWorkspaceD ) => { try { // set loader and clear store + console.log("fetchIssues"); runInAction(() => { this.setLoader(loadType); }); @@ -82,6 +83,8 @@ export class WorkspaceDraftIssues extends BaseIssuesStore implements IWorkspaceD // after fetching issues, call the base method to process the response further this.onfetchIssues(response, options, workspaceSlug, undefined, undefined, !isExistingPaginationOptions); + console.log("response", response); + return response; } catch (error) { // set loader to undefined if errored out From 623ada4a53b28b22b14b5d88352ecfa552611c82 Mon Sep 17 00:00:00 2001 From: gurusainath Date: Tue, 8 Oct 2024 20:34:40 +0530 Subject: [PATCH 06/21] conflicts: resolved merge conflicts --- apiserver/plane/app/serializers/draft.py | 25 +- apiserver/plane/app/views/workspace/draft.py | 1 - .../(projects)/drafts/page.tsx | 13 +- web/core/hooks/store/index.ts | 1 + .../services/issue/workspace_draft.service.ts | 36 +-- .../issue/workspace-draft/issue.store.ts | 306 +++++++++--------- 6 files changed, 205 insertions(+), 177 deletions(-) diff --git a/apiserver/plane/app/serializers/draft.py b/apiserver/plane/app/serializers/draft.py index 2128b927d22..8acdb638f86 100644 --- a/apiserver/plane/app/serializers/draft.py +++ b/apiserver/plane/app/serializers/draft.py @@ -46,7 +46,30 @@ class DraftIssueCreateSerializer(BaseSerializer): class Meta: model = DraftIssue - fields = "__all__" + fields = [ + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "created_at", + "updated_at", + "created_by", + "updated_by", + "state_id", + "label_ids", + "assignee_ids", + ] read_only_fields = [ "workspace", "created_by", diff --git a/apiserver/plane/app/views/workspace/draft.py b/apiserver/plane/app/views/workspace/draft.py index ad543a75635..a50a6b450ab 100644 --- a/apiserver/plane/app/views/workspace/draft.py +++ b/apiserver/plane/app/views/workspace/draft.py @@ -44,7 +44,6 @@ class WorkspaceDraftIssueViewSet(BaseViewSet): - model = DraftIssue @method_decorator(gzip_page) diff --git a/web/app/[workspaceSlug]/(projects)/drafts/page.tsx b/web/app/[workspaceSlug]/(projects)/drafts/page.tsx index c00e38400b7..f94fc872aeb 100644 --- a/web/app/[workspaceSlug]/(projects)/drafts/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/drafts/page.tsx @@ -1,17 +1,24 @@ "use client"; +import { useParams } from "next/navigation"; // components import { PageHead } from "@/components/core"; -import { WorkspaceDraftIssueLayoutRoot } from "@/components/issues/issue-layouts/filters/applied-filters/roots/workspace-draft-root"; +import { WorkspaceDraftIssuesRoot } from "@/components/issues/workspace-draft"; const WorkspaceDraftPage = () => { + // router + const { workspaceSlug: routeWorkspaceSlug } = useParams(); const pageTitle = "Workspace Draft"; + // derived values + const workspaceSlug = (routeWorkspaceSlug as string) || undefined; + + if (!workspaceSlug) return null; return ( <> -
- +
+
); diff --git a/web/core/hooks/store/index.ts b/web/core/hooks/store/index.ts index 1be070cb89b..dc786172421 100644 --- a/web/core/hooks/store/index.ts +++ b/web/core/hooks/store/index.ts @@ -31,3 +31,4 @@ export * from "./use-webhook"; export * from "./use-workspace"; export * from "./user"; export * from "./use-transient"; +export * from "./workspace-draft"; diff --git a/web/core/services/issue/workspace_draft.service.ts b/web/core/services/issue/workspace_draft.service.ts index 56a8ffaa111..eff58b13a13 100644 --- a/web/core/services/issue/workspace_draft.service.ts +++ b/web/core/services/issue/workspace_draft.service.ts @@ -1,46 +1,40 @@ import { TIssue, TIssuesResponse } from "@plane/types"; +// helpers import { API_BASE_URL } from "@/helpers/common.helper"; -import { APIService } from "../api.service"; +// services +import { APIService } from "@/services/api.service"; export class WorkspaceDraftService extends APIService { constructor() { super(API_BASE_URL); } - async getIssues(workspaceSlug: string, query?: any, config = {}): Promise { - return this.get( - `/api/workspaces/${workspaceSlug}/draft-issues/`, - { - params: { ...query }, - }, - config - ) + async getIssues(workspaceSlug: string, query: object = {}): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/draft-issues/`, { params: { ...query } }) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; }); } - async getIssueById(workspaceSlug: string, issueId: string, queries?: any): Promise { - return this.get(`/api/workspaces/${workspaceSlug}/draft-issues/${issueId}/`, { - params: queries, - }) + async getIssueById(workspaceSlug: string, issueId: string): Promise { + return this.get(`/api/workspaces/${workspaceSlug}/draft-issues/${issueId}/`) .then((response) => response?.data) .catch((error) => { throw error?.response; }); } - async createIssue(workspaceSlug: string, data: any): Promise { - return this.post(`/api/workspaces/${workspaceSlug}/draft-issues/`, data) + async createIssue(workspaceSlug: string, payload: Partial): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/draft-issues/`, payload) .then((response) => response?.data) .catch((error) => { throw error?.response; }); } - async updateIssue(workspaceSlug: string, issueId: string, data: any): Promise { - return this.patch(`/api/workspaces/${workspaceSlug}/draft-issues/${issueId}/`, data) + async updateIssue(workspaceSlug: string, issueId: string, payload: Partial): Promise { + return this.patch(`/api/workspaces/${workspaceSlug}/draft-issues/${issueId}/`, payload) .then((response) => response?.data) .catch((error) => { throw error?.response; @@ -55,11 +49,15 @@ export class WorkspaceDraftService extends APIService { }); } - async moveToIssues(workspaceSlug: string, issueId: string, data: Partial): Promise { - return this.post(`/api/workspaces/${workspaceSlug}/draft-to-issue/${issueId}/`, data) + async moveIssue(workspaceSlug: string, issueId: string, payload: Partial): Promise { + return this.post(`/api/workspaces/${workspaceSlug}/draft-to-issue/${issueId}/`, payload) .then((response) => response?.data) .catch((error) => { throw error?.response; }); } } + +const workspaceDraftService = new WorkspaceDraftService(); + +export default workspaceDraftService; diff --git a/web/core/store/issue/workspace-draft/issue.store.ts b/web/core/store/issue/workspace-draft/issue.store.ts index c0bd3cfed44..344adb44bad 100644 --- a/web/core/store/issue/workspace-draft/issue.store.ts +++ b/web/core/store/issue/workspace-draft/issue.store.ts @@ -1,202 +1,202 @@ -import { clone } from "lodash"; -import { action, makeObservable, runInAction } from "mobx"; -// base class -import { IssuePaginationOptions, TIssue, TIssuesResponse, TLoader, ViewFlags } from "@plane/types"; +import clone from "lodash/clone"; +import set from "lodash/set"; +import unset from "lodash/unset"; +import update from "lodash/update"; +import { action, makeObservable, observable, runInAction } from "mobx"; +import { computedFn } from "mobx-utils"; +import { IssuePaginationOptions, TIssue, TIssuesResponse } from "@plane/types"; +// helpers +import { getCurrentDateTimeInISO } from "@/helpers/date-time.helper"; // services -import { WorkspaceDraftService } from "@/services/issue"; +import workspaceDraftService from "@/services/issue/workspace_draft.service"; // types -import { BaseIssuesStore, IBaseIssuesStore } from "../helpers/base-issues.store"; import { IIssueRootStore } from "../root.store"; -import { IWorkspaceDraftIssuesFilter } from "./filter.store"; -export interface IWorkspaceDraftIssues extends IBaseIssuesStore { - // observable - viewFlags: ViewFlags; +export type TLoader = + | "init-loader" + | "mutation" + | "pagination" + | "loaded" + | "create" + | "update" + | "delete" + | "move" + | undefined; + +export type TPaginationInfo = { + next_cursor: string | undefined; + prev_cursor: string | undefined; + next_page_results: boolean | undefined; + prev_page_results: boolean | undefined; + total_pages: number | undefined; + extra_stats: string | undefined; + count: number | undefined; // current paginated results count + total_count: number | undefined; // total available results count + results: T[] | undefined; + grouped_by: string | undefined; + sub_grouped_by: string | undefined; +}; + +export interface IWorkspaceDraftIssues { + // observables + issuesMap: Record; + paginationInfo: Omit, "results"> | undefined; + loader: TLoader; + // computed actions + getIssueById: (issueId: string) => TIssue | undefined; + // helper actions + addIssue: (issues: TIssue[]) => void; + mutateIssue: (issueId: string, data: Partial) => void; + removeIssue: (issueId: string) => void; // actions - fetchIssues: ( - workspaceSlug: string, - loadType: TLoader, - options: IssuePaginationOptions - ) => Promise; - fetchIssuesWithExistingPagination: (workspaceSlug: string, loadType: TLoader) => Promise; - fetchNextIssues: ( - workspaceSlug: string, - groupId?: string, - subGroupId?: string - ) => Promise; - - createWorkspaceDraftIssue: (workspaceSlug: string, data: Partial) => Promise; - updateWorkspaceDraftIssue: (workspaceSlug: string, issueId: string, data: Partial) => Promise; - deleteWorkspaceDraftIssue: (workspaceSlug: string, issueId: string) => Promise; - moveToIssues: (workspaceSlug: string, issueId: string, data: Partial) => Promise; + fetchIssues: (workspaceSlug: string, loadType: TLoader) => Promise; + createIssue: (workspaceSlug: string, payload: Partial) => Promise; + updateIssue: (workspaceSlug: string, issueId: string, payload: Partial) => Promise; + deleteIssue: (workspaceSlug: string, issueId: string) => Promise; + moveIssue: (workspaceSlug: string, issueId: string, payload: Partial) => Promise; } -export class WorkspaceDraftIssues extends BaseIssuesStore implements IWorkspaceDraftIssues { - viewFlags = { - enableQuickAdd: true, - enableIssueCreation: true, - enableInlineEditing: true, - }; - // service - workspaceDraftService; - // filterStore - issueFilterStore; - - constructor(_rootStore: IIssueRootStore, issueFilterStore: IWorkspaceDraftIssuesFilter) { - super(_rootStore, issueFilterStore); +export class WorkspaceDraftIssues implements IWorkspaceDraftIssues { + // local constants + paginatedCount = 100; + // observables + paginationInfo: Omit, "results"> | undefined = undefined; + loader: TLoader = undefined; + issuesMap: Record = {}; + constructor(private store: IIssueRootStore) { makeObservable(this, { + issuesMap: observable, + loader: observable.ref, // action fetchIssues: action, - fetchNextIssues: action, - fetchIssuesWithExistingPagination: action, - createWorkspaceDraftIssue: action, - updateWorkspaceDraftIssue: action, - deleteWorkspaceDraftIssue: action, + createIssue: action, + updateIssue: action, + deleteIssue: action, + moveIssue: action, }); - // services - this.workspaceDraftService = new WorkspaceDraftService(); - // filter store - this.issueFilterStore = issueFilterStore; } - fetchIssues = async ( - workspaceSlug: string, - loadType: TLoader, - options: IssuePaginationOptions, - isExistingPaginationOptions: boolean = false - ) => { - try { - // set loader and clear store - console.log("fetchIssues"); - runInAction(() => { - this.setLoader(loadType); + // helper actions + addIssue = (issues: TIssue[]) => { + if (issues && issues.length <= 0) return; + runInAction(() => { + issues.forEach((issue) => { + if (!this.issuesMap[issue.id]) set(this.issuesMap, issue.id, issue); + else update(this.issuesMap, issue.id, (prevIssue) => ({ ...prevIssue, ...issue })); }); - this.clear(!isExistingPaginationOptions); + }); + }; + + getIssueById = computedFn((issueId: string) => { + if (!issueId || !this.issuesMap[issueId]) return undefined; + return this.issuesMap[issueId]; + }); + + mutateIssue = (issueId: string, issue: Partial) => { + if (!issue || !issueId || !this.issuesMap[issueId]) return; + runInAction(() => { + set(this.issuesMap, [issueId, "updated_at"], getCurrentDateTimeInISO()); + Object.keys(issue).forEach((key) => { + set(this.issuesMap, [issueId, key], issue[key as keyof TIssue]); + }); + }); + }; + + removeIssue = (issueId: string) => { + if (!issueId || !this.issuesMap[issueId]) return; + runInAction(() => unset(this.issuesMap, issueId)); + }; + + // actions + fetchIssues = async (workspaceSlug: string, loadType: TLoader) => { + try { + this.loader = loadType; // get params from pagination options - const params = this.issueFilterStore?.getFilterParams(options, workspaceSlug, undefined, undefined, undefined); + // const params = this.issueFilterStore?.getFilterParams(options, workspaceSlug, undefined, undefined, undefined); + const params = {}; + // call the fetch issues API with the params - const response = await this.workspaceDraftService.getIssues(workspaceSlug, params, { - signal: this.controller.signal, - }); + const response = await workspaceDraftService.getIssues(workspaceSlug, params); - // after fetching issues, call the base method to process the response further - this.onfetchIssues(response, options, workspaceSlug, undefined, undefined, !isExistingPaginationOptions); console.log("response", response); return response; } catch (error) { // set loader to undefined if errored out - this.setLoader(undefined); + this.loader = 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 viewId - * @param groupId - * @param subGroupId - * @returns - */ - fetchNextIssues = async (workspaceSlug: 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; + createIssue = async (workspaceSlug: string, payload: Partial): Promise => { try { - // set Loader - this.setLoader("pagination", groupId, subGroupId); - - // get params from stored pagination options - const params = this.issueFilterStore?.getFilterParams( - this.paginationOptions, - workspaceSlug, - this.getNextCursor(groupId, subGroupId), - groupId, - subGroupId - ); - // call the fetch issues API with the params for next page in issues - const response = await this.workspaceDraftService.getIssues(workspaceSlug, params); - - // after the next page of issues are fetched, call the base method to process the response - this.onfetchNexIssues(response, groupId, subGroupId); + this.loader = "create"; + + const response = await workspaceDraftService.createIssue(workspaceSlug, payload); + if (response) { + runInAction(() => { + if (!this.issuesMap[response.id]) set(this.issuesMap, response.id, response); + else update(this.issuesMap, response.id, (prevIssue) => ({ ...prevIssue, ...response })); + }); + } + + this.loader = undefined; return response; } catch (error) { - // set Loader as undefined if errored out - this.setLoader(undefined, groupId, subGroupId); + this.loader = undefined; 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 viewId - * @param loadType - * @returns - */ - fetchIssuesWithExistingPagination = async (workspaceSlug: string, loadType: TLoader) => { - if (!this.paginationOptions) return; - return await this.fetchIssues(workspaceSlug, loadType, this.paginationOptions, true); - }; + updateIssue = async (workspaceSlug: string, issueId: string, payload: Partial) => { + try { + this.loader = "create"; + + const response = await workspaceDraftService.updateIssue(workspaceSlug, issueId, payload); + if (response) { + runInAction(() => { + if (!this.issuesMap[response.id]) set(this.issuesMap, response.id, response); + else update(this.issuesMap, response.id, (prevIssue) => ({ ...prevIssue, ...response })); + }); + } - createWorkspaceDraftIssue = async (workspaceSlug: string, data: Partial) => { - const response = await this.workspaceDraftService.createIssue(workspaceSlug, data); - this.addIssue(response); - return response; + this.loader = undefined; + return response; + } catch (error) { + this.loader = undefined; + throw error; + } }; - updateWorkspaceDraftIssue = async (workspaceSlug: string, issueId: string, data: Partial) => { - // Store Before state of the issue - const issueBeforeUpdate = clone(this.rootIssueStore.issues.getIssueById(issueId)); + deleteIssue = async (workspaceSlug: string, issueId: string) => { try { - // Update the Respective Stores - this.rootIssueStore.issues.updateIssue(issueId, data); - this.updateIssueList({ ...issueBeforeUpdate, ...data } as TIssue, issueBeforeUpdate); + this.loader = "delete"; + + const response = await workspaceDraftService.deleteIssue(workspaceSlug, issueId); + runInAction(() => {}); - // call API to update the issue - await this.workspaceDraftService.updateIssue(workspaceSlug, issueId, data); + this.loader = undefined; + return response; } 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); + this.loader = undefined; throw error; } }; - deleteWorkspaceDraftIssue = async (workspaceSlug: string, issueId: string) => { - // Male API call - await this.workspaceDraftService.deleteIssue(workspaceSlug, issueId); - // Remove from Respective issue Id list - runInAction(() => { - this.removeIssueFromList(issueId); - }); - // Remove issue from main issue Map store - this.rootIssueStore.issues.removeIssue(issueId); - }; + moveIssue = async (workspaceSlug: string, issueId: string, payload: Partial) => { + try { + this.loader = "move"; - moveToIssues = async (workspaceSlug: string, issueId: string, data: Partial) => { - // Make API call - await this.workspaceDraftService.moveToIssues(workspaceSlug, issueId, data); - // Remove from Respective issue Id list - runInAction(() => { - this.removeIssueFromList(issueId); - }); - // Remove issue from main issue Map store - this.rootIssueStore.issues.removeIssue(issueId); - }; + const response = await workspaceDraftService.moveIssue(workspaceSlug, issueId, payload); + runInAction(() => {}); - fetchParentStats = (workspaceSlug: string, projectId?: string, id?: string) => { - // Implement the method logic here - console.log(`Fetching parent stats for workspace: ${workspaceSlug}, project: ${projectId}, id: ${id}`); - }; - updateParentStats = (prevIssueState?: TIssue, nextIssueState?: TIssue, id?: string) => { - // Implement the method logic here - console.log(`Updating parent stats for issue: ${id}`); + this.loader = undefined; + return response; + } catch (error) { + this.loader = undefined; + throw error; + } }; } From 4520959cba7b64bf145e690dce206f85e5fb8de4 Mon Sep 17 00:00:00 2001 From: gurusainath Date: Tue, 8 Oct 2024 21:25:38 +0530 Subject: [PATCH 07/21] conflicts: handled draft issue store --- .../issues/workspace-draft/index.ts | 1 + .../issues/workspace-draft/root.tsx | 26 +++++++++++++++ web/core/hooks/store/workspace-draft/index.ts | 2 ++ .../use-workspace-draft-issue-filters.ts | 12 +++++++ .../use-workspace-draft-issue.ts | 12 +++++++ .../issue/workspace-draft/issue.store.ts | 32 ++++++++++++++++++- 6 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 web/core/components/issues/workspace-draft/index.ts create mode 100644 web/core/components/issues/workspace-draft/root.tsx create mode 100644 web/core/hooks/store/workspace-draft/index.ts create mode 100644 web/core/hooks/store/workspace-draft/use-workspace-draft-issue-filters.ts create mode 100644 web/core/hooks/store/workspace-draft/use-workspace-draft-issue.ts diff --git a/web/core/components/issues/workspace-draft/index.ts b/web/core/components/issues/workspace-draft/index.ts new file mode 100644 index 00000000000..1efe34c51ec --- /dev/null +++ b/web/core/components/issues/workspace-draft/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/web/core/components/issues/workspace-draft/root.tsx b/web/core/components/issues/workspace-draft/root.tsx new file mode 100644 index 00000000000..24e905adc49 --- /dev/null +++ b/web/core/components/issues/workspace-draft/root.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { FC } from "react"; +import { observer } from "mobx-react"; +import useSWR from "swr"; +// hooks +import { useWorkspaceDraftIssues } from "@/hooks/store"; + +type TWorkspaceDraftIssuesRoot = { + workspaceSlug: string; +}; + +export const WorkspaceDraftIssuesRoot: FC = observer((props) => { + const { workspaceSlug } = props; + // hooks + const { fetchIssues } = useWorkspaceDraftIssues(); + + useSWR( + workspaceSlug ? `WORKSPACE_DRAFT_ISSUES_${fetchIssues}` : null, + async () => await fetchIssues(workspaceSlug, "init-loader") + ); + + console.log("workspaceSlug", workspaceSlug); + + return
WorkspaceDraftIssueRoot
; +}); diff --git a/web/core/hooks/store/workspace-draft/index.ts b/web/core/hooks/store/workspace-draft/index.ts new file mode 100644 index 00000000000..59f8e2d9650 --- /dev/null +++ b/web/core/hooks/store/workspace-draft/index.ts @@ -0,0 +1,2 @@ +export * from "./use-workspace-draft-issue"; +export * from "./use-workspace-draft-issue-filters"; diff --git a/web/core/hooks/store/workspace-draft/use-workspace-draft-issue-filters.ts b/web/core/hooks/store/workspace-draft/use-workspace-draft-issue-filters.ts new file mode 100644 index 00000000000..8898b38a803 --- /dev/null +++ b/web/core/hooks/store/workspace-draft/use-workspace-draft-issue-filters.ts @@ -0,0 +1,12 @@ +import { useContext } from "react"; +// mobx store +import { StoreContext } from "@/lib/store-context"; +// types +import { IWorkspaceDraftIssues } from "@/store/issue/workspace-draft"; + +export const useWorkspaceDraftIssueFilters = (): IWorkspaceDraftIssues => { + const context = useContext(StoreContext); + if (context === undefined) throw new Error("useWorkspaceDraftIssueFilters must be used within StoreProvider"); + + return context.issue.workspaceDraftIssues; +}; diff --git a/web/core/hooks/store/workspace-draft/use-workspace-draft-issue.ts b/web/core/hooks/store/workspace-draft/use-workspace-draft-issue.ts new file mode 100644 index 00000000000..095c1e00e41 --- /dev/null +++ b/web/core/hooks/store/workspace-draft/use-workspace-draft-issue.ts @@ -0,0 +1,12 @@ +import { useContext } from "react"; +// mobx store +import { StoreContext } from "@/lib/store-context"; +// types +import { IWorkspaceDraftIssues } from "@/store/issue/workspace-draft"; + +export const useWorkspaceDraftIssues = (): IWorkspaceDraftIssues => { + const context = useContext(StoreContext); + if (context === undefined) throw new Error("useWorkspaceDraftIssues must be used within StoreProvider"); + + return context.issue.workspaceDraftIssues; +}; diff --git a/web/core/store/issue/workspace-draft/issue.store.ts b/web/core/store/issue/workspace-draft/issue.store.ts index 344adb44bad..bf70ba6468d 100644 --- a/web/core/store/issue/workspace-draft/issue.store.ts +++ b/web/core/store/issue/workspace-draft/issue.store.ts @@ -4,7 +4,7 @@ import unset from "lodash/unset"; import update from "lodash/update"; import { action, makeObservable, observable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; -import { IssuePaginationOptions, TIssue, TIssuesResponse } from "@plane/types"; +import { TIssue, TIssuesResponse } from "@plane/types"; // helpers import { getCurrentDateTimeInISO } from "@/helpers/date-time.helper"; // services @@ -23,6 +23,19 @@ export type TLoader = | "move" | undefined; +export enum EDraftIssuePaginationType { + INIT = "INIT", + NEXT = "NEXT", + PREV = "PREV", + CURRENT = "CURRENT", +} + +export type TDraftIssuePaginationType = EDraftIssuePaginationType; + +export type TNotificationQueryParams = { + cursor: string; +}; + export type TPaginationInfo = { next_cursor: string | undefined; prev_cursor: string | undefined; @@ -108,6 +121,23 @@ export class WorkspaceDraftIssues implements IWorkspaceDraftIssues { runInAction(() => unset(this.issuesMap, issueId)); }; + generateNotificationQueryParams = (paramType: TDraftIssuePaginationType): TNotificationQueryParams => { + const queryCursorNext: string = + paramType === EDraftIssuePaginationType.INIT + ? `${this.paginatedCount}:0:0` + : paramType === EDraftIssuePaginationType.CURRENT + ? `${this.paginatedCount}:${0}:0` + : paramType === EDraftIssuePaginationType.NEXT && this.paginationInfo + ? (this.paginationInfo?.next_cursor ?? `${this.paginatedCount}:${0}:0`) + : `${this.paginatedCount}:${0}:0`; + + const queryParams: TNotificationQueryParams = { + cursor: queryCursorNext, + }; + + return queryParams; + }; + // actions fetchIssues = async (workspaceSlug: string, loadType: TLoader) => { try { From ff4b2c1f9a6fb3fd2801bd6fc40a39de947f383e Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Tue, 8 Oct 2024 21:43:00 +0530 Subject: [PATCH 08/21] chore: draft issue modal --- .../(projects)/drafts/header.tsx | 7 +------ .../components/issues/issue-modal/base.tsx | 20 +++++++++++++------ 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/web/app/[workspaceSlug]/(projects)/drafts/header.tsx b/web/app/[workspaceSlug]/(projects)/drafts/header.tsx index c4c27e7b040..514a26bcac6 100644 --- a/web/app/[workspaceSlug]/(projects)/drafts/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/drafts/header.tsx @@ -28,12 +28,7 @@ export const WorkspaceDraftHeader: FC = observer(() => { return ( <> - setIsDraftIssueModalOpen(false)} - storeType={EIssuesStoreType.WORKSPACE_DRAFT} - isDraft - /> + setIsDraftIssueModalOpen(false)} isDraft />
diff --git a/web/core/components/issues/issue-modal/base.tsx b/web/core/components/issues/issue-modal/base.tsx index 0d1763011c4..b86b0ccbad2 100644 --- a/web/core/components/issues/issue-modal/base.tsx +++ b/web/core/components/issues/issue-modal/base.tsx @@ -13,10 +13,19 @@ import { ISSUE_CREATED, ISSUE_UPDATED } from "@/constants/event-tracker"; import { EIssuesStoreType } from "@/constants/issue"; // hooks import { useIssueModal } from "@/hooks/context/use-issue-modal"; -import { useEventTracker, useCycle, useIssues, useModule, useIssueDetail, useUser } from "@/hooks/store"; +import { + useEventTracker, + useCycle, + useIssues, + useModule, + useIssueDetail, + useUser, + useWorkspaceDraftIssues, +} 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"; +import workspaceDraftService from "@/services/issue/workspace_draft.service"; // local components import { DraftIssueLayout } from "./draft-issue-layout"; import { IssueFormRoot } from "./form"; @@ -50,7 +59,7 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( const { fetchModuleDetails } = useModule(); const { issues } = useIssues(storeType); const { issues: projectIssues } = useIssues(EIssuesStoreType.PROJECT); - const { issues: draftIssues } = useIssues(EIssuesStoreType.DRAFT); + const { createIssue: createDraftIssue, updateIssue: updateDraftIssue } = useWorkspaceDraftIssues(); const { fetchIssue } = useIssueDetail(); const { handleCreateUpdatePropertyValues } = useIssueModal(); // pathname @@ -70,7 +79,7 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( if (!workspaceSlug) return; if (!projectId || issueId === undefined || !fetchIssueDetails) { - // Set description to the issue description from the props if available + // Set description to the issue description from the props if available setDescription(data?.description_html || "

"); return; } @@ -151,10 +160,9 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( try { let response; - // if draft issue, use draft issue store to create issue if (is_draft_issue) { - response = await draftIssues.createIssue(workspaceSlug.toString(), payload.project_id, payload); + response = await createDraftIssue(workspaceSlug.toString(), payload); } // if cycle id in payload does not match the cycleId in url // or if the moduleIds in Payload does not match the moduleId in url @@ -238,7 +246,7 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( try { isDraft - ? await draftIssues.updateIssue(workspaceSlug.toString(), payload.project_id, data.id, payload) + ? await updateDraftIssue(workspaceSlug.toString(), data.id, payload) : updateIssue && (await updateIssue(payload.project_id, data.id, payload)); // add other property values From 90a0fcaafd8a710d5ed7924198914be221fd847c Mon Sep 17 00:00:00 2001 From: NarayanBavisetti Date: Wed, 9 Oct 2024 10:13:49 +0530 Subject: [PATCH 09/21] chore: code optimisation --- apiserver/plane/app/serializers/draft.py | 25 +-- apiserver/plane/app/views/workspace/draft.py | 171 +++++-------------- 2 files changed, 47 insertions(+), 149 deletions(-) diff --git a/apiserver/plane/app/serializers/draft.py b/apiserver/plane/app/serializers/draft.py index 8acdb638f86..2128b927d22 100644 --- a/apiserver/plane/app/serializers/draft.py +++ b/apiserver/plane/app/serializers/draft.py @@ -46,30 +46,7 @@ class DraftIssueCreateSerializer(BaseSerializer): class Meta: model = DraftIssue - fields = [ - "id", - "name", - "state_id", - "sort_order", - "completed_at", - "estimate_point", - "priority", - "start_date", - "target_date", - "project_id", - "parent_id", - "cycle_id", - "module_ids", - "label_ids", - "assignee_ids", - "created_at", - "updated_at", - "created_by", - "updated_by", - "state_id", - "label_ids", - "assignee_ids", - ] + fields = "__all__" read_only_fields = [ "workspace", "created_by", diff --git a/apiserver/plane/app/views/workspace/draft.py b/apiserver/plane/app/views/workspace/draft.py index a50a6b450ab..5bd8c2dfae7 100644 --- a/apiserver/plane/app/views/workspace/draft.py +++ b/apiserver/plane/app/views/workspace/draft.py @@ -46,15 +46,9 @@ class WorkspaceDraftIssueViewSet(BaseViewSet): model = DraftIssue - @method_decorator(gzip_page) - @allow_permission( - allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE" - ) - def list(self, request, slug): - filters = issue_filters(request.query_params, "GET") - issues = ( - DraftIssue.objects.filter(workspace__slug=slug) - .filter(created_by=request.user) + def get_queryset(self): + return ( + DraftIssue.objects.filter(workspace__slug=self.kwargs.get("slug")) .select_related("workspace", "project", "state", "parent") .prefetch_related( "assignees", "labels", "draft_issue_module__module" @@ -90,6 +84,17 @@ def list(self, request, slug): Value([], output_field=ArrayField(UUIDField())), ), ) + ).distinct() + + @method_decorator(gzip_page) + @allow_permission( + allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE" + ) + def list(self, request, slug): + filters = issue_filters(request.query_params, "GET") + issues = ( + self.get_queryset() + .filter(created_by=request.user) .order_by("-created_at") ) @@ -119,7 +124,34 @@ def create(self, request, slug): ) if serializer.is_valid(): serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) + issue = ( + self.get_queryset() + .filter(pk=serializer.data.get("id")) + .values( + "id", + "name", + "state_id", + "sort_order", + "completed_at", + "estimate_point", + "priority", + "start_date", + "target_date", + "project_id", + "parent_id", + "cycle_id", + "module_ids", + "label_ids", + "assignee_ids", + "created_at", + "updated_at", + "created_by", + "updated_by", + ) + .first() + ) + + return Response(issue, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) @allow_permission( @@ -130,45 +162,7 @@ def create(self, request, slug): ) def partial_update(self, request, slug, pk): issue = ( - DraftIssue.objects.filter(workspace__slug=slug) - .filter(pk=pk) - .filter(created_by=request.user) - .select_related("workspace", "project", "state", "parent") - .prefetch_related( - "assignees", "labels", "draft_issue_module__module" - ) - .annotate(cycle_id=F("draft_issue_cycle__cycle_id")) - .annotate( - label_ids=Coalesce( - ArrayAgg( - "labels__id", - distinct=True, - filter=~Q(labels__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - assignee_ids=Coalesce( - ArrayAgg( - "assignees__id", - distinct=True, - filter=~Q(assignees__id__isnull=True) - & Q(assignees__member_project__is_active=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - module_ids=Coalesce( - ArrayAgg( - "draft_issue_module__module_id", - distinct=True, - filter=~Q(draft_issue_module__module_id__isnull=True) - & Q( - draft_issue_module__module__archived_at__isnull=True - ), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - ) - .first() + self.get_queryset().filter(pk=pk, created_by=request.user).first() ) if not issue: @@ -201,46 +195,8 @@ def partial_update(self, request, slug, pk): ) def retrieve(self, request, slug, pk=None): issue = ( - DraftIssue.objects.filter(workspace__slug=slug) - .filter(pk=pk) - .filter(created_by=request.user) - .select_related("workspace", "project", "state", "parent") - .prefetch_related( - "assignees", "labels", "draft_issue_module__module" - ) - .annotate(cycle_id=F("draft_issue_cycle__cycle_id")) - .filter(pk=pk) - .annotate( - label_ids=Coalesce( - ArrayAgg( - "labels__id", - distinct=True, - filter=~Q(labels__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - assignee_ids=Coalesce( - ArrayAgg( - "assignees__id", - distinct=True, - filter=~Q(assignees__id__isnull=True) - & Q(assignees__member_project__is_active=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - module_ids=Coalesce( - ArrayAgg( - "draft_issue_module__module_id", - distinct=True, - filter=~Q(draft_issue_module__module_id__isnull=True) - & Q( - draft_issue_module__module__archived_at__isnull=True - ), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - ) - ).first() + self.get_queryset().filter(pk=pk, created_by=request.user).first() + ) if not issue: return Response( @@ -267,42 +223,7 @@ def destroy(self, request, slug, pk=None): level="WORKSPACE", ) def create_draft_to_issue(self, request, slug, draft_id): - draft_issue = ( - DraftIssue.objects.filter(workspace__slug=slug, pk=draft_id) - .annotate(cycle_id=F("draft_issue_cycle__cycle_id")) - .annotate( - label_ids=Coalesce( - ArrayAgg( - "labels__id", - distinct=True, - filter=~Q(labels__id__isnull=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - assignee_ids=Coalesce( - ArrayAgg( - "assignees__id", - distinct=True, - filter=~Q(assignees__id__isnull=True) - & Q(assignees__member_project__is_active=True), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - module_ids=Coalesce( - ArrayAgg( - "draft_issue_module__module_id", - distinct=True, - filter=~Q(draft_issue_module__module_id__isnull=True) - & Q( - draft_issue_module__module__archived_at__isnull=True - ), - ), - Value([], output_field=ArrayField(UUIDField())), - ), - ) - .select_related("project", "workspace") - .first() - ) + draft_issue = self.get_queryset().filter(pk=draft_id).first() if not draft_issue.project_id: return Response( From d4046711edac2a7d8b125e00a69f98f52a097694 Mon Sep 17 00:00:00 2001 From: gurusainath Date: Wed, 9 Oct 2024 13:27:32 +0530 Subject: [PATCH 10/21] chore: ui changes --- web/app/[workspaceSlug]/(projects)/drafts/header.tsx | 2 -- .../issues/issue-layouts/properties/all-properties.tsx | 4 +--- web/core/components/issues/workspace-draft/root.tsx | 10 +++++++++- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/web/app/[workspaceSlug]/(projects)/drafts/header.tsx b/web/app/[workspaceSlug]/(projects)/drafts/header.tsx index 514a26bcac6..3bfb1b6b31d 100644 --- a/web/app/[workspaceSlug]/(projects)/drafts/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/drafts/header.tsx @@ -9,8 +9,6 @@ import { Breadcrumbs, Button, Header } from "@plane/ui"; // components import { BreadcrumbLink } from "@/components/common"; import { CreateUpdateIssueModal } from "@/components/issues"; -// constants -import { EIssuesStoreType } from "@/constants/issue"; // hooks import { useUserPermissions } from "@/hooks/store"; diff --git a/web/core/components/issues/issue-layouts/properties/all-properties.tsx b/web/core/components/issues/issue-layouts/properties/all-properties.tsx index e439635feeb..36bb6fafccb 100644 --- a/web/core/components/issues/issue-layouts/properties/all-properties.tsx +++ b/web/core/components/issues/issue-layouts/properties/all-properties.tsx @@ -37,9 +37,7 @@ import { WithDisplayPropertiesHOC } from "./with-display-properties-HOC"; export interface IIssueProperties { issue: TIssue; - updateIssue: - | ((projectId: string | null, issueId: string, data: Partial) => Promise) - | undefined; + updateIssue: ((projectId: string | null, issueId: string, data: Partial) => Promise) | undefined; displayProperties: IIssueDisplayProperties | undefined; isReadOnly: boolean; className: string; diff --git a/web/core/components/issues/workspace-draft/root.tsx b/web/core/components/issues/workspace-draft/root.tsx index 24e905adc49..69690657910 100644 --- a/web/core/components/issues/workspace-draft/root.tsx +++ b/web/core/components/issues/workspace-draft/root.tsx @@ -3,6 +3,7 @@ import { FC } from "react"; import { observer } from "mobx-react"; import useSWR from "swr"; +import { Button } from "@plane/ui"; // hooks import { useWorkspaceDraftIssues } from "@/hooks/store"; @@ -22,5 +23,12 @@ export const WorkspaceDraftIssuesRoot: FC = observer( console.log("workspaceSlug", workspaceSlug); - return
WorkspaceDraftIssueRoot
; + return ( +
+
WorkspaceDraftIssueRoot
+
+ +
+
+ ); }); From 131519aae2a8c2a881883bae5ac52c6d570b8498 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Wed, 9 Oct 2024 17:31:59 +0530 Subject: [PATCH 11/21] chore: workspace draft store and modal updated --- .../(projects)/drafts/header.tsx | 9 +++- .../components/issues/issue-modal/base.tsx | 29 ++++------ .../issues/issue-modal/draft-issue-layout.tsx | 9 ++-- web/core/hooks/store/use-issues.ts | 5 ++ .../issue/workspace-draft/issue.store.ts | 54 +++++++++++++++++-- 5 files changed, 76 insertions(+), 30 deletions(-) diff --git a/web/app/[workspaceSlug]/(projects)/drafts/header.tsx b/web/app/[workspaceSlug]/(projects)/drafts/header.tsx index 3bfb1b6b31d..edc6693bd77 100644 --- a/web/app/[workspaceSlug]/(projects)/drafts/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/drafts/header.tsx @@ -9,6 +9,8 @@ import { Breadcrumbs, Button, Header } from "@plane/ui"; // components import { BreadcrumbLink } from "@/components/common"; import { CreateUpdateIssueModal } from "@/components/issues"; +// constants +import { EIssuesStoreType } from "@/constants/issue"; // hooks import { useUserPermissions } from "@/hooks/store"; @@ -26,7 +28,12 @@ export const WorkspaceDraftHeader: FC = observer(() => { return ( <> - setIsDraftIssueModalOpen(false)} isDraft /> + setIsDraftIssueModalOpen(false)} + isDraft + />
diff --git a/web/core/components/issues/issue-modal/base.tsx b/web/core/components/issues/issue-modal/base.tsx index b86b0ccbad2..d6ccf590921 100644 --- a/web/core/components/issues/issue-modal/base.tsx +++ b/web/core/components/issues/issue-modal/base.tsx @@ -13,19 +13,10 @@ import { ISSUE_CREATED, ISSUE_UPDATED } from "@/constants/event-tracker"; import { EIssuesStoreType } from "@/constants/issue"; // hooks import { useIssueModal } from "@/hooks/context/use-issue-modal"; -import { - useEventTracker, - useCycle, - useIssues, - useModule, - useIssueDetail, - useUser, - useWorkspaceDraftIssues, -} from "@/hooks/store"; +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"; -import workspaceDraftService from "@/services/issue/workspace_draft.service"; // local components import { DraftIssueLayout } from "./draft-issue-layout"; import { IssueFormRoot } from "./form"; @@ -59,7 +50,7 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( const { fetchModuleDetails } = useModule(); const { issues } = useIssues(storeType); const { issues: projectIssues } = useIssues(EIssuesStoreType.PROJECT); - const { createIssue: createDraftIssue, updateIssue: updateDraftIssue } = useWorkspaceDraftIssues(); + const { issues: draftIssues } = useIssues(EIssuesStoreType.WORKSPACE_DRAFT); const { fetchIssue } = useIssueDetail(); const { handleCreateUpdatePropertyValues } = useIssueModal(); // pathname @@ -162,7 +153,7 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( let response; // if draft issue, use draft issue store to create issue if (is_draft_issue) { - response = await createDraftIssue(workspaceSlug.toString(), payload); + response = await draftIssues.createIssue(workspaceSlug.toString(), payload); } // if cycle id in payload does not match the cycleId in url // or if the moduleIds in Payload does not match the moduleId in url @@ -221,8 +212,8 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( payload: { ...response, state: "SUCCESS" }, path: pathname, }); - !createMore && handleClose(); - if (createMore) issueTitleRef && issueTitleRef?.current?.focus(); + if (!createMore) handleClose(); + if (createMore && issueTitleRef) issueTitleRef?.current?.focus(); setDescription("

"); setChangesMade(null); return response; @@ -245,9 +236,8 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( if (!workspaceSlug || !payload.project_id || !data?.id) return; try { - isDraft - ? await updateDraftIssue(workspaceSlug.toString(), data.id, payload) - : updateIssue && (await updateIssue(payload.project_id, data.id, payload)); + if (isDraft) await draftIssues.updateIssue(workspaceSlug.toString(), data.id, payload); + else if (updateIssue) await updateIssue(payload.project_id, data.id, payload); // add other property values await handleCreateUpdatePropertyValues({ @@ -268,6 +258,7 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( }); handleClose(); } catch (error) { + console.error(error); setToast({ type: TOAST_TYPE.ERROR, title: "Error!", @@ -322,7 +313,7 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( issueTitleRef={issueTitleRef} onChange={handleFormChange} onClose={handleClose} - onSubmit={handleFormSubmit} + onSubmit={(payload) => handleFormSubmit(payload, isDraft)} projectId={activeProjectId} isCreateMoreToggleEnabled={createMore} onCreateMoreToggleChange={handleCreateMoreToggleChange} @@ -340,7 +331,7 @@ export const CreateUpdateIssueModalBase: React.FC = observer(( onClose={() => handleClose(false)} isCreateMoreToggleEnabled={createMore} onCreateMoreToggleChange={handleCreateMoreToggleChange} - onSubmit={handleFormSubmit} + onSubmit={(payload) => handleFormSubmit(payload, isDraft)} projectId={activeProjectId} isDraft={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 49bb1734de5..38a230a2365 100644 --- a/web/core/components/issues/issue-modal/draft-issue-layout.tsx +++ b/web/core/components/issues/issue-modal/draft-issue-layout.tsx @@ -16,7 +16,7 @@ import { isEmptyHtmlString } from "@/helpers/string.helper"; import { useIssueModal } from "@/hooks/context/use-issue-modal"; import { useEventTracker } from "@/hooks/store"; // services -import { IssueDraftService } from "@/services/issue"; +import workspaceDraftService from "@/services/issue/workspace_draft.service"; // local components import { IssueFormRoot } from "./form"; @@ -33,8 +33,6 @@ export interface DraftIssueProps { isDraft: boolean; } -const issueDraftService = new IssueDraftService(); - export const DraftIssueLayout: React.FC = observer((props) => { const { changesMade, @@ -95,10 +93,11 @@ export const DraftIssueLayout: React.FC = observer((props) => { const payload = { ...changesMade, name: changesMade?.name && changesMade?.name?.trim() !== "" ? changesMade.name?.trim() : "Untitled", + project_id: projectId, }; - const response = await issueDraftService - .createDraftIssue(workspaceSlug.toString(), projectId.toString(), payload) + const response = await workspaceDraftService + .createIssue(workspaceSlug.toString(), payload) .then((res) => { setToast({ type: TOAST_TYPE.SUCCESS, diff --git a/web/core/hooks/store/use-issues.ts b/web/core/hooks/store/use-issues.ts index 9fd68d48c21..ca40dce9ddf 100644 --- a/web/core/hooks/store/use-issues.ts +++ b/web/core/hooks/store/use-issues.ts @@ -82,6 +82,11 @@ export const useIssues = (storeType?: T): TStoreIssu issues: context.issue.workspaceDraftIssues, issuesFilter: context.issue.workspaceDraftIssuesFilter, }) as TStoreIssues[T]; + case EIssuesStoreType.WORKSPACE_DRAFT: + return merge(defaultStore, { + issues: context.issue.workspaceDraftIssues, + issuesFilter: context.issue.workspaceDraftIssuesFilter, + }) as TStoreIssues[T]; case EIssuesStoreType.PROFILE: return merge(defaultStore, { issues: context.issue.profileIssues, diff --git a/web/core/store/issue/workspace-draft/issue.store.ts b/web/core/store/issue/workspace-draft/issue.store.ts index bf70ba6468d..9e83ef9eba0 100644 --- a/web/core/store/issue/workspace-draft/issue.store.ts +++ b/web/core/store/issue/workspace-draft/issue.store.ts @@ -1,4 +1,3 @@ -import clone from "lodash/clone"; import set from "lodash/set"; import unset from "lodash/unset"; import update from "lodash/update"; @@ -9,8 +8,7 @@ import { TIssue, TIssuesResponse } from "@plane/types"; import { getCurrentDateTimeInISO } from "@/helpers/date-time.helper"; // services import workspaceDraftService from "@/services/issue/workspace_draft.service"; -// types -import { IIssueRootStore } from "../root.store"; +import { IIssueDetail } from "../issue-details/root.store"; export type TLoader = | "init-loader" @@ -67,17 +65,27 @@ export interface IWorkspaceDraftIssues { updateIssue: (workspaceSlug: string, issueId: string, payload: Partial) => Promise; deleteIssue: (workspaceSlug: string, issueId: string) => Promise; moveIssue: (workspaceSlug: string, issueId: string, payload: Partial) => Promise; + addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise; + changeModulesInIssue: ( + workspaceSlug: string, + projectId: string, + issueId: string, + addModuleIds: string[], + removeModuleIds: string[] + ) => Promise; } export class WorkspaceDraftIssues implements IWorkspaceDraftIssues { // local constants paginatedCount = 100; + // root store + rootIssueDetailStore: IIssueDetail; // observables paginationInfo: Omit, "results"> | undefined = undefined; loader: TLoader = undefined; issuesMap: Record = {}; - constructor(private store: IIssueRootStore) { + constructor(rootStore: IIssueDetail) { makeObservable(this, { issuesMap: observable, loader: observable.ref, @@ -87,7 +95,10 @@ export class WorkspaceDraftIssues implements IWorkspaceDraftIssues { updateIssue: action, deleteIssue: action, moveIssue: action, + addIssueToCycle: action, + changeModulesInIssue: action, }); + this.rootIssueDetailStore = rootStore; } // helper actions @@ -150,7 +161,11 @@ export class WorkspaceDraftIssues implements IWorkspaceDraftIssues { // call the fetch issues API with the params const response = await workspaceDraftService.getIssues(workspaceSlug, params); - console.log("response", response); + // update the issues map with the response + // TODO: update the logic to handle pagination + runInAction(() => { + if (response?.results) this.addIssue(response?.results as TIssue[]); + }); return response; } catch (error) { @@ -229,4 +244,33 @@ export class WorkspaceDraftIssues implements IWorkspaceDraftIssues { throw error; } }; + + addIssueToCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => { + await this.rootIssueDetailStore.rootIssueStore.cycleIssues.addIssueToCycle( + workspaceSlug, + projectId, + cycleId, + issueIds, + false + ); + if (issueIds && issueIds.length > 0) + await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueIds[0]); + }; + + changeModulesInIssue = async ( + workspaceSlug: string, + projectId: string, + issueId: string, + addModuleIds: string[], + removeModuleIds: string[] + ) => { + await this.rootIssueDetailStore.rootIssueStore.moduleIssues.changeModulesInIssue( + workspaceSlug, + projectId, + issueId, + addModuleIds, + removeModuleIds + ); + await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId); + }; } From 83ed3fb0ffc93b41f9de0e5481e0d3fe41a1b5ce Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Wed, 9 Oct 2024 19:12:53 +0530 Subject: [PATCH 12/21] chore: workspace draft issue component added --- .../workspace-draft.tsx | 91 ++-- .../workspace-draft/draft-issue-block.tsx | 149 +++++++ .../draft-issue-properties.tsx | 392 ++++++++++++++++++ .../issues/workspace-draft/index.ts | 2 + .../issues/workspace-draft/root.tsx | 18 +- 5 files changed, 587 insertions(+), 65 deletions(-) create mode 100644 web/core/components/issues/workspace-draft/draft-issue-block.tsx create mode 100644 web/core/components/issues/workspace-draft/draft-issue-properties.tsx diff --git a/web/core/components/issues/issue-layouts/quick-action-dropdowns/workspace-draft.tsx b/web/core/components/issues/issue-layouts/quick-action-dropdowns/workspace-draft.tsx index 92755f07cc5..c0889ab305d 100644 --- a/web/core/components/issues/issue-layouts/quick-action-dropdowns/workspace-draft.tsx +++ b/web/core/components/issues/issue-layouts/quick-action-dropdowns/workspace-draft.tsx @@ -15,9 +15,6 @@ import { CreateUpdateIssueModal, DeleteIssueModal } from "@/components/issues"; import { EIssuesStoreType } from "@/constants/issue"; // helpers import { cn } from "@/helpers/common.helper"; -// hooks -import { useEventTracker, useIssues, useUserPermissions } from "@/hooks/store"; -import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; // types import { IQuickActionProps } from "../list/list-view-types"; @@ -29,7 +26,6 @@ export const WorkspaceDraftIssueQuickActions: React.FC = obse handleMoveToIssues, customActionButton, portalElement, - readOnly = false, placements = "bottom-end", parentRef, } = props; @@ -37,16 +33,6 @@ export const WorkspaceDraftIssueQuickActions: React.FC = obse const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false); const [issueToEdit, setIssueToEdit] = useState(undefined); const [deleteIssueModal, setDeleteIssueModal] = useState(false); - // store hooks - const { allowPermissions } = useUserPermissions(); - const { setTrackElement } = useEventTracker(); - const { issuesFilter } = useIssues(EIssuesStoreType.PROJECT); - // derived values - const activeLayout = `${issuesFilter.issueFilters?.displayFilters?.layout} layout`; - // auth - const isEditingAllowed = - allowPermissions([EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.PROJECT) && !readOnly; - const isDeletingAllowed = isEditingAllowed; const duplicateIssuePayload = omit( { @@ -63,46 +49,34 @@ export const WorkspaceDraftIssueQuickActions: React.FC = obse title: "Edit", icon: Pencil, action: () => { - setTrackElement(activeLayout); setIssueToEdit(issue); setCreateUpdateIssueModal(true); }, - shouldRender: isEditingAllowed, }, { key: "make-a-copy", title: "Make a copy", icon: Copy, action: () => { - setTrackElement(activeLayout); setCreateUpdateIssueModal(true); }, - shouldRender: isEditingAllowed, }, { key: "move-to-issues", title: "Move to issues", icon: SquareStackIcon, action: () => handleMoveToIssues && handleMoveToIssues(), - shouldRender: isEditingAllowed, }, { key: "delete", title: "Delete", icon: Trash2, action: () => { - setTrackElement(activeLayout); setDeleteIssueModal(true); }, - shouldRender: isDeletingAllowed, }, ]; - // check if any of the menu items should render - const shouldRenderQuickAction = MENU_ITEMS.some((item) => item.shouldRender); - - if (!shouldRenderQuickAction) return <>; - return ( <> = obse useCaptureForOutsideClick closeOnSelect > - {MENU_ITEMS.map((item) => { - if (item.shouldRender === false) return null; - return ( - { - e.preventDefault(); - e.stopPropagation(); - item.action(); - }} - className={cn( - "flex items-center gap-2", - { - "text-custom-text-400": item.disabled, - }, - item.className + {MENU_ITEMS.map((item) => ( + { + e.preventDefault(); + e.stopPropagation(); + item.action(); + }} + className={cn( + "flex items-center gap-2", + { + "text-custom-text-400": item.disabled, + }, + item.className + )} + disabled={item.disabled} + > + {item.icon && } +
+
{item.title}
+ {item.description && ( +

+ {item.description} +

)} - disabled={item.disabled} - > - {item.icon && } -
-
{item.title}
- {item.description && ( -

- {item.description} -

- )} -
- - ); - })} +
+
+ ))} ); diff --git a/web/core/components/issues/workspace-draft/draft-issue-block.tsx b/web/core/components/issues/workspace-draft/draft-issue-block.tsx new file mode 100644 index 00000000000..c437a0a3d37 --- /dev/null +++ b/web/core/components/issues/workspace-draft/draft-issue-block.tsx @@ -0,0 +1,149 @@ +"use client"; +import React, { FC, useRef } from "react"; +import { observer } from "mobx-react"; +// plane types +import { TIssue } from "@plane/types"; +// ui +import { Row, Spinner, Tooltip } from "@plane/ui"; +// helper +import { cn } from "@/helpers/common.helper"; +// hooks +import { useAppTheme, useIssueDetail, useProject, useWorkspaceDraftIssues } from "@/hooks/store"; +// plane-web components +import { IdentifierText } from "@/plane-web/components/issues"; +// local components +import { WorkspaceDraftIssueQuickActions } from "../issue-layouts"; +import { DraftIssueProperties } from "./draft-issue-properties"; + +type Props = { + workspaceSlug: string; + issueId: string; +}; + +export const DraftIssueBlock: FC = observer((props) => { + const { workspaceSlug, issueId } = props; + const { issuesMap, updateIssue } = useWorkspaceDraftIssues(); + const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme(); + const { getIsIssuePeeked, setPeekIssue } = useIssueDetail(); + const { getProjectIdentifierById } = useProject(); + + const issueRef = useRef(null); + + const issue = issuesMap[issueId]; + + const projectIdentifier = getProjectIdentifierById(issue.project_id); + + const handleIssuePeekOverview = (issue: TIssue) => + workspaceSlug && + issue && + issue.project_id && + issue.id && + !getIsIssuePeeked(issue.id) && + setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id }); + + return ( +
handleIssuePeekOverview(issue)} + className=" relative border-b border-b-custom-border-200 w-full cursor-pointer" + > + +
+
+
+ {/* {displayProperties && (displayProperties.key || displayProperties.issue_type) && ( */} +
+ {issue.project_id && ( +
+ +
+ )} +
+ {/* )} */} + + {/* sub-issues chevron */} +
+ + {issue?.tempId !== undefined && ( +
+ )} +
+ + +

{issue.name}

+
+
+ {!issue?.tempId && ( +
+ {}} + handleDelete={async () => {}} + /> +
+ )} +
+
+ {!issue?.tempId ? ( + <> + { + await updateIssue(workspaceSlug, issueId, data); + }} + activeLayout="List" + /> +
{ + e.preventDefault(); + e.stopPropagation(); + }} + > + {}} + handleDelete={async () => {}} + /> +
+ + ) : ( +
+ +
+ )} +
+ +
+ ); +}); diff --git a/web/core/components/issues/workspace-draft/draft-issue-properties.tsx b/web/core/components/issues/workspace-draft/draft-issue-properties.tsx new file mode 100644 index 00000000000..8a64dd3b432 --- /dev/null +++ b/web/core/components/issues/workspace-draft/draft-issue-properties.tsx @@ -0,0 +1,392 @@ +"use client"; + +import { useCallback, useMemo } from "react"; +import xor from "lodash/xor"; +import { observer } from "mobx-react"; +import { useParams, usePathname } from "next/navigation"; +// icons +import { CalendarCheck2, CalendarClock } from "lucide-react"; +// types +import { TIssue, TIssuePriorities } from "@plane/types"; +// components +import { + DateDropdown, + EstimateDropdown, + PriorityDropdown, + MemberDropdown, + ModuleDropdown, + CycleDropdown, + StateDropdown, +} from "@/components/dropdowns"; +// constants +import { ISSUE_UPDATED } from "@/constants/event-tracker"; +// helpers +import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper"; +import { shouldHighlightIssueDueDate } from "@/helpers/issue.helper"; +// hooks +import { + useEventTracker, + useLabel, + useProjectState, + useProject, + useProjectEstimates, + useWorkspaceDraftIssues, +} from "@/hooks/store"; +import { usePlatformOS } from "@/hooks/use-platform-os"; +// local components +import { IssuePropertyLabels } from "../issue-layouts"; + +export interface IIssueProperties { + issue: TIssue; + updateIssue: ((projectId: string | null, issueId: string, data: Partial) => Promise) | undefined; + className: string; + activeLayout: string; +} + +export const DraftIssueProperties: React.FC = observer((props) => { + const { issue, updateIssue, activeLayout, className } = props; + // store hooks + const { getProjectById } = useProject(); + const { labelMap } = useLabel(); + const { captureIssueEvent } = useEventTracker(); + const { changeModulesInIssue } = useWorkspaceDraftIssues(); + const { areEstimateEnabledByProjectId } = useProjectEstimates(); + const { getStateById } = useProjectState(); + const { isMobile } = usePlatformOS(); + const projectDetails = getProjectById(issue.project_id); + + // router + const { workspaceSlug } = useParams(); + const pathname = usePathname(); + + const currentLayout = `${activeLayout} layout`; + // derived values + const stateDetails = getStateById(issue.state_id); + + const issueOperations = useMemo( + () => ({ + addModulesToIssue: async (moduleIds: string[]) => { + if (!workspaceSlug || !issue.project_id || !issue.id) return; + await changeModulesInIssue?.(workspaceSlug.toString(), issue.project_id, issue.id, moduleIds, []); + }, + removeModulesFromIssue: async (moduleIds: string[]) => { + if (!workspaceSlug || !issue.project_id || !issue.id) return; + await changeModulesInIssue?.(workspaceSlug.toString(), issue.project_id, issue.id, [], moduleIds); + }, + addIssueToCycle: async (cycleId: string) => { + if (!workspaceSlug || !issue.project_id || !issue.id) return; + // TODO: Uncomment this after adding function to draft issue store + // await addCycleToIssue?.(workspaceSlug.toString(), issue.project_id, cycleId, issue.id); + }, + removeIssueFromCycle: async () => { + if (!workspaceSlug || !issue.project_id || !issue.id) return; + // TODO: Uncomment this after adding function to draft issue store + // await removeCycleFromIssue?.(workspaceSlug.toString(), issue.project_id, issue.id); + }, + }), + [workspaceSlug, issue, changeModulesInIssue] + ); + + const handleState = (stateId: string) => { + updateIssue && + updateIssue(issue.project_id, issue.id, { state_id: stateId }).then(() => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...issue, state: "SUCCESS", element: currentLayout }, + path: pathname, + updates: { + changed_property: "state", + change_details: stateId, + }, + }); + }); + }; + + const handlePriority = (value: TIssuePriorities) => { + updateIssue && + updateIssue(issue.project_id, issue.id, { priority: value }).then(() => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...issue, state: "SUCCESS", element: currentLayout }, + path: pathname, + updates: { + changed_property: "priority", + change_details: value, + }, + }); + }); + }; + + const handleLabel = (ids: string[]) => { + updateIssue && + updateIssue(issue.project_id, issue.id, { label_ids: ids }).then(() => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...issue, state: "SUCCESS", element: currentLayout }, + path: pathname, + updates: { + changed_property: "labels", + change_details: ids, + }, + }); + }); + }; + + const handleAssignee = (ids: string[]) => { + updateIssue && + updateIssue(issue.project_id, issue.id, { assignee_ids: ids }).then(() => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...issue, state: "SUCCESS", element: currentLayout }, + path: pathname, + updates: { + changed_property: "assignees", + change_details: ids, + }, + }); + }); + }; + + const handleModule = useCallback( + (moduleIds: string[] | null) => { + if (!issue || !issue.module_ids || !moduleIds) return; + + const updatedModuleIds = xor(issue.module_ids, moduleIds); + const modulesToAdd: string[] = []; + const modulesToRemove: string[] = []; + for (const moduleId of updatedModuleIds) + if (issue.module_ids.includes(moduleId)) modulesToRemove.push(moduleId); + else modulesToAdd.push(moduleId); + if (modulesToAdd.length > 0) issueOperations.addModulesToIssue(modulesToAdd); + if (modulesToRemove.length > 0) issueOperations.removeModulesFromIssue(modulesToRemove); + + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...issue, state: "SUCCESS", element: currentLayout }, + path: pathname, + updates: { changed_property: "module_ids", change_details: { module_ids: moduleIds } }, + }); + }, + [issueOperations, captureIssueEvent, currentLayout, pathname, issue] + ); + + const handleCycle = useCallback( + (cycleId: string | null) => { + if (!issue || issue.cycle_id === cycleId) return; + if (cycleId) issueOperations.addIssueToCycle?.(cycleId); + else issueOperations.removeIssueFromCycle?.(); + + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...issue, state: "SUCCESS", element: currentLayout }, + path: pathname, + updates: { changed_property: "cycle", change_details: { cycle_id: cycleId } }, + }); + }, + [issue, issueOperations, captureIssueEvent, currentLayout, pathname] + ); + + const handleStartDate = (date: Date | null) => { + updateIssue && + updateIssue(issue.project_id, issue.id, { start_date: date ? renderFormattedPayloadDate(date) : null }).then( + () => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...issue, state: "SUCCESS", element: currentLayout }, + path: pathname, + updates: { + changed_property: "start_date", + change_details: date ? renderFormattedPayloadDate(date) : null, + }, + }); + } + ); + }; + + const handleTargetDate = (date: Date | null) => { + updateIssue && + updateIssue(issue.project_id, issue.id, { target_date: date ? renderFormattedPayloadDate(date) : null }).then( + () => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...issue, state: "SUCCESS", element: currentLayout }, + path: pathname, + updates: { + changed_property: "target_date", + change_details: date ? renderFormattedPayloadDate(date) : null, + }, + }); + } + ); + }; + + const handleEstimate = (value: string | undefined) => { + updateIssue && + updateIssue(issue.project_id, issue.id, { estimate_point: value }).then(() => { + captureIssueEvent({ + eventName: ISSUE_UPDATED, + payload: { ...issue, state: "SUCCESS", element: currentLayout }, + path: pathname, + updates: { + changed_property: "estimate_point", + change_details: value, + }, + }); + }); + }; + + if (!issue.project_id) return null; + + const defaultLabelOptions = issue?.label_ids?.map((id) => labelMap[id]) || []; + + const minDate = getDate(issue.start_date); + minDate?.setDate(minDate.getDate()); + + const maxDate = getDate(issue.target_date); + maxDate?.setDate(maxDate.getDate()); + + const handleEventPropagation = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + }; + + return ( +
+ {/* basic properties */} + {/* state */} +
+ +
+ {/* */} + + {/* priority */} +
+ +
+ {/* */} + + {/* label */} + +
+ +
+ + {/* start date */} +
+ } + buttonVariant={issue.start_date ? "border-with-text" : "border-without-text"} + optionsClassName="z-10" + renderByDefault={isMobile} + showTooltip + /> +
+ + {/* target/due date */} +
+ } + buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"} + buttonClassName={shouldHighlightIssueDueDate(issue.target_date, stateDetails?.group) ? "text-red-500" : ""} + clearIconClassName="!text-custom-text-100" + optionsClassName="z-10" + renderByDefault={isMobile} + showTooltip + /> +
+ + {/* assignee */} +
+ 0 ? "transparent-without-text" : "border-without-text"} + buttonClassName={issue.assignee_ids?.length > 0 ? "hover:bg-transparent px-0" : ""} + showTooltip={issue?.assignee_ids?.length === 0} + placeholder="Assignees" + optionsClassName="z-10" + tooltipContent="" + renderByDefault={isMobile} + /> +
+ + {/* modules */} + {projectDetails?.module_view && ( +
+ +
+ )} + + {/* cycles */} + {projectDetails?.cycle_view && ( +
+ +
+ )} + + {/* estimates */} + {issue.project_id && areEstimateEnabledByProjectId(issue.project_id?.toString()) && ( +
+ +
+ )} +
+ ); +}); diff --git a/web/core/components/issues/workspace-draft/index.ts b/web/core/components/issues/workspace-draft/index.ts index 1efe34c51ec..097173b945a 100644 --- a/web/core/components/issues/workspace-draft/index.ts +++ b/web/core/components/issues/workspace-draft/index.ts @@ -1 +1,3 @@ +export * from "./draft-issue-block"; +export * from "./draft-issue-properties"; export * from "./root"; diff --git a/web/core/components/issues/workspace-draft/root.tsx b/web/core/components/issues/workspace-draft/root.tsx index 69690657910..3945e0502f3 100644 --- a/web/core/components/issues/workspace-draft/root.tsx +++ b/web/core/components/issues/workspace-draft/root.tsx @@ -3,9 +3,13 @@ import { FC } from "react"; import { observer } from "mobx-react"; import useSWR from "swr"; +// ui import { Button } from "@plane/ui"; // hooks import { useWorkspaceDraftIssues } from "@/hooks/store"; +// components +import { IssuePeekOverview } from "../peek-overview"; +import { DraftIssueBlock } from "./draft-issue-block"; type TWorkspaceDraftIssuesRoot = { workspaceSlug: string; @@ -14,21 +18,25 @@ type TWorkspaceDraftIssuesRoot = { export const WorkspaceDraftIssuesRoot: FC = observer((props) => { const { workspaceSlug } = props; // hooks - const { fetchIssues } = useWorkspaceDraftIssues(); + const { fetchIssues, issuesMap } = useWorkspaceDraftIssues(); useSWR( - workspaceSlug ? `WORKSPACE_DRAFT_ISSUES_${fetchIssues}` : null, + workspaceSlug ? `WORKSPACE_DRAFT_ISSUES_${workspaceSlug}` : null, async () => await fetchIssues(workspaceSlug, "init-loader") ); - console.log("workspaceSlug", workspaceSlug); - return (
-
WorkspaceDraftIssueRoot
+
+ {issuesMap && + Object.keys(issuesMap).map((issueId: string) => ( + + ))} +
+
); }); From 11d1c5ff4bee2c0d0cc87441c4874d2cafb95a44 Mon Sep 17 00:00:00 2001 From: gurusainath Date: Thu, 10 Oct 2024 14:32:00 +0530 Subject: [PATCH 13/21] chore: updated store and workflow in draft issues --- apiserver/plane/utils/paginator.py | 5 +- packages/types/src/index.d.ts | 1 + packages/types/src/issues/base.d.ts | 1 + .../src/workspace-draft-issues/base.d.ts | 61 +++++ .../issues/workspace-draft/empty-state.tsx | 33 +++ .../issues/workspace-draft/loader.tsx | 20 ++ .../issues/workspace-draft/root.tsx | 51 +++- web/core/constants/empty-state.ts | 13 + web/core/constants/workspace-drafts.ts | 6 + .../services/issue/workspace_draft.service.ts | 22 +- .../issue/workspace-draft/issue.store.ts | 253 +++++++++--------- .../workspace-draft/issue-dark.webp | Bin 0 -> 12688 bytes .../workspace-draft/issue-light.webp | Bin 0 -> 13680 bytes 13 files changed, 328 insertions(+), 138 deletions(-) create mode 100644 packages/types/src/workspace-draft-issues/base.d.ts create mode 100644 web/core/components/issues/workspace-draft/empty-state.tsx create mode 100644 web/core/components/issues/workspace-draft/loader.tsx create mode 100644 web/core/constants/workspace-drafts.ts create mode 100644 web/public/empty-state/workspace-draft/issue-dark.webp create mode 100644 web/public/empty-state/workspace-draft/issue-light.webp diff --git a/apiserver/plane/utils/paginator.py b/apiserver/plane/utils/paginator.py index 65f0aa7f746..219b646b25b 100644 --- a/apiserver/plane/utils/paginator.py +++ b/apiserver/plane/utils/paginator.py @@ -150,7 +150,7 @@ def get_result(self, limit=1000, cursor=None): raise BadPaginationError("Pagination offset cannot be negative") results = queryset[offset:stop] - + print(limit, "limit") if cursor.value != limit: results = results[-(limit + 1) :] @@ -761,7 +761,7 @@ def paginate( ): """Paginate the request""" per_page = self.get_per_page(request, default_per_page, max_per_page) - + print(per_page, "per_page") # Convert the cursor value to integer and float from string input_cursor = None try: @@ -788,6 +788,7 @@ def paginate( paginator = paginator_cls(**paginator_kwargs) try: + print(per_page, "per_page 2") cursor_result = paginator.get_result( limit=per_page, cursor=input_cursor ) diff --git a/packages/types/src/index.d.ts b/packages/types/src/index.d.ts index 6dfddc6b638..4559e79c837 100644 --- a/packages/types/src/index.d.ts +++ b/packages/types/src/index.d.ts @@ -29,3 +29,4 @@ export * from "./pragmatic"; export * from "./publish"; export * from "./workspace-notifications"; export * from "./favorite"; +export * from "./workspace-draft-issues/base"; diff --git a/packages/types/src/issues/base.d.ts b/packages/types/src/issues/base.d.ts index 8292c111649..05f679cce28 100644 --- a/packages/types/src/issues/base.d.ts +++ b/packages/types/src/issues/base.d.ts @@ -10,6 +10,7 @@ export * from "./issue_relation"; export * from "./issue_sub_issues"; export * from "./activity/base"; + export type TLoader = | "init-loader" | "mutation" diff --git a/packages/types/src/workspace-draft-issues/base.d.ts b/packages/types/src/workspace-draft-issues/base.d.ts new file mode 100644 index 00000000000..f0272defd5e --- /dev/null +++ b/packages/types/src/workspace-draft-issues/base.d.ts @@ -0,0 +1,61 @@ +import { TIssuePriorities } from "../issues"; + +export type TWorkspaceDraftIssue = { + id: string; + name: string; + sort_order: number; + + state_id: string | undefined; + priority: TIssuePriorities | undefined; + label_ids: string[]; + assignee_ids: string[]; + estimate_point: string | undefined; + + project_id: string | undefined; + parent_id: string | undefined; + cycle_id: string | undefined; + module_ids: string[] | undefined; + + start_date: string | undefined; + target_date: string | undefined; + completed_at: string | undefined; + + created_at: string; + updated_at: string; + created_by: string; + updated_by: string; + + is_draft: boolean; +}; + +export type TWorkspaceDraftPaginationInfo = { + next_cursor: string | undefined; + prev_cursor: string | undefined; + next_page_results: boolean | undefined; + prev_page_results: boolean | undefined; + total_pages: number | undefined; + count: number | undefined; // current paginated results count + total_count: number | undefined; // total available results count + total_results: number | undefined; + results: T[] | undefined; + extra_stats: string | undefined; + grouped_by: string | undefined; + sub_grouped_by: string | undefined; +}; + +export type TWorkspaceDraftQueryParams = { + per_page: number; + cursor: string; +}; + +export type TWorkspaceDraftIssueLoader = + | "init-loader" + | "empty-state" + | "mutation" + | "pagination" + | "loaded" + | "create" + | "update" + | "delete" + | "move" + | undefined; diff --git a/web/core/components/issues/workspace-draft/empty-state.tsx b/web/core/components/issues/workspace-draft/empty-state.tsx new file mode 100644 index 00000000000..4a1292d6160 --- /dev/null +++ b/web/core/components/issues/workspace-draft/empty-state.tsx @@ -0,0 +1,33 @@ +"use client"; + +import { FC, Fragment, useState } from "react"; +// components +import { EmptyState } from "@/components/empty-state"; +import { CreateUpdateIssueModal } from "@/components/issues"; +// constants +import { EmptyStateType } from "@/constants/empty-state"; +import { EIssuesStoreType } from "@/constants/issue"; + +export const WorkspaceDraftEmptyState: FC = () => { + // state + const [isDraftIssueModalOpen, setIsDraftIssueModalOpen] = useState(false); + + return ( + + setIsDraftIssueModalOpen(false)} + isDraft + /> +
+ { + setIsDraftIssueModalOpen(true); + }} + /> +
+
+ ); +}; diff --git a/web/core/components/issues/workspace-draft/loader.tsx b/web/core/components/issues/workspace-draft/loader.tsx new file mode 100644 index 00000000000..d663a0d035e --- /dev/null +++ b/web/core/components/issues/workspace-draft/loader.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { FC } from "react"; +// components +import { ListLoaderItemRow } from "@/components/ui"; + +type TWorkspaceDraftIssuesLoader = { + items?: number; +}; + +export const WorkspaceDraftIssuesLoader: FC = (props) => { + const { items = 14 } = props; + return ( +
+ {[...Array(items)].map((_, index) => ( + + ))} +
+ ); +}; diff --git a/web/core/components/issues/workspace-draft/root.tsx b/web/core/components/issues/workspace-draft/root.tsx index 3945e0502f3..e5e61d3aae0 100644 --- a/web/core/components/issues/workspace-draft/root.tsx +++ b/web/core/components/issues/workspace-draft/root.tsx @@ -3,13 +3,17 @@ import { FC } from "react"; import { observer } from "mobx-react"; import useSWR from "swr"; -// ui -import { Button } from "@plane/ui"; +// constants +import { EDraftIssuePaginationType } from "@/constants/workspace-drafts"; +// helpers +import { cn } from "@/helpers/common.helper"; // hooks import { useWorkspaceDraftIssues } from "@/hooks/store"; // components import { IssuePeekOverview } from "../peek-overview"; import { DraftIssueBlock } from "./draft-issue-block"; +import { WorkspaceDraftEmptyState } from "./empty-state"; +import { WorkspaceDraftIssuesLoader } from "./loader"; type TWorkspaceDraftIssuesRoot = { workspaceSlug: string; @@ -18,24 +22,51 @@ type TWorkspaceDraftIssuesRoot = { export const WorkspaceDraftIssuesRoot: FC = observer((props) => { const { workspaceSlug } = props; // hooks - const { fetchIssues, issuesMap } = useWorkspaceDraftIssues(); + const { loader, paginationInfo, fetchIssues, issuesMap, issueIds } = useWorkspaceDraftIssues(); + // fetching issues useSWR( - workspaceSlug ? `WORKSPACE_DRAFT_ISSUES_${workspaceSlug}` : null, - async () => await fetchIssues(workspaceSlug, "init-loader") + workspaceSlug && issueIds.length <= 0 ? `WORKSPACE_DRAFT_ISSUES_${workspaceSlug}` : null, + workspaceSlug && issueIds.length <= 0 ? async () => await fetchIssues(workspaceSlug, "init-loader") : null ); + // handle nest issues + const handleNextIssues = async () => { + if (!paginationInfo?.next_page_results) return; + await fetchIssues(workspaceSlug, "pagination", EDraftIssuePaginationType.NEXT); + }; + + if (loader === "init-loader" && issueIds.length <= 0) { + return ; + } + + if (loader === "empty-state" && issueIds.length <= 0) return ; + return ( -
-
+
+
{issuesMap && Object.keys(issuesMap).map((issueId: string) => ( ))}
-
- -
+ {loader === "pagination" && issueIds.length >= 0 ? ( + + ) : ( +
+ Load More ↓ +
+ )}
); diff --git a/web/core/constants/empty-state.ts b/web/core/constants/empty-state.ts index 1d8af5722e1..34a205d7e45 100644 --- a/web/core/constants/empty-state.ts +++ b/web/core/constants/empty-state.ts @@ -105,6 +105,8 @@ export enum EmptyStateType { INBOX_SIDEBAR_CLOSED_TAB = "inbox-sidebar-closed-tab", INBOX_SIDEBAR_FILTER_EMPTY_STATE = "inbox-sidebar-filter-empty-state", INBOX_DETAIL_EMPTY_STATE = "inbox-detail-empty-state", + + WORKSPACE_DRAFT_ISSUES = "workspace-draft-issues", } const emptyStateDetails = { @@ -757,6 +759,17 @@ const emptyStateDetails = { title: "Select an issue to view its details.", path: "/empty-state/intake/issue-detail", }, + [EmptyStateType.WORKSPACE_DRAFT_ISSUES]: { + key: EmptyStateType.WORKSPACE_DRAFT_ISSUES, + title: "No Draft Issues Yet", + description: "There are no draft issues in your workspace right now. Begin by adding your first one.", + path: "/empty-state/workspace-draft/issue", + primaryButton: { + text: "Create draft issue", + }, + accessType: "workspace", + access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + }, } as const; export const EMPTY_STATE_DETAILS: Record = emptyStateDetails; diff --git a/web/core/constants/workspace-drafts.ts b/web/core/constants/workspace-drafts.ts new file mode 100644 index 00000000000..4c1c88f9f60 --- /dev/null +++ b/web/core/constants/workspace-drafts.ts @@ -0,0 +1,6 @@ +export enum EDraftIssuePaginationType { + INIT = "INIT", + NEXT = "NEXT", + PREV = "PREV", + CURRENT = "CURRENT", +} diff --git a/web/core/services/issue/workspace_draft.service.ts b/web/core/services/issue/workspace_draft.service.ts index eff58b13a13..b15e39e03ad 100644 --- a/web/core/services/issue/workspace_draft.service.ts +++ b/web/core/services/issue/workspace_draft.service.ts @@ -1,4 +1,4 @@ -import { TIssue, TIssuesResponse } from "@plane/types"; +import { TWorkspaceDraftIssue, TWorkspaceDraftPaginationInfo } from "@plane/types"; // helpers import { API_BASE_URL } from "@/helpers/common.helper"; // services @@ -9,7 +9,10 @@ export class WorkspaceDraftService extends APIService { super(API_BASE_URL); } - async getIssues(workspaceSlug: string, query: object = {}): Promise { + async getIssues( + workspaceSlug: string, + query: object = {} + ): Promise | undefined> { return this.get(`/api/workspaces/${workspaceSlug}/draft-issues/`, { params: { ...query } }) .then((response) => response?.data) .catch((error) => { @@ -17,7 +20,7 @@ export class WorkspaceDraftService extends APIService { }); } - async getIssueById(workspaceSlug: string, issueId: string): Promise { + async getIssueById(workspaceSlug: string, issueId: string): Promise { return this.get(`/api/workspaces/${workspaceSlug}/draft-issues/${issueId}/`) .then((response) => response?.data) .catch((error) => { @@ -25,7 +28,10 @@ export class WorkspaceDraftService extends APIService { }); } - async createIssue(workspaceSlug: string, payload: Partial): Promise { + async createIssue( + workspaceSlug: string, + payload: Partial + ): Promise { return this.post(`/api/workspaces/${workspaceSlug}/draft-issues/`, payload) .then((response) => response?.data) .catch((error) => { @@ -33,7 +39,11 @@ export class WorkspaceDraftService extends APIService { }); } - async updateIssue(workspaceSlug: string, issueId: string, payload: Partial): Promise { + async updateIssue( + workspaceSlug: string, + issueId: string, + payload: Partial + ): Promise { return this.patch(`/api/workspaces/${workspaceSlug}/draft-issues/${issueId}/`, payload) .then((response) => response?.data) .catch((error) => { @@ -49,7 +59,7 @@ export class WorkspaceDraftService extends APIService { }); } - async moveIssue(workspaceSlug: string, issueId: string, payload: Partial): Promise { + async moveIssue(workspaceSlug: string, issueId: string, payload: Partial): Promise { return this.post(`/api/workspaces/${workspaceSlug}/draft-to-issue/${issueId}/`, payload) .then((response) => response?.data) .catch((error) => { diff --git a/web/core/store/issue/workspace-draft/issue.store.ts b/web/core/store/issue/workspace-draft/issue.store.ts index 9e83ef9eba0..e4c9f53dd87 100644 --- a/web/core/store/issue/workspace-draft/issue.store.ts +++ b/web/core/store/issue/workspace-draft/issue.store.ts @@ -1,108 +1,109 @@ +import orderBy from "lodash/orderBy"; import set from "lodash/set"; import unset from "lodash/unset"; import update from "lodash/update"; -import { action, makeObservable, observable, runInAction } from "mobx"; +import { action, computed, makeObservable, observable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; -import { TIssue, TIssuesResponse } from "@plane/types"; +import { + TWorkspaceDraftIssue, + TWorkspaceDraftPaginationInfo, + TWorkspaceDraftIssueLoader, + TWorkspaceDraftQueryParams, +} from "@plane/types"; +// constants +import { EDraftIssuePaginationType } from "@/constants/workspace-drafts"; // helpers -import { getCurrentDateTimeInISO } from "@/helpers/date-time.helper"; +import { getCurrentDateTimeInISO, convertToISODateString } from "@/helpers/date-time.helper"; // services import workspaceDraftService from "@/services/issue/workspace_draft.service"; import { IIssueDetail } from "../issue-details/root.store"; -export type TLoader = - | "init-loader" - | "mutation" - | "pagination" - | "loaded" - | "create" - | "update" - | "delete" - | "move" - | undefined; - -export enum EDraftIssuePaginationType { - INIT = "INIT", - NEXT = "NEXT", - PREV = "PREV", - CURRENT = "CURRENT", -} - export type TDraftIssuePaginationType = EDraftIssuePaginationType; -export type TNotificationQueryParams = { - cursor: string; -}; - -export type TPaginationInfo = { - next_cursor: string | undefined; - prev_cursor: string | undefined; - next_page_results: boolean | undefined; - prev_page_results: boolean | undefined; - total_pages: number | undefined; - extra_stats: string | undefined; - count: number | undefined; // current paginated results count - total_count: number | undefined; // total available results count - results: T[] | undefined; - grouped_by: string | undefined; - sub_grouped_by: string | undefined; -}; - export interface IWorkspaceDraftIssues { // observables - issuesMap: Record; - paginationInfo: Omit, "results"> | undefined; - loader: TLoader; - // computed actions - getIssueById: (issueId: string) => TIssue | undefined; + issuesMap: Record; + paginationInfo: Omit, "results"> | undefined; + loader: TWorkspaceDraftIssueLoader; + // computed + issueIds: string[]; + // computed functions + getIssueById: (issueId: string) => TWorkspaceDraftIssue | undefined; // helper actions - addIssue: (issues: TIssue[]) => void; - mutateIssue: (issueId: string, data: Partial) => void; + addIssue: (issues: TWorkspaceDraftIssue[]) => void; + mutateIssue: (issueId: string, data: Partial) => void; removeIssue: (issueId: string) => void; // actions - fetchIssues: (workspaceSlug: string, loadType: TLoader) => Promise; - createIssue: (workspaceSlug: string, payload: Partial) => Promise; - updateIssue: (workspaceSlug: string, issueId: string, payload: Partial) => Promise; + fetchIssues: ( + workspaceSlug: string, + loadType: TWorkspaceDraftIssueLoader, + paginationType?: TDraftIssuePaginationType + ) => Promise | undefined>; + createIssue: ( + workspaceSlug: string, + payload: Partial + ) => Promise; + updateIssue: ( + workspaceSlug: string, + issueId: string, + payload: Partial + ) => Promise; deleteIssue: (workspaceSlug: string, issueId: string) => Promise; - moveIssue: (workspaceSlug: string, issueId: string, payload: Partial) => Promise; - addIssueToCycle: (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => Promise; - changeModulesInIssue: ( + moveIssue: (workspaceSlug: string, issueId: string, payload: Partial) => Promise; + addCycleToIssue: ( workspaceSlug: string, - projectId: string, issueId: string, - addModuleIds: string[], - removeModuleIds: string[] - ) => Promise; + cycleId: string + ) => Promise; + addModulesToIssue: ( + workspaceSlug: string, + issueId: string, + moduleIds: string[] + ) => Promise; } export class WorkspaceDraftIssues implements IWorkspaceDraftIssues { // local constants - paginatedCount = 100; - // root store - rootIssueDetailStore: IIssueDetail; + paginatedCount = 50; // observables - paginationInfo: Omit, "results"> | undefined = undefined; - loader: TLoader = undefined; - issuesMap: Record = {}; + paginationInfo: Omit, "results"> | undefined = undefined; + loader: TWorkspaceDraftIssueLoader = undefined; + issuesMap: Record = {}; - constructor(rootStore: IIssueDetail) { + constructor(private store: IIssueDetail) { makeObservable(this, { - issuesMap: observable, + paginationInfo: observable, loader: observable.ref, + issuesMap: observable, + // computed + issueIds: computed, // action fetchIssues: action, createIssue: action, updateIssue: action, deleteIssue: action, moveIssue: action, - addIssueToCycle: action, - changeModulesInIssue: action, + addCycleToIssue: action, + addModulesToIssue: action, }); - this.rootIssueDetailStore = rootStore; } + // 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 + ); + } + + // computed functions + getIssueById = computedFn((issueId: string) => { + if (!issueId || !this.issuesMap[issueId]) return undefined; + return this.issuesMap[issueId]; + }); + // helper actions - addIssue = (issues: TIssue[]) => { + addIssue = (issues: TWorkspaceDraftIssue[]) => { if (issues && issues.length <= 0) return; runInAction(() => { issues.forEach((issue) => { @@ -112,17 +113,12 @@ export class WorkspaceDraftIssues implements IWorkspaceDraftIssues { }); }; - getIssueById = computedFn((issueId: string) => { - if (!issueId || !this.issuesMap[issueId]) return undefined; - return this.issuesMap[issueId]; - }); - - mutateIssue = (issueId: string, issue: Partial) => { + mutateIssue = (issueId: string, issue: Partial) => { if (!issue || !issueId || !this.issuesMap[issueId]) return; runInAction(() => { set(this.issuesMap, [issueId, "updated_at"], getCurrentDateTimeInISO()); Object.keys(issue).forEach((key) => { - set(this.issuesMap, [issueId, key], issue[key as keyof TIssue]); + set(this.issuesMap, [issueId, key], issue[key as keyof TWorkspaceDraftIssue]); }); }); }; @@ -132,7 +128,10 @@ export class WorkspaceDraftIssues implements IWorkspaceDraftIssues { runInAction(() => unset(this.issuesMap, issueId)); }; - generateNotificationQueryParams = (paramType: TDraftIssuePaginationType): TNotificationQueryParams => { + generateNotificationQueryParams = ( + paramType: TDraftIssuePaginationType, + filterParams = {} + ): TWorkspaceDraftQueryParams => { const queryCursorNext: string = paramType === EDraftIssuePaginationType.INIT ? `${this.paginatedCount}:0:0` @@ -142,32 +141,43 @@ export class WorkspaceDraftIssues implements IWorkspaceDraftIssues { ? (this.paginationInfo?.next_cursor ?? `${this.paginatedCount}:${0}:0`) : `${this.paginatedCount}:${0}:0`; - const queryParams: TNotificationQueryParams = { + const queryParams: TWorkspaceDraftQueryParams = { + per_page: this.paginatedCount, cursor: queryCursorNext, + ...filterParams, }; return queryParams; }; // actions - fetchIssues = async (workspaceSlug: string, loadType: TLoader) => { + fetchIssues = async ( + workspaceSlug: string, + loadType: TWorkspaceDraftIssueLoader, + paginationType: TDraftIssuePaginationType = EDraftIssuePaginationType.INIT + ) => { try { this.loader = loadType; - // get params from pagination options - // const params = this.issueFilterStore?.getFilterParams(options, workspaceSlug, undefined, undefined, undefined); - const params = {}; + // filter params and pagination params + const filterParams = {}; + const params = this.generateNotificationQueryParams(paginationType, filterParams); - // call the fetch issues API with the params - const response = await workspaceDraftService.getIssues(workspaceSlug, params); + // fetching the paginated workspace draft issues + const draftIssuesResponse = await workspaceDraftService.getIssues(workspaceSlug, { ...params }); + if (!draftIssuesResponse) return undefined; - // update the issues map with the response - // TODO: update the logic to handle pagination + const { results, ...paginationInfo } = draftIssuesResponse; runInAction(() => { - if (response?.results) this.addIssue(response?.results as TIssue[]); + if (results && results.length > 0) { + this.addIssue(results as TWorkspaceDraftIssue[]); + this.loader = undefined; + } else { + this.loader = "empty-state"; + } + set(this, "paginationInfo", paginationInfo); }); - - return response; + return draftIssuesResponse; } catch (error) { // set loader to undefined if errored out this.loader = undefined; @@ -175,16 +185,16 @@ export class WorkspaceDraftIssues implements IWorkspaceDraftIssues { } }; - createIssue = async (workspaceSlug: string, payload: Partial): Promise => { + createIssue = async ( + workspaceSlug: string, + payload: Partial + ): Promise => { try { this.loader = "create"; const response = await workspaceDraftService.createIssue(workspaceSlug, payload); if (response) { - runInAction(() => { - if (!this.issuesMap[response.id]) set(this.issuesMap, response.id, response); - else update(this.issuesMap, response.id, (prevIssue) => ({ ...prevIssue, ...response })); - }); + runInAction(() => set(this.issuesMap, response.id, response)); } this.loader = undefined; @@ -195,7 +205,7 @@ export class WorkspaceDraftIssues implements IWorkspaceDraftIssues { } }; - updateIssue = async (workspaceSlug: string, issueId: string, payload: Partial) => { + updateIssue = async (workspaceSlug: string, issueId: string, payload: Partial) => { try { this.loader = "create"; @@ -230,11 +240,13 @@ export class WorkspaceDraftIssues implements IWorkspaceDraftIssues { } }; - moveIssue = async (workspaceSlug: string, issueId: string, payload: Partial) => { + moveIssue = async (workspaceSlug: string, issueId: string, payload: Partial) => { try { this.loader = "move"; const response = await workspaceDraftService.moveIssue(workspaceSlug, issueId, payload); + + // remove the issue from the draft issues list and fetch the issue from the issue list runInAction(() => {}); this.loader = undefined; @@ -245,32 +257,33 @@ export class WorkspaceDraftIssues implements IWorkspaceDraftIssues { } }; - addIssueToCycle = async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => { - await this.rootIssueDetailStore.rootIssueStore.cycleIssues.addIssueToCycle( - workspaceSlug, - projectId, - cycleId, - issueIds, - false - ); - if (issueIds && issueIds.length > 0) - await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueIds[0]); + addCycleToIssue = async (workspaceSlug: string, issueId: string, cycleId: string) => { + try { + this.loader = "update"; + + const response = this.updateIssue(workspaceSlug, issueId, { cycle_id: cycleId }); + runInAction(() => {}); + + this.loader = undefined; + return response; + } catch (error) { + this.loader = undefined; + throw error; + } }; - changeModulesInIssue = async ( - workspaceSlug: string, - projectId: string, - issueId: string, - addModuleIds: string[], - removeModuleIds: string[] - ) => { - await this.rootIssueDetailStore.rootIssueStore.moduleIssues.changeModulesInIssue( - workspaceSlug, - projectId, - issueId, - addModuleIds, - removeModuleIds - ); - await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId); + addModulesToIssue = async (workspaceSlug: string, issueId: string, moduleIds: string[]) => { + try { + this.loader = "update"; + + const response = this.updateIssue(workspaceSlug, issueId, { module_ids: moduleIds }); + runInAction(() => {}); + + this.loader = undefined; + return response; + } catch (error) { + this.loader = undefined; + throw error; + } }; } diff --git a/web/public/empty-state/workspace-draft/issue-dark.webp b/web/public/empty-state/workspace-draft/issue-dark.webp new file mode 100644 index 0000000000000000000000000000000000000000..76306f646ef2906c91a7aeecdec9628a44327263 GIT binary patch literal 12688 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 literal 0 HcmV?d00001 diff --git a/web/public/empty-state/workspace-draft/issue-light.webp b/web/public/empty-state/workspace-draft/issue-light.webp new file mode 100644 index 0000000000000000000000000000000000000000..5ade38de5b8198a66c96837fc532b33099c53e7d GIT binary patch literal 13680 zcmbW8Q;;UG)~>&{ZM&y!+qP|+)Alr`ZQC}dZJX1!ZGL@bpR@1IfAz0QQk9FOl3MG{ zN$~_;vz~KN?cm70002_U$p}RxPSnJ<&>nd{`Ej4h2rhA-lYVR#Kq(e z$g|g5YiLgP<~uz|6ULCV-DhqDb5OqW0<{dA-2AFZ_jAnc1Y@-)a`&}o%x-iS;J+!R zxn$R2zVC?sSH3>x&NoAEkGIFZ>jH4s`_7`S2C2AP7O4G-_G-A# z_xhdk^*t;&A*lVW{?&UQb%=GZc>I0Y+wI-%OJpU`0QC9(QO;N^sPbI{L>q4V>b>?a z2AY2@0^{D00+Eh(R(rR;13u$l7e5(4i0=KRdX>JxzG*+pj!xE~?gdW+j=$Ris=ud! zn&1Cy*zoxK?K|f)_c`kw=`HSS=L87&&JA$;9tKLT-+X_!-u!EO&c5q=fkD&gI@O?N zZ@rZ)tdSJ?A@RyJC5e6d-!3nwL4IRQ#vuawK`Nfu% zbPh@?tjn03RMa@s)QR^8`$3%XZ3vs+Sk0FA>sNZqUlNJ&#)x?$x8Iv*`06 zznX~HBdr1lwBs^|*@TSgMyE|OiJ4MOS72M>VgBqC>~gyj4uE2yfBjVQM)5sUJQ6wN zXkX8QfSW@uyDxCT)3cWk_9vb?TqMBT>c zH+lh03;$50Qi@LQzBrXkjiNJYGKEH2dx8vVmFh+yZ*%QvwzTGIjHK4dUw6G~p3fo>nm;=v#N9`n2^N8nnBA%#|d03Pm?GDof(rR9)Kv?h$COLki2 zq;K=;#muj%*BIr}_E*7nS!nd@`JV+%}@ z-bF6ajycWjT-JSd3EE#p9SX9`<^{f|wEvkW`U(#ndy!p%jR*E_CjwzuM=hh>Zs_#E zTGaIlQV+SjNhpw|L|S8owA-GNi|a}g=Vc|>E?sncMSR@iYYLB4q>GBD%)I(}!eQ(; z^*9o9`*4atIdX@+(&xJ8vo|kM5GDJA;>jeH(U_ugtdLJ`(pYlht_)|)v; z7NGeB_RIn$d%k47iaGAk*mf@{lL6(YqYeeOv#Cmg_ZVWdPbVT4z7pjDTBb9w{)$x%5QjI|L-Sgmm!au6SFRgG!?|m0kqn;Lm8z^DPvB&lXN8ha4piR{t+B%V7xMkHF%{ z{Cmn{Yw>yJ3IIaM9kNGF-NFHYi%Qaidf8KwNsl3 ztc^FqdnweII4tVq%yzq~Q6BrJ!~Up$QY?qTMbF*%j}}wF&Fs2I)%ldPs%qbxgPYjV z-0XW*-?qp}Ev5;3z};w)d@ zf)+W0&QS^cd9iAnY5zd)L}G;pMZ+F@nOD0e_;1)C%3F%q%>FkZ2-EkKasll7C9w;l zV(T`D^jrKer;tIe+F0gC3*<_sBF=BV0I>c3yv3V*QyJU0X|MbY5ZC~k|9E!6Fc6CK z%jEm+wq>UEi38@<9ZXe6#6nx^`&W{5I*N>5DB&llV0#*s$L)K=?_=V`miGZZk>nCG zn}4H+ud5D!(du8la)NsW+QwiPx?}|FiHv|l&=C)H+S%>Y#(%`Y5Wp7qMn5uypmssN z4d)m#TF;f;ZI1}oWMx#$+ShWF_KQ4%Q-7^(Z)m&$a=#nm?ADdbAMdw5@be_<$^LDZ zSg=S@&rUGny7vOffZNlNvDU;Y!|-a98no5dw6SSnJbMr3+gD@yb1kETaCQ4+KfwEZ zvJg+CiduY>Gti3l$x1|P;vrF$^Y`ay?gF9mQ%ps@Qa1?9L11XSfKy!?3hA|H6+i;m zA20yW$mY_}_*24mHJ){;?gG-HfRU#>ZE9n?5GaoSg5~^QA%qI1z{NkqoudDCx{I7R zxzF!VarnQx_$=28Tg6i~D)0tH2dZHqS>A&XXe)If=>4@PCi(x8go^ulDX|drvmE0e z3YdIv2ujy_9A3jG{{mttQjU9aTY*D^v?iU4sU&BKe z%dPIBH(J}%(cWY->avkZakCQoQ=kOa66um}D2XMdW3muS!`hZan-wy7sO*`35n{wwkzhh-RE?Yr zFoq3-|p{Gp-5}$Z_v}iT9@y<4=9KyUBa_95Npe`OajLzxvV!rXk!k%-S)Elb6*ribS#T!;a1>KDBN@5d zN$`~dzP{@m(}`CB>?UW&q&J7@7y@Cp-c%Q;W0ph53Bla0$~L?0SzDk7OR_e!LVCRk zY=gAb!fcqVOH0u8M-5O1X1rrMV_VcB04f|ec_=&;x@%Hb{iJi56$#rCS##rBYpi9| zzxu+0t>XuNnt?RU!Q{P+Pg`A)+Ba>I=8*EudgSrj{+eciVLCR@4d_0s3HPJG7azO4 z2~oGx{bQ|(;&;ibI!{OsU375J@`jp_2P~N)l*d@}>0|Ef|D?V6DVued6c1yqk>!Qd z61}%Fk0I%T%unt4CpSfTCr}K02Ed|d!Ess;gI9U-%hm{=i@||TUhg)>4lEpEdiZx$ zE=oJ^0Vj4^xe zdhC4#O4JJ*HXJyoL>K7XsACFfkL8INKaNU0VL$E9S+qzykIF|@Tnm&;OJl;5NRC5E zbOd)Y*i$p%>2wQl-impBNUz<(E|!b!aq0RKt%2{zX4}QxQ7=mQ z?O}y^W2ixJi1XFUGnM*1{5B_6Se2_NOK_AyVxE3=EX(lgvs97Kh3(FN(0nF8Bvx{f zf0d2*9I@hp#XHyT>I6_AAfl#6(%}T+(ih~FCtEG>^%6E*O@_bnXtQJ{2x8&Y?yT?e z5-L@wt6&ED0y_cWue6-zNgqExj6EM$CDIXzsTtF63K^_cNujejDcjN=G1>OS2XC^L z0zwg~G9HK{q?M+i;8$K+juM#)44~qGsRq@>74KxCy_c`JG*lHuAg+G-A)`FZ_||b> zTdrZS!~&8-15TN;o3){CfKN^DJObQeibwBTHLP{ZMIz`n?~L$QK&5Il{d~lM9<^o$ zqFi?OsMzlzQ91ubwBO%%Y8XL$NPo1as2cr@qIkTCF&}lb!Dfz+s1P@AZQ&U_c%LM7MwF+jXW|#a8Gh2C z*%bTM+he{(-8_2rxl<$k@}WqzfFi?If4K`w_f5MzE8L@xCoIw*53p!X{Cc&+MB@^g zaRP$|#wX|}uSzy8Y3w=`k!@Ig_pfprGu#T`M1&=QL9XaC&T4)#aQ7N^qb`G(H2h5v z9!)54A&oA4On7V^>ret0r}WdEz$YEaU5082E)%46R&!^whTRS}n>{KbPvWZ+UQZz@ zCPS$`_joAyb7*F^>tL77Iijb=j_JysVGeQn*I=)pS1r0uR&M8Yxo-42fLI?Tj1 zeQ&Ir2(-pNB+U9WOX@Ln>L`H~*O~@?W!g1*HZ9qz3`4s{t;nTj-Ud^=F0L4qvH?F$ z&Pxv+X{`=4P5k(sRDn=l41O}PJn0GM@$Am8g~3&!ihDTO4fmO2|N2-@qDL*Rs3)L& zDyg9$F&<(5RUMk7#Z8+U5oC*cyvMB3)qwlorB@U*{n5WwUz2M0MVm4dWcHJQ=0K&7 z=?QBre*K@}+{IDj0h*yxrm;M{{`zVZ^*f$pej}0SG|ZN=a^G@HTe1n6JzU*dk%#x3 z#=GddPgwC1kfV};mLkmF?ty8K7|Z1k*;ItpFyY6MZptQ@8++yV4IRk1dg|?|vXbVN zP(*Jv8SFCwdQ)H>kQMF%MrD0)u7RWCCvj|UxS;4q1B%J)DhUDJRkCzMLiRaX7q>6a ziY$#IF+YKMSiWSin~5h48&oI*nQ0cY{AFnEFWSUp0_+s+S#UiRe5b%=&v7cV%Dd#?sAiefVAMl$ij1tPaz_#&hZ4O(BfH1}bAv`F9=%QZIF!qPPojgqHuK&B8RO@oJBkYvE2R`I*T z!KeNJvBU~PA=UH&uVG1-iRyPUeX{tic_V?z^hyZ26T6BIr-hXxB(<0z-`RyXY9`Q7 zVt!Y8C%#N(Ap^Nfm1&Ed4_L+#V8kq}^{Ir$M*?fbc~QF^Iq8g~=itCu@h(X0Qb$(8 z6@uZ+0`1;nR>Xp1-LUKz`o|-TxhvJ%U__~uuoZ}c%hiunCPfrZD0klE^!~(mq|fWq ziu1L1?8Kj=?%|q3*A+t!uke+8t6PLk=@VR```Yj-6OK|S6uUcU>KksKw#I6w*{{w! z#;Xh5@_xv%tZcla9RxJca-H^oCTf2#zNzyx-wd{lmr?=UZvTwi?GQZ&Xx-}|;)ry2a-k#6Es z#@18`F+`!?g*t~xqV^U`c~*v!`YCrN9wjH*X{Gzxa#KDs5Ql||ubh3%7S`QLJ_FcU zg#ZFU@Wp1(mrsUjQBlzLyo*cc!g+j(js1j;28G*fT&beV|evp4ol)=P|uJXFR?1=H5=z`2<@Um#d!Nc(vuSr z0WuhHqQ(;OU7{@Ezp5oN3%bPq6#LZM;L9d-8$@@pb#@SRh!PExnr-?|{fz#}YCBD= zIisjUo;5dc4*lKPhh6$~jX;fTSEFD6fk<@AK0e-hNQ0U) zhxUiN$zxEg_SCP~bY{!|cu?sW|4`les%HWKh@sKIX|iU+U647}F3P#X>eo8tGFHxl zYYyHhnyEmQK=rMB9y`YJdateJuo4JgYFSl5+~cQmcYinVTMU#@vj;oU+3AdO2j)Kb zS;W19AlyLur>{7Y<cPCyu$b*tv9Y_5i#{ihy%C)`1FVwb+=7lH&z#p)QyDF=oA@erefW~ z2b86Fd6#x7^J-$SsbeyfQ{t-m0U{X7^|*}&DkmrqWrlWBa!^Hy zp(i_VB1bFX^E+9;)ZXFkEUhQrjVsSAxR<%BCdhNy`yYp=(K_JjA7fLE_H)Qi4)bl{ z9ZfLZZNx?Y0&61B*Oo>AXJK(#Evv@}w08`ND^I3Zu5KspdjC2i zdiSdfCHX54<*BTL5lupC!c$(pYyMrh+n@7e5_CvLU?Qv9-z}V_0xq%*Hw@!Wr7x*0 zy#CC7#^W5+k3hNKSDSXrzECBm&Z|LU6EvFiEH}FL4~GS*IR$x5G@|Y3Z|HKev~x+N zD(U1pMGnTQ^iY|)JO*5DdqgGNs5LKDOZ<+(XLPd&(3}2EOHTQw!jxvPy5Eg=VVhQz?ro|)B;Ej!B31;%|9gx!OYv# z2@pvl=Z#M=w*1zt+fiM#)y9|th6&I&vtK=l4+EObw4#CQ_xXS>&K-qV)G5)=h5)ZM z93ayEs*kq)MqLc1Jw{d+V_m`zR}*0;oizPlj((rMWp%5me2TiV483ml2$T6ksfq9h znQD*@m+IV~w)PH)r5gwiD`4g77)uW7551MWEMgKBLQzAgrTnkByXFU8=hMg9oclis zJT?!T)35v{e_vKYBT=cSAlnkCn;No=Y-gj zj_r;g+ok!`T8cU>!8=v3+Mm`TM1EO2uxw*GVhd#_&aAk3>5jt4qYI z$5SXhp`OA&4ML-LiXfiUP$5A)Hh(IB@ zmC{=2B2DSGbHAak@q+@ffIG?kwK%!L1w)yweZs5tNdBm}a#LWTjk#+E#Ew7_z(SQ> zty8wg>g%b4@ugjx8haOP(0!eEGpnRYWCP-OyP|X-Uc5JBj7VXqd_zc-hZ=NqLuyRm znuY_S--7`~f`D~b*0*dI0q^sfXrPETcYa^ z+uw8o5Sgf^a~hy*e<8t6pNh|cyLnl-KxI$X%LWHRdUzY2J>C9Nh!1cA02U$A|0(L1 z)!KyOabe>UC(Kdsv7kW9FxpYqZeT5Qx~a|6u5XiTd@qnG?wNu~C$)bo#?S{s0F_4N z>3kSTEmVeKZ=UL>2&1f%bMaZ7W3=U_LWD!4p+Z3N^UFg*h5wm<#Xl-`wI!wQUiy83Es z;QqJ(%P_v(bhX7@fQ*Xu_n$)7>W}uH=)afE734gqLyK+-U~D82%u3i}XK1Rc^8sMh z%wrCpafx!^Ed32iQ;%Vj>tkNunCbWJ`_HNzk&y6~bvh^P{@IQNa3fQ$ztyQITr6YF zP)Y4~4kM!88=YBVNYQ#1zi%lo`rrccXmAojB$dh}@@xGS9UQQP=B;b)lX0F1)zPDa zrKC42&mE5>ltW7cegs}|9nkDx!gBLs1}$lZE?XEC?%}^;u7%hV*HT4J^QuB;+zP|> zo_t=^Dh07vW4?BtHrwqm*J;cAp*nKo*`sEEOO_71ktmzR+S7fs<6)_bR8;qg$Rl|; z0ad?mc6-k?w5R0=#(gqBEIaHf)Z!@(XrE#$n00$Z=g{#{0UR?TY_a zSr05~ysMRlua3YiwfMu`@e2NX7=~WjCX&AH{1~E_OYhB`f4J7uE+m!|JTzGjxsxUrObJN)6iQGt!;CC04~H zh4QYLOAN;FB-vt$)N8Ssy0NFZ8a=)i9 ze$$xJb;jt3U^1YX*XcKd$Ao#de=C}R*9XQ=Zfb9Yw^7mwC)3{wnDl%ccaSz~uSA-D6Io%Vjw9a8ssH>N( zz}=Gyu`<)UWC+xD?4H3^LRhuoyUydQJvm?mC-wN$yy!xEg z%}i(Dr7OHE44^uhQd{m73h)6T#(^6#_ITaHP|0^jR{6e0y_vkL>JZj6{H+R!Nulm9 zJZz$=qdQG>&>Z|NroU|@YtTu1RMl3_vbdGOt~>jH=!o_@o|c5~3%HZZ1?&b$5Jli_ z!H5CP;$0{~Cb$5edX^FTZJ=(bc44u79z>g4nRz~0>3fW~wqdXz5Zs3hp^Foabz2po zPDnrU;YL7Lx=WeRhT)?$99J$$dXabtLagNJdOi>j<=Rod!@ZXm;J6KD3D70ntPk0S zMJZaWpQ0i{@8BBueuDY`iY|E@Pv!VyS2sU{aayG?#b0gT9~OZ|$9#-NJo^@1HF{)) zyNnY{=!iW{XcpK5Nc{&d+a7qJNLq6tp@eNcd$7pIP{$e@L$vHar!aMEQ3s(4z?E5J!^?1s?JyjM&wR^6gPu z)ShIOlN?|GTVDLqdYMusa;t>DNqfdKn+=(2gfn@EL;7H@ z`Dpv4*g%j{3yYnOb)*Y{S`TDa9bogDZVidP3m|!FZyA;|Y3uUmbr%s0n$;^ByD!kH z)81Vz(_uO=VrF>ZS$NFqOZj+x>U-RuYz^W>l{4sjABPel>VW99Yf|ox`)Vi(U!?pK zQ;+1Hu80#aN^+xapR_larFZNeudk~eab@U5pGBfc2xl($qFD*k%#6nhBGX?5>cWUdB-&)af>2;PiM#;eRL=V`oJevv z&B)J+L8>(1*@p315D=-hM^t0NqrWWwflkg(6&#xzHf75X-oV^9lnXin-yd#QS2?2` zHkf>K7fO1D=sSow2P=5aCEl8MaOoUk0X(57;f91p^$y*`eoC~MZg1n?#6y9Vy(x2@ zBjfF?9Af$h?vb@{WmExH;SoD&femMUgz7xhW7(sqg5Hd~^i+ME|9oSfc=#T!}J z#>f*Ea3D2X5`xBCfA5Ki+0!wh$QuYZ%4bj^6_1Gnc!`v8E8cxqQb@#DxAuXKilG;T z3PtuA4L>bx*_rl;A8{5Hm+I#;f1c#_ADXH$f;=MR1E9SMns^#MuC;7A;?3=n55K2| z9pTb0qC*y>OiVUu@=Six;XtYj6fbgBj<(IJO}u~oXx8?IsfKu4(mY~e1(a1nDQ)cb z{`rKLl%$P8o{i<4^DW0iS87)@W$ty52;wFe42KZ=HI*XANRqaNf0vd9Z<~6m0$n6k z;+gnWRXPq|kIaF+F)DmNet0(KnOFpyL!D(OY`gYoGf(BHLlrl75(oEFFT`&n1{t~KQjVm z9eZ?I3AcYb(4x^559*XYn(&y!8=jV$GbNo>k(Y={VTKyc5Zw_);3eiFaZ~s)-?M)7 z$768;O`|CTwG3Fy?v{@`!QnST<-tEorHBF%I4>F#`syrrPCp~g9Q*8mZ*G@C+wz52 zfk^3d=$}tlUM$u6l4W(H1dw!0dpx#VM1Ru*;HsR1(@KDX$(Ow2Uy{RTTUP7nd7Xhm zoLf56RwS%kMWh^AWqht=^&&zxv-3_v)5M2-aO;Bi?btfBh1#41eRvESDb>myLq@l* z9zQfaFUSL&`-R<@*KlKpvRaKFhi!&-fY4Hi%RTm#bPV&Oer7(pUR^41+i z-b6)Ere~l3_LA+vX4|SXlwc$h**!c;p)Yrc)T8>=X2p&FwX9;_4a`dQY>0Y>&M;Ge z{sD%8>j-i2vI?^jXc{rY=f_Y_Ylu88sdhatBeZPhsp{1e0YY6&rUctxLC!=Mh5=sp&HUFfH_Bcg#_cE8Zea z&B&IA;el6BXE8db&}g9aaAJZ_3n?0A6hYr~$s{=uE@CK1C}M6DuK=AG_$0?mo`QQ$ z-vyq2c(TF>>*f`+uhLEOy{v2ye<_C=WaN(>Dy$iQw-4ZV*Iq4OSTZ-EXH8nt=E`KE zNE~qJ2OUE!i;cd>f30Q`+wp|%d#OY{&WN#GmOONmb!TDWLZE|YZdU3|(mXhdHj zk^d+Pe%R6W#|MEJKiZFnsGI@*l|{A4k3%rs&=j&Y)}6g=^FTM#L0syE;E-94C+GMh z>j4p4?GWRdiJgI&5ECk7^(Fu=JS_QL)+ec8-@ zvgyj&VJy00;@BCMJGP2_JgqI4Wh`FPJb;UP7xf1mMv)LX|CyKaH^Q%^ETvkMqge~{ zLaLd^S{hXAV?szd*hiWulY8m@2V`gcH$}ogdgOP8Ff)4b*66vIX1^Li`4llC*Vb<5 z|M4bk9yZ>Xn{Jn&G?s_1ID43+1P72dU>5p*%Aj(T zzhCWRcQ?c02-FS_*2+0Vpkt)X zl<9FqeC9y@)^7n$y_B;b2aunuA5P8&375E}PL*JW9}qWvOdPAtd-7v?E>HP3-Aglg zGRjQ}=uWUaFznA*TmJe&QSv$@Ar@bO9E0A?Pw+85&btZUis7(ihMp76nM5;QIbjd? zz+2+SS85Yt?3$8T*JFY=F6&{G$=}xGyDlv#3}$Kxl=`iR=Z>hTLreWEu4bu z8IsT+Y06!x8o)c-;u5qY;X6coYT=O9w zgfM&lW@mWhR@*Zw_W2u$*oamu7tl4&fWYXL>T|EhDJXg^U-6l#^Fd+N0@x^Hw+^&FM0z=4 zdRCi59!#%JFO{2v#1c}7nVJY-6KrujH&ApaYlqly((6Z+aG0(D#NzZxo-b`oE1~*1_k_rOS1kMeTwrp5)oRk&`RIPm3j7Ma27>eE zjEvzSNDt3E<`sMrZKe3DwXI}Yro!VcJ>cg7*Cv4Cn`N1llwQAD<}JkPQ=bXbBU89J znK%f9QML(}74W{$8Sna#dAT7T>WNkJga9K~LMJ%vi@3h3yW)TSO=~uOxxbgY3y*s4 zv1%17rAFouyFl+S$wkreLG4$vdIuFXCr}Rxjc@E~K&C36ybgELL~}DEuz4Z8SI)5k z%yny&yl*FYMJrn8r%zMxzl6*0038et!-~R4Q0sXwotlB1B4zvE=<1emQs|<+Sa~Qr z`r-bX5(sn5je(>~Ee7;991=WJA_gFp^FhK1QE&o1d|(j2l_52HkYHY!P{q zFeAutH)2XHK@BpNWE z%mLSw;=Q}$`WBmP3{=@5R9lejeMYOY2Dr`NB*72&Ag)B==HdJBRPw$TK9e|oUYTo- zGJQmlnZ5KMWYFAo>-brO_4RJn_;G`<%xgQt4RQWvNwGX^ zXE*kmnATFXK#MD2*v?=Tq8sHJ)tEn>!pnj~^<&d|@6cX-5q*RH^>O>1+uPAY{ zSzSCkB7_);-?f(2e<(}*Iy{2t!a?{VfC8H{%2XIVi}J=fy^T+>FbwRg84 z=$s_vSd4vZ#1GHYg1k~(bs;c&HE4CE`tsI|{pEe{iKN&+gr71i{^Ic@-l@8qYZDK}!_!*pu{5u`06`a=)`EcTN zNbyx391VpF+Y7K0{^E7)4YUMJ*%)dXn&h3BY&;2)7_W%x1^G1@24R^4bV(YORqyl4 zR=gStnmt8T0&#Xe|>n^Xx##6iemM+r7K6e(e;K z{q~zw{6n1jEEspaMZv%9GmAz)%33!UPdl_`SlaU%U_uxq2kmG~L?ijcWi-&x@~sH!&kqr2WA4C)=aR*J^) zWxWu)bbKwR*R|kcf>q=F0DAYc=$DTtAJM_V!QRC>==*-k0|#B?)-{ z9!x&2Ny4PjFa{33bNyAaN9l`@-IqU22|qjo`cp=9IEC(hfm7=Fo#&h@p~{nBCh$=PM97b#BB6JJu6l6Xly>Ks7|OJEoKuK{nP&%mO8 zPikPj=^wOMZD+&cq6uNNS*qd}+e58|K3M@znmGYr!Tt)8#^|$1XE!lGvX79u)h=v0 z3=Y?`lx?oe60{Jk*{5l@^jLNgKQR!kQZ31Oo!wgWlt?RAp-nh&ny1|f`tZOlnaXhW zrvQRe)4euc3qfW`45Po8=X=H<;WAEAqfPl>qH^a#ll9v45Z|9t1l{2V=VXGz=4;Tq z413H8)#3c zQ}Unda+D3na!KyS$>Dd#9+uR` z$o(kn0OB+1q^5jHHgT-(Z23xAgG`q6!K!H26@h@r6<$JwH(oztM$fJz;LRiw#A&)4 z<`y0u=33^Mt1o^gqBr4ZjZRIJec{~#1H8o5c7+uw-0P6Ig^kOOiFo!eHa$R_q#~W@ z1nGMfyI5iwL!Tb^{AujQM^|51}GzIaV_ zFc8-$TPVdjyX7QGU_=$%dbQ5UTDU-_sJf48MhI)voPclQL;n*1WqH)4`*#fPPDF0MN5r!VijO*leadrDDq{G$#6bLGe)&g|1 z`uJ?yyE?=hQ2YDic3OEgQMvW~niExv!^J{qNoG!Wn!bd8@m3J8HIWhh4sBLQo9%H2 z;I$bA2ttO0R!@}BKoFm%7)EETNoa`%Xytck>H8d za{zXnNeI(AO1E1o%}W4fEZCIb31;UQq+$aCm8sMLi%-kh!lrbBdalADD;I%VsUhb^ zMlh>=J?@sI=Kt;G{@0FRQfFtgA8^)y<*O{J*DRZ1n6zhF)ahVU{I)Axn*L2#B!G`s zi_utUe%vhos=Ni_i>64QL!C_ Date: Thu, 10 Oct 2024 14:36:12 +0530 Subject: [PATCH 14/21] chore: updated issue draft store --- .../issue/workspace-draft/issue.store.ts | 23 ++++--------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/web/core/store/issue/workspace-draft/issue.store.ts b/web/core/store/issue/workspace-draft/issue.store.ts index e4c9f53dd87..a0b82f98f85 100644 --- a/web/core/store/issue/workspace-draft/issue.store.ts +++ b/web/core/store/issue/workspace-draft/issue.store.ts @@ -207,14 +207,11 @@ export class WorkspaceDraftIssues implements IWorkspaceDraftIssues { updateIssue = async (workspaceSlug: string, issueId: string, payload: Partial) => { try { - this.loader = "create"; + this.loader = "update"; const response = await workspaceDraftService.updateIssue(workspaceSlug, issueId, payload); if (response) { - runInAction(() => { - if (!this.issuesMap[response.id]) set(this.issuesMap, response.id, response); - else update(this.issuesMap, response.id, (prevIssue) => ({ ...prevIssue, ...response })); - }); + runInAction(() => update(this.issuesMap, response.id, (prevIssue) => ({ ...prevIssue, ...response }))); } this.loader = undefined; @@ -230,7 +227,7 @@ export class WorkspaceDraftIssues implements IWorkspaceDraftIssues { this.loader = "delete"; const response = await workspaceDraftService.deleteIssue(workspaceSlug, issueId); - runInAction(() => {}); + runInAction(() => unset(this.issuesMap, issueId)); this.loader = undefined; return response; @@ -245,9 +242,7 @@ export class WorkspaceDraftIssues implements IWorkspaceDraftIssues { this.loader = "move"; const response = await workspaceDraftService.moveIssue(workspaceSlug, issueId, payload); - - // remove the issue from the draft issues list and fetch the issue from the issue list - runInAction(() => {}); + runInAction(() => unset(this.issuesMap, issueId)); this.loader = undefined; return response; @@ -260,11 +255,7 @@ export class WorkspaceDraftIssues implements IWorkspaceDraftIssues { addCycleToIssue = async (workspaceSlug: string, issueId: string, cycleId: string) => { try { this.loader = "update"; - - const response = this.updateIssue(workspaceSlug, issueId, { cycle_id: cycleId }); - runInAction(() => {}); - - this.loader = undefined; + const response = await this.updateIssue(workspaceSlug, issueId, { cycle_id: cycleId }); return response; } catch (error) { this.loader = undefined; @@ -275,11 +266,7 @@ export class WorkspaceDraftIssues implements IWorkspaceDraftIssues { addModulesToIssue = async (workspaceSlug: string, issueId: string, moduleIds: string[]) => { try { this.loader = "update"; - const response = this.updateIssue(workspaceSlug, issueId, { module_ids: moduleIds }); - runInAction(() => {}); - - this.loader = undefined; return response; } catch (error) { this.loader = undefined; From 8fab9edb574621641865ef22ad6d6a751591aed0 Mon Sep 17 00:00:00 2001 From: gurusainath Date: Thu, 10 Oct 2024 14:56:03 +0530 Subject: [PATCH 15/21] chore: updated issue type cleanup in components --- .../quick-action-dropdowns/index.ts | 2 +- .../workspace-draft/draft-issue-block.tsx | 123 +++++++----------- .../draft-issue-properties.tsx | 14 +- .../quick-action.tsx} | 2 +- .../issues/workspace-draft/root.tsx | 9 +- 5 files changed, 63 insertions(+), 87 deletions(-) rename web/core/components/issues/{issue-layouts/quick-action-dropdowns/workspace-draft.tsx => workspace-draft/quick-action.tsx} (98%) diff --git a/web/core/components/issues/issue-layouts/quick-action-dropdowns/index.ts b/web/core/components/issues/issue-layouts/quick-action-dropdowns/index.ts index d283cf3ec6d..dbc1e9f5a74 100644 --- a/web/core/components/issues/issue-layouts/quick-action-dropdowns/index.ts +++ b/web/core/components/issues/issue-layouts/quick-action-dropdowns/index.ts @@ -4,4 +4,4 @@ export * from "./project-issue"; export * from "./archived-issue"; export * from "./draft-issue"; export * from "./all-issue"; -export * from "./workspace-draft"; +export * from "../../workspace-draft/quick-action"; diff --git a/web/core/components/issues/workspace-draft/draft-issue-block.tsx b/web/core/components/issues/workspace-draft/draft-issue-block.tsx index c437a0a3d37..3368c2fbb06 100644 --- a/web/core/components/issues/workspace-draft/draft-issue-block.tsx +++ b/web/core/components/issues/workspace-draft/draft-issue-block.tsx @@ -1,8 +1,6 @@ "use client"; import React, { FC, useRef } from "react"; import { observer } from "mobx-react"; -// plane types -import { TIssue } from "@plane/types"; // ui import { Row, Spinner, Tooltip } from "@plane/ui"; // helper @@ -21,32 +19,22 @@ type Props = { }; export const DraftIssueBlock: FC = observer((props) => { + // props const { workspaceSlug, issueId } = props; - const { issuesMap, updateIssue } = useWorkspaceDraftIssues(); + // hooks + const { getIssueById, updateIssue } = useWorkspaceDraftIssues(); const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme(); const { getIsIssuePeeked, setPeekIssue } = useIssueDetail(); const { getProjectIdentifierById } = useProject(); - + // ref const issueRef = useRef(null); - - const issue = issuesMap[issueId]; - - const projectIdentifier = getProjectIdentifierById(issue.project_id); - - const handleIssuePeekOverview = (issue: TIssue) => - workspaceSlug && - issue && - issue.project_id && - issue.id && - !getIsIssuePeeked(issue.id) && - setPeekIssue({ workspaceSlug, projectId: issue.project_id, issueId: issue.id }); + // derived values + const issue = getIssueById(issueId); + const projectIdentifier = (issue && issue.project_id && getProjectIdentifierById(issue.project_id)) || undefined; + if (!issue || !projectIdentifier) return null; return ( -
handleIssuePeekOverview(issue)} - className=" relative border-b border-b-custom-border-200 w-full cursor-pointer" - > +
= observer((props) => { {/* sub-issues chevron */}
- - {issue?.tempId !== undefined && ( -
- )}
= observer((props) => {

{issue.name}

- {!issue?.tempId && ( -
- {}} - handleDelete={async () => {}} - /> -
- )} + + {/* quick actions */} +
+ {}} + handleDelete={async () => {}} + /> +
+
- {!issue?.tempId ? ( - <> - { - await updateIssue(workspaceSlug, issueId, data); - }} - activeLayout="List" - /> -
{ - e.preventDefault(); - e.stopPropagation(); - }} - > - {}} - handleDelete={async () => {}} - /> -
- - ) : ( -
- -
- )} + { + await updateIssue(workspaceSlug, issueId, data); + }} + activeLayout="List" + /> +
{ + e.preventDefault(); + e.stopPropagation(); + }} + > + {}} + handleDelete={async () => {}} + /> +
diff --git a/web/core/components/issues/workspace-draft/draft-issue-properties.tsx b/web/core/components/issues/workspace-draft/draft-issue-properties.tsx index 8a64dd3b432..28b25ed6073 100644 --- a/web/core/components/issues/workspace-draft/draft-issue-properties.tsx +++ b/web/core/components/issues/workspace-draft/draft-issue-properties.tsx @@ -7,7 +7,7 @@ import { useParams, usePathname } from "next/navigation"; // icons import { CalendarCheck2, CalendarClock } from "lucide-react"; // types -import { TIssue, TIssuePriorities } from "@plane/types"; +import { TIssue, TIssuePriorities, TWorkspaceDraftIssue } from "@plane/types"; // components import { DateDropdown, @@ -38,7 +38,9 @@ import { IssuePropertyLabels } from "../issue-layouts"; export interface IIssueProperties { issue: TIssue; - updateIssue: ((projectId: string | null, issueId: string, data: Partial) => Promise) | undefined; + updateIssue: + | ((projectId: string | null, issueId: string, data: Partial) => Promise) + | undefined; className: string; activeLayout: string; } @@ -49,7 +51,7 @@ export const DraftIssueProperties: React.FC = observer((props) const { getProjectById } = useProject(); const { labelMap } = useLabel(); const { captureIssueEvent } = useEventTracker(); - const { changeModulesInIssue } = useWorkspaceDraftIssues(); + const { addCycleToIssue, addModulesToIssue } = useWorkspaceDraftIssues(); const { areEstimateEnabledByProjectId } = useProjectEstimates(); const { getStateById } = useProjectState(); const { isMobile } = usePlatformOS(); @@ -67,11 +69,11 @@ export const DraftIssueProperties: React.FC = observer((props) () => ({ addModulesToIssue: async (moduleIds: string[]) => { if (!workspaceSlug || !issue.project_id || !issue.id) return; - await changeModulesInIssue?.(workspaceSlug.toString(), issue.project_id, issue.id, moduleIds, []); + await addModulesToIssue?.(workspaceSlug.toString(), issue.project_id, issue.id, moduleIds, []); }, removeModulesFromIssue: async (moduleIds: string[]) => { if (!workspaceSlug || !issue.project_id || !issue.id) return; - await changeModulesInIssue?.(workspaceSlug.toString(), issue.project_id, issue.id, [], moduleIds); + await addModulesToIssue?.(workspaceSlug.toString(), issue.project_id, issue.id, [], moduleIds); }, addIssueToCycle: async (cycleId: string) => { if (!workspaceSlug || !issue.project_id || !issue.id) return; @@ -84,7 +86,7 @@ export const DraftIssueProperties: React.FC = observer((props) // await removeCycleFromIssue?.(workspaceSlug.toString(), issue.project_id, issue.id); }, }), - [workspaceSlug, issue, changeModulesInIssue] + [workspaceSlug, issue, addModulesToIssue] ); const handleState = (stateId: string) => { diff --git a/web/core/components/issues/issue-layouts/quick-action-dropdowns/workspace-draft.tsx b/web/core/components/issues/workspace-draft/quick-action.tsx similarity index 98% rename from web/core/components/issues/issue-layouts/quick-action-dropdowns/workspace-draft.tsx rename to web/core/components/issues/workspace-draft/quick-action.tsx index c0889ab305d..dd223d67595 100644 --- a/web/core/components/issues/issue-layouts/quick-action-dropdowns/workspace-draft.tsx +++ b/web/core/components/issues/workspace-draft/quick-action.tsx @@ -16,7 +16,7 @@ import { EIssuesStoreType } from "@/constants/issue"; // helpers import { cn } from "@/helpers/common.helper"; // types -import { IQuickActionProps } from "../list/list-view-types"; +import { IQuickActionProps } from "../issue-layouts/list/list-view-types"; export const WorkspaceDraftIssueQuickActions: React.FC = observer((props) => { const { diff --git a/web/core/components/issues/workspace-draft/root.tsx b/web/core/components/issues/workspace-draft/root.tsx index e5e61d3aae0..a61e48aa01f 100644 --- a/web/core/components/issues/workspace-draft/root.tsx +++ b/web/core/components/issues/workspace-draft/root.tsx @@ -10,7 +10,6 @@ import { cn } from "@/helpers/common.helper"; // hooks import { useWorkspaceDraftIssues } from "@/hooks/store"; // components -import { IssuePeekOverview } from "../peek-overview"; import { DraftIssueBlock } from "./draft-issue-block"; import { WorkspaceDraftEmptyState } from "./empty-state"; import { WorkspaceDraftIssuesLoader } from "./loader"; @@ -45,10 +44,9 @@ export const WorkspaceDraftIssuesRoot: FC = observer( return (
- {issuesMap && - Object.keys(issuesMap).map((issueId: string) => ( - - ))} + {issueIds.map((issueId: string) => ( + + ))}
{loader === "pagination" && issueIds.length >= 0 ? ( @@ -67,7 +65,6 @@ export const WorkspaceDraftIssuesRoot: FC = observer( Load More ↓
)} -
); }); From d7d8f99a8e1d56e12b12b91441df868027001ea0 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Thu, 10 Oct 2024 15:15:14 +0530 Subject: [PATCH 16/21] chore: code refactor --- .../issues/workspace-draft/delete-modal.tsx | 105 +++++++++++ .../workspace-draft/draft-issue-block.tsx | 5 +- .../draft-issue-properties.tsx | 169 ++++-------------- .../issues/workspace-draft/index.ts | 1 + .../issues/workspace-draft/quick-action.tsx | 24 ++- 5 files changed, 163 insertions(+), 141 deletions(-) create mode 100644 web/core/components/issues/workspace-draft/delete-modal.tsx diff --git a/web/core/components/issues/workspace-draft/delete-modal.tsx b/web/core/components/issues/workspace-draft/delete-modal.tsx new file mode 100644 index 00000000000..25e181924c0 --- /dev/null +++ b/web/core/components/issues/workspace-draft/delete-modal.tsx @@ -0,0 +1,105 @@ +"use client"; + +import { useEffect, useState } from "react"; +// types +import { TWorkspaceDraftIssue } from "@plane/types"; +// ui +import { AlertModalCore, TOAST_TYPE, setToast } from "@plane/ui"; +// constants +import { PROJECT_ERROR_MESSAGES } from "@/constants/project"; +// hooks +import { useIssues, useProject, useUser, useUserPermissions } from "@/hooks/store"; +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; +type Props = { + isOpen: boolean; + handleClose: () => void; + dataId?: string | null | undefined; + data?: TWorkspaceDraftIssue; + isSubIssue?: boolean; + onSubmit?: () => Promise; +}; + +export const WorkspaceDraftIssueDeleteIssueModal: React.FC = (props) => { + const { dataId, data, isOpen, handleClose, isSubIssue = false, onSubmit } = props; + // states + const [isDeleting, setIsDeleting] = useState(false); + // store hooks + const { issueMap } = useIssues(); + const { getProjectById } = useProject(); + const { allowPermissions } = useUserPermissions(); + + const { data: currentUser } = useUser(); + + // derived values + const canPerformProjectAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); + + useEffect(() => { + setIsDeleting(false); + }, [isOpen]); + + if (!dataId && !data) return null; + + // derived values + const issue = data ? data : issueMap[dataId!]; + const projectDetails = getProjectById(issue?.project_id); + const isIssueCreator = issue?.created_by === currentUser?.id; + const authorized = isIssueCreator || canPerformProjectAdminActions; + + const onClose = () => { + setIsDeleting(false); + handleClose(); + }; + + const handleIssueDelete = async () => { + setIsDeleting(true); + + if (!authorized) { + setToast({ + title: PROJECT_ERROR_MESSAGES.permissionError.title, + type: TOAST_TYPE.ERROR, + message: PROJECT_ERROR_MESSAGES.permissionError.message, + }); + onClose(); + return; + } + if (onSubmit) + await onSubmit() + .then(() => { + setToast({ + type: TOAST_TYPE.SUCCESS, + title: "Success!", + message: `${isSubIssue ? "Sub-issue" : "Issue"} deleted successfully`, + }); + onClose(); + }) + .catch((errors) => { + const isPermissionError = errors?.error === "Only admin or creator can delete the issue"; + const currentError = isPermissionError + ? PROJECT_ERROR_MESSAGES.permissionError + : PROJECT_ERROR_MESSAGES.issueDeleteError; + setToast({ + title: currentError.title, + type: TOAST_TYPE.ERROR, + message: currentError.message, + }); + }) + .finally(() => onClose()); + }; + + return ( + + Are you sure you want to delete issue{" "} + {projectDetails?.identifier} + {""}? All of the data related to the issue will be permanently removed. This action cannot be undone. + + } + /> + ); +}; diff --git a/web/core/components/issues/workspace-draft/draft-issue-block.tsx b/web/core/components/issues/workspace-draft/draft-issue-block.tsx index 3368c2fbb06..711204ec1a8 100644 --- a/web/core/components/issues/workspace-draft/draft-issue-block.tsx +++ b/web/core/components/issues/workspace-draft/draft-issue-block.tsx @@ -2,11 +2,11 @@ import React, { FC, useRef } from "react"; import { observer } from "mobx-react"; // ui -import { Row, Spinner, Tooltip } from "@plane/ui"; +import { Row, Tooltip } from "@plane/ui"; // helper import { cn } from "@/helpers/common.helper"; // hooks -import { useAppTheme, useIssueDetail, useProject, useWorkspaceDraftIssues } from "@/hooks/store"; +import { useAppTheme, useProject, useWorkspaceDraftIssues } from "@/hooks/store"; // plane-web components import { IdentifierText } from "@/plane-web/components/issues"; // local components @@ -24,7 +24,6 @@ export const DraftIssueBlock: FC = observer((props) => { // hooks const { getIssueById, updateIssue } = useWorkspaceDraftIssues(); const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme(); - const { getIsIssuePeeked, setPeekIssue } = useIssueDetail(); const { getProjectIdentifierById } = useProject(); // ref const issueRef = useRef(null); diff --git a/web/core/components/issues/workspace-draft/draft-issue-properties.tsx b/web/core/components/issues/workspace-draft/draft-issue-properties.tsx index 28b25ed6073..7150012764c 100644 --- a/web/core/components/issues/workspace-draft/draft-issue-properties.tsx +++ b/web/core/components/issues/workspace-draft/draft-issue-properties.tsx @@ -37,7 +37,7 @@ import { usePlatformOS } from "@/hooks/use-platform-os"; import { IssuePropertyLabels } from "../issue-layouts"; export interface IIssueProperties { - issue: TIssue; + issue: TWorkspaceDraftIssue; updateIssue: | ((projectId: string | null, issueId: string, data: Partial) => Promise) | undefined; @@ -68,86 +68,37 @@ export const DraftIssueProperties: React.FC = observer((props) const issueOperations = useMemo( () => ({ addModulesToIssue: async (moduleIds: string[]) => { - if (!workspaceSlug || !issue.project_id || !issue.id) return; - await addModulesToIssue?.(workspaceSlug.toString(), issue.project_id, issue.id, moduleIds, []); + if (!workspaceSlug || !issue.id) return; + await addModulesToIssue(workspaceSlug.toString(), issue.id, moduleIds); }, removeModulesFromIssue: async (moduleIds: string[]) => { - if (!workspaceSlug || !issue.project_id || !issue.id) return; - await addModulesToIssue?.(workspaceSlug.toString(), issue.project_id, issue.id, [], moduleIds); + if (!workspaceSlug || !issue.id) return; + await addModulesToIssue(workspaceSlug.toString(), issue.id, moduleIds); }, addIssueToCycle: async (cycleId: string) => { - if (!workspaceSlug || !issue.project_id || !issue.id) return; - // TODO: Uncomment this after adding function to draft issue store - // await addCycleToIssue?.(workspaceSlug.toString(), issue.project_id, cycleId, issue.id); + if (!workspaceSlug || !issue.id) return; + await addCycleToIssue(workspaceSlug.toString(), issue.id, cycleId); }, removeIssueFromCycle: async () => { - if (!workspaceSlug || !issue.project_id || !issue.id) return; - // TODO: Uncomment this after adding function to draft issue store - // await removeCycleFromIssue?.(workspaceSlug.toString(), issue.project_id, issue.id); + if (!workspaceSlug || !issue.id) return; + // TODO: To be checked + await addCycleToIssue(workspaceSlug.toString(), issue.id, ""); }, }), - [workspaceSlug, issue, addModulesToIssue] + [workspaceSlug, issue, addCycleToIssue, addModulesToIssue] ); - const handleState = (stateId: string) => { - updateIssue && - updateIssue(issue.project_id, issue.id, { state_id: stateId }).then(() => { - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...issue, state: "SUCCESS", element: currentLayout }, - path: pathname, - updates: { - changed_property: "state", - change_details: stateId, - }, - }); - }); - }; + const handleState = (stateId: string) => + issue?.project_id && updateIssue && updateIssue(issue.project_id, issue.id, { state_id: stateId }); - const handlePriority = (value: TIssuePriorities) => { - updateIssue && - updateIssue(issue.project_id, issue.id, { priority: value }).then(() => { - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...issue, state: "SUCCESS", element: currentLayout }, - path: pathname, - updates: { - changed_property: "priority", - change_details: value, - }, - }); - }); - }; + const handlePriority = (value: TIssuePriorities) => + issue?.project_id && updateIssue && updateIssue(issue.project_id, issue.id, { priority: value }); - const handleLabel = (ids: string[]) => { - updateIssue && - updateIssue(issue.project_id, issue.id, { label_ids: ids }).then(() => { - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...issue, state: "SUCCESS", element: currentLayout }, - path: pathname, - updates: { - changed_property: "labels", - change_details: ids, - }, - }); - }); - }; + const handleLabel = (ids: string[]) => + issue?.project_id && updateIssue && updateIssue(issue.project_id, issue.id, { label_ids: ids }); - const handleAssignee = (ids: string[]) => { - updateIssue && - updateIssue(issue.project_id, issue.id, { assignee_ids: ids }).then(() => { - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...issue, state: "SUCCESS", element: currentLayout }, - path: pathname, - updates: { - changed_property: "assignees", - change_details: ids, - }, - }); - }); - }; + const handleAssignee = (ids: string[]) => + issue?.project_id && updateIssue && updateIssue(issue.project_id, issue.id, { assignee_ids: ids }); const handleModule = useCallback( (moduleIds: string[] | null) => { @@ -161,15 +112,8 @@ export const DraftIssueProperties: React.FC = observer((props) else modulesToAdd.push(moduleId); if (modulesToAdd.length > 0) issueOperations.addModulesToIssue(modulesToAdd); if (modulesToRemove.length > 0) issueOperations.removeModulesFromIssue(modulesToRemove); - - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...issue, state: "SUCCESS", element: currentLayout }, - path: pathname, - updates: { changed_property: "module_ids", change_details: { module_ids: moduleIds } }, - }); }, - [issueOperations, captureIssueEvent, currentLayout, pathname, issue] + [issueOperations, currentLayout, pathname, issue] ); const handleCycle = useCallback( @@ -177,65 +121,26 @@ export const DraftIssueProperties: React.FC = observer((props) if (!issue || issue.cycle_id === cycleId) return; if (cycleId) issueOperations.addIssueToCycle?.(cycleId); else issueOperations.removeIssueFromCycle?.(); - - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...issue, state: "SUCCESS", element: currentLayout }, - path: pathname, - updates: { changed_property: "cycle", change_details: { cycle_id: cycleId } }, - }); }, - [issue, issueOperations, captureIssueEvent, currentLayout, pathname] + [issue, issueOperations, currentLayout, pathname] ); - const handleStartDate = (date: Date | null) => { + const handleStartDate = (date: Date | null) => + issue?.project_id && updateIssue && - updateIssue(issue.project_id, issue.id, { start_date: date ? renderFormattedPayloadDate(date) : null }).then( - () => { - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...issue, state: "SUCCESS", element: currentLayout }, - path: pathname, - updates: { - changed_property: "start_date", - change_details: date ? renderFormattedPayloadDate(date) : null, - }, - }); - } - ); - }; + updateIssue(issue.project_id, issue.id, { + start_date: date ? (renderFormattedPayloadDate(date) ?? undefined) : undefined, + }); - const handleTargetDate = (date: Date | null) => { + const handleTargetDate = (date: Date | null) => + issue?.project_id && updateIssue && - updateIssue(issue.project_id, issue.id, { target_date: date ? renderFormattedPayloadDate(date) : null }).then( - () => { - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...issue, state: "SUCCESS", element: currentLayout }, - path: pathname, - updates: { - changed_property: "target_date", - change_details: date ? renderFormattedPayloadDate(date) : null, - }, - }); - } - ); - }; + updateIssue(issue.project_id, issue.id, { + target_date: date ? (renderFormattedPayloadDate(date) ?? undefined) : undefined, + }); - const handleEstimate = (value: string | undefined) => { - updateIssue && - updateIssue(issue.project_id, issue.id, { estimate_point: value }).then(() => { - captureIssueEvent({ - eventName: ISSUE_UPDATED, - payload: { ...issue, state: "SUCCESS", element: currentLayout }, - path: pathname, - updates: { - changed_property: "estimate_point", - change_details: value, - }, - }); - }); - }; + const handleEstimate = (value: string | undefined) => + issue?.project_id && updateIssue && updateIssue(issue.project_id, issue.id, { estimate_point: value }); if (!issue.project_id) return null; @@ -267,7 +172,6 @@ export const DraftIssueProperties: React.FC = observer((props) showTooltip />
- {/* */} {/* priority */}
@@ -280,7 +184,6 @@ export const DraftIssueProperties: React.FC = observer((props) showTooltip />
- {/* */} {/* label */} @@ -319,7 +222,9 @@ export const DraftIssueProperties: React.FC = observer((props) placeholder="Due date" icon={} buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"} - buttonClassName={shouldHighlightIssueDueDate(issue.target_date, stateDetails?.group) ? "text-red-500" : ""} + buttonClassName={ + shouldHighlightIssueDueDate(issue?.target_date || null, stateDetails?.group) ? "text-red-500" : "" + } clearIconClassName="!text-custom-text-100" optionsClassName="z-10" renderByDefault={isMobile} @@ -367,7 +272,7 @@ export const DraftIssueProperties: React.FC = observer((props) Promise; + handleUpdate?: (data: TIssue) => Promise; + handleMoveToIssues?: () => Promise; + customActionButton?: React.ReactElement; + portalElement?: HTMLDivElement | null; + placements?: Placement; + parentRef: React.RefObject; +} export const WorkspaceDraftIssueQuickActions: React.FC = observer((props) => { const { @@ -31,7 +43,7 @@ export const WorkspaceDraftIssueQuickActions: React.FC = obse } = props; // states const [createUpdateIssueModal, setCreateUpdateIssueModal] = useState(false); - const [issueToEdit, setIssueToEdit] = useState(undefined); + const [issueToEdit, setIssueToEdit] = useState(undefined); const [deleteIssueModal, setDeleteIssueModal] = useState(false); const duplicateIssuePayload = omit( @@ -79,7 +91,7 @@ export const WorkspaceDraftIssueQuickActions: React.FC = obse return ( <> - setDeleteIssueModal(false)} From 6f90514ad964c2835804e813f41d2b3dc0356321 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Thu, 10 Oct 2024 15:18:41 +0530 Subject: [PATCH 17/21] fix: build error --- web/app/[workspaceSlug]/(projects)/drafts/header.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/app/[workspaceSlug]/(projects)/drafts/header.tsx b/web/app/[workspaceSlug]/(projects)/drafts/header.tsx index edc6693bd77..dfe9c5fa320 100644 --- a/web/app/[workspaceSlug]/(projects)/drafts/header.tsx +++ b/web/app/[workspaceSlug]/(projects)/drafts/header.tsx @@ -1,7 +1,6 @@ "use client"; import { FC, useState } from "react"; -import { EUserPermissions, EUserPermissionsLevel } from "ee/constants/user-permissions"; import { observer } from "mobx-react"; import { PenSquare } from "lucide-react"; // ui @@ -13,6 +12,8 @@ import { CreateUpdateIssueModal } from "@/components/issues"; import { EIssuesStoreType } from "@/constants/issue"; // hooks import { useUserPermissions } from "@/hooks/store"; +// plane-web +import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; export const WorkspaceDraftHeader: FC = observer(() => { // state From 54c22b3354af6e1864856d3732bc81aaf44bf4a7 Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Thu, 10 Oct 2024 15:27:16 +0530 Subject: [PATCH 18/21] fix: quick actions --- .../issues/workspace-draft/draft-issue-block.tsx | 12 +++++++----- .../issues/workspace-draft/quick-action.tsx | 8 ++++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/web/core/components/issues/workspace-draft/draft-issue-block.tsx b/web/core/components/issues/workspace-draft/draft-issue-block.tsx index 711204ec1a8..8aad8ced739 100644 --- a/web/core/components/issues/workspace-draft/draft-issue-block.tsx +++ b/web/core/components/issues/workspace-draft/draft-issue-block.tsx @@ -22,7 +22,7 @@ export const DraftIssueBlock: FC = observer((props) => { // props const { workspaceSlug, issueId } = props; // hooks - const { getIssueById, updateIssue } = useWorkspaceDraftIssues(); + const { getIssueById, updateIssue, deleteIssue, moveIssue } = useWorkspaceDraftIssues(); const { sidebarCollapsed: isSidebarCollapsed } = useAppTheme(); const { getProjectIdentifierById } = useProject(); // ref @@ -86,8 +86,9 @@ export const DraftIssueBlock: FC = observer((props) => { {}} - handleDelete={async () => {}} + handleUpdate={async (data) => updateIssue(workspaceSlug, issueId, data)} + handleDelete={async () => deleteIssue(workspaceSlug, issueId)} + handleMoveToIssues={async () => moveIssue(workspaceSlug, issueId, issue)} />
@@ -114,8 +115,9 @@ export const DraftIssueBlock: FC = observer((props) => { {}} - handleDelete={async () => {}} + handleUpdate={async (data) => updateIssue(workspaceSlug, issueId, data)} + handleDelete={async () => deleteIssue(workspaceSlug, issueId)} + handleMoveToIssues={async () => moveIssue(workspaceSlug, issueId, issue)} />
diff --git a/web/core/components/issues/workspace-draft/quick-action.tsx b/web/core/components/issues/workspace-draft/quick-action.tsx index f86a806356f..0185857f5d4 100644 --- a/web/core/components/issues/workspace-draft/quick-action.tsx +++ b/web/core/components/issues/workspace-draft/quick-action.tsx @@ -7,7 +7,7 @@ import { observer } from "mobx-react"; // icons import { Copy, Pencil, SquareStackIcon, Trash2 } from "lucide-react"; // types -import { TIssue, TWorkspaceDraftIssue } from "@plane/types"; +import { TWorkspaceDraftIssue } from "@plane/types"; // ui import { ContextMenu, CustomMenu, TContextMenuItem } from "@plane/ui"; // components @@ -22,7 +22,7 @@ import { WorkspaceDraftIssueDeleteIssueModal } from "./delete-modal"; export interface IQuickActionProps { issue: TWorkspaceDraftIssue; handleDelete: () => Promise; - handleUpdate?: (data: TIssue) => Promise; + handleUpdate: (payload: Partial) => Promise; handleMoveToIssues?: () => Promise; customActionButton?: React.ReactElement; portalElement?: HTMLDivElement | null; @@ -105,9 +105,9 @@ export const WorkspaceDraftIssueQuickActions: React.FC = obse }} data={issueToEdit ?? duplicateIssuePayload} onSubmit={async (data) => { - if (issueToEdit && handleUpdate) await handleUpdate(data); + if (issueToEdit && handleUpdate) await handleUpdate(data as TWorkspaceDraftIssue); }} - storeType={EIssuesStoreType.DRAFT} + storeType={EIssuesStoreType.WORKSPACE_DRAFT} isDraft /> From 684d5fcfb72ae9d63e79a521fdae7e6d3d3642bb Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Thu, 10 Oct 2024 15:47:05 +0530 Subject: [PATCH 19/21] fix: update mutation --- .../store/issue/workspace-draft/issue.store.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/web/core/store/issue/workspace-draft/issue.store.ts b/web/core/store/issue/workspace-draft/issue.store.ts index a0b82f98f85..e72eb24bd17 100644 --- a/web/core/store/issue/workspace-draft/issue.store.ts +++ b/web/core/store/issue/workspace-draft/issue.store.ts @@ -17,6 +17,7 @@ import { getCurrentDateTimeInISO, convertToISODateString } from "@/helpers/date- // services import workspaceDraftService from "@/services/issue/workspace_draft.service"; import { IIssueDetail } from "../issue-details/root.store"; +import { clone } from "lodash"; export type TDraftIssuePaginationType = EDraftIssuePaginationType; @@ -206,18 +207,21 @@ export class WorkspaceDraftIssues implements IWorkspaceDraftIssues { }; updateIssue = async (workspaceSlug: string, issueId: string, payload: Partial) => { + const issueBeforeUpdate = clone(this.getIssueById(issueId)); try { this.loader = "update"; - + runInAction(() => { + set(this.issuesMap, [issueId, "updated_at"], getCurrentDateTimeInISO()); + set(this.issuesMap, [issueId], { ...issueBeforeUpdate, ...payload }); + }); const response = await workspaceDraftService.updateIssue(workspaceSlug, issueId, payload); - if (response) { - runInAction(() => update(this.issuesMap, response.id, (prevIssue) => ({ ...prevIssue, ...response }))); - } - this.loader = undefined; return response; } catch (error) { this.loader = undefined; + runInAction(() => { + set(this.issuesMap, [issueId], issueBeforeUpdate); + }); throw error; } }; From 0dd4cbe2c4718fe31c21527cb58a4e35a9b4242f Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Thu, 10 Oct 2024 15:58:30 +0530 Subject: [PATCH 20/21] fix: create update modal --- .../components/issues/issue-modal/form.tsx | 39 ++++++------------- .../issues/workspace-draft/quick-action.tsx | 1 + 2 files changed, 13 insertions(+), 27 deletions(-) diff --git a/web/core/components/issues/issue-modal/form.tsx b/web/core/components/issues/issue-modal/form.tsx index 35785d3487d..615d935f561 100644 --- a/web/core/components/issues/issue-modal/form.tsx +++ b/web/core/components/issues/issue-modal/form.tsx @@ -266,7 +266,9 @@ export const IssueFormRoot: FC = observer((props) => { )}
handleFormSubmit(data))}>
-

{data?.id ? "Update" : "Create new"} issue

+

+ {data?.id ? "Update" : isDraft ? "Create draft" : "Create new"} issue +

{/* Disable project selection if editing an issue */}
= observer((props) => { > Discard - {isDraft && ( - <> - {data?.id ? ( - - ) : ( - - )} - - )}
diff --git a/web/core/components/issues/workspace-draft/quick-action.tsx b/web/core/components/issues/workspace-draft/quick-action.tsx index 0185857f5d4..5a3f8268830 100644 --- a/web/core/components/issues/workspace-draft/quick-action.tsx +++ b/web/core/components/issues/workspace-draft/quick-action.tsx @@ -108,6 +108,7 @@ export const WorkspaceDraftIssueQuickActions: React.FC = obse if (issueToEdit && handleUpdate) await handleUpdate(data as TWorkspaceDraftIssue); }} storeType={EIssuesStoreType.WORKSPACE_DRAFT} + fetchIssueDetails={false} isDraft /> From a9f429449f1e0f4d7a77532810e9670e29fb98ad Mon Sep 17 00:00:00 2001 From: Anmol Singh Bhatia Date: Thu, 10 Oct 2024 16:00:51 +0530 Subject: [PATCH 21/21] chore: commented project draft issue code --- web/core/components/workspace/sidebar/projects-list-item.tsx | 4 ++-- web/core/components/workspace/sidebar/quick-actions.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/web/core/components/workspace/sidebar/projects-list-item.tsx b/web/core/components/workspace/sidebar/projects-list-item.tsx index a512b9f33c7..2f809bb47d7 100644 --- a/web/core/components/workspace/sidebar/projects-list-item.tsx +++ b/web/core/components/workspace/sidebar/projects-list-item.tsx @@ -428,7 +428,7 @@ export const SidebarProjectsListItem: React.FC = observer((props) => {
)} - {isAuthorized && ( + {/* {isAuthorized && (
@@ -437,7 +437,7 @@ export const SidebarProjectsListItem: React.FC = observer((props) => {
- )} + )} */} diff --git a/web/core/components/workspace/sidebar/quick-actions.tsx b/web/core/components/workspace/sidebar/quick-actions.tsx index 5df8eb952e1..699660b268a 100644 --- a/web/core/components/workspace/sidebar/quick-actions.tsx +++ b/web/core/components/workspace/sidebar/quick-actions.tsx @@ -102,7 +102,7 @@ export const SidebarQuickActions = observer(() => { {!isSidebarCollapsed && New issue} - {!disabled && workspaceDraftIssue && ( + {/* {!disabled && workspaceDraftIssue && ( <> {!isSidebarCollapsed && (