2
0
mirror of https://github.com/inventree/InvenTree.git synced 2026-06-12 03:28:37 +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
@@ -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