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:
@@ -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.
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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