fix: manage widgets integrations (#6331)

* wip

* chore: wip

* fix: preserved old component

* fix

* fix: seperate route added

* fix

* Only return user ID of project members

* Return issue ID

* fix: recents api integrations

* fix: types

* fix: types

* fix: added tooltips

* chore: added apis

* fix: widgets fix

* fix: lint

* fix: integrated dashboard apis

* fix: added ee dummy component for header

* fix: removed logs

---------

Co-authored-by: sangeethailango <sangeethailango21@gmail.com>
This commit is contained in:
Akshita Goyal 2025-01-07 12:57:35 +05:30 committed by GitHub
parent edb68a1bc6
commit 0af9b09844
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 110 additions and 74 deletions

View file

@ -2,7 +2,7 @@ import { TLogoProps } from "./common";
import { TIssuePriorities } from "./issues";
export type TRecentActivityFilterKeys = "all item" | "issue" | "page" | "project";
export type THomeWidgetKeys = "quick_links" | "recent_activity" | "stickies";
export type THomeWidgetKeys = "quick_links" | "recents" | "my_stickies" | "quick_tutorial" | "new_at_plane";
export type THomeWidgetProps = {
workspaceSlug: string;
@ -69,7 +69,7 @@ export type TLinkIdMap = {
};
export type TWidgetEntityData = {
key: string;
key: THomeWidgetKeys;
name: string;
is_enabled: boolean;
sort_order: number;

View file

@ -12,18 +12,20 @@ import { DashboardQuickLinks } from "./widgets/links";
import { ManageWidgetsModal } from "./widgets/manage";
const WIDGETS_LIST: {
[key in THomeWidgetKeys]: { component: React.FC<THomeWidgetProps>; fullWidth: boolean };
[key in THomeWidgetKeys]: { component: React.FC<THomeWidgetProps> | null; fullWidth: boolean };
} = {
quick_links: { component: DashboardQuickLinks, fullWidth: false },
recent_activity: { component: RecentActivityWidget, fullWidth: false },
stickies: { component: StickiesWidget, fullWidth: false },
recents: { component: RecentActivityWidget, fullWidth: false },
my_stickies: { component: StickiesWidget, fullWidth: false },
new_at_plane: { component: null, fullWidth: false },
quick_tutorial: { component: null, fullWidth: false },
};
export const DashboardWidgets = observer(() => {
// router
const { workspaceSlug } = useParams();
// store hooks
const { toggleWidgetSettings, showWidgetSettings } = useHome();
const { toggleWidgetSettings, widgetsMap, showWidgetSettings, orderedWidgets } = useHome();
if (!workspaceSlug) return null;
@ -36,9 +38,11 @@ export const DashboardWidgets = observer(() => {
handleOnClose={() => toggleWidgetSettings(false)}
/>
{Object.entries(WIDGETS_LIST).map(([key, widget]) => {
const WidgetComponent = widget.component;
if (widget.fullWidth)
{orderedWidgets.map((key) => {
const WidgetComponent = WIDGETS_LIST[key]?.component;
const isEnabled = widgetsMap[key]?.is_enabled;
if (!WidgetComponent || !isEnabled) return null;
if (WIDGETS_LIST[key]?.fullWidth)
return (
<div key={key} className="lg:col-span-2">
<WidgetComponent workspaceSlug={workspaceSlug.toString()} />

View file

@ -1,7 +1,7 @@
import { useEffect } from "react";
import { observer } from "mobx-react";
import { useParams } from "next/navigation";
// components
import useSWR from "swr";
import { ContentWrapper } from "@plane/ui";
import { EmptyState } from "@/components/empty-state";
import { TourRoot } from "@/components/onboarding";
@ -11,7 +11,7 @@ import { PRODUCT_TOUR_COMPLETED } from "@/constants/event-tracker";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import { useCommandPalette, useUserProfile, useEventTracker, useDashboard, useProject, useUser } from "@/hooks/store";
import { useCommandPalette, useUserProfile, useEventTracker, useProject, useUser } from "@/hooks/store";
import { useHome } from "@/hooks/store/use-home";
import useSize from "@/hooks/use-window-size";
import { IssuePeekOverview } from "../issues";
@ -29,12 +29,20 @@ export const WorkspaceHomeView = observer(() => {
const { data: currentUser } = useUser();
const { data: currentUserProfile, updateTourCompleted } = useUserProfile();
const { captureEvent } = useEventTracker();
const { homeDashboardId, fetchHomeDashboardWidgets } = useDashboard();
const { toggleWidgetSettings } = useHome();
const { toggleWidgetSettings, fetchWidgets } = useHome();
const { joinedProjectIds, loader } = useProject();
const [windowWidth] = useSize();
useSWR(
workspaceSlug ? `HOME_DASHBOARD_WIDGETS_${workspaceSlug}` : null,
workspaceSlug ? () => fetchWidgets(workspaceSlug?.toString()) : null,
{
revalidateIfStale: true,
revalidateOnFocus: false,
revalidateOnReconnect: true,
}
);
const handleTourCompleted = () => {
updateTourCompleted()
.then(() => {
@ -48,13 +56,6 @@ export const WorkspaceHomeView = observer(() => {
});
};
// fetch home dashboard widgets on workspace change
useEffect(() => {
if (!workspaceSlug) return;
fetchHomeDashboardWidgets(workspaceSlug?.toString());
}, [fetchHomeDashboardWidgets, workspaceSlug]);
// TODO: refactor loader implementation
return (
<>
@ -63,7 +64,7 @@ export const WorkspaceHomeView = observer(() => {
<TourRoot onComplete={handleTourCompleted} />
</div>
)}
{homeDashboardId && joinedProjectIds && (
{joinedProjectIds && (
<>
{joinedProjectIds.length > 0 || loader ? (
<>

View file

@ -51,13 +51,13 @@ export const UserGreetingsView: FC<IUserGreetingsView> = (props) => {
</div>
</h6>
</div>
{/* <button
<button
onClick={handleWidgetModal}
className="flex items-center gap-2 font-medium text-custom-text-300 justify-center border border-custom-border-200 rounded p-2 my-auto mb-0"
>
<Shapes size={16} />
<div className="text-xs font-medium">Manage widgets</div>
</button> */}
</button>
</div>
);
};

View file

@ -2,10 +2,10 @@ import { FC, useEffect, useState } from "react";
import { observer } from "mobx-react";
// computed
import { useHome } from "@/hooks/store/use-home";
import { EWidgetKeys, WidgetLoader } from "../loaders";
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">;

View file

@ -1,10 +1,10 @@
import { observer } from "mobx-react";
import useSWR from "swr";
import { THomeWidgetProps } from "@plane/types";
import { useHome } from "@/hooks/store/use-home";
import { LinkCreateUpdateModal } from "./create-update-link-modal";
import { ProjectLinkList } from "./links";
import { useLinks } from "./use-links";
import { THomeWidgetProps } from "@plane/types";
export const DashboardQuickLinks = observer((props: THomeWidgetProps) => {
const { workspaceSlug } = props;

View file

@ -1,6 +1,6 @@
// components
import { RecentActivityWidgetLoader } from "./recent-activity";
import { QuickLinksWidgetLoader } from "./quick-links";
import { RecentActivityWidgetLoader } from "./recent-activity";
// types

View file

@ -14,38 +14,45 @@ import { attachInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree
import { observer } from "mobx-react";
// plane helpers
import { useParams } from "next/navigation";
import { createRoot } from "react-dom/client";
// ui
import { InstructionType } from "@plane/types";
import { InstructionType, TWidgetEntityData } from "@plane/types";
// components
import { DropIndicator, ToggleSwitch } from "@plane/ui";
// helpers
import { cn } from "@plane/utils";
import { useHome } from "@/hooks/store/use-home";
import { WidgetItemDragHandle } from "./widget-item-drag-handle";
import { getCanDrop, getInstructionFromPayload } from "./widget.helpers";
type Props = {
widgetId: string;
isLastChild: boolean;
widget: any;
handleDrop: (self: DropTargetRecord, source: ElementDragPayload, location: DragLocationHistory) => void;
handleToggle: (workspaceSlug: string, widgetKey: string, is_enabled: boolean) => void;
};
export const WidgetItem: FC<Props> = observer((props) => {
// props
const { isLastChild, widget, handleDrop } = props;
const { widgetId, isLastChild, handleDrop, handleToggle } = props;
const { workspaceSlug } = useParams();
//state
const [isDragging, setIsDragging] = useState(false);
const [instruction, setInstruction] = useState<InstructionType | undefined>(undefined);
//ref
const elementRef = useRef<HTMLDivElement>(null);
// hooks
const { widgetsMap } = useHome();
// derived values
const widget = widgetsMap[widgetId] as TWidgetEntityData;
// drag and drop
useEffect(() => {
const element = elementRef.current;
if (!element) return;
const initialData = { id: widget.id, isGroup: false };
const initialData = { id: widget.key, isGroup: false };
return combine(
draggable({
element,
@ -62,7 +69,7 @@ export const WidgetItem: FC<Props> = observer((props) => {
getOffset: pointerOutsideOfPreview({ x: "0px", y: "0px" }),
render: ({ container }) => {
const root = createRoot(container);
root.render(<div className="rounded bg-custom-background-100 text-sm p-1 pr-2">{widget.title}</div>);
root.render(<div className="rounded bg-custom-background-100 text-sm p-1 pr-2">{widget.key}</div>);
return () => root.unmount();
},
nativeSetDragImage,
@ -104,7 +111,7 @@ export const WidgetItem: FC<Props> = observer((props) => {
})
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [elementRef?.current, isDragging, isLastChild, widget.id]);
}, [elementRef?.current, isDragging, isLastChild, widget.key]);
return (
<div className="">
@ -120,9 +127,12 @@ export const WidgetItem: FC<Props> = observer((props) => {
>
<div className="flex items-center">
<WidgetItemDragHandle sort_order={widget.sort_order} isDragging={isDragging} />
<div>{widget.title}</div>
<div>{widget.key.replaceAll("_", " ")}</div>
</div>
{/* <ToggleSwitch /> */}
<ToggleSwitch
value={widget.is_enabled}
onChange={() => handleToggle(workspaceSlug.toString(), widget.key, !widget.is_enabled)}
/>
</div>
{isLastChild && <DropIndicator isVisible={instruction === "reorder-below"} />}
</div>

View file

@ -3,17 +3,14 @@ import {
DropTargetRecord,
ElementDragPayload,
} from "@atlaskit/pragmatic-drag-and-drop/dist/types/internal-types";
import { observer } from "mobx-react";
import { setToast, TOAST_TYPE } from "@plane/ui";
import { useHome } from "@/hooks/store/use-home";
import { WidgetItem } from "./widget-item";
import { getInstructionFromPayload, TargetData } from "./widget.helpers";
const WIDGETS_LIST = [
{ id: 1, title: "quick links" },
{ id: 2, title: "recents" },
{ id: 3, title: "stickies" },
];
export const WidgetList = ({ workspaceSlug }: { workspaceSlug: string }) => {
const { reorderWidget } = useHome();
export const WidgetList = observer(({ workspaceSlug }: { workspaceSlug: string }) => {
const { orderedWidgets, reorderWidget, toggleWidget } = useHome();
const handleDrop = (self: DropTargetRecord, source: ElementDragPayload, location: DragLocationHistory) => {
const dropTargets = location?.current?.dropTargets ?? [];
@ -30,20 +27,34 @@ export const WidgetList = ({ workspaceSlug }: { workspaceSlug: string }) => {
if (!sourceData.id) return;
if (droppedId) {
reorderWidget(workspaceSlug, sourceData.id, droppedId, instruction); /** sequence */
try {
reorderWidget(workspaceSlug, sourceData.id, droppedId, instruction); /** sequence */
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Widget reordered successfully.",
});
} catch {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Error occurred while reordering widget.",
});
}
}
};
return (
<div className="my-4">
{WIDGETS_LIST.map((widget, index) => (
{orderedWidgets.map((widget, index) => (
<WidgetItem
key={widget.id}
widget={widget}
isLastChild={index === WIDGETS_LIST.length - 1}
key={widget}
widgetId={widget}
isLastChild={index === orderedWidgets.length - 1}
handleDrop={handleDrop}
handleToggle={toggleWidget}
/>
))}
</div>
);
};
});

View file

@ -1,5 +1,5 @@
import { extractInstruction } from "@atlaskit/pragmatic-drag-and-drop-hitbox/tree-item";
import { IFavorite, InstructionType, IPragmaticPayloadLocation, TDropTarget } from "@plane/types";
import { InstructionType, IPragmaticPayloadLocation, TDropTarget, TWidgetEntityData } from "@plane/types";
export type TargetData = {
id: string;
@ -11,7 +11,7 @@ export type TargetData = {
/**
* extracts the Payload and translates the instruction for the current dropTarget based on drag and drop payload
* @param dropTarget dropTarget for which the instruction is required
* @param source the dragging favorite data that is being dragged on the dropTarget
* @param source the dragging widget data that is being dragged on the dropTarget
* @param location location includes the data of all the dropTargets the source is being dragged on
* @returns Instruction for dropTarget
*/
@ -37,7 +37,7 @@ export const getInstructionFromPayload = (
instruction = dropTargetData.isChild ? "reorder-above" : "make-child";
}
// if source that is being dragged is a group. A group cannon be a child of any other favorite,
// if source that is being dragged is a group. A group cannon be a child of any other widget,
// hence if current instruction is to be a child of dropTarget then reorder-above instead
if (instruction === "make-child" && sourceData.isGroup) instruction = "reorder-above";
@ -45,18 +45,18 @@ export const getInstructionFromPayload = (
};
/**
* This provides a boolean to indicate if the favorite can be dropped onto the droptarget
* This provides a boolean to indicate if the widget can be dropped onto the droptarget
* @param source
* @param favorite
* @param widget
* @returns
*/
export const getCanDrop = (source: TDropTarget, favorite: IFavorite | undefined) => {
export const getCanDrop = (source: TDropTarget, widget: TWidgetEntityData | undefined) => {
const sourceData = source?.data;
if (!sourceData) return false;
// a favorite cannot be dropped on to itself
if (sourceData.id === favorite?.id) return false;
// a widget cannot be dropped on to itself
if (sourceData.id === widget?.key) return false;
return true;
};

View file

@ -3,9 +3,9 @@
import { FC } from "react";
import { observer } from "mobx-react";
import { ChevronDown } from "lucide-react";
import { TRecentActivityFilterKeys } from "@plane/types";
import { CustomMenu } from "@plane/ui";
import { cn } from "@plane/utils";
import { TRecentActivityFilterKeys } from "@plane/types";
export type TFiltersDropdown = {
className?: string;

View file

@ -3,18 +3,18 @@
import { useRef, useState } from "react";
import { observer } from "mobx-react";
// types
import useSWR from "swr";
import { Briefcase, FileText } from "lucide-react";
import { TActivityEntityData, THomeWidgetProps, TRecentActivityFilterKeys } from "@plane/types";
// components
import { LayersIcon } from "@plane/ui";
import { WorkspaceService } from "@/plane-web/services";
import { EmptyWorkspace } from "../empty-states";
import { EWidgetKeys, WidgetLoader } from "../loaders";
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";
import { RecentProject } from "./project";
const WIDGET_KEY = EWidgetKeys.RECENT_ACTIVITY;
const workspaceService = new WorkspaceService();

View file

@ -1,11 +1,11 @@
import { useRouter } from "next/navigation";
import { FileText } from "lucide-react";
import { TActivityEntityData, TPageEntityData } from "@plane/types";
import { Avatar, Logo } from "@plane/ui";
import { getFileURL } from "@plane/utils";
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;

View file

@ -1,9 +1,9 @@
import { useRouter } from "next/navigation";
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";
import { calculateTimeAgo } from "@/helpers/date-time.helper";
type BlockProps = {
activity: TActivityEntityData;

View file

@ -1,7 +1,7 @@
import orderBy from "lodash/orderBy";
import set from "lodash/set";
import { action, makeObservable, observable, runInAction } from "mobx";
import { TWidgetEntityData } from "@plane/types";
import { action, computed, makeObservable, observable, runInAction } from "mobx";
import { THomeWidgetKeys, TWidgetEntityData } from "@plane/types";
import { WorkspaceService } from "@/plane-web/services";
import { IWorkspaceLinkStore, WorkspaceLinkStore } from "./link.store";
@ -9,6 +9,9 @@ export interface IHomeStore {
// observables
showWidgetSettings: boolean;
widgetsMap: Record<string, TWidgetEntityData>;
widgets: THomeWidgetKeys[];
// computed
orderedWidgets: THomeWidgetKeys[];
//stores
quickLinks: IWorkspaceLinkStore;
// actions
@ -22,7 +25,7 @@ export class HomeStore implements IHomeStore {
// observables
showWidgetSettings = false;
widgetsMap: Record<string, TWidgetEntityData> = {};
widgets: string[] = [];
widgets: THomeWidgetKeys[] = [];
// stores
quickLinks: IWorkspaceLinkStore;
// services
@ -34,6 +37,8 @@ export class HomeStore implements IHomeStore {
showWidgetSettings: observable,
widgetsMap: observable,
widgets: observable,
// computed
orderedWidgets: computed,
// actions
toggleWidgetSettings: action,
fetchWidgets: action,
@ -47,6 +52,10 @@ export class HomeStore implements IHomeStore {
this.quickLinks = new WorkspaceLinkStore();
}
get orderedWidgets() {
return orderBy(Object.values(this.widgetsMap), "sort_order", "desc").map((widget) => widget.key);
}
toggleWidgetSettings = (value?: boolean) => {
this.showWidgetSettings = value !== undefined ? value : !this.showWidgetSettings;
};
@ -55,7 +64,7 @@ export class HomeStore implements IHomeStore {
try {
const widgets = await this.workspaceService.fetchWorkspaceWidgets(workspaceSlug);
runInAction(() => {
this.widgets = widgets.map((widget) => widget.key);
this.widgets = orderBy(Object.values(widgets), "sort_order", "desc").map((widget) => widget.key);
widgets.forEach((widget) => {
this.widgetsMap[widget.key] = widget;
});

View file

@ -0,0 +1 @@
export const HomePageHeader = () => <></>;