mirror of
https://github.com/inventree/InvenTree.git
synced 2026-05-22 01:06:50 +00:00
[UI] Calendar tooltips (#11980)
* Allow tooltips for calendar entries * Render tooltip for purchase order calendar * Calendar tooltip renderers * Add tooltip for build calendar * Add key to filter list * Display responsible owner
This commit is contained in:
@@ -1,4 +1,8 @@
|
||||
import type { CalendarOptions, DatesSetArg } from '@fullcalendar/core';
|
||||
import type {
|
||||
CalendarOptions,
|
||||
DatesSetArg,
|
||||
EventContentArg
|
||||
} from '@fullcalendar/core';
|
||||
import allLocales from '@fullcalendar/core/locales-all';
|
||||
import dayGridPlugin from '@fullcalendar/daygrid';
|
||||
import interactionPlugin from '@fullcalendar/interaction';
|
||||
@@ -15,6 +19,7 @@ import {
|
||||
Box,
|
||||
Button,
|
||||
Group,
|
||||
HoverCard,
|
||||
Indicator,
|
||||
LoadingOverlay,
|
||||
Popover,
|
||||
@@ -29,7 +34,13 @@ import {
|
||||
IconDownload,
|
||||
IconFilter
|
||||
} from '@tabler/icons-react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState
|
||||
} from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import {
|
||||
defaultLocale,
|
||||
@@ -44,6 +55,7 @@ export interface InvenTreeCalendarProps extends CalendarOptions {
|
||||
enableDownload?: boolean;
|
||||
enableFilters?: boolean;
|
||||
enableSearch?: boolean;
|
||||
eventTooltipContent?: (event: EventContentArg) => ReactNode;
|
||||
filters?: TableFilter[];
|
||||
isLoading?: boolean;
|
||||
state: CalendarState;
|
||||
@@ -53,6 +65,7 @@ export default function Calendar({
|
||||
enableDownload,
|
||||
enableFilters = false,
|
||||
enableSearch,
|
||||
eventTooltipContent,
|
||||
isLoading,
|
||||
filters,
|
||||
state,
|
||||
@@ -112,6 +125,36 @@ export default function Calendar({
|
||||
[calendarProps.datesSet, state.ref, state.setMonthName]
|
||||
);
|
||||
|
||||
const wrappedEventContent = useCallback(
|
||||
(arg: EventContentArg) => {
|
||||
const inner =
|
||||
typeof calendarProps.eventContent === 'function'
|
||||
? calendarProps.eventContent(arg, null)
|
||||
: (calendarProps.eventContent ?? null);
|
||||
|
||||
if (!eventTooltipContent) return inner;
|
||||
|
||||
const tooltip = eventTooltipContent(arg);
|
||||
|
||||
if (!tooltip) return inner;
|
||||
|
||||
return (
|
||||
<HoverCard
|
||||
openDelay={300}
|
||||
closeDelay={100}
|
||||
shadow='md'
|
||||
position='top-start'
|
||||
>
|
||||
<HoverCard.Target>
|
||||
<div style={{ width: '100%', overflow: 'hidden' }}>{inner}</div>
|
||||
</HoverCard.Target>
|
||||
<HoverCard.Dropdown>{tooltip}</HoverCard.Dropdown>
|
||||
</HoverCard>
|
||||
);
|
||||
},
|
||||
[calendarProps.eventContent, eventTooltipContent]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{state.exportModal.modal}
|
||||
@@ -217,6 +260,7 @@ export default function Calendar({
|
||||
footerToolbar={false}
|
||||
{...calendarProps}
|
||||
datesSet={datesSet}
|
||||
eventContent={wrappedEventContent}
|
||||
/>
|
||||
</Box>
|
||||
</Stack>
|
||||
|
||||
@@ -46,12 +46,14 @@ export default function OrderCalendar({
|
||||
model,
|
||||
role,
|
||||
params,
|
||||
filters
|
||||
filters,
|
||||
tooltip
|
||||
}: {
|
||||
model: ModelType;
|
||||
role: UserRoles;
|
||||
params: Record<string, any>;
|
||||
filters?: TableFilter[];
|
||||
tooltip?: (event: EventContentArg) => React.ReactNode;
|
||||
}) {
|
||||
const navigate = useNavigate();
|
||||
const user = useUserState();
|
||||
@@ -106,6 +108,7 @@ export default function OrderCalendar({
|
||||
const end: string = order.target_date || start;
|
||||
|
||||
return {
|
||||
order: order,
|
||||
id: order.pk,
|
||||
title: order.reference,
|
||||
description: order.description,
|
||||
@@ -209,6 +212,7 @@ export default function OrderCalendar({
|
||||
state={calendarState}
|
||||
filters={calendarFilters}
|
||||
editable={true}
|
||||
eventTooltipContent={tooltip}
|
||||
eventContent={renderOrder}
|
||||
eventClick={onClickOrder}
|
||||
eventChange={onEditOrder}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { EventContentArg } from '@fullcalendar/core';
|
||||
import type { ModelType } from '@lib/enums/ModelType';
|
||||
import { resolveItem } from '@lib/functions/Conversion';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Badge, Divider, Group, Stack, Text } from '@mantine/core';
|
||||
import { formatDate } from '../../defaults/formatters';
|
||||
import { RenderInstance } from '../render/Instance';
|
||||
import { RenderOwner } from '../render/User';
|
||||
|
||||
export default function OrderCalendarToolTip({
|
||||
event,
|
||||
instanceLookup,
|
||||
modelType
|
||||
}: {
|
||||
event: EventContentArg;
|
||||
instanceLookup: string;
|
||||
modelType: ModelType;
|
||||
}) {
|
||||
// Extract the order instance from the event
|
||||
const order = event?.event?._def?.extendedProps?.order;
|
||||
|
||||
const instance = resolveItem(order, instanceLookup);
|
||||
|
||||
if (!order) return null;
|
||||
|
||||
return (
|
||||
<Stack gap='xs'>
|
||||
<RenderInstance model={modelType} instance={instance} />
|
||||
<Divider />
|
||||
<Group gap='xs'>
|
||||
<Text size='sm' fw='bold'>
|
||||
{order.reference}
|
||||
</Text>
|
||||
<Text size='xs'>{order.description || order.title}</Text>
|
||||
</Group>
|
||||
{order.start_date && (
|
||||
<Group gap='xs'>
|
||||
<Text size='sm' fw='bold'>{t`Start Date`}</Text>
|
||||
<Text size='xs'>{formatDate(order.start_date)}</Text>
|
||||
</Group>
|
||||
)}
|
||||
{order.target_date && (
|
||||
<Group gap='xs'>
|
||||
<Text size='sm' fw='bold'>{t`Target Date`}</Text>
|
||||
<Text size='xs'>{formatDate(order.target_date)}</Text>
|
||||
{order.overdue && (
|
||||
<Badge size='sm' color='red'>
|
||||
{t`Overdue`}
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
)}
|
||||
{order.responsible && (
|
||||
<Group gap='xs'>
|
||||
<Text size='sm' fw='bold'>{t`Responsible`}</Text>
|
||||
<RenderOwner instance={order.responsible_detail} />
|
||||
</Group>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -50,11 +50,11 @@ export function RenderCompany(
|
||||
return (
|
||||
<RenderInlineModel
|
||||
{...props}
|
||||
image={instance.thumbnail || instance.image}
|
||||
primary={instance.name}
|
||||
suffix={instance.description}
|
||||
image={instance?.thumbnail || instance?.image}
|
||||
primary={instance?.name}
|
||||
suffix={instance?.description}
|
||||
url={
|
||||
props.link ? getDetailUrl(ModelType.company, instance.pk) : undefined
|
||||
props.link ? getDetailUrl(ModelType.company, instance?.pk) : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -6,14 +6,16 @@ import {
|
||||
IconTable,
|
||||
IconTools
|
||||
} from '@tabler/icons-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import type { EventContentArg } from '@fullcalendar/core';
|
||||
import { ModelType, PluginPanelKey } from '@lib/enums/ModelType';
|
||||
import { UserRoles } from '@lib/enums/Roles';
|
||||
import type { TableFilter } from '@lib/types/Filters';
|
||||
import type { PanelType } from '@lib/types/Panel';
|
||||
import { useLocalStorage } from '@mantine/hooks';
|
||||
import OrderCalendar from '../../components/calendar/OrderCalendar';
|
||||
import OrderCalendarToolTip from '../../components/calendar/OrderCalendarToolTip';
|
||||
import PermissionDenied from '../../components/errors/PermissionDenied';
|
||||
import { PageDetail } from '../../components/nav/PageDetail';
|
||||
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||
@@ -34,12 +36,21 @@ function BuildOrderCalendar() {
|
||||
});
|
||||
}, [globalSettings]);
|
||||
|
||||
const renderTooltip = useCallback((event: EventContentArg) => {
|
||||
return OrderCalendarToolTip({
|
||||
event: event,
|
||||
modelType: ModelType.part,
|
||||
instanceLookup: 'part_detail'
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<OrderCalendar
|
||||
model={ModelType.build}
|
||||
role={UserRoles.build}
|
||||
params={{ outstanding: true }}
|
||||
params={{ outstanding: true, part_detail: true }}
|
||||
filters={calendarFilters}
|
||||
tooltip={renderTooltip}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,13 +10,15 @@ import {
|
||||
IconShoppingCart,
|
||||
IconTable
|
||||
} from '@tabler/icons-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import type { EventContentArg } from '@fullcalendar/core';
|
||||
import { ModelType, PluginPanelKey } from '@lib/enums/ModelType';
|
||||
import { UserRoles } from '@lib/enums/Roles';
|
||||
import type { TableFilter } from '@lib/index';
|
||||
import { useLocalStorage } from '@mantine/hooks';
|
||||
import OrderCalendar from '../../components/calendar/OrderCalendar';
|
||||
import OrderCalendarToolTip from '../../components/calendar/OrderCalendarToolTip';
|
||||
import PermissionDenied from '../../components/errors/PermissionDenied';
|
||||
import { PageDetail } from '../../components/nav/PageDetail';
|
||||
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||
@@ -37,12 +39,21 @@ function PurchaseOrderCalendar() {
|
||||
return PurchaseOrderFilters({ includeDateFilters: false });
|
||||
}, []);
|
||||
|
||||
const renderTooltip = useCallback((event: EventContentArg) => {
|
||||
return OrderCalendarToolTip({
|
||||
event: event,
|
||||
modelType: ModelType.company,
|
||||
instanceLookup: 'supplier_detail'
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<OrderCalendar
|
||||
model={ModelType.purchaseorder}
|
||||
role={UserRoles.purchase_order}
|
||||
params={{ outstanding: true }}
|
||||
params={{ outstanding: true, supplier_detail: true }}
|
||||
filters={calendarFilters}
|
||||
tooltip={renderTooltip}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,13 +9,15 @@ import {
|
||||
IconTruckDelivery,
|
||||
IconTruckReturn
|
||||
} from '@tabler/icons-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import type { EventContentArg } from '@fullcalendar/core';
|
||||
import { ModelType, PluginPanelKey } from '@lib/enums/ModelType';
|
||||
import { UserRoles } from '@lib/enums/Roles';
|
||||
import type { TableFilter } from '@lib/index';
|
||||
import { useLocalStorage } from '@mantine/hooks';
|
||||
import OrderCalendar from '../../components/calendar/OrderCalendar';
|
||||
import OrderCalendarToolTip from '../../components/calendar/OrderCalendarToolTip';
|
||||
import PermissionDenied from '../../components/errors/PermissionDenied';
|
||||
import { PageDetail } from '../../components/nav/PageDetail';
|
||||
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||
@@ -35,12 +37,21 @@ function SalesOrderCalendar() {
|
||||
return SalesOrderFilters({ includeDateFilters: false });
|
||||
}, []);
|
||||
|
||||
const renderTooltip = useCallback((event: EventContentArg) => {
|
||||
return OrderCalendarToolTip({
|
||||
event: event,
|
||||
modelType: ModelType.company,
|
||||
instanceLookup: 'customer_detail'
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<OrderCalendar
|
||||
model={ModelType.salesorder}
|
||||
role={UserRoles.sales_order}
|
||||
params={{ outstanding: true }}
|
||||
params={{ outstanding: true, customer_detail: true }}
|
||||
filters={calendarFilters}
|
||||
tooltip={renderTooltip}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -50,12 +61,21 @@ const ReturnOrderCalendar = () => {
|
||||
return SalesOrderFilters({ includeDateFilters: false });
|
||||
}, []);
|
||||
|
||||
const renderTooltip = useCallback((event: EventContentArg) => {
|
||||
return OrderCalendarToolTip({
|
||||
event: event,
|
||||
modelType: ModelType.company,
|
||||
instanceLookup: 'customer_detail'
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<OrderCalendar
|
||||
model={ModelType.returnorder}
|
||||
role={UserRoles.return_order}
|
||||
params={{ outstanding: true }}
|
||||
params={{ outstanding: true, customer_detail: true }}
|
||||
filters={calendarFilters}
|
||||
tooltip={renderTooltip}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -276,6 +276,7 @@ export default function InvenTreeTableHeader({
|
||||
<Divider />
|
||||
{tableState.filterSet.activeFilters?.map((filter) => (
|
||||
<FilterPreview
|
||||
key={filter.name}
|
||||
filter={filter}
|
||||
filters={tableProps.tableFilters}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user