diff --git a/packages/constants/issue.ts b/packages/constants/issue.ts index 67f8af56fd6..5db398c7634 100644 --- a/packages/constants/issue.ts +++ b/packages/constants/issue.ts @@ -13,6 +13,19 @@ export enum EIssueGroupByToServerOptions { "created_by" = "created_by", } +export enum EIssueGroupBYServerToProperty { + "state_id" = "state_id", + "priority" = "priority", + "labels__id" = "label_ids", + "state__group" = "state__group", + "assignees__id" = "assignee_ids", + "cycle_id" = "cycle_id", + "issue_module__module_id" = "module_ids", + "target_date" = "target_date", + "project_id" = "project_id", + "created_by" = "created_by", +} + export enum EServerGroupByToFilterOptions { "state_id" = "state", "priority" = "priority", diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/[archivedIssueId]/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/[archivedIssueId]/page.tsx index f0c41ab85d5..69d24b167b2 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/[archivedIssueId]/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/archives/issues/(detail)/[archivedIssueId]/page.tsx @@ -19,12 +19,12 @@ const ArchivedIssueDetailsPage = observer(() => { // hooks const { fetchIssue, - issue: { getIssueById }, + issue: { getIssueById, isFetchingIssueDetails }, } = useIssueDetail(); const { getProjectById } = useProject(); - const { isLoading } = useSWR( + useSWR( workspaceSlug && projectId && archivedIssueId ? `ARCHIVED_ISSUE_DETAIL_${workspaceSlug}_${projectId}_${archivedIssueId}` : null, @@ -40,7 +40,7 @@ const ArchivedIssueDetailsPage = observer(() => { if (!issue) return <>; - const issueLoader = !issue || isLoading ? true : false; + const issueLoader = !issue || isFetchingIssueDetails ? true : false; return ( <> diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/[issueId]/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/[issueId]/page.tsx index c5f604abc36..7956c96fb86 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/[issueId]/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/issues/(detail)/[issueId]/page.tsx @@ -27,12 +27,12 @@ const IssueDetailsPage = observer(() => { // store hooks const { fetchIssue, - issue: { getIssueById }, + issue: { getIssueById, isFetchingIssueDetails }, } = useIssueDetail(); const { getProjectById } = useProject(); const { toggleIssueDetailSidebar, issueDetailSidebarCollapsed } = useAppTheme(); // fetching issue details - const { isLoading, error } = useSWR( + const { error } = useSWR( workspaceSlug && projectId && issueId ? `ISSUE_DETAIL_${workspaceSlug}_${projectId}_${issueId}` : null, workspaceSlug && projectId && issueId ? () => fetchIssue(workspaceSlug.toString(), projectId.toString(), issueId.toString()) @@ -41,7 +41,7 @@ const IssueDetailsPage = observer(() => { // derived values const issue = getIssueById(issueId?.toString() || "") || undefined; const project = (issue?.project_id && getProjectById(issue?.project_id)) || undefined; - const issueLoader = !issue || isLoading ? true : false; + const issueLoader = !issue || isFetchingIssueDetails ? true : false; const pageTitle = project && issue ? `${project?.identifier}-${issue?.sequence_id} ${issue?.name}` : undefined; useEffect(() => { diff --git a/web/core/components/common/logo.tsx b/web/core/components/common/logo.tsx index 52d3dedb9bf..02b26a10be9 100644 --- a/web/core/components/common/logo.tsx +++ b/web/core/components/common/logo.tsx @@ -3,12 +3,12 @@ import { FC } from "react"; // emoji-picker-react import { Emoji } from "emoji-picker-react"; -// import { icons } from "lucide-react"; -import useFontFaceObserver from "use-font-face-observer"; import { TLogoProps } from "@plane/types"; // helpers import { LUCIDE_ICONS_LIST } from "@plane/ui"; import { emojiCodeToUnicode } from "@/helpers/emoji.helper"; +// import { icons } from "lucide-react"; +import useFontFaceObserver from "use-font-face-observer"; type Props = { logo: TLogoProps; diff --git a/web/core/components/issues/issue-layouts/kanban/base-kanban-root.tsx b/web/core/components/issues/issue-layouts/kanban/base-kanban-root.tsx index 0578348a4ee..fe2b2eee1c1 100644 --- a/web/core/components/issues/issue-layouts/kanban/base-kanban-root.tsx +++ b/web/core/components/issues/issue-layouts/kanban/base-kanban-root.tsx @@ -4,7 +4,6 @@ import { FC, useCallback, useEffect, useRef, useState } from "react"; import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine"; import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; import { autoScrollForElements } from "@atlaskit/pragmatic-drag-and-drop-auto-scroll/element"; -import debounce from "lodash/debounce"; import { observer } from "mobx-react"; import { useParams, usePathname } from "next/navigation"; import { DeleteIssueModal } from "@/components/issues"; @@ -93,12 +92,6 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas [fetchNextIssues] ); - const debouncedFetchMoreIssues = debounce( - (groupId?: string, subgroupId?: string) => fetchMoreIssues(groupId, subgroupId), - 300, - { leading: true, trailing: false } - ); - const groupedIssueIds = issues?.groupedIssueIds; const userDisplayFilters = displayFilters || null; @@ -275,7 +268,7 @@ export const BaseKanBanRoot: React.FC = observer((props: IBas addIssuesToView={addIssuesToView} scrollableContainerRef={scrollableContainerRef} handleOnDrop={handleOnDrop} - loadMoreIssues={debouncedFetchMoreIssues} + loadMoreIssues={fetchMoreIssues} /> diff --git a/web/core/components/issues/issue-layouts/kanban/block.tsx b/web/core/components/issues/issue-layouts/kanban/block.tsx index e894398c571..29696b3d4f7 100644 --- a/web/core/components/issues/issue-layouts/kanban/block.tsx +++ b/web/core/components/issues/issue-layouts/kanban/block.tsx @@ -39,6 +39,7 @@ interface IssueBlockProps { quickActions: TRenderQuickActions; canEditProperties: (projectId: string | undefined) => boolean; scrollableContainerRef?: MutableRefObject; + shouldRenderByDefault?: boolean; } interface IssueDetailsBlockProps { @@ -114,6 +115,7 @@ export const KanbanIssueBlock: React.FC = observer((props) => { quickActions, canEditProperties, scrollableContainerRef, + shouldRenderByDefault, } = props; const cardRef = useRef(null); @@ -222,6 +224,7 @@ export const KanbanIssueBlock: React.FC = observer((props) => { defaultHeight="100px" horizontalOffset={100} verticalOffset={200} + defaultValue={shouldRenderByDefault} > = observer((p <> {issueIds && issueIds.length > 0 ? ( <> - {issueIds.map((issueId) => { + {issueIds.map((issueId, index) => { if (!issueId) return null; let draggableId = issueId; @@ -50,6 +50,7 @@ export const KanbanIssueBlocksList: React.FC = observer((p issueId={issueId} groupId={groupId} subGroupId={sub_group_id} + shouldRenderByDefault={index <= 10} issuesMap={issuesMap} displayProperties={displayProperties} updateIssue={updateIssue} diff --git a/web/core/components/issues/issue-layouts/kanban/default.tsx b/web/core/components/issues/issue-layouts/kanban/default.tsx index ebc98baa229..696ba5d2b84 100644 --- a/web/core/components/issues/issue-layouts/kanban/default.tsx +++ b/web/core/components/issues/issue-layouts/kanban/default.tsx @@ -186,7 +186,7 @@ export const KanBan: React.FC = observer((props) => { verticalOffset={100} horizontalOffset={100} root={scrollableContainerRef} - classNames="relative h-full" + classNames="h-full min-h-[120px]" defaultHeight={`${groupHeight}px`} placeholderChildren={ = observer((props) => { const { peekIssue, setPeekIssue, - issue: { fetchIssue }, + issue: { fetchIssue, isFetchingIssueDetails }, fetchActivities, } = useIssueDetail(); const { issues } = useIssuesStore(); const { captureIssueEvent } = useEventTracker(); // state - const [loader, setLoader] = useState(true); const [error, setError] = useState(false); const removeRoutePeekId = () => { @@ -54,7 +53,6 @@ export const IssuePeekOverview: FC = observer((props) => { () => ({ fetch: async (workspaceSlug: string, projectId: string, issueId: string, loader = true) => { try { - setLoader(loader); setError(false); await fetchIssue( workspaceSlug, @@ -62,10 +60,8 @@ export const IssuePeekOverview: FC = observer((props) => { issueId, is_archived ? "ARCHIVED" : is_draft ? "DRAFT" : "DEFAULT" ); - setLoader(false); setError(false); } catch (error) { - setLoader(false); setError(true); console.error("Error fetching the parent issue"); } @@ -348,7 +344,7 @@ export const IssuePeekOverview: FC = observer((props) => { workspaceSlug={peekIssue.workspaceSlug} projectId={peekIssue.projectId} issueId={peekIssue.issueId} - isLoading={loader} + isLoading={isFetchingIssueDetails} isError={error} is_archived={is_archived} disabled={!isEditable} diff --git a/web/core/hooks/use-local-storage.tsx b/web/core/hooks/use-local-storage.tsx index e13165bf80e..538b8a93b77 100644 --- a/web/core/hooks/use-local-storage.tsx +++ b/web/core/hooks/use-local-storage.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useCallback } from "react"; -const getValueFromLocalStorage = (key: string, defaultValue: any) => { +export const getValueFromLocalStorage = (key: string, defaultValue: any) => { if (typeof window === undefined || typeof window === "undefined") return defaultValue; try { const item = window.localStorage.getItem(key); @@ -11,6 +11,16 @@ const getValueFromLocalStorage = (key: string, defaultValue: any) => { } }; +export const setValueIntoLocalStorage = (key: string, value: any) => { + if (typeof window === undefined || typeof window === "undefined") return false; + try { + window.localStorage.setItem(key, JSON.stringify(value)); + return true; + } catch (error) { + return false; + } +}; + const useLocalStorage = (key: string, initialValue: T) => { const [storedValue, setStoredValue] = useState(() => getValueFromLocalStorage(key, initialValue)); diff --git a/web/core/store/issue/cycle/filter.store.ts b/web/core/store/issue/cycle/filter.store.ts index b2045704974..954fe3370e4 100644 --- a/web/core/store/issue/cycle/filter.store.ts +++ b/web/core/store/issue/cycle/filter.store.ts @@ -119,7 +119,12 @@ export class CycleIssuesFilter extends IssueFilterHelperStore implements ICycleI groupId: string | undefined, subGroupId: string | undefined ) => { - const filterParams = this.getAppliedFilters(cycleId); + let filterParams = this.getAppliedFilters(cycleId); + + if (!filterParams) { + filterParams = {}; + } + filterParams["cycle"] = cycleId; const paginationParams = this.getPaginationParams(filterParams, options, cursor, groupId, subGroupId); return paginationParams; diff --git a/web/core/store/issue/cycle/issue.store.ts b/web/core/store/issue/cycle/issue.store.ts index c8d34ecdbee..39a1d397885 100644 --- a/web/core/store/issue/cycle/issue.store.ts +++ b/web/core/store/issue/cycle/issue.store.ts @@ -185,7 +185,7 @@ export class CycleIssues extends BaseIssuesStore implements ICycleIssues { // get params from pagination options const params = this.issueFilterStore?.getFilterParams(options, cycleId, undefined, undefined, undefined); // call the fetch issues API with the params - const response = await this.cycleService.getCycleIssues(workspaceSlug, projectId, cycleId, params, { + const response = await this.issueService.getIssues(workspaceSlug, projectId, params, { signal: this.controller.signal, }); @@ -233,7 +233,7 @@ export class CycleIssues extends BaseIssuesStore implements ICycleIssues { subGroupId ); // call the fetch issues API with the params for next page in issues - const response = await this.cycleService.getCycleIssues(workspaceSlug, projectId, cycleId, params); + const response = await this.issueService.getIssues(workspaceSlug, projectId, cycleId, params); // after the next page of issues are fetched, call the base method to process the response this.onfetchNexIssues(response, groupId, subGroupId); diff --git a/web/core/store/issue/issue-details/issue.store.ts b/web/core/store/issue/issue-details/issue.store.ts index 871be85e026..120103f2cdd 100644 --- a/web/core/store/issue/issue-details/issue.store.ts +++ b/web/core/store/issue/issue-details/issue.store.ts @@ -1,4 +1,4 @@ -import { makeObservable } from "mobx"; +import { makeObservable, observable } from "mobx"; import { computedFn } from "mobx-utils"; // types import { TIssue } from "@plane/types"; @@ -32,11 +32,13 @@ export interface IIssueStoreActions { } export interface IIssueStore extends IIssueStoreActions { + isFetchingIssueDetails: boolean; // helper methods getIssueById: (issueId: string) => TIssue | undefined; } export class IssueStore implements IIssueStore { + isFetchingIssueDetails: boolean = false; // root store rootIssueDetailStore: IIssueDetail; // services @@ -45,7 +47,9 @@ export class IssueStore implements IIssueStore { issueDraftService; constructor(rootStore: IIssueDetail) { - makeObservable(this, {}); + makeObservable(this, { + isFetchingIssueDetails: observable.ref, + }); // root store this.rootIssueDetailStore = rootStore; // services @@ -66,7 +70,9 @@ export class IssueStore implements IIssueStore { expand: "issue_reactions,issue_attachment,issue_link,parent", }; - let issue: TIssue; + let issue: TIssue | undefined; + + this.isFetchingIssueDetails = true; if (issueType === "ARCHIVED") issue = await this.issueArchiveService.retrieveArchivedIssue(workspaceSlug, projectId, issueId, query); @@ -76,38 +82,7 @@ export class IssueStore implements IIssueStore { if (!issue) throw new Error("Issue not found"); - const issuePayload: TIssue = { - id: issue?.id, - sequence_id: issue?.sequence_id, - name: issue?.name, - description_html: issue?.description_html, - sort_order: issue?.sort_order, - state_id: issue?.state_id, - priority: issue?.priority, - label_ids: issue?.label_ids, - assignee_ids: issue?.assignee_ids, - estimate_point: issue?.estimate_point, - sub_issues_count: issue?.sub_issues_count, - attachment_count: issue?.attachment_count, - link_count: issue?.link_count, - project_id: issue?.project_id, - parent_id: issue?.parent_id, - cycle_id: issue?.cycle_id, - module_ids: issue?.module_ids, - type_id: issue?.type_id, - created_at: issue?.created_at, - updated_at: issue?.updated_at, - start_date: issue?.start_date, - target_date: issue?.target_date, - completed_at: issue?.completed_at, - archived_at: issue?.archived_at, - created_by: issue?.created_by, - updated_by: issue?.updated_by, - is_draft: issue?.is_draft, - is_subscribed: issue?.is_subscribed, - }; - - this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issuePayload]); + this.addIssueToStore(issue); // store handlers from issue detail // parent @@ -150,6 +125,44 @@ export class IssueStore implements IIssueStore { return issue; }; + addIssueToStore = (issue: TIssue) => { + const issuePayload: TIssue = { + id: issue?.id, + sequence_id: issue?.sequence_id, + name: issue?.name, + description_html: issue?.description_html, + sort_order: issue?.sort_order, + state_id: issue?.state_id, + priority: issue?.priority, + label_ids: issue?.label_ids, + assignee_ids: issue?.assignee_ids, + estimate_point: issue?.estimate_point, + sub_issues_count: issue?.sub_issues_count, + attachment_count: issue?.attachment_count, + link_count: issue?.link_count, + project_id: issue?.project_id, + parent_id: issue?.parent_id, + cycle_id: issue?.cycle_id, + module_ids: issue?.module_ids, + type_id: issue?.type_id, + created_at: issue?.created_at, + updated_at: issue?.updated_at, + start_date: issue?.start_date, + target_date: issue?.target_date, + completed_at: issue?.completed_at, + archived_at: issue?.archived_at, + created_by: issue?.created_by, + updated_by: issue?.updated_by, + is_draft: issue?.is_draft, + is_subscribed: issue?.is_subscribed, + }; + + this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issuePayload]); + this.isFetchingIssueDetails = false; + + return issuePayload; + }; + updateIssue = async (workspaceSlug: string, projectId: string, issueId: string, data: Partial) => { await this.rootIssueDetailStore.rootIssueStore.projectIssues.updateIssue(workspaceSlug, projectId, issueId, data); await this.rootIssueDetailStore.activity.fetchActivities(workspaceSlug, projectId, issueId); diff --git a/web/core/store/issue/module/filter.store.ts b/web/core/store/issue/module/filter.store.ts index 1e02ba13adf..d64a4c0f015 100644 --- a/web/core/store/issue/module/filter.store.ts +++ b/web/core/store/issue/module/filter.store.ts @@ -119,7 +119,12 @@ export class ModuleIssuesFilter extends IssueFilterHelperStore implements IModul groupId: string | undefined, subGroupId: string | undefined ) => { - const filterParams = this.getAppliedFilters(moduleId); + let filterParams = this.getAppliedFilters(moduleId); + + if (!filterParams) { + filterParams = {}; + } + filterParams["module"] = moduleId; const paginationParams = this.getPaginationParams(filterParams, options, cursor, groupId, subGroupId); return paginationParams; diff --git a/web/core/store/issue/module/issue.store.ts b/web/core/store/issue/module/issue.store.ts index 9131c8833f0..9f08f32b65e 100644 --- a/web/core/store/issue/module/issue.store.ts +++ b/web/core/store/issue/module/issue.store.ts @@ -142,7 +142,7 @@ export class ModuleIssues extends BaseIssuesStore implements IModuleIssues { // get params from pagination options const params = this.issueFilterStore?.getFilterParams(options, moduleId, undefined, undefined, undefined); // call the fetch issues API with the params - const response = await this.moduleService.getModuleIssues(workspaceSlug, projectId, moduleId, params, { + const response = await this.issueService.getIssues(workspaceSlug, projectId, params, { signal: this.controller.signal, }); @@ -190,7 +190,7 @@ export class ModuleIssues extends BaseIssuesStore implements IModuleIssues { subGroupId ); // call the fetch issues API with the params for next page in issues - const response = await this.moduleService.getModuleIssues(workspaceSlug, projectId, moduleId, params); + const response = await this.issueService.getIssues(workspaceSlug, projectId, params); // after the next page of issues are fetched, call the base method to process the response this.onfetchNexIssues(response, groupId, subGroupId);