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 allLocales from '@fullcalendar/core/locales-all';
|
||||||
import dayGridPlugin from '@fullcalendar/daygrid';
|
import dayGridPlugin from '@fullcalendar/daygrid';
|
||||||
import interactionPlugin from '@fullcalendar/interaction';
|
import interactionPlugin from '@fullcalendar/interaction';
|
||||||
@@ -15,6 +19,7 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Group,
|
Group,
|
||||||
|
HoverCard,
|
||||||
Indicator,
|
Indicator,
|
||||||
LoadingOverlay,
|
LoadingOverlay,
|
||||||
Popover,
|
Popover,
|
||||||
@@ -29,7 +34,13 @@ import {
|
|||||||
IconDownload,
|
IconDownload,
|
||||||
IconFilter
|
IconFilter
|
||||||
} from '@tabler/icons-react';
|
} 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 { useShallow } from 'zustand/react/shallow';
|
||||||
import {
|
import {
|
||||||
defaultLocale,
|
defaultLocale,
|
||||||
@@ -44,6 +55,7 @@ export interface InvenTreeCalendarProps extends CalendarOptions {
|
|||||||
enableDownload?: boolean;
|
enableDownload?: boolean;
|
||||||
enableFilters?: boolean;
|
enableFilters?: boolean;
|
||||||
enableSearch?: boolean;
|
enableSearch?: boolean;
|
||||||
|
eventTooltipContent?: (event: EventContentArg) => ReactNode;
|
||||||
filters?: TableFilter[];
|
filters?: TableFilter[];
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
state: CalendarState;
|
state: CalendarState;
|
||||||
@@ -53,6 +65,7 @@ export default function Calendar({
|
|||||||
enableDownload,
|
enableDownload,
|
||||||
enableFilters = false,
|
enableFilters = false,
|
||||||
enableSearch,
|
enableSearch,
|
||||||
|
eventTooltipContent,
|
||||||
isLoading,
|
isLoading,
|
||||||
filters,
|
filters,
|
||||||
state,
|
state,
|
||||||
@@ -112,6 +125,36 @@ export default function Calendar({
|
|||||||
[calendarProps.datesSet, state.ref, state.setMonthName]
|
[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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{state.exportModal.modal}
|
{state.exportModal.modal}
|
||||||
@@ -217,6 +260,7 @@ export default function Calendar({
|
|||||||
footerToolbar={false}
|
footerToolbar={false}
|
||||||
{...calendarProps}
|
{...calendarProps}
|
||||||
datesSet={datesSet}
|
datesSet={datesSet}
|
||||||
|
eventContent={wrappedEventContent}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -46,12 +46,14 @@ export default function OrderCalendar({
|
|||||||
model,
|
model,
|
||||||
role,
|
role,
|
||||||
params,
|
params,
|
||||||
filters
|
filters,
|
||||||
|
tooltip
|
||||||
}: {
|
}: {
|
||||||
model: ModelType;
|
model: ModelType;
|
||||||
role: UserRoles;
|
role: UserRoles;
|
||||||
params: Record<string, any>;
|
params: Record<string, any>;
|
||||||
filters?: TableFilter[];
|
filters?: TableFilter[];
|
||||||
|
tooltip?: (event: EventContentArg) => React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const user = useUserState();
|
const user = useUserState();
|
||||||
@@ -106,6 +108,7 @@ export default function OrderCalendar({
|
|||||||
const end: string = order.target_date || start;
|
const end: string = order.target_date || start;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
order: order,
|
||||||
id: order.pk,
|
id: order.pk,
|
||||||
title: order.reference,
|
title: order.reference,
|
||||||
description: order.description,
|
description: order.description,
|
||||||
@@ -209,6 +212,7 @@ export default function OrderCalendar({
|
|||||||
state={calendarState}
|
state={calendarState}
|
||||||
filters={calendarFilters}
|
filters={calendarFilters}
|
||||||
editable={true}
|
editable={true}
|
||||||
|
eventTooltipContent={tooltip}
|
||||||
eventContent={renderOrder}
|
eventContent={renderOrder}
|
||||||
eventClick={onClickOrder}
|
eventClick={onClickOrder}
|
||||||
eventChange={onEditOrder}
|
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 (
|
return (
|
||||||
<RenderInlineModel
|
<RenderInlineModel
|
||||||
{...props}
|
{...props}
|
||||||
image={instance.thumbnail || instance.image}
|
image={instance?.thumbnail || instance?.image}
|
||||||
primary={instance.name}
|
primary={instance?.name}
|
||||||
suffix={instance.description}
|
suffix={instance?.description}
|
||||||
url={
|
url={
|
||||||
props.link ? getDetailUrl(ModelType.company, instance.pk) : undefined
|
props.link ? getDetailUrl(ModelType.company, instance?.pk) : undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,14 +6,16 @@ import {
|
|||||||
IconTable,
|
IconTable,
|
||||||
IconTools
|
IconTools
|
||||||
} from '@tabler/icons-react';
|
} 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 { ModelType, PluginPanelKey } from '@lib/enums/ModelType';
|
||||||
import { UserRoles } from '@lib/enums/Roles';
|
import { UserRoles } from '@lib/enums/Roles';
|
||||||
import type { TableFilter } from '@lib/types/Filters';
|
import type { TableFilter } from '@lib/types/Filters';
|
||||||
import type { PanelType } from '@lib/types/Panel';
|
import type { PanelType } from '@lib/types/Panel';
|
||||||
import { useLocalStorage } from '@mantine/hooks';
|
import { useLocalStorage } from '@mantine/hooks';
|
||||||
import OrderCalendar from '../../components/calendar/OrderCalendar';
|
import OrderCalendar from '../../components/calendar/OrderCalendar';
|
||||||
|
import OrderCalendarToolTip from '../../components/calendar/OrderCalendarToolTip';
|
||||||
import PermissionDenied from '../../components/errors/PermissionDenied';
|
import PermissionDenied from '../../components/errors/PermissionDenied';
|
||||||
import { PageDetail } from '../../components/nav/PageDetail';
|
import { PageDetail } from '../../components/nav/PageDetail';
|
||||||
import { PanelGroup } from '../../components/panels/PanelGroup';
|
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||||
@@ -34,12 +36,21 @@ function BuildOrderCalendar() {
|
|||||||
});
|
});
|
||||||
}, [globalSettings]);
|
}, [globalSettings]);
|
||||||
|
|
||||||
|
const renderTooltip = useCallback((event: EventContentArg) => {
|
||||||
|
return OrderCalendarToolTip({
|
||||||
|
event: event,
|
||||||
|
modelType: ModelType.part,
|
||||||
|
instanceLookup: 'part_detail'
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OrderCalendar
|
<OrderCalendar
|
||||||
model={ModelType.build}
|
model={ModelType.build}
|
||||||
role={UserRoles.build}
|
role={UserRoles.build}
|
||||||
params={{ outstanding: true }}
|
params={{ outstanding: true, part_detail: true }}
|
||||||
filters={calendarFilters}
|
filters={calendarFilters}
|
||||||
|
tooltip={renderTooltip}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,13 +10,15 @@ import {
|
|||||||
IconShoppingCart,
|
IconShoppingCart,
|
||||||
IconTable
|
IconTable
|
||||||
} from '@tabler/icons-react';
|
} 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 { ModelType, PluginPanelKey } from '@lib/enums/ModelType';
|
||||||
import { UserRoles } from '@lib/enums/Roles';
|
import { UserRoles } from '@lib/enums/Roles';
|
||||||
import type { TableFilter } from '@lib/index';
|
import type { TableFilter } from '@lib/index';
|
||||||
import { useLocalStorage } from '@mantine/hooks';
|
import { useLocalStorage } from '@mantine/hooks';
|
||||||
import OrderCalendar from '../../components/calendar/OrderCalendar';
|
import OrderCalendar from '../../components/calendar/OrderCalendar';
|
||||||
|
import OrderCalendarToolTip from '../../components/calendar/OrderCalendarToolTip';
|
||||||
import PermissionDenied from '../../components/errors/PermissionDenied';
|
import PermissionDenied from '../../components/errors/PermissionDenied';
|
||||||
import { PageDetail } from '../../components/nav/PageDetail';
|
import { PageDetail } from '../../components/nav/PageDetail';
|
||||||
import { PanelGroup } from '../../components/panels/PanelGroup';
|
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||||
@@ -37,12 +39,21 @@ function PurchaseOrderCalendar() {
|
|||||||
return PurchaseOrderFilters({ includeDateFilters: false });
|
return PurchaseOrderFilters({ includeDateFilters: false });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const renderTooltip = useCallback((event: EventContentArg) => {
|
||||||
|
return OrderCalendarToolTip({
|
||||||
|
event: event,
|
||||||
|
modelType: ModelType.company,
|
||||||
|
instanceLookup: 'supplier_detail'
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OrderCalendar
|
<OrderCalendar
|
||||||
model={ModelType.purchaseorder}
|
model={ModelType.purchaseorder}
|
||||||
role={UserRoles.purchase_order}
|
role={UserRoles.purchase_order}
|
||||||
params={{ outstanding: true }}
|
params={{ outstanding: true, supplier_detail: true }}
|
||||||
filters={calendarFilters}
|
filters={calendarFilters}
|
||||||
|
tooltip={renderTooltip}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,15 @@ import {
|
|||||||
IconTruckDelivery,
|
IconTruckDelivery,
|
||||||
IconTruckReturn
|
IconTruckReturn
|
||||||
} from '@tabler/icons-react';
|
} 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 { ModelType, PluginPanelKey } from '@lib/enums/ModelType';
|
||||||
import { UserRoles } from '@lib/enums/Roles';
|
import { UserRoles } from '@lib/enums/Roles';
|
||||||
import type { TableFilter } from '@lib/index';
|
import type { TableFilter } from '@lib/index';
|
||||||
import { useLocalStorage } from '@mantine/hooks';
|
import { useLocalStorage } from '@mantine/hooks';
|
||||||
import OrderCalendar from '../../components/calendar/OrderCalendar';
|
import OrderCalendar from '../../components/calendar/OrderCalendar';
|
||||||
|
import OrderCalendarToolTip from '../../components/calendar/OrderCalendarToolTip';
|
||||||
import PermissionDenied from '../../components/errors/PermissionDenied';
|
import PermissionDenied from '../../components/errors/PermissionDenied';
|
||||||
import { PageDetail } from '../../components/nav/PageDetail';
|
import { PageDetail } from '../../components/nav/PageDetail';
|
||||||
import { PanelGroup } from '../../components/panels/PanelGroup';
|
import { PanelGroup } from '../../components/panels/PanelGroup';
|
||||||
@@ -35,12 +37,21 @@ function SalesOrderCalendar() {
|
|||||||
return SalesOrderFilters({ includeDateFilters: false });
|
return SalesOrderFilters({ includeDateFilters: false });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const renderTooltip = useCallback((event: EventContentArg) => {
|
||||||
|
return OrderCalendarToolTip({
|
||||||
|
event: event,
|
||||||
|
modelType: ModelType.company,
|
||||||
|
instanceLookup: 'customer_detail'
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OrderCalendar
|
<OrderCalendar
|
||||||
model={ModelType.salesorder}
|
model={ModelType.salesorder}
|
||||||
role={UserRoles.sales_order}
|
role={UserRoles.sales_order}
|
||||||
params={{ outstanding: true }}
|
params={{ outstanding: true, customer_detail: true }}
|
||||||
filters={calendarFilters}
|
filters={calendarFilters}
|
||||||
|
tooltip={renderTooltip}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -50,12 +61,21 @@ const ReturnOrderCalendar = () => {
|
|||||||
return SalesOrderFilters({ includeDateFilters: false });
|
return SalesOrderFilters({ includeDateFilters: false });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const renderTooltip = useCallback((event: EventContentArg) => {
|
||||||
|
return OrderCalendarToolTip({
|
||||||
|
event: event,
|
||||||
|
modelType: ModelType.company,
|
||||||
|
instanceLookup: 'customer_detail'
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OrderCalendar
|
<OrderCalendar
|
||||||
model={ModelType.returnorder}
|
model={ModelType.returnorder}
|
||||||
role={UserRoles.return_order}
|
role={UserRoles.return_order}
|
||||||
params={{ outstanding: true }}
|
params={{ outstanding: true, customer_detail: true }}
|
||||||
filters={calendarFilters}
|
filters={calendarFilters}
|
||||||
|
tooltip={renderTooltip}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -276,6 +276,7 @@ export default function InvenTreeTableHeader({
|
|||||||
<Divider />
|
<Divider />
|
||||||
{tableState.filterSet.activeFilters?.map((filter) => (
|
{tableState.filterSet.activeFilters?.map((filter) => (
|
||||||
<FilterPreview
|
<FilterPreview
|
||||||
|
key={filter.name}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
filters={tableProps.tableFilters}
|
filters={tableProps.tableFilters}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user