2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-06-11 19:27:02 +00:00

[UI] Month scroll (#12092)

* Vertical scrolling calendar view

* Add new setting to control calendar horizon

* Keep refetching data as user scrolss

* Adjust view based on configured setting

* Reduce padding

* Simplify calendar layout

* Docs tweak

* Split out display settings

* Adjust playwright test

* Reduce reliance on transient dialogs
This commit is contained in:
Oliver
2026-06-06 11:05:33 +10:00
committed by GitHub
parent b9c063fe31
commit 37b409e991
11 changed files with 755 additions and 698 deletions
+5 -1
View File
@@ -208,7 +208,7 @@ For tables which reference other objects within the system, clicking on a row wi
## Calendar Views
Some [table views](#table-views) can be switched to a calendar view, which provides a visual representation of data based on date fields. The calendar view allows users to easily see and interact with data that is organized by date, such as scheduled tasks, events, or deadlines.
Some [table views](#table-views) associated with various order types can be switched to a calendar view, which provides a visual representation of data based on date fields. The calendar view allows users to easily see and interact with data that is organized by date, such as scheduled tasks, events, or deadlines.
To switch to the "calendar view" (for a table which supports it), click on the "calendar view" button located above and to the right of the table view:
@@ -218,6 +218,10 @@ This will display the data in a calendar format:
{{ image("concepts/ui_calendar_view.png", "Calendar View") }}
### Calendar Horizon
The calendar view provides a configurable "horizon" setting, which allows users to adjust the number of months displayed in the calendar view.
## Parametric Views
Some [table views](#table-views) can be switched to a parametric view, which provides a visual representation of data based on specific parameters or attributes. The parametric view allows users to easily see and interact with data that is organized by certain characteristics, such as categories, types, or other relevant attributes.
+1
View File
@@ -33,6 +33,7 @@ Configuration of basic server settings:
{{ globalsetting("DISPLAY_FULL_NAMES") }}
{{ globalsetting("DISPLAY_PROFILE_INFO") }}
{{ globalsetting("WEEK_STARTS_ON") }}
{{ globalsetting("CALENDAR_HORIZON_MONTHS") }}
{{ globalsetting("INVENTREE_UPLOAD_MAX_SIZE") }}
{{ globalsetting("INVENTREE_STRICT_URLS") }}
@@ -1243,6 +1243,15 @@ SYSTEM_SETTINGS: dict[str, InvenTreeSettingsKeyType] = {
('6', _('Saturday')),
],
},
'CALENDAR_HORIZON_MONTHS': {
'name': _('Calendar Horizon'),
'description': _(
'Number of months into the future to display in calendar views'
),
'default': 12,
'validator': [int, MinValueValidator(1)],
'units': _('months'),
},
'TEST_STATION_DATA': {
'name': _('Enable Test Station Data'),
'description': _('Enable test station data collection for test results'),
@@ -76,6 +76,18 @@ export default function Calendar({
}: Readonly<InvenTreeCalendarProps>) {
const globalSettings = useGlobalSettingsState();
const horizonMonths = useMemo(
() =>
Number.parseInt(
globalSettings.getSetting('CALENDAR_HORIZON_MONTHS') ?? '12',
10
),
[globalSettings]
);
// When the horizon is a single month, fall back to the standard month grid.
const isScrollView = horizonMonths > 1;
const [monthSelectOpened, setMonthSelectOpened] = useState<boolean>(false);
const [filtersVisible, setFiltersVisible] = useState<boolean>(false);
@@ -114,10 +126,15 @@ export default function Calendar({
const datesSet = useCallback(
(dateInfo: DatesSetArg) => {
if (state.ref?.current) {
const api = state.ref.current.getApi();
// Show the starting month of the view (advance 15 days past any padding days)
const viewStart = new Date(dateInfo.start);
viewStart.setDate(viewStart.getDate() + 15);
const startMonthLabel = new Intl.DateTimeFormat(calendarLocale, {
month: 'long',
year: 'numeric'
}).format(viewStart);
// Update calendar state
state.setMonthName(api.view.title);
state.setMonthName(startMonthLabel);
state.setStartDate(dateInfo.start);
state.setEndDate(dateInfo.end);
}
@@ -125,7 +142,14 @@ export default function Calendar({
// Pass the dates set to the parent component
calendarProps.datesSet?.(dateInfo);
},
[calendarProps.datesSet, state.ref, state.setMonthName]
[
calendarLocale,
calendarProps.datesSet,
state.ref,
state.setMonthName,
state.setStartDate,
state.setEndDate
]
);
const wrappedEventContent = useCallback(
@@ -264,7 +288,16 @@ export default function Calendar({
<FullCalendar
ref={state.ref}
plugins={[dayGridPlugin, interactionPlugin]}
initialView='dayGridMonth'
initialView={isScrollView ? 'scrollMultiMonth' : 'dayGridMonth'}
{...(isScrollView && {
views: {
scrollMultiMonth: {
type: 'dayGrid',
duration: { months: horizonMonths }
}
},
height: 'calc(100vh - 160px)'
})}
locales={allLocales}
locale={calendarLocale}
firstDay={Number.parseInt(
+12 -11
View File
@@ -74,8 +74,6 @@ export default function useCalendar({
// Generate a set of API query filters
const queryFilters: Record<string, any> = useMemo(() => {
// Expand date range by one month, to ensure we capture all events
let params = {
...(queryParams || {})
};
@@ -91,9 +89,7 @@ export default function useCalendar({
min_date: startDate
? dayjs(startDate).subtract(1, 'month').format('YYYY-MM-DD')
: null,
max_date: endDate
? dayjs(endDate).add(1, 'month').format('YYYY-MM-DD')
: null,
max_date: endDate ? dayjs(endDate).format('YYYY-MM-DD') : null,
search: searchTerm
};
@@ -102,7 +98,7 @@ export default function useCalendar({
const query = useQuery({
enabled: !!startDate && !!endDate,
queryKey: ['calendar', name, endpoint, queryFilters, startDate, endDate],
queryKey: ['calendar', name, endpoint, queryFilters],
throwOnError: (error: any) => {
showApiErrorMessage({
error: error,
@@ -112,7 +108,6 @@ export default function useCalendar({
return true;
},
queryFn: async () => {
// Fetch data from the API
return api
.get(apiUrl(endpoint), {
params: queryFilters
@@ -123,14 +118,20 @@ export default function useCalendar({
}
});
// Navigate to the previous month
// Navigate to the previous month (move start date back by 1 month)
const prevMonth = useCallback(() => {
ref.current?.getApi().prev();
const api = ref.current?.getApi();
if (api) {
api.gotoDate(dayjs(api.getDate()).subtract(1, 'month').toDate());
}
}, [ref]);
// Navigate to the next month
// Navigate to the next month (move start date forward by 1 month)
const nextMonth = useCallback(() => {
ref.current?.getApi().next();
const api = ref.current?.getApi();
if (api) {
api.gotoDate(dayjs(api.getDate()).add(1, 'month').toDate());
}
}, [ref]);
// Navigate to the current month
@@ -5,6 +5,7 @@ import {
IconBox,
IconBuildingFactory2,
IconCurrencyDollar,
IconDeviceDesktop,
IconFileAnalytics,
IconFingerprint,
IconList,
@@ -54,9 +55,6 @@ export default function SystemSettings() {
'INVENTREE_SHOW_SUPERUSER_BANNER',
'INVENTREE_SHOW_ADMIN_BANNER',
'INVENTREE_RESTRICT_ABOUT',
'DISPLAY_FULL_NAMES',
'DISPLAY_PROFILE_INFO',
'WEEK_STARTS_ON',
'INVENTREE_UPLOAD_MAX_SIZE',
'INVENTREE_STRICT_URLS'
]}
@@ -128,6 +126,21 @@ export default function SystemSettings() {
/>
)
},
{
name: 'display',
label: t`Display`,
icon: <IconDeviceDesktop />,
content: (
<GlobalSettingList
keys={[
'DISPLAY_FULL_NAMES',
'DISPLAY_PROFILE_INFO',
'WEEK_STARTS_ON',
'CALENDAR_HORIZON_MONTHS'
]}
/>
)
},
{
name: 'notifications',
label: t`Notifications`,
+5
View File
@@ -1,3 +1,8 @@
/* fullcalendar multimonth single-column (scroll-year) overrides */
.fc-multimonth-singlecol .fc-multimonth-title {
padding: 0.35em 0;
}
/* mantine-datatable overrides */
.mantine-datatable-pointer-cursor,
.mantine-datatable-context-menu-cursor {
+3 -1
View File
@@ -557,7 +557,9 @@ test('Build Order - Consume Stock', async ({ browser }) => {
// Issue the order
await page.getByRole('button', { name: 'Issue Order' }).click();
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByText('Order issued').waitFor();
await page.getByText('Production').first().waitFor();
await page.getByRole('button', { name: 'Complete Order' }).waitFor();
// Navigate to the "required parts" tab - and auto-allocate stock
await loadTab(page, 'Required Parts');
@@ -278,7 +278,7 @@ test('Purchase Orders - Calendar', async ({ browser }) => {
await page.getByRole('button', { name: 'Feb' }).waitFor();
await page.getByRole('button', { name: 'Dec' }).click();
await page.getByText('December').waitFor();
await page.getByText('December').first().waitFor();
// Put back into table view
await activateTableView(page);
@@ -719,7 +719,6 @@ test('Transfer Order - Allocate and Transfer', async ({ browser }) => {
// Issue the order
await page.getByRole('button', { name: 'Issue Order' }).click();
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByText('Order issued').waitFor();
await page.getByText('Issued', { exact: true }).first().waitFor();
await loadTab(page, 'Line Items');
+665 -675
View File
File diff suppressed because it is too large Load Diff