[WEB-3203] fix: dashboard widgets' empty state content and assets (#6450)

* fix: empty state content

* chore: replace margin with padding
This commit is contained in:
Aaryan Khandelwal 2025-01-24 15:23:41 +05:30 committed by GitHub
parent 0b53912295
commit d08c03f557
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 157 additions and 162 deletions

View file

@ -105,14 +105,14 @@ ul[data-type="taskList"] li > div {
}
ul[data-type="taskList"] li > label input[type="checkbox"] {
border: 1px solid rgba(var(--color-border-300)) !important;
border: 1px solid rgba(var(--color-text-100), 0.2) !important;
outline: none;
border-radius: 2px;
transform: scale(1.05);
}
.ProseMirror[contenteditable="true"] input[type="checkbox"]:hover {
background-color: rgba(var(--color-background-80));
background-color: rgba(var(--color-text-100), 0.1);
}
.ProseMirror[contenteditable="false"] input[type="checkbox"] {

View file

@ -12,7 +12,7 @@ import { getEditorFileHandlers } from "@/helpers/editor.helper";
// plane web hooks
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
import { useFileSize } from "@/plane-web/hooks/use-file-size";
import { Toolbar } from "./toolbar";
import { StickyEditorToolbar } from "./toolbar";
interface StickyEditorWrapperProps
extends Omit<ILiteTextEditor, "disabledExtensions" | "fileHandler" | "mentionHandler"> {
@ -87,7 +87,7 @@ export const StickyEditor = React.forwardRef<EditorRefApi, StickyEditorWrapperPr
isFocused ? "max-h-[200px] opacity-100 scale-y-100 mt-3" : "max-h-0 opacity-0 scale-y-0 invisible"
)}
>
<Toolbar
<StickyEditorToolbar
executeCommand={(item) => {
// TODO: update this while toolbar homogenization
// @ts-expect-error type mismatch here

View file

@ -23,7 +23,7 @@ type Props = {
const toolbarItems = TOOLBAR_ITEMS.sticky;
export const Toolbar: React.FC<Props> = (props) => {
export const StickyEditorToolbar: React.FC<Props> = (props) => {
const { executeCommand, editorRef, handleColorChange, handleDelete } = props;
// State to manage active states of toolbar items
@ -69,7 +69,11 @@ export const Toolbar: React.FC<Props> = (props) => {
</p>
}
>
<button onClick={() => setShowColorPalette(!showColorPalette)} className="flex text-custom-text-300">
<button
type="button"
onClick={() => setShowColorPalette(!showColorPalette)}
className="flex text-custom-text-100/50"
>
<Palette className="size-4 my-auto" />
</button>
</Tooltip>
@ -95,7 +99,7 @@ export const Toolbar: React.FC<Props> = (props) => {
type="button"
onClick={() => executeCommand(item)}
className={cn(
"grid place-items-center aspect-square rounded-sm p-0.5 text-custom-text-300",
"grid place-items-center aspect-square rounded-sm p-0.5 text-custom-text-100/50",
{}
)}
>
@ -122,7 +126,7 @@ export const Toolbar: React.FC<Props> = (props) => {
</p>
}
>
<button onClick={handleDelete} className="my-auto text-custom-text-300">
<button type="button" onClick={handleDelete} className="my-auto text-custom-text-100/50">
<Trash2 className="size-4" />
</button>
</Tooltip>

View file

@ -25,7 +25,7 @@ export const HOME_WIDGETS_LIST: {
quick_links: {
component: DashboardQuickLinks,
fullWidth: false,
title: "Quick links",
title: "Quicklinks",
},
recents: {
component: RecentActivityWidget,

View file

@ -1 +1,4 @@
export * from "./root";
export * from "./links";
export * from "./no-projects";
export * from "./recents";
export * from "./stickies";

View file

@ -1,12 +1,10 @@
import { Link2 } from "lucide-react";
export const LinksEmptyState = () => (
<div className="min-h-[110px] flex w-full justify-center py-6 bg-custom-border-100 rounded">
<div className="m-auto flex gap-2">
<Link2 size={30} className="text-custom-text-400/40 -rotate-45" />
<div className="text-custom-text-400 text-sm text-center my-auto">
Add any links you need for quick access to your work.
</div>
</div>
<div className="min-h-[110px] w-full flex items-center justify-center gap-2 py-6 bg-custom-background-90 text-custom-text-400 rounded">
<div className="flex-shrink-0 size-[30px] grid place-items-center">
<Link2 className="size-6 -rotate-45" />
</div>
);
<p className="text-sm text-center font-medium">Save links to work things that you{"'"}d like handy.</p>
</div>
);

View file

@ -2,6 +2,8 @@ import React from "react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { Briefcase, Hotel, Users } from "lucide-react";
// plane ui
import { Avatar } from "@plane/ui";
// helpers
import { getFileURL } from "@/helpers/file.helper";
// hooks
@ -9,7 +11,7 @@ import { useCommandPalette, useEventTracker, useUser, useUserPermissions } from
// plane web constants
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants";
export const EmptyWorkspace = () => {
export const NoProjectsEmptyState = () => {
// navigation
const { workspaceSlug } = useParams();
// store hooks
@ -26,11 +28,11 @@ export const EmptyWorkspace = () => {
const EMPTY_STATE_DATA = [
{
id: "create-project",
title: "Create a project",
description: "Create your first project now to get started",
icon: <Briefcase className="w-[40px] h-[40px] text-custom-primary-100" />,
title: "Create a project.",
description: "Most things start with a project in Plane.",
icon: <Briefcase className="size-12 text-custom-primary-100" />,
cta: {
text: "Create Project",
text: "Get started",
onClick: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
if (!canCreateProject) return;
e.preventDefault();
@ -42,66 +44,56 @@ export const EmptyWorkspace = () => {
},
{
id: "invite-team",
title: "Invite your team",
description: "The sub text will be of two lines and that will be placed.",
icon: <Users className="w-[40px] h-[40px] text-custom-primary-100" />,
title: "Invite your team.",
description: "Build, ship, and manage with coworkers.",
icon: <Users className="size-12 text-custom-primary-100" />,
cta: {
text: "Invite now",
text: "Get them in",
link: `/${workspaceSlug}/settings/members`,
},
},
{
id: "configure-workspace",
title: "Configure your workspace",
description: "The sub text will be of two lines and that will be placed.",
icon: <Hotel className="w-[40px] h-[40px] text-custom-primary-100" />,
title: "Set up your workspace.",
description: "Turn features on or off or go beyond that.",
icon: <Hotel className="size-12 text-custom-primary-100" />,
cta: {
text: "Configure workspace",
text: "Configure this workspace",
link: "settings",
},
},
{
id: "personalize-account",
title: "Personalize your account",
description: "The sub text will be of two lines and that will be placed.",
icon:
currentUser?.avatar_url && currentUser?.avatar_url.trim() !== "" ? (
<Link href={`/${workspaceSlug}/profile/${currentUser?.id}`}>
<span className="relative flex h-6 w-6 items-center justify-center rounded-full p-4 capitalize text-white">
<img
src={getFileURL(currentUser?.avatar_url)}
className="absolute left-0 top-0 h-full w-full rounded-full object-cover"
alt={currentUser?.display_name || currentUser?.email}
/>
</span>
</Link>
) : (
<Link href={`/${workspaceSlug}/profile/${currentUser?.id}`}>
<span className="relative flex h-6 w-6 items-center justify-center rounded-full bg-gray-700 p-4 capitalize text-white text-sm">
{(currentUser?.email ?? currentUser?.display_name ?? "?")[0]}
</span>
</Link>
),
title: "Make Plane yours.",
description: "Choose your picture, colors, and more.",
icon: (
<Avatar
src={getFileURL(currentUser?.avatar_url ?? "")}
name={currentUser?.display_name}
size={48}
className="text-xl"
showTooltip={false}
/>
),
cta: {
text: "Personalize account",
text: "Personalize now",
link: "/profile",
},
},
];
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
{EMPTY_STATE_DATA.map((item) => (
<div
key={item.id}
className="flex flex-col items-center justify-center py-8 bg-custom-background-100 rounded-lg text-center border border-custom-border-200/40"
className="flex flex-col items-center justify-center p-6 bg-custom-background-100 rounded-lg text-center border border-custom-border-200/40"
>
<div className="flex items-center justify-center bg-custom-primary-100/10 rounded-full w-[80px] h-[80px] mb-4">
<div className="grid place-items-center bg-custom-primary-100/10 rounded-full size-24 mb-3">
<span className="text-3xl my-auto">{item.icon}</span>
</div>
<h3 className="text-lg font-medium text-custom-text-100 mb-2">{item.title}</h3>
<p className="text-sm text-custom-text-200 mb-4 w-[80%] flex-1">{item.description}</p>
<h3 className="text-base font-medium text-custom-text-100 mb-2">{item.title}</h3>
<p className="text-sm text-custom-text-300 mb-2">{item.description}</p>
{item.cta.link ? (
<Link
href={item.cta.link}
@ -111,6 +103,7 @@ export const EmptyWorkspace = () => {
</Link>
) : (
<button
type="button"
className="text-custom-primary-100 hover:text-custom-primary-200 text-sm font-medium"
onClick={item.cta.onClick}
>

View file

@ -1,38 +1,41 @@
import { Briefcase, FileText, History } from "lucide-react";
// plane ui
import { LayersIcon } from "@plane/ui";
const getDisplayContent = (type: string) => {
switch (type) {
case "project":
return {
icon: Briefcase,
text: "Projects you go into or have assigned work in will show up here.",
};
case "page":
return {
icon: FileText,
text: "Create, see, or change something on pages you have access to and see them here.",
};
case "issue":
return {
icon: LayersIcon,
text: "Let's see some issues to see them show up here.",
};
default:
return {
icon: History,
text: "Whatever you see and act on in Plane will show up here.",
};
}
};
export const RecentsEmptyState = ({ type }: { type: string }) => {
const getDisplayContent = () => {
switch (type) {
case "project":
return {
icon: <Briefcase size={30} className="text-custom-text-400/40" />,
text: "Your recent projects will appear here once you visit one.",
};
case "page":
return {
icon: <FileText size={30} className="text-custom-text-400/40" />,
text: "Your recent pages will appear here once you visit one.",
};
case "issue":
return {
icon: <LayersIcon className="text-custom-text-400/40 w-[30px] h-[30px]" />,
text: "Your recent issues will appear here once you visit one.",
};
default:
return {
icon: <History size={30} className="text-custom-text-400/40" />,
text: "You dont have any recent items yet.",
};
}
};
const { icon, text } = getDisplayContent();
const displayContent = getDisplayContent(type);
return (
<div className="min-h-[120px] flex w-full justify-center py-6 bg-custom-border-100 rounded">
<div className="m-auto flex gap-2">
{icon} <div className="text-custom-text-400 text-sm text-center my-auto">{text}</div>
<div className="min-h-[110px] w-full flex items-center justify-center gap-2 py-6 bg-custom-background-90 text-custom-text-400 rounded">
<div className="flex-shrink-0 size-[30px] grid place-items-center">
<displayContent.icon className="size-6" />
</div>
<p className="text-sm text-center font-medium">{displayContent.text}</p>
</div>
);
};

View file

@ -2,12 +2,12 @@
import { RecentStickyIcon } from "@plane/ui";
export const StickiesEmptyState = () => (
<div className="min-h-[110px] flex w-full justify-center py-6 bg-custom-border-100 rounded">
<div className="m-auto flex gap-2">
<RecentStickyIcon className="h-[30px] w-[30px] text-custom-text-400/40" />
<div className="text-custom-text-400 text-sm text-center my-auto">
No stickies yet. Add one to start making quick notes.
</div>
<div className="min-h-[110px] w-full flex items-center justify-center gap-2 py-6 bg-custom-background-90 text-custom-text-400 rounded">
<div className="flex-shrink-0 size-[30px] grid place-items-center">
<RecentStickyIcon className="size-6" />
</div>
<p className="text-sm text-center font-medium">
Jot down an idea, capture an aha, or record a brainwave. Add a sticky to get started.
</p>
</div>
);

View file

@ -14,7 +14,7 @@ export const AddLink = (props: TProps) => {
<div className="rounded p-2 bg-custom-background-80/40 w-8 h-8 my-auto">
<PlusIcon className="h-4 w-4 stroke-2 text-custom-text-350" />
</div>
<div className="text-sm font-medium my-auto">Add quick Link</div>
<div className="text-sm font-medium my-auto">Add quicklink</div>
</button>
);
};

View file

@ -66,9 +66,7 @@ export const LinkCreateUpdateModal: FC<TLinkCreateEditModal> = observer((props)
<ModalCore isOpen={isModalOpen} handleClose={onClose}>
<form onSubmit={handleSubmit(handleFormSubmit)}>
<div className="space-y-5 p-5">
<h3 className="text-xl font-medium text-custom-text-200">
{preloadedData?.id ? "Update" : "Add"} quick link
</h3>
<h3 className="text-xl font-medium text-custom-text-200">{preloadedData?.id ? "Update" : "Add"} quicklink</h3>
<div className="mt-2 space-y-3">
<div>
<label htmlFor="url" className="mb-2 text-custom-text-200 text-base font-medium">
@ -124,7 +122,7 @@ export const LinkCreateUpdateModal: FC<TLinkCreateEditModal> = observer((props)
Cancel
</Button>
<Button variant="primary" size="sm" type="submit" loading={isSubmitting}>
{preloadedData?.id ? (isSubmitting ? "Updating" : "Update") : isSubmitting ? "Adding" : "Add"} quick link
{preloadedData?.id ? (isSubmitting ? "Updating" : "Update") : isSubmitting ? "Adding" : "Add"} quicklink
</Button>
</div>
</form>

View file

@ -1,15 +1,17 @@
"use client";
import { FC } from "react";
// hooks
// ui
import { observer } from "mobx-react";
import { Pencil, Trash2, ExternalLink, EllipsisVertical, Link2, Link } from "lucide-react";
import { Pencil, Trash2, ExternalLink, Link2, Link } from "lucide-react";
// plane ui
import { TOAST_TYPE, setToast, CustomMenu, TContextMenuItem } from "@plane/ui";
// helpers
// plane utils
import { cn, copyTextToClipboard } from "@plane/utils";
// helpers
import { calculateTimeAgo } from "@/helpers/date-time.helper";
// hooks
import { useHome } from "@/hooks/store/use-home";
// types
import { TLinkOperations } from "./use-links";
export type TProjectLinkDetail = {
@ -75,54 +77,47 @@ export const ProjectLinkDetail: FC<TProjectLinkDetail> = observer((props) => {
return (
<div
onClick={handleOpenInNewTab}
className="cursor-pointer group btn btn-primary flex bg-custom-background-100 px-4 w-[230px] h-[56px] border-[0.5px] border-custom-border-200 rounded-md gap-4 hover:shadow-md"
className="cursor-pointer group flex items-center bg-custom-background-100 px-4 w-[230px] h-[56px] border-[0.5px] border-custom-border-200 rounded-md gap-4 hover:shadow-md transition-shadow"
>
<div className="rounded p-2 bg-custom-background-80/40 w-8 h-8 my-auto">
<Link2 className="h-4 w-4 stroke-2 text-custom-text-350 -rotate-45" />
<div className="flex-shrink-0 size-8 rounded p-2 bg-custom-background-80 grid place-items-center">
<Link2 className="size-4 stroke-2 text-custom-text-350 -rotate-45" />
</div>
<div className="my-auto flex-1">
<div className="flex-1 truncate">
<div className="text-sm font-medium truncate">{linkDetail.title || linkDetail.url}</div>
<div className="text-xs font-medium text-custom-text-400">{calculateTimeAgo(linkDetail.created_at)}</div>
</div>
<CustomMenu
customButton={
<EllipsisVertical className="opacity-0 h-4 w-4 stroke-2 text-custom-text-350 group-hover:opacity-100" />
}
placement="bottom-end"
menuItemsClassName="z-20"
closeOnSelect
className=" my-auto"
>
{MENU_ITEMS.map((item) => (
<CustomMenu.MenuItem
key={item.key}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
item.action();
}}
className={cn("flex items-center gap-2 w-full ", {
"text-custom-text-400": item.disabled,
})}
disabled={item.disabled}
>
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
<div>
<h5>{item.title}</h5>
{item.description && (
<p
className={cn("text-custom-text-300 whitespace-pre-line", {
"text-custom-text-400": item.disabled,
})}
>
{item.description}
</p>
)}
</div>
</CustomMenu.MenuItem>
))}
</CustomMenu>
<div className="hidden group-hover:block">
<CustomMenu placement="bottom-end" menuItemsClassName="z-20" closeOnSelect verticalEllipsis>
{MENU_ITEMS.map((item) => (
<CustomMenu.MenuItem
key={item.key}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
item.action();
}}
className={cn("flex items-center gap-2 w-full ", {
"text-custom-text-400": item.disabled,
})}
disabled={item.disabled}
>
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
<div>
<h5>{item.title}</h5>
{item.description && (
<p
className={cn("text-custom-text-300 whitespace-pre-line", {
"text-custom-text-400": item.disabled,
})}
>
{item.description}
</p>
)}
</div>
</CustomMenu.MenuItem>
))}
</CustomMenu>
</div>
</div>
);
});

View file

@ -38,9 +38,9 @@ export const ProjectLinkList: FC<TProjectLinkList> = observer((props) => {
buttonClassName="bg-custom-background-90/20"
>
<div className="flex gap-2 mb-2 flex-wrap flex-1">
{links &&
links.length > 0 &&
links.map((linkId) => <ProjectLinkDetail key={linkId} linkId={linkId} linkOperations={linkOperations} />)}
{links.map((linkId) => (
<ProjectLinkDetail key={linkId} linkId={linkId} linkOperations={linkOperations} />
))}
</div>
</ContentOverflowWrapper>
</div>

View file

@ -34,14 +34,14 @@ export const DashboardQuickLinks = observer((props: THomeWidgetProps) => {
/>
<div className="mb-2">
<div className="flex items-center justify-between mb-4">
<div className="text-base font-semibold text-custom-text-350">Quick links</div>
<div className="text-base font-semibold text-custom-text-350">Quicklinks</div>
<button
onClick={() => {
toggleLinkModal(true);
}}
className="flex gap-1 text-sm font-medium text-custom-primary-100 my-auto"
>
<Plus className="size-4 my-auto" /> <span>Add quick link</span>
<Plus className="size-4 my-auto" /> <span>Add quicklink</span>
</button>
</div>
<div className="flex flex-wrap w-full">

View file

@ -11,8 +11,7 @@ import { LayersIcon } from "@plane/ui";
import { ContentOverflowWrapper } from "@/components/core/content-overflow-HOC";
import { useProject } from "@/hooks/store";
import { WorkspaceService } from "@/plane-web/services";
import { EmptyWorkspace } from "../empty-states";
import { RecentsEmptyState } from "../empty-states/recents";
import { NoProjectsEmptyState, RecentsEmptyState } from "../empty-states";
import { EWidgetKeys, WidgetLoader } from "../loaders";
import { FiltersDropdown } from "./filters";
import { RecentIssue } from "./issue";
@ -34,6 +33,7 @@ export const RecentActivityWidget: React.FC<THomeWidgetProps> = observer((props)
const [filter, setFilter] = useState<TRecentActivityFilterKeys>(filters[0].name);
// ref
const ref = useRef<HTMLDivElement>(null);
// store hooks
const { joinedProjectIds, loader } = useProject();
const { data: recents, isLoading } = useSWR(
@ -65,10 +65,11 @@ export const RecentActivityWidget: React.FC<THomeWidgetProps> = observer((props)
}
};
if (!loader && joinedProjectIds?.length === 0) return <EmptyWorkspace />;
if (!loader && joinedProjectIds?.length === 0) return <NoProjectsEmptyState />;
if (!isLoading && recents?.length === 0)
return (
<div ref={ref} className=" max-h-[500px] overflow-y-scroll">
<div ref={ref} className="max-h-[500px] overflow-y-scroll">
<div className="flex items-center justify-between mb-4">
<div className="text-base font-semibold text-custom-text-350">Recents</div>
<FiltersDropdown filters={filters} activeFilter={filter} setActiveFilter={setFilter} />
@ -94,10 +95,9 @@ export const RecentActivityWidget: React.FC<THomeWidgetProps> = observer((props)
<div className="min-h-[250px] flex flex-col">
{isLoading && <WidgetLoader widgetKey={WIDGET_KEY} />}
{!isLoading &&
recents?.length > 0 &&
recents
.filter((recent: TActivityEntityData) => recent.entity_data)
.map((activity: TActivityEntityData) => <div key={activity.id}>{resolveRecent(activity)}</div>)}
?.filter((recent) => recent.entity_data)
.map((activity) => <div key={activity.id}>{resolveRecent(activity)}</div>)}
</div>
</ContentOverflowWrapper>
);

View file

@ -84,7 +84,7 @@ export const StickiesList = observer((props: TProps) => {
if (loader === "loaded" && workspaceStickyIds.length === 0) {
return (
<div className="size-full grid place-items-center">
<div className="size-full grid place-items-center px-2">
{isStickiesPage || searchQuery ? (
<EmptyState
type={searchQuery ? EmptyStateType.STICKIES_SEARCH : EmptyStateType.STICKIES}

View file

@ -922,7 +922,7 @@ const emptyStateDetails: Record<EmptyStateType, EmptyStateDetails> = {
key: EmptyStateType.STICKIES,
title: "Stickies are quick notes and to-dos you take down on the fly.",
description:
"Capture your thoughts and ideas effortlessly by creating stickies that you can access anytime and from anywhere.",
"Capture ideas, ahas, brainwaves, light-bulb moments without scrambling for a pen and paper, hunting for the recorder app on your phone, or firing up a notes app only to forget all about it later. Keep them all right next to your work so you can easily come back, build them up, or well, discard them.",
path: "/empty-state/stickies/stickies",
primaryButton: {
icon: <Plus className="size-4" />,
@ -945,8 +945,8 @@ const emptyStateDetails: Record<EmptyStateType, EmptyStateDetails> = {
},
[EmptyStateType.HOME_WIDGETS]: {
key: EmptyStateType.HOME_WIDGETS,
title: "It's Quiet Without Widgets, Turn Them On",
description: "It looks like all your widgets are turned off. Enable them\nnow to enhance your experience!",
title: "So much to add, yet such empty",
description: "You have turned off all your widgets. Turn some or\nall of them back on to see delightful things.",
path: "/empty-state/dashboard/widgets",
primaryButton: {
icon: <Shapes className="size-4" />,

View file

@ -16,6 +16,7 @@ import {
TSearchResponse,
TSearchEntityRequestPayload,
TWidgetEntityData,
TActivityEntityData,
} from "@plane/types";
import { APIService } from "@/services/api.service";
// helpers
@ -282,7 +283,7 @@ export class WorkspaceService extends APIService {
});
}
// quick links
// quicklinks
async fetchWorkspaceLinks(workspaceSlug: string): Promise<TLink[]> {
return this.get(`/api/workspaces/${workspaceSlug}/quick-links/`)
.then((response) => response?.data)
@ -329,7 +330,7 @@ export class WorkspaceService extends APIService {
}
// recents
async fetchWorkspaceRecents(workspaceSlug: string, entity_name?: string) {
async fetchWorkspaceRecents(workspaceSlug: string, entity_name?: string): Promise<TActivityEntityData[]> {
return this.get(`/api/workspaces/${workspaceSlug}/recent-visits/`, {
params: {
entity_name,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Before After
Before After