Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import type { Meta, StoryObj } from "@storybook/react-webpack5";
import dxScheduler from "devextreme/ui/scheduler";
import { wrapDxWithReact } from "../utils";
import { resources } from "./data";

const Scheduler = wrapDxWithReact(dxScheduler);

const data = [
{
text: '1 minute',
roomId: 1,
assigneeId: [1],
priorityId: 1,
startDate: new Date(2026, 2, 15, 10, 0),
endDate: new Date(2026, 2, 15, 10, 1)
},
{
text: '5 minutes',
roomId: 1,
assigneeId: [2],
priorityId: 1,
startDate: new Date(2026, 2, 16, 10, 0),
endDate: new Date(2026, 2, 16, 10, 5)
},
{
text: '15 minutes',
roomId: 2,
assigneeId: [3],
priorityId: 2,
startDate: new Date(2026, 2, 17, 10, 0),
endDate: new Date(2026, 2, 17, 10, 15)
},
{
text: '30 minutes',
roomId: 2,
assigneeId: [1],
priorityId: 2,
startDate: new Date(2026, 2, 18, 10, 0),
endDate: new Date(2026, 2, 18, 10, 30)
},
{
text: '46 minutes',
roomId: 3,
assigneeId: [2],
priorityId: 1,
startDate: new Date(2026, 2, 19, 10, 0),
endDate: new Date(2026, 2, 19, 10, 46)
},
{
text: '1 hour',
roomId: 2,
assigneeId: [4],
priorityId: 1,
startDate: new Date(2026, 2, 20, 10, 0),
endDate: new Date(2026, 2, 20, 11, 0)
},
];

const viewNames = ['day', 'week', 'workWeek', 'month', 'agenda', 'timelineDay', 'timelineWeek', 'timelineWorkWeek', 'timelineMonth'];

const meta: Meta<typeof Scheduler> = {
title: 'Components/Scheduler/SnapToCellsMode',
component: Scheduler,
parameters: { layout: 'padded' },
};
export default meta;

type Story = StoryObj<typeof Scheduler>;

export const Overview: Story = {
args: {
height: 600,
views: viewNames,
currentView: 'week',
currentDate: new Date(2026, 2, 15),
startDayHour: 9,
endDayHour: 22,
dataSource: data,
resources,
snapToCellsMode: 'auto',
},
argTypes: {
height: { control: 'number' },
views: { control: 'object' },
snapToCellsMode: { control: 'select', options: ['always', 'auto', 'never'] },
currentView: { control: 'select', options: viewNames },
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {
afterEach, beforeEach, describe, expect, it,
} from '@jest/globals';

import { createScheduler } from './__mock__/create_scheduler';
import {
DEFAULT_CELL_HEIGHT,
setupSchedulerTestEnvironment,
} from './__mock__/m_mock_scheduler';

describe('snapToCellsMode', () => {
beforeEach(() => {
setupSchedulerTestEnvironment();
});

afterEach(() => {
document.body.innerHTML = '';
});

it('default snapToCellsMode on day view', async () => {
const { POM } = await createScheduler({
width: 800,
height: 600,
views: ['day'],
currentView: 'day',
currentDate: new Date(2026, 2, 15),
cellDuration: 30,
startDayHour: 9,
endDayHour: 18,
dataSource: [{
text: 'short',
startDate: new Date(2026, 2, 15, 10, 0),
endDate: new Date(2026, 2, 15, 10, 10),
}],
});

const appH = POM.getAppointment('short').getGeometry().height;

expect(appH).toBeLessThan(DEFAULT_CELL_HEIGHT / 2);
});

it('root snapToCellsMode always overrides default on day view', async () => {
const { POM } = await createScheduler({
width: 800,
height: 600,
views: ['day'],
currentView: 'day',
currentDate: new Date(2026, 2, 15),
cellDuration: 30,
startDayHour: 9,
endDayHour: 18,
dataSource: [{
text: 'short',
startDate: new Date(2026, 2, 15, 10, 0),
endDate: new Date(2026, 2, 15, 10, 10),
}],
snapToCellsMode: 'always',
});

const appH = POM.getAppointment('short').getGeometry().height;

expect(appH).toEqual(DEFAULT_CELL_HEIGHT);
});

it('views[].snapToCellsMode always overrides default on day view', async () => {
const { POM } = await createScheduler({
width: 800,
height: 600,
views: [{ type: 'day', snapToCellsMode: 'always' }],
currentView: 'day',
currentDate: new Date(2026, 2, 15),
cellDuration: 30,
startDayHour: 9,
endDayHour: 18,
dataSource: [{
text: 'short',
startDate: new Date(2026, 2, 15, 10, 0),
endDate: new Date(2026, 2, 15, 10, 10),
}],
});

const appH = POM.getAppointment('short').getGeometry().height;

expect(appH).toEqual(DEFAULT_CELL_HEIGHT);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export const DEFAULT_SCHEDULER_OPTIONS: Properties = {
mode: 'standard',
},
allDayPanelMode: 'all',
snapToCellsMode: undefined,
toolbar: {
disabled: false,
multiline: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ type RequiredOptions = 'views'
| 'adaptivityEnabled'
| 'scrolling'
| 'allDayPanelMode'
| 'snapToCellsMode'
| 'toolbar';
export type DateOption = 'currentDate' | 'min' | 'max';
export type SafeSchedulerOptions = SchedulerInternalOptions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const getSchedulerMock = ({
resourceManager,
dateRange,
skippedDays,
isVirtualScrolling = false,
}: {
type: string;
startDayHour: number;
Expand All @@ -19,6 +20,7 @@ export const getSchedulerMock = ({
resourceManager?: ResourceManager;
skippedDays?: number[];
dateRange?: Date[];
isVirtualScrolling?: boolean;
}): Scheduler => ({
timeZoneCalculator: mockTimeZoneCalculator,
currentView: { type, skippedDays: skippedDays ?? [] },
Expand All @@ -37,6 +39,7 @@ export const getSchedulerMock = ({
}[name]),
option: (name: string) => ({ firstDayOfWeek: 0, showAllDayPanel: true }[name]),
getViewOffsetMs: () => offsetMinutes * 60_000,
isVirtualScrolling: () => isVirtualScrolling,
resourceManager: resourceManager ?? new ResourceManager([]),
_dataAccessors: mockAppointmentDataAccessor,
}) as unknown as Scheduler;
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const sortAppointments = (
const {
isMonthView,
hasAllDayPanel,
snapToCellsMode,
viewOffset,
compareOptions: { endDayHour },
} = optionManager.options;
Expand All @@ -40,9 +41,11 @@ export const sortAppointments = (
sortByStartDate(innerStep1);
sortByGroupIndex(innerStep1);
const innerStep2 = addPosition(innerStep1, optionManager.getCells(panelName));
const innerStep3 = isMonthView || panelName === 'allDayPanel'
? snapToCells(innerStep2, optionManager.getCells(panelName))
: innerStep2;
const innerStep3 = snapToCells(
innerStep2,
optionManager.getCells(panelName),
panelName === 'allDayPanel' ? 'always' : snapToCellsMode,
);
const innerStep4 = addCollector(innerStep3, optionManager.getCollectorOptions(panelName));
return innerStep4;
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,48 @@
import type { Orientation } from '@js/common';
import type { SnapToCellsMode } from '@js/ui/scheduler';
import type Scheduler from '@ts/scheduler/m_scheduler';

import type { ViewType } from '../../../types';
import { getCompareOptions } from '../../common/get_compare_options';
import type { CompareOptions } from '../../types';

const configByView: Record<Exclude<ViewType, 'agenda'>, {
interface ViewConfig {
isTimelineView: boolean;
isMonthView: boolean;
viewOrientation: 'horizontal' | 'vertical';
}> = {
day: { isTimelineView: false, isMonthView: false, viewOrientation: 'vertical' },
week: { isTimelineView: false, isMonthView: false, viewOrientation: 'vertical' },
workWeek: { isTimelineView: false, isMonthView: false, viewOrientation: 'vertical' },
month: { isTimelineView: false, isMonthView: true, viewOrientation: 'horizontal' },
timelineDay: { isTimelineView: true, isMonthView: false, viewOrientation: 'horizontal' },
timelineWeek: { isTimelineView: true, isMonthView: false, viewOrientation: 'horizontal' },
timelineWorkWeek: { isTimelineView: true, isMonthView: false, viewOrientation: 'horizontal' },
timelineMonth: { isTimelineView: true, isMonthView: true, viewOrientation: 'horizontal' },
snapToCellsMode: SnapToCellsMode;
}

const configByView: Record<Exclude<ViewType, 'agenda'>, ViewConfig> = {
day: {
isTimelineView: false, isMonthView: false, viewOrientation: 'vertical', snapToCellsMode: 'never',
},
week: {
isTimelineView: false, isMonthView: false, viewOrientation: 'vertical', snapToCellsMode: 'never',
},
workWeek: {
isTimelineView: false, isMonthView: false, viewOrientation: 'vertical', snapToCellsMode: 'never',
},
month: {
isTimelineView: false, isMonthView: true, viewOrientation: 'horizontal', snapToCellsMode: 'always',
},
timelineDay: {
isTimelineView: true, isMonthView: false, viewOrientation: 'horizontal', snapToCellsMode: 'never',
},
timelineWeek: {
isTimelineView: true, isMonthView: false, viewOrientation: 'horizontal', snapToCellsMode: 'never',
},
timelineWorkWeek: {
isTimelineView: true, isMonthView: false, viewOrientation: 'horizontal', snapToCellsMode: 'never',
},
timelineMonth: {
isTimelineView: true, isMonthView: true, viewOrientation: 'horizontal', snapToCellsMode: 'always',
},
};

export interface ViewModelOptions {
type: ViewType;
snapToCellsMode: SnapToCellsMode;
viewOffset: number;
groupOrientation?: Orientation;
isGroupByDate: boolean;
Expand All @@ -47,16 +68,23 @@ export const getViewModelOptions = (schedulerStore: Scheduler): ViewModelOptions
&& schedulerStore.getViewOption('groupByDate'),
);
const compareOptions = getCompareOptions(schedulerStore);
const { isTimelineView, isMonthView, viewOrientation } = configByView[type];
const {
isTimelineView,
isMonthView,
viewOrientation,
snapToCellsMode: defaultSnapToCellsMode,
} = configByView[type];
const isRTLEnabled = Boolean(schedulerStore.option('rtlEnabled'));
const isAdaptivityEnabled = Boolean(schedulerStore.option('adaptivityEnabled'));
const cellDurationMinutes = schedulerStore.getViewOption('cellDuration');
const allDayPanelMode = schedulerStore.getViewOption('allDayPanelMode');
const snapToCellsMode = schedulerStore.getViewOption('snapToCellsMode');
const showAllDayPanel = schedulerStore.getViewOption('showAllDayPanel');
const isVirtualScrolling = schedulerStore.isVirtualScrolling();

return {
type,
snapToCellsMode: snapToCellsMode ?? defaultSnapToCellsMode,
viewOffset,
groupOrientation,
isGroupByDate,
Expand Down
Loading
Loading