Skip to content
Closed
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
Expand Up @@ -870,13 +870,21 @@ $fluent-scheduler-agenda-time-panel-cell-padding: 8px;
}

.dx-scheduler-appointment:not(.dx-scheduler-appointment-has-resource-color) {
container-type: size;

.dx-scheduler-appointment-strip {
display: block;
position: absolute;
width: 6px;
height: 100%;
background-color: $base-accent;
}

@container (max-width: 8px) {
.dx-scheduler-appointment-strip {
display: none;
}
}
}

.dx-rtl.dx-scheduler-appointment:not(.dx-scheduler-appointment-has-resource-color) {
Expand Down
1 change: 1 addition & 0 deletions packages/devextreme/js/__internal/scheduler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type Direction = 'vertical' | 'horizontal';
export type GroupOrientation = 'vertical' | 'horizontal';
export type ViewType = 'agenda' | 'day' | 'month' | 'timelineDay' | 'timelineMonth' | 'timelineWeek' | 'timelineWorkWeek' | 'week' | 'workWeek';
export type AllDayPanelModeType = 'all' | 'allDay' | 'hidden';
export type SnapToCellsModeType = 'always' | 'auto' | 'never';
export type RenderStrategyName = 'horizontal' | 'horizontalMonth' | 'horizontalMonthLine' | 'vertical' | 'week' | 'agenda';
export type FilterItemType = Record<string, string | number> | string | number;
export type HeaderCellTextFormat = string | ((date: Date) => string);
Expand Down
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 @@ -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
@@ -0,0 +1,19 @@
import { describe, expect, it } from '@jest/globals';

import { getDefaultSnapToCellsModeForView } from './get_view_model_options';

describe('getDefaultSnapToCellsModeForView', () => {
it.each([
{ viewType: 'month' as const, expected: 'always' },
{ viewType: 'agenda' as const, expected: 'always' },
{ viewType: 'timelineMonth' as const, expected: 'always' },
{ viewType: 'day' as const, expected: 'never' },
{ viewType: 'week' as const, expected: 'never' },
{ viewType: 'workWeek' as const, expected: 'never' },
{ viewType: 'timelineDay' as const, expected: 'never' },
{ viewType: 'timelineWeek' as const, expected: 'never' },
{ viewType: 'timelineWorkWeek' as const, expected: 'never' },
])('should return $expected for $viewType', ({ viewType, expected }) => {
expect(getDefaultSnapToCellsModeForView(viewType)).toBe(expected);
});
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Orientation } from '@js/common';
import type Scheduler from '@ts/scheduler/m_scheduler';

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

Expand All @@ -22,6 +22,7 @@ const configByView: Record<Exclude<ViewType, 'agenda'>, {

export interface ViewModelOptions {
type: ViewType;
snapToCellsMode: SnapToCellsModeType;
viewOffset: number;
groupOrientation?: Orientation;
isGroupByDate: boolean;
Expand All @@ -37,6 +38,10 @@ export interface ViewModelOptions {
isVirtualScrolling: boolean;
}

export const getDefaultSnapToCellsModeForView = (type: ViewType): SnapToCellsModeType => (
['month', 'agenda', 'timelineMonth'].includes(type) ? 'always' : 'never'
);

export const getViewModelOptions = (schedulerStore: Scheduler): ViewModelOptions => {
const viewOffset = schedulerStore.getViewOffsetMs();
const { groupOrientation, type } = schedulerStore.currentView;
Expand All @@ -52,11 +57,13 @@ export const getViewModelOptions = (schedulerStore: Scheduler): ViewModelOptions
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 ?? getDefaultSnapToCellsModeForView(type),
viewOffset,
groupOrientation,
isGroupByDate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,94 +2,109 @@ import { describe, expect, it } from '@jest/globals';

import { snapToCells } from './snap_to_cells';

const cells = [
{
min: 0, max: 10, cellIndex: 0, rowIndex: 0, columnIndex: 0,
},
{
min: 10, max: 20, cellIndex: 1, rowIndex: 0, columnIndex: 1,
},
{
min: 20, max: 30, cellIndex: 2, rowIndex: 0, columnIndex: 2,
},
{
min: 30, max: 40, cellIndex: 3, rowIndex: 1, columnIndex: 0,
},
{
min: 40, max: 50, cellIndex: 4, rowIndex: 2, columnIndex: 1,
},
];

describe('snapToCells', () => {
it('should snap appointments to cells', () => {
const items = [{
duration: 0,
cellIndex: 0,
endCellIndex: 0,
rowIndex: 0,
columnIndex: 0,
},
{
duration: 0,
cellIndex: 3,
endCellIndex: 4,
rowIndex: 1,
columnIndex: 0,
},
{
duration: 0,
cellIndex: 0,
endCellIndex: 2,
rowIndex: 0,
columnIndex: 0,
},
{
duration: 0,
cellIndex: 4,
endCellIndex: 4,
rowIndex: 2,
columnIndex: 1,
},
{
duration: 0,
cellIndex: 5,
endCellIndex: 5,
rowIndex: 3,
columnIndex: 2,
}];
describe('always mode', () => {
it('should snap appointments to cell boundaries', () => {
const items = [
{
cellIndex: 0, endCellIndex: 0, startDateUTC: 2, endDateUTC: 8, duration: 6,
},
{
cellIndex: 3, endCellIndex: 4, startDateUTC: 32, endDateUTC: 48, duration: 16,
},
{
cellIndex: 0, endCellIndex: 2, startDateUTC: 3, endDateUTC: 27, duration: 24,
},
];

expect(snapToCells(items as any, cells, 'always')).toEqual([
expect.objectContaining({
startDateUTC: 0, endDateUTC: 10, duration: 10,
}),
expect.objectContaining({
startDateUTC: 30, endDateUTC: 50, duration: 20,
}),
expect.objectContaining({
startDateUTC: 0, endDateUTC: 30, duration: 30,
}),
]);
});
});

describe('never mode', () => {
it('should return same reference without changes', () => {
const items = [
{
cellIndex: 0, endCellIndex: 0, startDateUTC: 2, endDateUTC: 10, duration: 8,
},
{
cellIndex: 1, endCellIndex: 2, startDateUTC: 12, endDateUTC: 27, duration: 15,
},
];

expect(snapToCells(items as any, cells, 'never')).toBe(items);
});
});

describe('auto mode', () => {
it('should snap both boundaries when cells are covered by more than 50%', () => {
const items = [
{
cellIndex: 0, endCellIndex: 1, startDateUTC: 2, endDateUTC: 16, duration: 14,
},
];

expect(snapToCells(items as any, cells, 'auto')).toEqual([
expect.objectContaining({
startDateUTC: 0, endDateUTC: 20, duration: 20,
}),
]);
});

it('should not snap boundary when cell is covered by less than 50%', () => {
const items = [
{
cellIndex: 0, endCellIndex: 0, startDateUTC: 2, endDateUTC: 7, duration: 5,
},
];

expect(snapToCells(items as any, cells, 'auto')).toEqual([
expect.objectContaining({
startDateUTC: 2, endDateUTC: 7, duration: 5,
}),
]);
});

it('should not snap boundary when cell is covered by exactly 50%', () => {
const items = [
{
cellIndex: 0, endCellIndex: 0, startDateUTC: 0, endDateUTC: 5, duration: 5,
},
];

expect(snapToCells(items as any, [
{
min: 0, max: 10, cellIndex: 0, rowIndex: 0, columnIndex: 0,
},
{
min: 10, max: 20, cellIndex: 1, rowIndex: 0, columnIndex: 1,
},
{
min: 20, max: 30, cellIndex: 2, rowIndex: 0, columnIndex: 2,
},
{
min: 30, max: 40, cellIndex: 3, rowIndex: 1, columnIndex: 0,
},
{
min: 40, max: 50, cellIndex: 4, rowIndex: 2, columnIndex: 1,
},
{
min: 50, max: 60, cellIndex: 5, rowIndex: 3, columnIndex: 2,
},
])).toEqual([
{
...items[0],
duration: 10,
startDateUTC: 0,
endDateUTC: 10,
},
{
...items[1],
duration: 20,
startDateUTC: 30,
endDateUTC: 50,
},
{
...items[2],
duration: 30,
startDateUTC: 0,
endDateUTC: 30,
},
{
...items[3],
duration: 10,
startDateUTC: 40,
endDateUTC: 50,
},
{
...items[4],
duration: 10,
startDateUTC: 50,
endDateUTC: 60,
},
]);
expect(snapToCells(items as any, cells, 'auto')).toEqual([
expect.objectContaining({
startDateUTC: 0, endDateUTC: 5, duration: 5,
}),
]);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,24 +1,52 @@
import type { SnapToCellsModeType } from '../../../types';
import type {
CellInterval, ListEntity, Position,
} from '../../types';

const getCellFill = (
startDateUTC: number,
endDateUTC: number,
cell: CellInterval,
): number => {
const cellDuration = cell.max - cell.min;
if (cellDuration <= 0) return 0;

const overlapStart = Math.max(startDateUTC, cell.min);
const overlapEnd = Math.min(endDateUTC, cell.max);
const overlapDuration = Math.max(0, overlapEnd - overlapStart);

return overlapDuration / cellDuration;
};

export const snapToCells = <T extends ListEntity & Position>(
entities: T[],
cells: CellInterval[],
isSnapToCell = true,
mode: SnapToCellsModeType = 'always',
): T[] => {
if (!isSnapToCell) {
return entities;
if (mode === 'never') return entities;

if (mode === 'always') {
return entities.map((entity) => {
const startDateUTC = cells[entity.cellIndex].min;
const endDateUTC = cells[entity.endCellIndex].max;

return {
...entity, startDateUTC, endDateUTC, duration: endDateUTC - startDateUTC,
};
});
}

return entities.map((entity) => {
const { cellIndex, endCellIndex } = entity;
const startCell = cells[entity.cellIndex];
const endCell = cells[entity.endCellIndex];

const startDateUTC = getCellFill(entity.startDateUTC, entity.endDateUTC, startCell) > 0.5
? startCell.min : entity.startDateUTC;
const endDateUTC = getCellFill(entity.startDateUTC, entity.endDateUTC, endCell) > 0.5
? endCell.max : entity.endDateUTC;

return {
...entity,
startDateUTC: cells[cellIndex].min,
endDateUTC: cells[endCellIndex].max,
duration: cells[endCellIndex].max - cells[cellIndex].min,
...entity, startDateUTC, endDateUTC, duration: endDateUTC - startDateUTC,
};
});
};
Loading