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:
@@ -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(
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user