UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: выравнивание home-шапки и системных уведомлений

This commit is contained in:
DCCONSTRUCTIONS 2026-04-26 11:52:29 +03:00
parent ba996998e8
commit 8b5f15333a
9 changed files with 109 additions and 110 deletions

View File

@ -34,9 +34,7 @@ class WorkspaceHomePreferenceViewSet(BaseAPIView):
if key not in ["quick_tutorial", "new_at_plane"] if key not in ["quick_tutorial", "new_at_plane"]
] ]
sort_order_counter = 1 for sort_order_counter, preference in enumerate(keys, start=1):
for preference in keys:
if preference not in get_preference.values_list("key", flat=True): if preference not in get_preference.values_list("key", flat=True):
create_preference_keys.append(preference) create_preference_keys.append(preference)
@ -55,7 +53,6 @@ class WorkspaceHomePreferenceViewSet(BaseAPIView):
batch_size=10, batch_size=10,
ignore_conflicts=True, ignore_conflicts=True,
) )
sort_order_counter += 1
preference = WorkspaceHomePreference.objects.filter(user=request.user, workspace_id=workspace.id) preference = WorkspaceHomePreference.objects.filter(user=request.user, workspace_id=workspace.id)

View File

@ -380,6 +380,7 @@ class WorkspaceHomePreference(BaseModel):
QUICK_LINKS = "quick_links", "Quick Links" QUICK_LINKS = "quick_links", "Quick Links"
RECENTS = "recents", "Recents" RECENTS = "recents", "Recents"
MY_STICKIES = "my_stickies", "My Stickies" MY_STICKIES = "my_stickies", "My Stickies"
PROJECT_LATEST_ISSUES = "project_latest_issues", "Project Latest Issues"
NEW_AT_PLANE = "new_at_plane", "New at Plane" NEW_AT_PLANE = "new_at_plane", "New at Plane"
QUICK_TUTORIAL = "quick_tutorial", "Quick Tutorial" QUICK_TUTORIAL = "quick_tutorial", "Quick Tutorial"

View File

@ -57,6 +57,11 @@ export const HOME_WIDGETS_LIST: {
fullWidth: false, fullWidth: false,
title: "stickies.title", title: "stickies.title",
}, },
project_latest_issues: {
component: null,
fullWidth: true,
title: "Последние задачи проекта",
},
new_at_plane: { new_at_plane: {
component: null, component: null,
fullWidth: false, fullWidth: false,
@ -164,6 +169,7 @@ export const DashboardWidgets = observer(function DashboardWidgets(props: Dashbo
const isRecentsEnabled = !!widgetsMap.recents?.is_enabled; const isRecentsEnabled = !!widgetsMap.recents?.is_enabled;
const isQuickLinksEnabled = !!widgetsMap.quick_links?.is_enabled; const isQuickLinksEnabled = !!widgetsMap.quick_links?.is_enabled;
const isStickiesEnabled = !!widgetsMap.my_stickies?.is_enabled; const isStickiesEnabled = !!widgetsMap.my_stickies?.is_enabled;
const isProjectLatestIssuesEnabled = widgetsMap.project_latest_issues?.is_enabled ?? true;
const hasSecondaryWidgets = isQuickLinksEnabled || isStickiesEnabled; const hasSecondaryWidgets = isQuickLinksEnabled || isStickiesEnabled;
if (!workspaceSlugValue) return null; if (!workspaceSlugValue) return null;
@ -199,6 +205,14 @@ export const DashboardWidgets = observer(function DashboardWidgets(props: Dashbo
handleOnClose={() => toggleWidgetSettings(false)} handleOnClose={() => toggleWidgetSettings(false)}
/> />
<HomePageHeader
currentUser={currentUser}
selectedProject={selectedProject}
selectedProjectAnalytics={selectedProjectAnalytics}
recents={workspaceRecents}
workspaceName={currentWorkspace?.name}
/>
<div className="nodedc-home-dashboard-grid grid xl:grid-cols-[minmax(320px,360px)_minmax(0,1fr)] xl:items-stretch"> <div className="nodedc-home-dashboard-grid grid xl:grid-cols-[minmax(320px,360px)_minmax(0,1fr)] xl:items-stretch">
<div className="flex min-w-0"> <div className="flex min-w-0">
<HomeProjectStack <HomeProjectStack
@ -212,13 +226,6 @@ export const DashboardWidgets = observer(function DashboardWidgets(props: Dashbo
/> />
</div> </div>
<div className="nodedc-home-main-column min-w-0"> <div className="nodedc-home-main-column min-w-0">
<HomePageHeader
currentUser={currentUser}
selectedProject={selectedProject}
selectedProjectAnalytics={selectedProjectAnalytics}
recents={workspaceRecents}
workspaceName={currentWorkspace?.name}
/>
<HomeGanttPreview <HomeGanttPreview
project={selectedProject} project={selectedProject}
analytics={selectedProjectAnalytics} analytics={selectedProjectAnalytics}
@ -252,7 +259,9 @@ export const DashboardWidgets = observer(function DashboardWidgets(props: Dashbo
/> />
</div> </div>
<HomeRecentIssueDecks project={selectedProject} workspaceSlug={workspaceSlugValue} /> {isProjectLatestIssuesEnabled && (
<HomeRecentIssueDecks project={selectedProject} workspaceSlug={workspaceSlugValue} />
)}
<div className="space-y-5"> <div className="space-y-5">
{!isWikiApp && <NoProjectsEmptyState />} {!isWikiApp && <NoProjectsEmptyState />}

View File

@ -31,6 +31,16 @@ import { HOME_WIDGETS_LIST } from "../../home-dashboard-widgets";
import { WidgetItemDragHandle } from "./widget-item-drag-handle"; import { WidgetItemDragHandle } from "./widget-item-drag-handle";
import { getCanDrop, getInstructionFromPayload } from "./widget.helpers"; import { getCanDrop, getInstructionFromPayload } from "./widget.helpers";
const WIDGET_TITLE_FALLBACKS: Record<string, string> = {
"home.project_latest_issues.title": "Последние задачи проекта",
my_stickies: "Ваши стикеры",
new_at_plane: "Новое в NODE.DC",
project_latest_issues: "Последние задачи проекта",
quick_links: "Быстрые ссылки",
quick_tutorial: "Быстрое обучение",
recents: "Недавние",
};
type Props = { type Props = {
widgetId: string; widgetId: string;
isLastChild: boolean; isLastChild: boolean;
@ -53,6 +63,11 @@ export const WidgetItem = observer(function WidgetItem(props: Props) {
// derived values // derived values
const widget = widgetsMap[widgetId]; const widget = widgetsMap[widgetId];
const widgetTitle = HOME_WIDGETS_LIST[widget.key]?.title; const widgetTitle = HOME_WIDGETS_LIST[widget.key]?.title;
const translatedWidgetTitle = widgetTitle ? t(widgetTitle, { count: 1 }) : undefined;
const widgetLabel =
!translatedWidgetTitle || translatedWidgetTitle === widgetTitle
? (WIDGET_TITLE_FALLBACKS[widgetTitle ?? ""] ?? WIDGET_TITLE_FALLBACKS[widget.key] ?? widget.key)
: translatedWidgetTitle;
// drag and drop // drag and drop
useEffect(() => { useEffect(() => {
@ -76,7 +91,7 @@ export const WidgetItem = observer(function WidgetItem(props: Props) {
getOffset: pointerOutsideOfPreview({ x: "0px", y: "0px" }), getOffset: pointerOutsideOfPreview({ x: "0px", y: "0px" }),
render: ({ container }) => { render: ({ container }) => {
const root = createRoot(container); const root = createRoot(container);
root.render(<div className="rounded-sm bg-surface-1 p-1 pr-2 text-13">{widget.key}</div>); root.render(<div className="rounded-sm bg-surface-1 p-1 pr-2 text-13">{widgetLabel}</div>);
return () => root.unmount(); return () => root.unmount();
}, },
nativeSetDragImage, nativeSetDragImage,
@ -118,7 +133,7 @@ export const WidgetItem = observer(function WidgetItem(props: Props) {
}) })
); );
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [elementRef?.current, isDragging, isLastChild, widget.key]); }, [elementRef?.current, isDragging, isLastChild, widget.key, widgetLabel]);
return ( return (
<div className=""> <div className="">
@ -134,7 +149,7 @@ export const WidgetItem = observer(function WidgetItem(props: Props) {
> >
<div className="flex items-center"> <div className="flex items-center">
<WidgetItemDragHandle sort_order={widget.sort_order} isDragging={isDragging} /> <WidgetItemDragHandle sort_order={widget.sort_order} isDragging={isDragging} />
<div>{t(widgetTitle, { count: 1 })}</div> <div>{widgetLabel}</div>
</div> </div>
<ToggleSwitch <ToggleSwitch
value={widget.is_enabled} value={widget.is_enabled}

View File

@ -1792,32 +1792,17 @@
.nodedc-home-route-surface { .nodedc-home-route-surface {
min-height: 100vh; min-height: 100vh;
background: #333333 !important; background: var(--background-color-surface-1) !important;
} }
main:has(.nodedc-home-route-surface) { main:has(.nodedc-home-route-surface) {
background: #333333 !important; background: var(--background-color-surface-1) !important;
} }
.nodedc-home-page-shell { .nodedc-home-page-shell {
max-width: min(1840px, calc(100vw - 5rem)); max-width: min(1840px, calc(100vw - 5rem));
} }
.nodedc-home-top-toolbar > .nodedc-glass-modal {
max-width: min(1840px, calc(100vw - 5rem));
margin-inline: auto;
border: 0 !important;
background: transparent !important;
padding-inline: 0 !important;
box-shadow: none !important;
-webkit-backdrop-filter: none !important;
backdrop-filter: none !important;
}
.nodedc-home-top-toolbar {
padding-inline: 0 !important;
}
.nodedc-home-dashboard-shell { .nodedc-home-dashboard-shell {
gap: 0.75rem; gap: 0.75rem;
} }
@ -1833,8 +1818,8 @@
} }
.nodedc-home-project-panel { .nodedc-home-project-panel {
margin-top: 1.75rem; margin-top: 0;
height: calc(100% - 1.75rem) !important; height: 100% !important;
min-height: 0; min-height: 0;
} }
@ -1879,7 +1864,7 @@
display: grid; display: grid;
min-width: 0; min-width: 0;
position: relative; position: relative;
min-height: 8.55rem; min-height: 11.1rem;
} }
@media (min-width: 1280px) { @media (min-width: 1280px) {
@ -1894,13 +1879,13 @@
inset: 0; inset: 0;
z-index: 1; z-index: 1;
display: flex; display: flex;
min-height: 8.55rem; min-height: 11.1rem;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
border-radius: 1.7rem; border-radius: 1.7rem;
background: #474747 !important; background: #474747 !important;
padding: 1.25rem; padding: 1.6rem 1.35rem;
padding-right: max(1.25rem, calc(100% - var(--nodedc-home-title-width))); padding-right: max(1.35rem, calc(100% - var(--nodedc-home-title-width)));
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.035) !important; box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.035) !important;
} }
@ -1963,12 +1948,12 @@
display: flex; display: flex;
width: auto; width: auto;
min-width: 0; min-width: 0;
min-height: 8.55rem; min-height: 11.1rem;
align-items: flex-end; align-items: flex-end;
gap: 1rem; gap: 1rem;
border-radius: 1.7rem !important; border-radius: 1.7rem !important;
background: rgb(var(--nodedc-card-active-rgb)) !important; background: rgb(var(--nodedc-card-active-rgb)) !important;
padding: 1rem; padding: 1.35rem 1.15rem;
color: rgb(var(--nodedc-on-card-active-rgb)); color: rgb(var(--nodedc-on-card-active-rgb));
box-shadow: box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.42), inset 0 1px 0 rgba(255, 255, 255, 0.42),

View File

@ -643,6 +643,9 @@ export default {
new_at_plane: { new_at_plane: {
title: "New at NODE.DC", title: "New at NODE.DC",
}, },
project_latest_issues: {
title: "Latest project tasks",
},
quick_tutorial: { quick_tutorial: {
title: "Quick tutorial", title: "Quick tutorial",
}, },

View File

@ -799,6 +799,9 @@ export default {
new_at_plane: { new_at_plane: {
title: "Новое в NODE.DC", title: "Новое в NODE.DC",
}, },
project_latest_issues: {
title: "Последние задачи проекта",
},
quick_tutorial: { quick_tutorial: {
title: "Быстрое обучение", title: "Быстрое обучение",
}, },

View File

@ -7,7 +7,6 @@
import * as React from "react"; import * as React from "react";
import { Toast as BaseToast } from "@base-ui-components/react/toast"; import { Toast as BaseToast } from "@base-ui-components/react/toast";
import { AlertTriangle, CheckIcon, InfoIcon, XIcon } from "lucide-react"; import { AlertTriangle, CheckIcon, InfoIcon, XIcon } from "lucide-react";
import { CloseIcon } from "../icons/actions/close-icon";
// spinner // spinner
import { CircularBarSpinner } from "../spinners/circular-bar-spinner"; import { CircularBarSpinner } from "../spinners/circular-bar-spinner";
import { cn } from "../utils/classname"; import { cn } from "../utils/classname";
@ -54,6 +53,14 @@ export type ToastProps = {
}; };
const toastManager = BaseToast.createToastManager(); const toastManager = BaseToast.createToastManager();
const DEFAULT_LOADING_TITLE = "Загрузка...";
const TOAST_SURFACE_CLASSNAME =
"!border-0 bg-[linear-gradient(180deg,rgba(255,255,255,0.07)_0%,rgba(255,255,255,0.025)_100%),rgba(55,55,56,0.78)] text-white shadow-[0_26px_64px_rgba(0,0,0,0.36),inset_0_1px_0_rgba(255,255,255,0.09)] !outline-none backdrop-blur-[34px]";
const TOAST_CLOSE_CLASSNAME =
"absolute top-1/2 left-4 grid size-12 -translate-y-1/2 cursor-pointer place-items-center rounded-full !border-0 bg-black/[0.68] p-0 text-white/[0.72] shadow-[inset_0_1px_0_rgba(255,255,255,0.06)] !outline-none transition-colors hover:bg-black/[0.8] hover:text-white focus:outline-none focus-visible:bg-black/[0.84] focus-visible:text-white";
const TOAST_STATUS_CLASSNAME =
"absolute top-1/2 right-5 grid size-12 -translate-y-1/2 place-items-center rounded-full bg-black/[0.24] text-white/[0.7] shadow-[inset_0_1px_0_rgba(255,255,255,0.08)]";
export function Toast(props: ToastProps) { export function Toast(props: ToastProps) {
return ( return (
@ -69,40 +76,28 @@ export function Toast(props: ToastProps) {
const TOAST_DATA = { const TOAST_DATA = {
[TOAST_TYPE.SUCCESS]: { [TOAST_TYPE.SUCCESS]: {
icon: <CheckIcon width={12} height={12} className="text-on-color" />, icon: <CheckIcon width={20} height={20} className="text-current" />,
iconBgClassName: "bg-success-primary", iconClassName: "text-[rgb(var(--nodedc-card-active-rgb,195_255_102))]",
backgroundColorClassName: "!bg-surface-1",
borderColorClassName: "border-subtle",
}, },
[TOAST_TYPE.ERROR]: { [TOAST_TYPE.ERROR]: {
icon: <XIcon width={12} height={12} className="text-on-color" />, icon: <XIcon width={20} height={20} className="text-current" />,
iconBgClassName: "bg-danger-primary", iconClassName: "text-[#ff4452]",
backgroundColorClassName: "bg-surface-1",
borderColorClassName: "border-subtle",
}, },
[TOAST_TYPE.WARNING]: { [TOAST_TYPE.WARNING]: {
icon: <AlertTriangle width={12} height={12} className="text-on-color" />, icon: <AlertTriangle width={20} height={20} className="text-current" />,
iconBgClassName: "bg-warning-primary", iconClassName: "text-[#ff8830]",
backgroundColorClassName: "bg-surface-1",
borderColorClassName: "border-subtle",
}, },
[TOAST_TYPE.INFO]: { [TOAST_TYPE.INFO]: {
icon: <InfoIcon width={12} height={12} className="text-on-color" />, icon: <InfoIcon width={20} height={20} className="text-current" />,
iconBgClassName: "bg-accent-primary", iconClassName: "text-[rgb(var(--nodedc-accent-rgb,51_163_255))]",
backgroundColorClassName: "bg-surface-1",
borderColorClassName: "border-subtle",
}, },
[TOAST_TYPE.LOADING]: { [TOAST_TYPE.LOADING]: {
icon: <CircularBarSpinner className="text-on-color" />, icon: <CircularBarSpinner className="text-current" />,
iconBgClassName: "bg-layer-2", iconClassName: "text-white",
backgroundColorClassName: "bg-surface-1",
borderColorClassName: "border-subtle",
}, },
[TOAST_TYPE.LOADING_TOAST]: { [TOAST_TYPE.LOADING_TOAST]: {
icon: <CircularBarSpinner className="text-on-color" />, icon: <CircularBarSpinner className="text-current" />,
iconBgClassName: "bg-layer-2", iconClassName: "text-white",
backgroundColorClassName: "bg-surface-1",
borderColorClassName: "border-subtle",
}, },
}; };
@ -122,7 +117,7 @@ function ToastRender({ id, toast }: { id: React.Key; toast: BaseToast.Root.Toast
key={id} key={id}
className={cn( className={cn(
// Base layout and positioning // Base layout and positioning
"group flex w-[350px] items-center rounded-lg border shadow-raised-200", "group flex min-h-[6.4rem] w-[min(430px,calc(100vw-2rem))] items-center rounded-[1.85rem]",
"absolute right-3 bottom-3 z-[calc(1000-var(--toast-index))]", "absolute right-3 bottom-3 z-[calc(1000-var(--toast-index))]",
"ease-&lsqb;cubic-bezier(0.22,1,0.36,1)&rsqb; transition-[opacity,transform] duration-500 select-none", "ease-&lsqb;cubic-bezier(0.22,1,0.36,1)&rsqb; transition-[opacity,transform] duration-500 select-none",
@ -150,8 +145,7 @@ function ToastRender({ id, toast }: { id: React.Key; toast: BaseToast.Root.Toast
// Default ending transform for non-limited toasts // Default ending transform for non-limited toasts
"data-[ending-style]:[&:not([data-limited])]:[transform:translateY(150%)]", "data-[ending-style]:[&:not([data-limited])]:[transform:translateY(150%)]",
data.backgroundColorClassName, TOAST_SURFACE_CLASSNAME
data.borderColorClassName
)} )}
style={{ style={{
["--gap" as string]: "1rem", ["--gap" as string]: "1rem",
@ -163,30 +157,24 @@ function ToastRender({ id, toast }: { id: React.Key; toast: BaseToast.Root.Toast
e.preventDefault(); e.preventDefault();
}} }}
> >
<BaseToast.Close className="absolute top-3 right-3 cursor-pointer text-icon-secondary hover:text-icon-tertiary"> <BaseToast.Close className={TOAST_CLOSE_CLASSNAME} aria-label="Закрыть уведомление">
<CloseIcon strokeWidth={1.5} width={16} height={16} /> <XIcon strokeWidth={1.75} width={16} height={16} />
</BaseToast.Close> </BaseToast.Close>
<div className="flex w-full items-start gap-2 p-4"> {data.icon && <div className={cn(TOAST_STATUS_CLASSNAME, data.iconClassName)}>{data.icon}</div>}
<div className="py-1"> <div className="flex min-h-[6.4rem] w-full min-w-0 flex-col justify-center py-5 pr-[5.4rem] pl-[5.35rem]">
{data.icon && ( <div className="flex min-w-0 flex-col gap-1.5">
<div <BaseToast.Title className="text-15 leading-5 font-semibold text-white">
className={cn("flex size-4 flex-shrink-0 items-center justify-center rounded-full", data.iconBgClassName)} {toastData.type === TOAST_TYPE.LOADING ? (toastData.title ?? DEFAULT_LOADING_TITLE) : toastData.title}
>
{data.icon}
</div>
)}
</div>
<div className="flex min-w-0 flex-1 flex-col gap-1">
<BaseToast.Title className="text-h6-medium text-primary">
{toastData.type === TOAST_TYPE.LOADING ? (toastData.title ?? "Loading...") : toastData.title}
</BaseToast.Title> </BaseToast.Title>
{toastData.type !== TOAST_TYPE.LOADING && toastData.message && ( {toastData.type !== TOAST_TYPE.LOADING && toastData.message && (
<BaseToast.Description className="text-body-xs-regular text-tertiary"> <BaseToast.Description className="text-13 leading-5 text-white/[0.66]">
{toastData.message} {toastData.message}
</BaseToast.Description> </BaseToast.Description>
)} )}
{toastData.type !== TOAST_TYPE.LOADING && toastData.actionItems && ( {toastData.type !== TOAST_TYPE.LOADING && toastData.actionItems && (
<div className="flex items-center gap-2">{toastData.actionItems}</div> <div className="flex items-center gap-2 text-12 font-semibold text-[rgb(var(--nodedc-card-active-rgb,195_255_102))]">
{toastData.actionItems}
</div>
)} )}
</div> </div>
</div> </div>
@ -211,36 +199,28 @@ export function ToastStatic({ type, title, message, actionItems, theme = "light"
<div <div
className={cn( className={cn(
// Base layout and positioning // Base layout and positioning
"group flex w-[350px] items-start rounded-lg border border-subtle-1 shadow-overlay-100", "group flex min-h-[6.4rem] w-[min(430px,calc(100vw-2rem))] items-center rounded-[1.85rem]",
"relative", "relative",
data.backgroundColorClassName, TOAST_SURFACE_CLASSNAME
data.borderColorClassName
)} )}
> >
<div className="absolute top-1 right-1 cursor-default text-icon-tertiary"> <div className={cn(TOAST_CLOSE_CLASSNAME, "pointer-events-none")}>
<CloseIcon strokeWidth={1.5} width={14} height={14} /> <XIcon strokeWidth={1.75} width={16} height={16} />
</div> </div>
<div className="flex w-full items-start gap-3 p-4"> {data.icon && <div className={cn(TOAST_STATUS_CLASSNAME, data.iconClassName)}>{data.icon}</div>}
<div className="py-1"> <div className="flex min-h-[6.4rem] w-full min-w-0 flex-col justify-center py-5 pr-[5.4rem] pl-[5.35rem]">
{data.icon && ( <div className="flex min-w-0 flex-col gap-1.5">
<div <div className="text-15 leading-5 font-semibold text-white">
className={cn( {type === TOAST_TYPE.LOADING ? (title ?? DEFAULT_LOADING_TITLE) : title}
"flex size-4 flex-shrink-0 items-center justify-center rounded-full",
data.iconBgClassName
)}
>
{data.icon}
</div>
)}
</div>
<div className="flex min-w-0 flex-1 flex-col gap-1">
<div className="text-h6-medium text-primary">
{type === TOAST_TYPE.LOADING ? (title ?? "Loading...") : title}
</div> </div>
{type !== TOAST_TYPE.LOADING && message && ( {type !== TOAST_TYPE.LOADING && message && (
<div className="text-body-xs-regular text-tertiary">{message}</div> <div className="text-13 leading-5 text-white/[0.66]">{message}</div>
)}
{type !== TOAST_TYPE.LOADING && actionItems && (
<div className="flex items-center gap-2 text-12 font-semibold text-[rgb(var(--nodedc-card-active-rgb,195_255_102))]">
{actionItems}
</div>
)} )}
{type !== TOAST_TYPE.LOADING && actionItems && <div className="flex items-center gap-2">{actionItems}</div>}
</div> </div>
</div> </div>
</div> </div>
@ -294,7 +274,7 @@ export const setPromiseToast = <ToastData,>(
toastManager.promise(promise, { toastManager.promise(promise, {
loading: { loading: {
data: { data: {
title: options.loading ?? "Loading...", title: options.loading ?? DEFAULT_LOADING_TITLE,
type: TOAST_TYPE.LOADING, type: TOAST_TYPE.LOADING,
message: undefined, message: undefined,
actionItems: undefined, actionItems: undefined,

View File

@ -8,7 +8,13 @@ import type { TLogoProps } from "./common";
import type { TIssuePriorities } from "./issues"; import type { TIssuePriorities } from "./issues";
export type TRecentActivityFilterKeys = "all item" | "issue" | "page" | "project" | "workspace_page"; export type TRecentActivityFilterKeys = "all item" | "issue" | "page" | "project" | "workspace_page";
export type THomeWidgetKeys = "quick_links" | "recents" | "my_stickies" | "quick_tutorial" | "new_at_plane"; export type THomeWidgetKeys =
| "quick_links"
| "recents"
| "my_stickies"
| "project_latest_issues"
| "quick_tutorial"
| "new_at_plane";
export type THomeWidgetProps = { export type THomeWidgetProps = {
workspaceSlug: string; workspaceSlug: string;