diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/appointments_resizing.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/appointments_resizing.test.ts new file mode 100644 index 000000000000..6095fec504d7 --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/__tests__/appointments_resizing.test.ts @@ -0,0 +1,104 @@ +import { + beforeEach, describe, expect, it, +} from '@jest/globals'; +import type { Properties } from '@js/ui/scheduler'; + +import { createScheduler } from './__mock__/create_scheduler'; +import { setupSchedulerTestEnvironment } from './__mock__/mock_scheduler'; + +const APPOINTMENT_SELECTOR = '.dx-scheduler-appointment'; +const REDUCED_SELECTOR = '.dx-scheduler-appointment-reduced'; +const HANDLE_LEFT_SELECTOR = '.dx-resizable-handle-left'; +const HANDLE_RIGHT_SELECTOR = '.dx-resizable-handle-right'; + +const getResizeHandles = (container: HTMLElement): string[][] => Array + .from(container.querySelectorAll(APPOINTMENT_SELECTOR)) + .map((part) => [ + ...(part.querySelector(HANDLE_LEFT_SELECTOR) ? ['left'] : []), + ...(part.querySelector(HANDLE_RIGHT_SELECTOR) ? ['right'] : []), + ]); + +const baseConfig: Properties = { + dataSource: [{ + text: 'Long', + roomId: 1, + startDate: new Date(2021, 3, 12, 9), + endDate: new Date(2021, 3, 14, 12), + }], + currentDate: new Date(2021, 3, 14), + editing: { allowUpdating: true, allowResizing: true }, + groups: ['roomId'], + resources: [{ + fieldExpr: 'roomId', + dataSource: [{ id: 1, text: 'Room 1' }, { id: 2, text: 'Room 2' }], + }], + height: 600, +}; + +describe('Appointments resizing', () => { + beforeEach(() => { + setupSchedulerTestEnvironment(); + }); + + describe('grouping by date', () => { + it('should render resize handles only on the edges of a long appointment [month]', async () => { + const { container } = await createScheduler({ + ...baseConfig, + views: ['month'], + currentView: 'month', + groupByDate: true, + }); + + expect(getResizeHandles(container)).toEqual([['left'], [], ['right']]); + }); + + it('should not render the reduced icon on parts of a long appointment [month]', async () => { + const { container } = await createScheduler({ + ...baseConfig, + views: ['month'], + currentView: 'month', + groupByDate: true, + }); + + expect(container.querySelectorAll(APPOINTMENT_SELECTOR).length).toBe(3); + expect(container.querySelectorAll(REDUCED_SELECTOR).length).toBe(0); + }); + + it('should render resize handles only on the edges of a long all-day appointment [week]', async () => { + const { container } = await createScheduler({ + ...baseConfig, + dataSource: [{ + text: 'Long', + roomId: 1, + allDay: true, + startDate: new Date(2021, 3, 12, 9), + endDate: new Date(2021, 3, 14, 12), + }], + views: ['week'], + currentView: 'week', + groupByDate: true, + }); + + expect(getResizeHandles(container)).toEqual([['left'], [], ['right']]); + }); + }); + + describe('grouping by resource', () => { + it('should render resize handles only on the edges of a long appointment [month]', async () => { + const { container } = await createScheduler({ + ...baseConfig, + dataSource: [{ + text: 'Long', + roomId: 1, + startDate: new Date(2021, 3, 9, 9), + endDate: new Date(2021, 3, 14, 12), + }], + views: ['month'], + currentView: 'month', + groupByDate: false, + }); + + expect(getResizeHandles(container)).toEqual([['left'], ['right']]); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/scheduler/appointments/appointment/m_appointment.ts b/packages/devextreme/js/__internal/scheduler/appointments/appointment/m_appointment.ts index 828f6172d282..5812d6aef781 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/appointment/m_appointment.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/appointment/m_appointment.ts @@ -64,6 +64,7 @@ export class Appointment extends DOMComponent { allowDrag: true, allowResize: true, reduced: null, + hideReducedIcon: false, isCompact: false, direction: 'vertical', resizableConfig: { keepAspectRatio: false }, @@ -102,6 +103,7 @@ export class Appointment extends DOMComponent { case 'allowDrag': case 'allowResize': case 'reduced': + case 'hideReducedIcon': case 'sortedIndex': case 'isCompact': case 'direction': @@ -230,7 +232,7 @@ export class Appointment extends DOMComponent { _renderReducedAppointment() { const reducedPart: any = this.option('reduced'); - if (!reducedPart) { + if (!reducedPart || this.option('hideReducedIcon')) { return; } diff --git a/packages/devextreme/js/__internal/scheduler/appointments/appointment/m_types.ts b/packages/devextreme/js/__internal/scheduler/appointments/appointment/m_types.ts index 890a589ca966..6e21188868d8 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/appointment/m_types.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/appointment/m_types.ts @@ -17,6 +17,7 @@ export interface AppointmentProperties extends Record { allowDelete: boolean; allDay: boolean; reduced: string; + hideReducedIcon: boolean; isCompact: boolean; startDate: Date; cellWidth: number; diff --git a/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts b/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts index 25885bd5de2e..b1c2fda84585 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts @@ -757,8 +757,8 @@ class SchedulerAppointments extends CollectionWidget { allowDrag, allowDelete, allDay, - // NOTE: hide reduced icon for grouped by date workspace - reduced: isGroupByDate ? undefined : settings.reduced, + reduced: settings.reduced, + hideReducedIcon: isGroupByDate, startDate: new Date(settings.info?.appointment.startDate), cellWidth: this.invoke('getCellWidth'), cellHeight: this.invoke('getCellHeight'), diff --git a/packages/devextreme/js/__internal/scheduler/appointments/resizing/m_core.test.ts b/packages/devextreme/js/__internal/scheduler/appointments/resizing/m_core.test.ts new file mode 100644 index 000000000000..769dc40b6b08 --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/appointments/resizing/m_core.test.ts @@ -0,0 +1,182 @@ +import { describe, expect, it } from '@jest/globals'; + +import { getAppointmentDateRange } from './m_core'; +import type { GetAppointmentDateRangeOptions, Rect } from './types'; + +const CELL_WIDTH = 100; +const CELL_HEIGHT = 24; +const FIRST_DATE = new Date(2021, 3, 11); + +const addDays = (date: Date, days: number): Date => new Date( + date.getFullYear(), + date.getMonth(), + date.getDate() + days, +); + +// NOTE: Every date column is repeated for each group when grouping by date, +// so a cell column index is not equal to a date index anymore. +const createAllDayRow = (dateCount: number, groupCount: number): { + startDate: Date; + endDate: Date; + index: number; + groupIndex: number; +}[] => Array.from({ length: dateCount * groupCount }, (_, columnIndex) => { + const dateIndex = Math.floor(columnIndex / groupCount); + const startDate = addDays(FIRST_DATE, dateIndex); + + return { + startDate, + endDate: startDate, + index: dateIndex, + groupIndex: columnIndex % groupCount, + }; +}); + +const createOptions = ({ + dateCount = 7, + groupCount = 2, + left, + width, + handles, + rtlEnabled = false, + appointment = { + startDate: new Date(2021, 3, 12), + endDate: new Date(2021, 3, 14), + allDay: false, + }, +}: { + dateCount?: number; + groupCount?: number; + left: number; + width: number; + handles: { left: boolean; right: boolean }; + rtlEnabled?: boolean; + appointment?: { startDate: Date; endDate: Date; allDay: boolean }; +}): GetAppointmentDateRangeOptions => { + const row = createAllDayRow(dateCount, groupCount); + const cellsMeta = row.map(() => ({ width: CELL_WIDTH, height: CELL_HEIGHT })); + const rect = (values: Partial): Rect => ({ + top: 0, left: 0, width: 0, height: 0, ...values, + }); + + return { + handles, + rtlEnabled, + isVerticalGroupedWorkSpace: false, + appointmentSettings: { + allDay: true, + rowIndex: 0, + columnIndex: 0, + info: { + appointment: { allDay: appointment.allDay }, + sourceAppointment: { + startDate: appointment.startDate, + endDate: appointment.endDate, + }, + }, + }, + appointmentRect: rect({ left, width, height: CELL_HEIGHT }), + parentAppointmentRect: rect({}), + DOMMetaData: { allDayPanelCellsMeta: cellsMeta }, + viewDataProvider: { + getCellData: ( + rowIndex: number, + columnIndex: number, + isAllDay: boolean, + rtl: boolean, + ) => ({ ...row[rtl ? row.length - 1 - columnIndex : columnIndex] }), + }, + } as unknown as GetAppointmentDateRangeOptions; +}; + +describe('getAppointmentDateRange', () => { + describe('grouping by date', () => { + it('should take the end date from the cell under the right appointment border', () => { + const dateRange = getAppointmentDateRange(createOptions({ + left: 2 * CELL_WIDTH, + width: 5 * CELL_WIDTH, + handles: { left: false, right: true }, + })); + + expect(dateRange).toEqual({ + startDate: new Date(2021, 3, 12), + endDate: new Date(2021, 3, 15), + }); + }); + + it('should take the start date from the cell under the left appointment border', () => { + const dateRange = getAppointmentDateRange(createOptions({ + left: 0, + width: 5 * CELL_WIDTH, + handles: { left: true, right: false }, + })); + + expect(dateRange).toEqual({ + startDate: new Date(2021, 3, 11), + endDate: new Date(2021, 3, 14), + }); + }); + + it('should not go outside of the row when the appointment is wider than the row', () => { + const dateRange = getAppointmentDateRange(createOptions({ + left: 12 * CELL_WIDTH, + width: 5 * CELL_WIDTH, + handles: { left: false, right: true }, + })); + + expect(dateRange).toEqual({ + startDate: new Date(2021, 3, 12), + endDate: new Date(2021, 3, 18), + }); + }); + + it('should mirror cell indexes in RTL', () => { + const dateRange = getAppointmentDateRange(createOptions({ + left: 11 * CELL_WIDTH, + width: 3 * CELL_WIDTH, + handles: { left: false, right: true }, + rtlEnabled: true, + })); + + expect(dateRange).toEqual({ + startDate: new Date(2021, 3, 11), + endDate: new Date(2021, 3, 14), + }); + }); + }); + + describe('without grouping', () => { + it('should take the end date from the cell under the right appointment border', () => { + const dateRange = getAppointmentDateRange(createOptions({ + groupCount: 1, + left: CELL_WIDTH, + width: 3 * CELL_WIDTH, + handles: { left: false, right: true }, + })); + + expect(dateRange).toEqual({ + startDate: new Date(2021, 3, 12), + endDate: new Date(2021, 3, 15), + }); + }); + + it('should keep the all day appointment end date inside the last occupied cell', () => { + const dateRange = getAppointmentDateRange(createOptions({ + groupCount: 1, + left: CELL_WIDTH, + width: 3 * CELL_WIDTH, + handles: { left: false, right: true }, + appointment: { + startDate: new Date(2021, 3, 12), + endDate: new Date(2021, 3, 14), + allDay: true, + }, + })); + + expect(dateRange).toEqual({ + startDate: new Date(2021, 3, 12), + endDate: new Date(2021, 3, 14), + }); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/scheduler/appointments/resizing/m_core.ts b/packages/devextreme/js/__internal/scheduler/appointments/resizing/m_core.ts index a235bc0515b4..302ac84d885f 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/resizing/m_core.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/resizing/m_core.ts @@ -38,61 +38,70 @@ const getCellData = ( return cellData; }; -const getAppointmentLeftCell = (options: GetAppointmentDateRangeOptionsExtended): ViewCellData => { - const { - cellHeight, - cellWidth, - relativeAppointmentRect, - appointment, - rtlEnabled, - } = options; +// NOTE: Cell indexes are always counted from the left table border, +// getCellData mirrors them in RTL. +const getAppointmentCellIndexes = ( + options: GetAppointmentDateRangeOptionsExtended, +): { rowIndex: number; columnIndex: number } => { + const { cellHeight, cellWidth, relativeAppointmentRect } = options; + + return { + rowIndex: Math.floor(relativeAppointmentRect.top / cellHeight), + columnIndex: Math.round(relativeAppointmentRect.left / cellWidth), + }; +}; - const cellRowIndex = Math.floor(relativeAppointmentRect.top / cellHeight); - const cellColumnIndex = Math.round(relativeAppointmentRect.left / cellWidth); +const getAppointmentLeftCell = (options: GetAppointmentDateRangeOptionsExtended): ViewCellData => { + const { appointment, rtlEnabled } = options; + const { rowIndex, columnIndex } = getAppointmentCellIndexes(options); return getCellData( options, - cellRowIndex, - cellColumnIndex, + rowIndex, + columnIndex, appointment.isOccupiedAllDay, appointment.isAllDay, rtlEnabled, ); }; -const getDateRangeHorizontal = (options: GetAppointmentDateRangeOptionsExtended): DateRange => { +const getAppointmentRightCell = (options: GetAppointmentDateRangeOptionsExtended): ViewCellData => { const { cellWidth, cellCountInRow, relativeAppointmentRect, appointment, - handles, + rtlEnabled, } = options; + const { rowIndex, columnIndex } = getAppointmentCellIndexes(options); + const cellsAmount = Math.round(relativeAppointmentRect.width / cellWidth); + const rightColumnIndex = Math.min(columnIndex + cellsAmount - 1, cellCountInRow - 1); - const appointmentFirstCell = getAppointmentLeftCell(options); - const appointmentCellsAmount = Math.round(relativeAppointmentRect.width / cellWidth); - const appointmentLastCellIndex = appointmentFirstCell.index + (appointmentCellsAmount - 1); + return getCellData( + options, + rowIndex, + rightColumnIndex, + appointment.isOccupiedAllDay, + appointment.isAllDay, + rtlEnabled, + ); +}; + +const getDateRangeHorizontal = (options: GetAppointmentDateRangeOptionsExtended): DateRange => { + const { appointment, handles } = options; if (handles.left) { + const { startDate } = getAppointmentLeftCell(options); + return { - startDate: appointmentFirstCell.startDate, - endDate: appointmentFirstCell.startDate > appointment.endDate - ? appointmentFirstCell.startDate + startDate, + endDate: startDate > appointment.endDate + ? startDate : appointment.endDate, }; } - const appointmentRowIndex = Math.floor(appointmentLastCellIndex / cellCountInRow); - const appointmentColumnIndex = appointmentLastCellIndex % cellCountInRow; - const appointmentLastCell = getCellData( - options, - appointmentRowIndex, - appointmentColumnIndex, - appointment.isOccupiedAllDay, - appointment.isAllDay, - ); - - const { endDate } = appointmentLastCell; + const { endDate } = getAppointmentRightCell(options); return { startDate: endDate < appointment.startDate @@ -103,38 +112,20 @@ const getDateRangeHorizontal = (options: GetAppointmentDateRangeOptionsExtended) }; const getDateRangeHorizontalRTL = (options: GetAppointmentDateRangeOptionsExtended): DateRange => { - const { - cellCountInRow, - appointment, - handles, - cellWidth, - relativeAppointmentRect, - } = options; - - const appointmentLastCell = getAppointmentLeftCell(options); + const { appointment, handles } = options; if (handles.right) { - const appointmentLastCellIndex = appointmentLastCell.index; - const appointmentCellsAmount = Math.round(relativeAppointmentRect.width / cellWidth); - const appointmentFirstCellIndex = appointmentLastCellIndex - appointmentCellsAmount + 1; - const appointmentRowIndex = Math.floor(appointmentLastCellIndex / cellCountInRow); - const appointmentFirstCell = getCellData( - options, - appointmentRowIndex, - appointmentFirstCellIndex, - appointment.isOccupiedAllDay, - appointment.isAllDay, - ); + const { startDate } = getAppointmentRightCell(options); return { - startDate: appointmentFirstCell.startDate, - endDate: appointmentFirstCell.startDate > appointment.endDate - ? appointmentFirstCell.startDate + startDate, + endDate: startDate > appointment.endDate + ? startDate : appointment.endDate, }; } - const { endDate } = appointmentLastCell; + const { endDate } = getAppointmentLeftCell(options); return { startDate: endDate < appointment.startDate