diff --git a/src/frontend/lib/index.ts b/src/frontend/lib/index.ts index bb325a25f7..3642e341ba 100644 --- a/src/frontend/lib/index.ts +++ b/src/frontend/lib/index.ts @@ -21,6 +21,12 @@ export type { StockAdjustmentFormsContext } from './types/Plugins'; +export type { + PanelIndicatorType, + PanelType, + PanelGroupType +} from './types/Panel'; + export type { RowAction, RowViewProps, diff --git a/src/frontend/src/components/panels/Panel.tsx b/src/frontend/lib/types/Panel.tsx similarity index 63% rename from src/frontend/src/components/panels/Panel.tsx rename to src/frontend/lib/types/Panel.tsx index 548dc4bac5..5e3bd103d6 100644 --- a/src/frontend/src/components/panels/Panel.tsx +++ b/src/frontend/lib/types/Panel.tsx @@ -1,5 +1,8 @@ import type { ReactNode } from 'react'; +// The type of indicator dot to be shown against a panel +export type PanelIndicatorType = 'info' | 'warning' | 'danger' | null; + /** * Type used to specify a single panel in a panel group */ @@ -8,7 +11,7 @@ export type PanelType = { label: string; controls?: ReactNode; icon?: ReactNode; - notification_dot?: 'info' | 'warning' | 'danger' | null; + notification_dot?: PanelIndicatorType | (() => Promise); content?: ReactNode; hidden?: boolean; disabled?: boolean; @@ -16,6 +19,9 @@ export type PanelType = { supportsDirty?: boolean; }; +/** + * Type used to specify a group of panels + */ export type PanelGroupType = { id: string; label: string; diff --git a/src/frontend/src/components/panels/AttachmentPanel.tsx b/src/frontend/src/components/panels/AttachmentPanel.tsx index 2c81acdc83..8102581de0 100644 --- a/src/frontend/src/components/panels/AttachmentPanel.tsx +++ b/src/frontend/src/components/panels/AttachmentPanel.tsx @@ -3,8 +3,10 @@ import { Skeleton } from '@mantine/core'; import { IconPaperclip } from '@tabler/icons-react'; import type { ModelType } from '@lib/enums/ModelType'; +import { ApiEndpoints, apiUrl } from '@lib/index'; +import type { PanelType } from '@lib/types/Panel'; +import { api } from '../../App'; import { AttachmentTable } from '../../tables/general/AttachmentTable'; -import type { PanelType } from './Panel'; export default function AttachmentPanel({ model_type, @@ -17,6 +19,21 @@ export default function AttachmentPanel({ name: 'attachments', label: t`Attachments`, icon: , + notification_dot: async () => { + if (!model_type || !model_id) { + return null; + } + + return api + .get(apiUrl(ApiEndpoints.attachment_list), { + params: { + model_type: model_type, + model_id: model_id, + limit: 1 + } + }) + .then((response) => ((response.data?.count ?? 0) > 0 ? 'info' : null)); + }, content: model_type && model_id ? ( diff --git a/src/frontend/src/components/panels/NotesPanel.tsx b/src/frontend/src/components/panels/NotesPanel.tsx index 6bee76d2b4..66696384bb 100644 --- a/src/frontend/src/components/panels/NotesPanel.tsx +++ b/src/frontend/src/components/panels/NotesPanel.tsx @@ -3,9 +3,9 @@ import { Skeleton } from '@mantine/core'; import { IconNotes } from '@tabler/icons-react'; import type { ModelType } from '@lib/enums/ModelType'; +import type { PanelType } from '@lib/types/Panel'; import { lazy } from 'react'; import { useUserState } from '../../states/UserState'; -import type { PanelType } from './Panel'; const NotesEditor = lazy(() => import('../editors/NotesEditor')); diff --git a/src/frontend/src/components/panels/PanelGroup.tsx b/src/frontend/src/components/panels/PanelGroup.tsx index 585eafc466..2288205206 100644 --- a/src/frontend/src/components/panels/PanelGroup.tsx +++ b/src/frontend/src/components/panels/PanelGroup.tsx @@ -5,6 +5,7 @@ import { Group, Indicator, Loader, + type MantineColor, Paper, Stack, Tabs, @@ -39,14 +40,19 @@ import { identifierString } from '@lib/functions/Conversion'; import { cancelEvent } from '@lib/functions/Events'; import { eventModified, getBaseUrl } from '@lib/functions/Navigation'; import { navigateToLink } from '@lib/functions/Navigation'; +import type { + PanelGroupType, + PanelIndicatorType, + PanelType +} from '@lib/types/Panel'; import { t } from '@lingui/core/macro'; -import { useWindowEvent } from '@mantine/hooks'; +import { useDocumentVisibility, useWindowEvent } from '@mantine/hooks'; +import { useQuery } from '@tanstack/react-query'; import { useShallow } from 'zustand/react/shallow'; import { generateUrl } from '../../functions/urls'; import { usePluginPanels } from '../../hooks/UsePluginPanels'; import { useLocalState } from '../../states/LocalState'; import { vars } from '../../theme'; -import type { PanelGroupType, PanelType } from '../panels/Panel'; import * as classes from './PanelGroup.css'; /** @@ -79,6 +85,99 @@ export type PanelProps = { pluginPanelKey?: PluginPanelKey; }; +/** + * Render a single panel tab within the side menu + */ +function PanelTabComponent({ + expanded, + panel, + onClick +}: { + expanded: boolean; + panel: PanelType; + onClick: (event: any) => void; +}) { + const visibility = useDocumentVisibility(); + + // Check if we should display an indicator dot for this panel + const notificationDot = useQuery({ + enabled: panel.notification_dot !== undefined && visibility === 'visible', + queryKey: ['panel-notification', panel.name], + queryFn: async () => { + if (panel.notification_dot === undefined) { + return null; + } else if (typeof panel.notification_dot === 'function') { + return await panel.notification_dot(); + } else { + return panel.notification_dot as PanelIndicatorType; + } + }, + staleTime: 5 * 60 * 1000, // cache for 5 minutes + refetchOnMount: false, + refetchOnWindowFocus: false + }); + + const indicatorColor: MantineColor | undefined = useMemo(() => { + switch (notificationDot.data) { + case 'info': + return 'blue'; + case 'warning': + return 'yellow'; + case 'danger': + return 'red'; + default: + return undefined; + } + }, [notificationDot.data]); + + return ( + + + {panel.icon} + + } + hidden={panel.hidden} + disabled={panel.disabled} + style={{ + cursor: panel.disabled ? 'unset' : 'pointer' + }} + onClick={(event: any) => onClick(event)} + > + + + {expanded && panel.label} + + + + + ); +} + function BasePanelGroup({ pageKey, panels, @@ -269,54 +368,13 @@ function BasePanelGroup({ {group.panels?.map( (panel) => !panel.hidden && ( - - - + + handlePanelChange(panel.name, event) + } + /> ) )} diff --git a/src/frontend/src/components/panels/ParametersPanel.tsx b/src/frontend/src/components/panels/ParametersPanel.tsx index 574909cdc6..a0bf301909 100644 --- a/src/frontend/src/components/panels/ParametersPanel.tsx +++ b/src/frontend/src/components/panels/ParametersPanel.tsx @@ -1,9 +1,12 @@ +import { ApiEndpoints } from '@lib/enums/ApiEndpoints'; import type { ModelType } from '@lib/enums/ModelType'; +import { apiUrl } from '@lib/functions/Api'; +import type { PanelType } from '@lib/types/Panel'; import { t } from '@lingui/core/macro'; import { Skeleton } from '@mantine/core'; import { IconListDetails } from '@tabler/icons-react'; +import { api } from '../../App'; import { ParameterTable } from '../../tables/general/ParameterTable'; -import type { PanelType } from './Panel'; export default function ParametersPanel({ model_type, @@ -21,6 +24,21 @@ export default function ParametersPanel({ label: t`Parameters`, icon: , hidden: hidden ?? false, + notification_dot: async () => { + if (!model_type || !model_id) { + return null; + } + + return api + .get(apiUrl(ApiEndpoints.parameter_list), { + params: { + model_type: model_type, + model_id: model_id, + limit: 1 + } + }) + .then((response) => ((response.data?.count ?? 0) > 0 ? 'info' : null)); + }, content: model_type && model_id ? (