fix: recents api integrations

This commit is contained in:
gakshita
2025-01-03 19:38:14 +05:30
parent 99d9702ef5
commit ff29170ec7
31 changed files with 379 additions and 277 deletions

View File

@@ -186,3 +186,41 @@ export type THomeDashboardResponse = {
dashboard: TDashboard;
widgets: TWidget[];
};
// home
type TPageEntityData = {
id: string;
name: string;
logo_props: TLogoProps;
project_id: string;
owned_by: string;
project_identifier: string;
};
type TProjectEntityData = {
id: string;
name: string;
logo_props: TLogoProps;
project_members: ProjectMember[];
identifier: string;
};
type TIssueEntityData = {
id: string;
name: string;
state: string;
priority: TIssuePriorities;
assignees: string[];
type: string | null;
sequence_id: number;
project_id: string;
project_identifier: string;
};
type TActivityEntityData = {
id: string;
entity_name: "page" | "project" | "issue";
entity_identifier: string;
visited_at: string;
entity_data: TPageEntityData | TProjectEntityData | TIssueEntityData;
};

View File

@@ -3,20 +3,20 @@ import { useParams } from "next/navigation";
// types
import { TWidgetKeys, WidgetProps } from "@plane/types";
// components
import { RecentActivityWidget, EmptyWorkspace } from "@/components/dashboard";
// hooks
import { useDashboard, useProject } from "@/hooks/store";
import { useHome } from "@/hooks/store/use-home";
import { HomePageHeader } from "@/plane-web/components/home/header";
import { DashboardQuickLinks } from "./links";
import { RecentActivityWidget } from "./widgets";
import { DashboardQuickLinks } from "./widgets/links";
import { ManageWidgetsModal } from "./widgets/manage";
import { StickiesWidget } from "@/plane-web/components/stickies";
const WIDGETS_LIST: {
[key in TWidgetKeys]: { component: React.FC<WidgetProps>; fullWidth: boolean };
[key: string]: { component: React.FC<WidgetProps>; fullWidth: boolean };
} = {
recent_activity: { component: RecentActivityWidget, fullWidth: false },
// recent_collaborators: { component: RecentCollaboratorsWidget, fullWidth: true },
// my_stickies: { component: StickiesWidget, fullWidth: false },
stickies: { component: StickiesWidget, fullWidth: false },
};
export const DashboardWidgets = observer(() => {
@@ -42,25 +42,20 @@ export const DashboardWidgets = observer(() => {
/>
<DashboardQuickLinks workspaceSlug={workspaceSlug.toString()} />
{totalProjectIds?.length === 0 ? (
<EmptyWorkspace />
) : (
Object.entries(WIDGETS_LIST).map(([key, widget]) => {
const WidgetComponent = widget.component;
// if the widget doesn't exist, return null
// if (!doesWidgetExist(key as TWidgetKeys)) return null;
// if the widget is full width, return it in a 2 column grid
console.log({ widget, key });
if (widget.fullWidth)
return (
<div key={key} className="lg:col-span-2">
<WidgetComponent dashboardId={homeDashboardId} workspaceSlug={workspaceSlug.toString()} />
</div>
);
else
return <WidgetComponent key={key} dashboardId={homeDashboardId} workspaceSlug={workspaceSlug.toString()} />;
})
)}
{Object.entries(WIDGETS_LIST).map(([key, widget]) => {
const WidgetComponent = widget.component;
// if the widget doesn't exist, return null
// if (!doesWidgetExist(key as TWidgetKeys)) return null;
// if the widget is full width, return it in a 2 column grid
if (widget.fullWidth)
return (
<div key={key} className="lg:col-span-2">
<WidgetComponent dashboardId={homeDashboardId} workspaceSlug={workspaceSlug.toString()} />
</div>
);
else
return <WidgetComponent key={key} dashboardId={homeDashboardId} workspaceSlug={workspaceSlug.toString()} />;
})}
</div>
);
});

View File

@@ -42,7 +42,7 @@ export const EmptyWorkspace = () => {
icon: <Users className="w-[40px] h-[40px] text-custom-primary-100" />,
cta: {
text: "Invite now",
link: "settings/members",
link: `/${workspaceSlug}/settings/members`,
},
},
{

View File

@@ -80,7 +80,7 @@ export const ProjectLinkDetail: FC<TProjectLinkDetail> = observer((props) => {
</div>
<div className="my-auto flex-1">
<div className="text-sm font-medium truncate">{linkDetail.title || linkDetail.url}</div>
<div className="text-xs font-semibold text-custom-text-400">{calculateTimeAgo(linkDetail.created_at)}</div>
<div className="text-xs font-medium text-custom-text-400">{calculateTimeAgo(linkDetail.created_at)}</div>
</div>
<CustomMenu

View File

@@ -5,12 +5,12 @@ import { useHome } from "@/hooks/store/use-home";
import { AddLink } from "./action";
import { ProjectLinkDetail } from "./link-detail";
import { TLinkOperations } from "./use-links";
import { EWidgetKeys, WidgetLoader } from "../loaders";
export type TLinkOperationsModal = Exclude<TLinkOperations, "create">;
export type TProjectLinkList = {
linkOperations: TLinkOperationsModal;
workspaceSlug: string;
};
@@ -50,7 +50,7 @@ export const ProjectLinkList: FC<TProjectLinkList> = observer((props) => {
return () => window.removeEventListener("resize", updateColumnCount);
}, []);
if (!links) return <></>;
if (links === undefined) return <WidgetLoader widgetKey={EWidgetKeys.QUICK_LINKS} />;
return (
<div>

View File

@@ -33,7 +33,7 @@ export const DashboardQuickLinks = observer((props: TProps) => {
preloadedData={linkData}
setLinkData={setLinkData}
/>
<div className="flex mx-auto flex-wrap border-b border-custom-border-100 pb-4">
<div className="flex mx-auto flex-wrap border-b border-custom-border-100 pb-4 w-full justify-center">
{/* rendering links */}
<ProjectLinkList workspaceSlug={workspaceSlug} linkOperations={linkOperations} />
</div>

View File

@@ -1,24 +0,0 @@
"use client";
// ui
import { Loader } from "@plane/ui";
export const AssignedIssuesWidgetLoader = () => (
<Loader className="bg-custom-background-100 p-6 rounded-xl">
<div className="flex items-center justify-between gap-2">
<Loader.Item height="17px" width="35%" />
<Loader.Item height="17px" width="10%" />
</div>
<div className="mt-6 space-y-7">
<Loader.Item height="29px" />
<Loader.Item height="17px" width="10%" />
</div>
<div className="mt-11 space-y-10">
<Loader.Item height="11px" width="35%" />
<Loader.Item height="11px" width="45%" />
<Loader.Item height="11px" width="55%" />
<Loader.Item height="11px" width="40%" />
<Loader.Item height="11px" width="60%" />
</div>
</Loader>
);

View File

@@ -1,17 +0,0 @@
"use client";
// ui
import { Loader } from "@plane/ui";
export const IssuesByPriorityWidgetLoader = () => (
<Loader className="bg-custom-background-100 rounded-xl p-6">
<Loader.Item height="17px" width="35%" />
<div className="flex items-center gap-1 h-full">
<Loader.Item height="119px" width="14%" />
<Loader.Item height="119px" width="26%" />
<Loader.Item height="119px" width="36%" />
<Loader.Item height="119px" width="18%" />
<Loader.Item height="119px" width="6%" />
</div>
</Loader>
);

View File

@@ -1,24 +0,0 @@
"use client";
import range from "lodash/range";
// ui
import { Loader } from "@plane/ui";
export const IssuesByStateGroupWidgetLoader = () => (
<Loader className="bg-custom-background-100 rounded-xl p-6">
<Loader.Item height="17px" width="35%" />
<div className="flex items-center justify-between gap-32 mt-12 pl-6">
<div className="w-1/2 grid place-items-center">
<div className="rounded-full overflow-hidden relative flex-shrink-0 h-[184px] w-[184px]">
<Loader.Item height="184px" width="184px" />
<div className="absolute h-[100px] w-[100px] top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-custom-background-100 rounded-full" />
</div>
</div>
<div className="w-1/2 space-y-7 flex-shrink-0">
{range(5).map((index) => (
<Loader.Item key={index} height="11px" width="100%" />
))}
</div>
</div>
</Loader>
);

View File

@@ -1,30 +1,24 @@
// components
import { TWidgetKeys } from "@plane/types";
import { AssignedIssuesWidgetLoader } from "./assigned-issues";
import { IssuesByPriorityWidgetLoader } from "./issues-by-priority";
import { IssuesByStateGroupWidgetLoader } from "./issues-by-state-group";
import { OverviewStatsWidgetLoader } from "./overview-stats";
import { RecentActivityWidgetLoader } from "./recent-activity";
import { RecentCollaboratorsWidgetLoader } from "./recent-collaborators";
import { RecentProjectsWidgetLoader } from "./recent-projects";
import { QuickLinksWidgetLoader } from "./quick-links";
// types
type Props = {
widgetKey: TWidgetKeys;
widgetKey: EWidgetKeys;
};
export enum EWidgetKeys {
RECENT_ACTIVITY = "recent_activity",
QUICK_LINKS = "quick_links",
}
export const WidgetLoader: React.FC<Props> = (props) => {
const { widgetKey } = props;
const loaders = {
overview_stats: <OverviewStatsWidgetLoader />,
assigned_issues: <AssignedIssuesWidgetLoader />,
created_issues: <AssignedIssuesWidgetLoader />,
issues_by_state_groups: <IssuesByStateGroupWidgetLoader />,
issues_by_priority: <IssuesByPriorityWidgetLoader />,
recent_activity: <RecentActivityWidgetLoader />,
recent_projects: <RecentProjectsWidgetLoader />,
recent_collaborators: <RecentCollaboratorsWidgetLoader />,
[EWidgetKeys.RECENT_ACTIVITY]: <RecentActivityWidgetLoader />,
[EWidgetKeys.QUICK_LINKS]: <QuickLinksWidgetLoader />,
};
return loaders[widgetKey];

View File

@@ -1,16 +0,0 @@
"use client";
import range from "lodash/range";
// ui
import { Loader } from "@plane/ui";
export const OverviewStatsWidgetLoader = () => (
<Loader className="bg-custom-background-100 rounded-xl py-6 grid grid-cols-4 gap-36 px-12">
{range(4).map((index) => (
<div key={index} className="space-y-3">
<Loader.Item height="11px" width="50%" />
<Loader.Item height="15px" />
</div>
))}
</Loader>
);

View File

@@ -0,0 +1,13 @@
"use client";
import range from "lodash/range";
// ui
import { Loader } from "@plane/ui";
export const QuickLinksWidgetLoader = () => (
<Loader className="bg-custom-background-100 rounded-xl gap-2 flex flex-wrap justify-center">
{range(4).map((index) => (
<Loader.Item key={index} height="56px" width="230px" />
))}
</Loader>
);

View File

@@ -5,16 +5,14 @@ import range from "lodash/range";
import { Loader } from "@plane/ui";
export const RecentActivityWidgetLoader = () => (
<Loader className="bg-custom-background-100 rounded-xl p-6 space-y-6">
<Loader.Item height="17px" width="35%" />
<Loader className="bg-custom-background-100 rounded-xl px-2 space-y-6">
{range(7).map((index) => (
<div key={index} className="flex items-start gap-3.5">
<div className="flex-shrink-0">
<Loader.Item height="16px" width="16px" />
<Loader.Item height="32px" width="32px" />
</div>
<div className="space-y-3 flex-shrink-0 w-full">
<div className="space-y-3 flex-shrink-0 w-full my-auto">
<Loader.Item height="15px" width="70%" />
<Loader.Item height="11px" width="10%" />
</div>
</div>
))}

View File

@@ -1,20 +0,0 @@
"use client";
import range from "lodash/range";
// ui
import { Loader } from "@plane/ui";
export const RecentCollaboratorsWidgetLoader = () => (
<>
{range(8).map((index) => (
<Loader key={index} className="bg-custom-background-100 rounded-xl px-6 pb-12">
<div className="space-y-11 flex flex-col items-center">
<div className="rounded-full overflow-hidden h-[69px] w-[69px]">
<Loader.Item height="69px" width="69px" />
</div>
<Loader.Item height="11px" width="70%" />
</div>
</Loader>
))}
</>
);

View File

@@ -1,22 +0,0 @@
"use client";
import range from "lodash/range";
// ui
import { Loader } from "@plane/ui";
export const RecentProjectsWidgetLoader = () => (
<Loader className="bg-custom-background-100 rounded-xl p-6 space-y-6">
<Loader.Item height="17px" width="35%" />
{range(5).map((index) => (
<div key={index} className="flex items-center gap-6">
<div className="flex-shrink-0">
<Loader.Item height="60px" width="60px" />
</div>
<div className="space-y-3 flex-shrink-0 w-full">
<Loader.Item height="17px" width="42%" />
<Loader.Item height="23px" width="10%" />
</div>
</div>
))}
</Loader>
);

View File

@@ -4,7 +4,7 @@ import { FC } from "react";
import { observer } from "mobx-react";
// plane types
// plane ui
import { Button, ModalCore } from "@plane/ui";
import { Button, EModalWidth, ModalCore } from "@plane/ui";
import { WidgetList } from "./widget-list";
export type TProps = {
@@ -18,7 +18,7 @@ export const ManageWidgetsModal: FC<TProps> = observer((props) => {
const { workspaceSlug, isModalOpen, handleOnClose } = props;
return (
<ModalCore isOpen={isModalOpen} handleClose={handleOnClose}>
<ModalCore isOpen={isModalOpen} handleClose={handleOnClose} width={EModalWidth.MD}>
<div className="p-4">
<div className="font-medium text-xl">Manage widgets</div>
<WidgetList workspaceSlug={workspaceSlug} />

View File

@@ -112,14 +112,16 @@ export const WidgetItem: FC<Props> = observer((props) => {
<div
ref={elementRef}
className={cn(
"px-2 relative flex items-center py-2 font-medium text-sm capitalize group/widget-item rounded hover:bg-custom-background-80",
"px-2 relative flex items-center py-2 font-medium text-sm capitalize group/widget-item rounded hover:bg-custom-background-80 justify-between",
{
"cursor-grabbing bg-custom-background-80": isDragging,
}
)}
>
<WidgetItemDragHandle sort_order={widget.sort_order} isDragging={isDragging} />
<div className="w-6/12">{widget.title}</div>
<div className="flex items-center">
<WidgetItemDragHandle sort_order={widget.sort_order} isDragging={isDragging} />
<div>{widget.title}</div>
</div>
<ToggleSwitch />
</div>
{isLastChild && <DropIndicator isVisible={instruction === "reorder-below"} />}

View File

@@ -3,9 +3,9 @@ import {
DropTargetRecord,
ElementDragPayload,
} from "@atlaskit/pragmatic-drag-and-drop/dist/types/internal-types";
import { useDashboard } from "@/hooks/store";
import { WidgetItem } from "./widget-item";
import { getInstructionFromPayload, TargetData } from "./widget.helpers";
import { useHome } from "@/hooks/store/use-home";
const WIDGETS_LIST = [
{ id: 1, title: "quick links" },
@@ -13,7 +13,7 @@ const WIDGETS_LIST = [
{ id: 3, title: "stickies" },
];
export const WidgetList = ({ workspaceSlug }: { workspaceSlug: string }) => {
const { reorderWidgets } = useDashboard();
const { reorderWidgets } = useHome();
const handleDrop = (self: DropTargetRecord, source: ElementDragPayload, location: DragLocationHistory) => {
const dropTargets = location?.current?.dropTargets ?? [];

View File

@@ -3,30 +3,30 @@
import { FC } from "react";
import { observer } from "mobx-react";
import { ChevronDown } from "lucide-react";
import { TRecentActivityWidgetFilters } from "@plane/types";
import { CustomMenu } from "@plane/ui";
import { cn } from "@plane/utils";
export type TFiltersDropdown = {
className?: string;
activeFilter: TRecentActivityWidgetFilters | undefined;
setActiveFilter: (filter: TRecentActivityWidgetFilters) => void;
activeFilter: string;
setActiveFilter: (filter: string) => void;
filters: { name: string; icon?: React.ReactNode }[];
};
const filters = ["all", "projects", "pages", "issues"];
export const FiltersDropdown: FC<TFiltersDropdown> = observer((props) => {
const { className, activeFilter, setActiveFilter } = props;
const { className, activeFilter, setActiveFilter, filters } = props;
const DropdownOptions = () =>
filters?.map((filter) => (
<CustomMenu.MenuItem
key={filter}
className="flex items-center gap-2 truncate"
key={filter.name}
className="flex items-center gap-2 truncate text-custom-text-200"
onClick={() => {
setActiveFilter(filter);
setActiveFilter(filter.name);
}}
>
<div className="truncate font-medium text-xs capitalize">{filter}</div>
{filter.icon && <div>{filter.icon}</div>}
<div className="truncate font-medium text-xs capitalize">{`${filter.name}s`}</div>
</CustomMenu.MenuItem>
));
@@ -37,7 +37,7 @@ export const FiltersDropdown: FC<TFiltersDropdown> = observer((props) => {
placement="bottom-start"
customButton={
<button className="flex hover:bg-custom-background-80 px-2 py-1 rounded gap-1 capitalize border border-custom-border-200">
<span className="font-medium text-sm my-auto"> {activeFilter && `${activeFilter}`}</span>
<span className="font-medium text-sm my-auto"> {activeFilter && `${activeFilter}s`}</span>
<ChevronDown className={cn("size-3 my-auto text-custom-text-300 hover:text-custom-text-200 duration-300")} />
</button>
}

View File

@@ -1,59 +1,78 @@
"use client";
import { useEffect, useRef } from "react";
import { useRef, useState } from "react";
import { observer } from "mobx-react";
import Link from "next/link";
import { WidgetProps } from "@/components/dashboard/widgets";
// types
import { TRecentActivityWidgetResponse } from "@plane/types";
import { TActivityEntityData } from "@plane/types";
// components
import { WidgetLoader, WidgetProps } from "@/components/dashboard/widgets";
// hooks
import { useDashboard, useUser } from "@/hooks/store";
import { FiltersDropdown } from "./filters";
import { RecentIssue } from "./issue";
import { WorkspaceService } from "@/plane-web/services";
import useSWR from "swr";
import { RecentProject } from "./project";
import { RecentPage } from "./page";
import { EWidgetKeys, WidgetLoader } from "../loaders";
import { Briefcase, FileText } from "lucide-react";
import { LayersIcon } from "@plane/ui";
import { EmptyWorkspace } from "../empty-states";
const WIDGET_KEY = "recent_activity";
const WIDGET_KEY = EWidgetKeys.RECENT_ACTIVITY;
const workspaceService = new WorkspaceService();
const filters = [
{ name: "all item" },
{ name: "issue", icon: <LayersIcon className="w-4 h-4" /> },
{ name: "page", icon: <FileText size={16} /> },
{ name: "project", icon: <Briefcase size={16} /> },
];
export const RecentActivityWidget: React.FC<WidgetProps> = observer((props) => {
const { dashboardId, workspaceSlug } = props;
const { workspaceSlug } = props;
const ref = useRef<HTMLDivElement>(null);
// store hooks
const { data: currentUser } = useUser();
const { activeFilter, setActiveFilter, fetchWidgetStats, getWidgetStats } = useDashboard();
// derived values
const widgetStats = getWidgetStats<TRecentActivityWidgetResponse[]>(workspaceSlug, dashboardId, WIDGET_KEY);
const redirectionLink = `/${workspaceSlug}/profile/${currentUser?.id}/activity`;
const [filter, setFilter] = useState(filters[0].name);
useEffect(() => {
fetchWidgetStats(workspaceSlug, dashboardId, {
widget_key: WIDGET_KEY,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const { data: recents, isLoading } = useSWR(
workspaceSlug ? `WORKSPACE_RECENT_ACTIVITY_${workspaceSlug}_${filter}` : null,
workspaceSlug
? () =>
workspaceService.fetchWorkspaceRecents(
workspaceSlug.toString(),
filter === filters[0].name ? undefined : filter
)
: null,
{
revalidateIfStale: false,
revalidateOnFocus: false,
revalidateOnReconnect: false,
}
);
if (!widgetStats) return <WidgetLoader widgetKey={WIDGET_KEY} />;
const resolveRecent = (activity: TRecentActivityWidgetResponse) => {
console.log();
return <RecentIssue activity={activity} ref={ref} />;
const resolveRecent = (activity: TActivityEntityData) => {
switch (activity.entity_name) {
case "page":
return <RecentPage activity={activity} ref={ref} workspaceSlug={workspaceSlug} />;
case "project":
return <RecentProject activity={activity} ref={ref} workspaceSlug={workspaceSlug} />;
case "issue":
return <RecentIssue activity={activity} ref={ref} workspaceSlug={workspaceSlug} />;
default:
return <></>;
}
};
return (
<div ref={ref}>
<div className="flex items-center justify-between">
<Link href={redirectionLink} className="text-base font-semibold text-custom-text-350 hover:underline">
Recent
</Link>
if (!isLoading && recents?.length === 0) return <EmptyWorkspace />;
<FiltersDropdown activeFilter={activeFilter} setActiveFilter={setActiveFilter} />
return (
<div ref={ref} className=" max-h-[500px] overflow-y-scroll">
<div className="flex items-center justify-between mb-2">
<div className="text-base font-semibold text-custom-text-350 hover:underline">Recents</div>
<FiltersDropdown filters={filters} activeFilter={filter} setActiveFilter={setFilter} />
</div>
{widgetStats.length > 0 && (
<div className="mt-2">
{widgetStats.map((activity) => (
<div key={activity.id}>{resolveRecent(activity)}</div>
))}
</div>
)}
{isLoading && <WidgetLoader widgetKey={WIDGET_KEY} />}
{!isLoading &&
recents?.length > 0 &&
recents.map((activity: TActivityEntityData) => <div key={activity.id}>{resolveRecent(activity)}</div>)}
</div>
);
});

View File

@@ -1,23 +1,25 @@
import { TRecentActivityWidgetResponse } from "@plane/types";
import { PriorityIcon } from "@plane/ui";
import { TActivityEntityData, TIssueEntityData } from "@plane/types";
import { PriorityIcon, StateGroupIcon } from "@plane/ui";
import { ListItem } from "@/components/core/list";
import { calculateTimeAgo } from "@/helpers/date-time.helper";
import { useIssueDetail } from "@/hooks/store";
import { IssueIdentifier } from "@/plane-web/components/issues";
import { MemberDropdown } from "@/components/dropdowns";
import { useIssueDetail, useProjectState } from "@/hooks/store";
type BlockProps = {
activity: TRecentActivityWidgetResponse;
activity: TActivityEntityData;
ref: React.RefObject<HTMLDivElement>;
workspaceSlug: string;
};
export const RecentIssue = (props: BlockProps) => {
const { activity, ref } = props;
const {
issue: { getIssueById },
} = useIssueDetail();
console.log({ ...activity.issue_detail, ...activity }, { ...getIssueById(activity?.issue) });
const { activity, ref, workspaceSlug } = props;
// hooks
const { getStateById } = useProjectState();
const { setPeekIssue } = useIssueDetail();
// derived values
const issueDetails: TIssueEntityData = activity.entity_data as TIssueEntityData;
const state = getStateById(issueDetails?.state);
if (!activity?.issue) return <></>;
const issueDetails = getIssueById(activity?.issue);
return (
<ListItem
key={activity.id}
@@ -26,43 +28,48 @@ export const RecentIssue = (props: BlockProps) => {
prependTitleElement={
<div className="flex flex-shrink-0 items-center justify-center rounded-md gap-4 ">
<IssueIdentifier
issueTypeId={activity.issue_detail?.type_id}
projectId={activity?.project || ""}
projectIdentifier={activity.project_detail?.identifier || ""}
issueSequenceId={activity.issue_detail?.sequence_id || ""}
issueTypeId={issueDetails?.type}
projectId={issueDetails?.project_id || ""}
projectIdentifier={issueDetails?.project_identifier || ""}
issueSequenceId={issueDetails?.sequence_id || ""}
textContainerClassName="text-custom-sidebar-text-400 text-sm whitespace-nowrap"
/>
<div className="text-custom-text-200 font-medium text-sm whitespace-nowrap">
{activity.issue_detail?.name}
</div>
<div className="font-semibold text-xs text-custom-text-400">{calculateTimeAgo(activity.updated_at)}</div>
<div className="text-custom-text-200 font-medium text-sm whitespace-nowrap">{issueDetails?.name}</div>
<div className="font-medium text-xs text-custom-text-400">{calculateTimeAgo(activity.visited_at)}</div>
</div>
}
quickActionElement={
<div>
<PriorityIcon priority={activity.issue_detail?.priority} withContainer size={12} />
<div className="h-5">
{/* <MemberDropdown
projectId={activity?.project}
value={activity?.assignee_ids}
onChange={handleAssignee}
disabled
multiple
buttonVariant={activity.assignee_ids?.length > 0 ? "transparent-without-text" : "border-without-text"}
buttonClassName={activity.assignee_ids?.length > 0 ? "hover:bg-transparent px-0" : ""}
showTooltip={activity?.assignee_ids?.length === 0}
placeholder="Assignees"
optionsClassName="z-10"
tooltipContent=""
renderByDefault={isMobile}
/> */}
</div>
<div className="flex gap-4">
<StateGroupIcon stateGroup={state?.group ?? "backlog"} color={state?.color} className="h-4 w-4 my-auto" />
<PriorityIcon priority={issueDetails?.priority} withContainer size={12} />
{issueDetails?.assignees?.length > 0 && (
<div className="h-5">
<MemberDropdown
projectId={issueDetails?.project_id}
value={issueDetails?.assignees}
onChange={() => {}}
disabled
multiple
buttonVariant={issueDetails?.assignees?.length > 0 ? "transparent-without-text" : "border-without-text"}
buttonClassName={issueDetails?.assignees?.length > 0 ? "hover:bg-transparent px-0" : ""}
showTooltip={issueDetails?.assignees?.length === 0}
placeholder="Assignees"
optionsClassName="z-10"
tooltipContent=""
/>
</div>
)}
</div>
}
parentRef={ref}
disableLink={false}
className="bg-transparent my-auto !px-2 border-custom-border-200/40 py-3"
className="bg-transparent my-auto !px-2 border-none py-3"
itemClassName="my-auto"
onItemClick={(e) => {
e.preventDefault();
e.stopPropagation();
setPeekIssue({ workspaceSlug, projectId: issueDetails?.project_id, issueId: activity.entity_data.id });
}}
/>
);
};

View File

@@ -0,0 +1,63 @@
import { TActivityEntityData, TPageEntityData } from "@plane/types";
import { Avatar, Logo } from "@plane/ui";
import { ListItem } from "@/components/core/list";
import { calculateTimeAgo } from "@/helpers/date-time.helper";
import { FileText } from "lucide-react";
import { getFileURL } from "@plane/utils";
import { useMember } from "@/hooks/store";
import { useRouter } from "next/navigation";
type BlockProps = {
activity: TActivityEntityData;
ref: React.RefObject<HTMLDivElement>;
workspaceSlug: string;
};
export const RecentPage = (props: BlockProps) => {
const { activity, ref, workspaceSlug } = props;
// router
const router = useRouter();
// hooks
const { getUserDetails } = useMember();
// derived values
const pageDetails: TPageEntityData = activity.entity_data as TPageEntityData;
const ownerDetails = getUserDetails(pageDetails?.owned_by);
return (
<ListItem
key={activity.id}
itemLink=""
title={""}
prependTitleElement={
<div className="flex flex-shrink-0 items-center justify-center rounded-md gap-4 ">
<div className="flex flex-shrink-0 items-center justify-center rounded gap-4 bg-custom-background-80 w-8 h-8">
<>
{pageDetails?.logo_props?.in_use ? (
<Logo logo={pageDetails?.logo_props} size={16} type="lucide" />
) : (
<FileText className="h-4 w-4 text-custom-text-300" />
)}
</>
</div>
<div className="font-medium text-custom-sidebar-text-400 text-sm whitespace-nowrap">
{pageDetails?.project_identifier}
</div>
<div className="text-custom-text-200 font-medium text-sm whitespace-nowrap">{pageDetails?.name}</div>
<div className="font-medium text-xs text-custom-text-400">{calculateTimeAgo(activity.visited_at)}</div>
</div>
}
quickActionElement={
<div className="flex gap-4">
<Avatar src={getFileURL(ownerDetails?.avatar_url ?? "")} name={ownerDetails?.display_name} />
</div>
}
parentRef={ref}
disableLink={false}
className="bg-transparent my-auto !px-2 border-none py-3"
itemClassName="my-auto"
onItemClick={(e) => {
e.preventDefault();
e.stopPropagation();
router.push(`/${workspaceSlug}/projects/${pageDetails?.project_id}/pages/${pageDetails.id}`);
}}
/>
);
};

View File

@@ -0,0 +1,71 @@
import { TActivityEntityData, TProjectEntityData } from "@plane/types";
import { Logo } from "@plane/ui";
import { ListItem } from "@/components/core/list";
import { calculateTimeAgo } from "@/helpers/date-time.helper";
import { MemberDropdown } from "@/components/dropdowns";
import { useRouter } from "next/navigation";
type BlockProps = {
activity: TActivityEntityData;
ref: React.RefObject<HTMLDivElement>;
workspaceSlug: string;
};
export const RecentProject = (props: BlockProps) => {
const { activity, ref, workspaceSlug } = props;
// router
const router = useRouter();
// derived values
const projectDetails: TProjectEntityData = activity.entity_data as TProjectEntityData;
return (
<ListItem
key={activity.id}
itemLink=""
title={""}
prependTitleElement={
<div className="flex flex-shrink-0 items-center justify-center rounded-md gap-4 ">
<div className="flex flex-shrink-0 items-center justify-center rounded gap-4 bg-custom-background-80 w-8 h-8">
<Logo logo={projectDetails?.logo_props} size={16} />
</div>
<div className="font-medium text-custom-sidebar-text-400 text-sm whitespace-nowrap">
{projectDetails?.identifier}
</div>
<div className="text-custom-text-200 font-medium text-sm whitespace-nowrap">{projectDetails?.name}</div>
<div className="font-medium text-xs text-custom-text-400">{calculateTimeAgo(activity.visited_at)}</div>
</div>
}
quickActionElement={
<div className="flex gap-4">
{projectDetails?.project_members?.length > 0 && (
<div className="h-5">
<MemberDropdown
projectId={projectDetails?.id}
value={projectDetails?.project_members}
onChange={() => {}}
disabled
multiple
buttonVariant={
projectDetails?.project_members?.length > 0 ? "transparent-without-text" : "border-without-text"
}
buttonClassName={projectDetails?.project_members?.length > 0 ? "hover:bg-transparent px-0" : ""}
showTooltip={projectDetails?.project_members?.length === 0}
placeholder="Assignees"
optionsClassName="z-10"
tooltipContent=""
/>
</div>
)}
</div>
}
parentRef={ref}
disableLink={false}
className="bg-transparent my-auto !px-2 border-none py-3"
itemClassName="my-auto"
onItemClick={(e) => {
e.preventDefault();
e.stopPropagation();
router.push(`/${workspaceSlug}/projects/${projectDetails?.id}/issues`);
}}
/>
);
};

View File

@@ -4,8 +4,6 @@ import { useCycle, useProjectEstimates, useLabel, useModule, useProjectState } f
export const useWorkspaceIssueProperties = (workspaceSlug: string | string[] | undefined) => {
const { fetchWorkspaceLabels } = useLabel();
const { fetchWorkspaceStates } = useProjectState();
const { getWorkspaceEstimates } = useProjectEstimates();
const { fetchWorkspaceModules } = useModule();
@@ -33,13 +31,6 @@ export const useWorkspaceIssueProperties = (workspaceSlug: string | string[] | u
{ revalidateIfStale: false, revalidateOnFocus: false }
);
// fetch workspace states
useSWR(
workspaceSlug ? `WORKSPACE_STATES_${workspaceSlug}` : null,
workspaceSlug ? () => fetchWorkspaceStates(workspaceSlug.toString()) : null,
{ revalidateIfStale: false, revalidateOnFocus: false }
);
// fetch workspace estimates
useSWR(
workspaceSlug ? `WORKSPACE_ESTIMATES_${workspaceSlug}` : null,

View File

@@ -14,7 +14,7 @@ import { Button, setToast, TOAST_TYPE, Tooltip } from "@plane/ui";
// components
import { LogoSpinner } from "@/components/common";
// hooks
import { useMember, useProject, useUser, useUserPermissions, useWorkspace } from "@/hooks/store";
import { useMember, useProject, useProjectState, useUser, useUserPermissions, useWorkspace } from "@/hooks/store";
import { useFavorite } from "@/hooks/store/use-favorite";
import { usePlatformOS } from "@/hooks/use-platform-os";
// local
@@ -48,6 +48,7 @@ export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props)
const { isMobile } = usePlatformOS();
const { loader, workspaceInfoBySlug, fetchUserWorkspaceInfo, fetchUserProjectPermissions, allowPermissions } =
useUserPermissions();
const { fetchWorkspaceStates } = useProjectState();
// derived values
const canPerformWorkspaceMemberActions = allowPermissions(
[EUserPermissions.ADMIN, EUserPermissions.MEMBER],
@@ -93,6 +94,12 @@ export const WorkspaceAuthWrapper: FC<IWorkspaceAuthWrapper> = observer((props)
: null,
{ revalidateIfStale: false, revalidateOnFocus: false }
);
// fetch workspace states
useSWR(
workspaceSlug ? `WORKSPACE_STATES_${workspaceSlug}` : null,
workspaceSlug ? () => fetchWorkspaceStates(workspaceSlug.toString()) : null,
{ revalidateIfStale: false, revalidateOnFocus: false }
);
// initialize the local database
const { isLoading: isDBInitializing } = useSWRImmutable(

View File

@@ -313,6 +313,7 @@ export class WorkspaceService extends APIService {
throw error?.response?.data;
});
}
async searchEntity(workspaceSlug: string, params: TSearchEntityRequestPayload): Promise<TSearchResponse> {
return this.get(`/api/workspaces/${workspaceSlug}/entity-search/`, {
params: {
@@ -325,4 +326,17 @@ export class WorkspaceService extends APIService {
throw error?.response?.data;
});
}
// recents
async fetchWorkspaceRecents(workspaceSlug: string, entity_name?: string) {
return this.get(`/api/workspaces/${workspaceSlug}/recent-visits/`, {
params: {
entity_name,
},
})
.then((response) => response?.data)
.catch((error) => {
throw error?.response;
});
}
}

View File

@@ -1,5 +1,6 @@
import { action, makeObservable, observable, runInAction } from "mobx";
import { IWorkspaceLinkStore, WorkspaceLinkStore } from "./link.store";
import { WorkspaceService } from "@/plane-web/services";
export interface IHomeStore {
// observables
@@ -18,6 +19,8 @@ export class HomeStore implements IHomeStore {
widgetsMap: Record<string, any> = {};
// stores
quickLinks: IWorkspaceLinkStore;
// services
workspaceService: WorkspaceService;
constructor() {
makeObservable(this, {
@@ -29,6 +32,7 @@ export class HomeStore implements IHomeStore {
reorderWidgets: action,
});
// services
this.workspaceService = new WorkspaceService();
// stores
this.quickLinks = new WorkspaceLinkStore();
@@ -69,4 +73,13 @@ export class HomeStore implements IHomeStore {
// throw error;
// }
};
// fetchRecentActivity = async (workspaceSlug: string) => {
// try {
// const response = await this.workspaceService.fetchWorkspaceRecents(workspaceSlug);
// } catch (error) {
// console.error("Failed to fetch recent activity");
// throw error;
// }
// };
}