mirror of
https://github.com/makeplane/plane.git
synced 2026-07-12 05:21:14 +02:00
fix: recents api integrations
This commit is contained in:
38
packages/types/src/dashboard.d.ts
vendored
38
packages/types/src/dashboard.d.ts
vendored
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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`,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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];
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
13
web/core/components/home/widgets/loaders/quick-links.tsx
Normal file
13
web/core/components/home/widgets/loaders/quick-links.tsx
Normal 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>
|
||||
);
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
@@ -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>
|
||||
);
|
||||
@@ -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} />
|
||||
|
||||
@@ -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"} />}
|
||||
|
||||
@@ -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 ?? [];
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
63
web/core/components/home/widgets/recents/page.tsx
Normal file
63
web/core/components/home/widgets/recents/page.tsx
Normal 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}`);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
71
web/core/components/home/widgets/recents/project.tsx
Normal file
71
web/core/components/home/widgets/recents/project.tsx
Normal 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`);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
// }
|
||||
// };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user