diff --git a/packages/devextreme/js/__internal/core/utils/math.test.ts b/packages/devextreme/js/__internal/core/utils/math.test.ts new file mode 100644 index 000000000000..47ad6ff7f8d8 --- /dev/null +++ b/packages/devextreme/js/__internal/core/utils/math.test.ts @@ -0,0 +1,50 @@ +import { shiftIntegerByModule } from '@ts/core/utils/math'; +import each from 'jest-each'; + +describe('Math utils tests', () => { + describe('shiftIntegerByModule', () => { + each` + value | module | expectedResult + ${0} | ${2} | ${0} + ${2} | ${2} | ${0} + ${2} | ${4} | ${2} + ${2} | ${1000} | ${2} + ${4} | ${2} | ${0} + ${5} | ${2} | ${1} + ${6} | ${2} | ${0} + ${1e10} | ${10} | ${0} + ${1e10 + 3} | ${10} | ${3} + ${-9} | ${3} | ${0} + ${-1} | ${6} | ${5} + ${-3} | ${9} | ${6} + ${-5} | ${9} | ${4} + ${-1e10} | ${10} | ${0} + ${-1e10 + 3} | ${10} | ${3} + ` + .it('should return correct result', ({ + value, + module, + expectedResult, + }) => { + const result = shiftIntegerByModule(value, module); + + expect(result).toEqual(expectedResult); + }); + + it('should throw error if value isn\'t integer', () => { + expect(() => shiftIntegerByModule(1.5, 3)).toThrow(); + }); + + it('should throw error if module value isn\'t integer', () => { + expect(() => shiftIntegerByModule(2, 2.5)).toThrow(); + }); + + it('should throw error if module value equals zero', () => { + expect(() => shiftIntegerByModule(2, 0)).toThrow(); + }); + + it('should throw error if module value less than zero', () => { + expect(() => shiftIntegerByModule(2, -2)).toThrow(); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/core/utils/math.ts b/packages/devextreme/js/__internal/core/utils/math.ts new file mode 100644 index 000000000000..ee90cd6d8a82 --- /dev/null +++ b/packages/devextreme/js/__internal/core/utils/math.ts @@ -0,0 +1,31 @@ +export const shiftIntegerByModule = ( + integerValue: number, + moduleValue: number, +): number => { + if (!Number.isInteger(integerValue)) { + throw Error(`Passed integer value ${integerValue} is not an integer.`); + } + + if (!Number.isInteger(moduleValue)) { + throw Error(`Passed module value ${moduleValue} is not an integer.`); + } + + if (moduleValue <= 0) { + throw Error(`Passed module value ${moduleValue} must be > 0.`); + } + + const normalizedInteger = integerValue % moduleValue; + + switch (true) { + // NOTE: In some cases we can have -0 or +0 values. + // So this is why we handle zero as separate case here. + case normalizedInteger === 0: + return 0; + case normalizedInteger > 0: + return normalizedInteger; + case normalizedInteger < 0: + return moduleValue + normalizedInteger; + default: + throw Error(`Unexpected division (${integerValue} % ${moduleValue}) occurred.`); + } +}; diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index 08eaeea020fd..cc630834d4c6 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -1671,7 +1671,7 @@ class Scheduler extends Widget { this._workSpaceRecalculation = new Deferred(); this._waitAsyncTemplate(() => { triggerResizeEvent(this._workSpace.$element()); - this._workSpace._refreshDateTimeIndication(); + this._workSpace.renderCurrentDateTimeLineAndShader(); }); } diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts index f0d41b39b23c..a579977c8b56 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_timeline.ts @@ -368,21 +368,6 @@ class SchedulerTimeline extends SchedulerWorkSpace { _setHorizontalGroupHeaderCellsHeight() { return noop(); } - _setCurrentTimeCells() { - const timePanelCells = this._getTimePanelCells(); - const currentTimeCellIndices = this._getCurrentTimePanelCellIndices(); - currentTimeCellIndices.forEach((timePanelCellIndex) => { - timePanelCells.eq(timePanelCellIndex) - .addClass(HEADER_CURRENT_TIME_CELL_CLASS); - }); - } - - _cleanCurrentTimeCells() { - (this.$element() as any) - .find(`.${HEADER_CURRENT_TIME_CELL_CLASS}`) - .removeClass(HEADER_CURRENT_TIME_CELL_CLASS); - } - _getTimePanelCells() { return (this.$element() as any) .find(`.${HEADER_PANEL_CELL_CLASS}:not(.${HEADER_PANEL_WEEK_CELL_CLASS})`); @@ -513,6 +498,24 @@ class SchedulerTimeline extends SchedulerWorkSpace { groupByDate, ); } + + // Old render methods. + // TODO Old render: delete these methods with the old render. + + _setCurrentTimeCells(): void { + const timePanelCells = this._getTimePanelCells(); + const currentTimeCellIndices = this._getCurrentTimePanelCellIndices(); + currentTimeCellIndices.forEach((timePanelCellIndex) => { + timePanelCells.eq(timePanelCellIndex) + .addClass(HEADER_CURRENT_TIME_CELL_CLASS); + }); + } + + _cleanCurrentTimeCells(): void { + (this.$element() as any) + .find(`.${HEADER_CURRENT_TIME_CELL_CLASS}`) + .removeClass(HEADER_CURRENT_TIME_CELL_CLASS); + } } registerComponent('dxSchedulerTimeline', SchedulerTimeline as any); diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts index 5963c60d301f..e720cba279e1 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts @@ -87,6 +87,18 @@ import HorizontalGroupedStrategy from './m_work_space_grouped_strategy_horizonta import VerticalGroupedStrategy from './m_work_space_grouped_strategy_vertical'; import ViewDataProvider from './view_model/m_view_data_provider'; +interface RenderComponentOptions { + header?: boolean; + timePanel?: boolean; + dateTable?: boolean; + allDayPanel?: boolean; +} + +interface RenderRWorkspaceOptions { + renderComponents: RenderComponentOptions; + generateNewData: boolean; +} + const { tableCreator } = tableCreatorModule; // TODO: The constant is needed so that the dragging is not sharp. To prevent small twitches @@ -165,6 +177,16 @@ const CELL_SELECTOR = `.${DATE_TABLE_CELL_CLASS}, .${ALL_DAY_TABLE_CELL_CLASS}`; const CELL_INDEX_CALCULATION_EPSILON = 0.05; +const DEFAULT_WORKSPACE_RENDER_OPTIONS: RenderRWorkspaceOptions = { + renderComponents: { + header: true, + timePanel: true, + dateTable: true, + allDayPanel: true, + }, + generateNewData: true, +}; + class SchedulerWorkSpace extends WidgetObserver { _viewDataProvider: any; @@ -798,6 +820,7 @@ class SchedulerWorkSpace extends WidgetObserver { currentDate: this.option('currentDate'), startDate: this.option('startDate'), firstDayOfWeek: this.option('firstDayOfWeek'), + showCurrentTimeIndicator: this.option('showCurrentTimeIndicator'), ...this.virtualScrollingDispatcher.getRenderState(), }; @@ -2020,16 +2043,27 @@ class SchedulerWorkSpace extends WidgetObserver { // Methods that render renovated components. Useless in renovation // ------------ - renderRWorkSpace(componentsToRender?: any) { - const allComponents = { - header: true, timePanel: true, dateTable: true, allDayPanel: true, - }; - const components = componentsToRender ?? allComponents; + renderRWorkSpace({ + header, + timePanel, + dateTable, + allDayPanel, + }: RenderComponentOptions = DEFAULT_WORKSPACE_RENDER_OPTIONS.renderComponents) { + if (header) { + this.renderRHeaderPanel(); + } + + if (timePanel) { + this.renderRTimeTable(); + } - components.header && this.renderRHeaderPanel(); - components.timePanel && this.renderRTimeTable(); - components.dateTable && this.renderRDateTable(); - components.allDayPanel && this.renderRAllDayPanel(); + if (dateTable) { + this.renderRDateTable(); + } + + if (allDayPanel) { + this.renderRAllDayPanel(); + } } renderRDateTable() { @@ -2725,11 +2759,13 @@ class SchedulerWorkSpace extends WidgetObserver { }); } - _renderDateTimeIndication() { return noop(); } + protected _renderDateTimeIndication() { return noop(); } + + protected renderCurrentDateTimeLineAndShader(): void { return noop(); } - _setIndicationUpdateInterval() { return noop(); } + protected renderCurrentDateTimeIndication(): void { return noop(); } - _refreshDateTimeIndication() { return noop(); } + protected _setIndicationUpdateInterval() { return noop(); } _detachGroupCountClass() { [ @@ -2775,7 +2811,7 @@ class SchedulerWorkSpace extends WidgetObserver { this._$allDayTitle && this._$allDayTitle.remove(); } - _cleanView() { + _cleanView(): void { this.cache.clear(); this._cleanTableWidths(); this.cellsSelectionState.clearSelectedAndFocusedCells(); @@ -2888,14 +2924,18 @@ class SchedulerWorkSpace extends WidgetObserver { } } - renderWorkSpace(isGenerateNewViewData = true) { + renderWorkSpace({ + generateNewData, + renderComponents, + } = DEFAULT_WORKSPACE_RENDER_OPTIONS): void { this.cache.clear(); - this.viewDataProvider.update(this.generateRenderOptions(), isGenerateNewViewData); + this.viewDataProvider.update(this.generateRenderOptions(), generateNewData); if (this.isRenovatedRender()) { - this.renderRWorkSpace(); + this.renderRWorkSpace(renderComponents); } else { + // TODO Old render: Delete this old render block after the SSR tests check. this._renderDateHeader(); this._renderTimePanel(); this._renderGroupAllDayPanel(); diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space_indicator.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space_indicator.ts index a5455d9ef481..b42f811b7c62 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space_indicator.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space_indicator.ts @@ -6,6 +6,7 @@ import { getBoundingRect } from '@js/core/utils/position'; import { setWidth } from '@js/core/utils/size'; import { hasWindow } from '@js/core/utils/window'; import { getToday } from '@js/renovation/ui/scheduler/view_model/to_test/views/utils/base'; +import { dateUtilsTs } from '@ts/core/utils/date'; import { HEADER_CURRENT_TIME_CELL_CLASS } from '../m_classes'; import timezoneUtils from '../m_utils_time_zone'; @@ -21,10 +22,12 @@ class SchedulerWorkSpaceIndicator extends SchedulerWorkSpace { // @ts-expect-error _getToday() { - return getToday(this.option('indicatorTime') as any, this.timeZoneCalculator); + const viewOffset = this.option('viewOffset') as number; + const today = getToday(this.option('indicatorTime') as Date, this.timeZoneCalculator); + return dateUtilsTs.addOffsets(today, [-viewOffset]); } - isIndicationOnView() { + isIndicationOnView(): boolean { if (this.option('showCurrentTimeIndicator')) { const today = this._getToday(); const endViewDate = dateUtils.trimTime(this.getEndViewDate()); @@ -56,24 +59,6 @@ class SchedulerWorkSpaceIndicator extends SchedulerWorkSpace { return dateUtils.dateInRange(today, firstViewDate, endViewDate); } - _renderDateTimeIndication() { - if (this.isIndicationAvailable()) { - if (this.option('shadeUntilCurrentTime')) { - this._shader.render(); - } - - if (this.isIndicationOnView() && this.isIndicatorVisible()) { - const groupCount = this._getGroupCount() || 1; - const $container = this._dateTableScrollable.$content(); - const height = this.getIndicationHeight(); - const rtlOffset = this._getRtlOffset(this.getCellWidth()); - - this._renderIndicator(height, rtlOffset, $container, groupCount); - this._setCurrentTimeCells(); - } - } - } - _renderIndicator(height, rtlOffset, $container, groupCount) { const groupedByDate = this.isGroupedByDate(); const repeatCount = groupedByDate ? 1 : groupCount; @@ -99,7 +84,7 @@ class SchedulerWorkSpaceIndicator extends SchedulerWorkSpace { return this.option('rtlEnabled') ? getBoundingRect(this._dateTableScrollable.$content().get(0)).width - this.getTimePanelWidth() - width : 0; } - _setIndicationUpdateInterval() { + protected _setIndicationUpdateInterval() { if (!this.option('showCurrentTimeIndicator') || this.option('indicatorUpdateInterval') === 0) { return; } @@ -107,7 +92,7 @@ class SchedulerWorkSpaceIndicator extends SchedulerWorkSpace { this._clearIndicatorUpdateInterval(); this._indicatorInterval = setInterval(() => { - this._refreshDateTimeIndication(); + this.renderCurrentDateTimeIndication(); }, this.option('indicatorUpdateInterval')); } @@ -172,25 +157,27 @@ class SchedulerWorkSpaceIndicator extends SchedulerWorkSpace { super._dispose.apply(this, arguments as any); } - _refreshDateTimeIndication() { - this._cleanDateTimeIndicator(); - this._cleanCurrentTimeCells(); + renderCurrentDateTimeIndication(): void { + this.renderCurrentDateTimeLineAndShader(); + + if (this.isRenovatedRender()) { + this.renderWorkSpace({ + generateNewData: true, + renderComponents: { + header: true, + timePanel: true, + }, + }); + } + } + renderCurrentDateTimeLineAndShader(): void { + this._cleanDateTimeIndicator(); this._shader?.clean(); - this._renderDateTimeIndication(); } - _setCurrentTimeCells() { - const timePanelCells = this._getTimePanelCells(); - const currentTimeCellIndices = this._getCurrentTimePanelCellIndices(); - currentTimeCellIndices.forEach((timePanelCellIndex) => { - timePanelCells.eq(timePanelCellIndex) - .addClass(TIME_PANEL_CURRENT_TIME_CELL_CLASS); - }); - } - - _isCurrentTimeHeaderCell(headerIndex) { + _isCurrentTimeHeaderCell(headerIndex: number): boolean { if (this.isIndicationOnView()) { const { completeDateHeaderMap } = this.viewDataProvider; const date = completeDateHeaderMap[completeDateHeaderMap.length - 1][headerIndex].startDate; @@ -220,19 +207,13 @@ class SchedulerWorkSpaceIndicator extends SchedulerWorkSpace { _dimensionChanged() { super._dimensionChanged(); - this._refreshDateTimeIndication(); + this.renderCurrentDateTimeLineAndShader(); } _cleanDateTimeIndicator() { (this.$element() as any).find(`.${SCHEDULER_DATE_TIME_INDICATOR_CLASS}`).remove(); } - _cleanCurrentTimeCells() { - (this.$element() as any) - .find(`.${TIME_PANEL_CURRENT_TIME_CELL_CLASS}`) - .removeClass(TIME_PANEL_CURRENT_TIME_CELL_CLASS); - } - _cleanWorkSpace() { super._cleanWorkSpace(); @@ -250,19 +231,13 @@ class SchedulerWorkSpaceIndicator extends SchedulerWorkSpace { this._setIndicationUpdateInterval(); break; case 'showAllDayPanel': - super._optionChanged(args); - this._refreshDateTimeIndication(); - break; case 'allDayExpanded': - super._optionChanged(args); - this._refreshDateTimeIndication(); - break; case 'crossScrollingEnabled': super._optionChanged(args); - this._refreshDateTimeIndication(); + this.renderCurrentDateTimeIndication(); break; case 'shadeUntilCurrentTime': - this._refreshDateTimeIndication(); + this.renderCurrentDateTimeIndication(); break; default: super._optionChanged(args); @@ -307,6 +282,49 @@ class SchedulerWorkSpaceIndicator extends SchedulerWorkSpace { ...cellIndices.map((cellIndex) => rowCountPerGroup * groupIndex + cellIndex), ], []); } + + protected _renderDateTimeIndication(): void { + if (!this.isIndicationAvailable()) { + return; + } + + if (this.option('shadeUntilCurrentTime')) { + this._shader.render(); + } + + if (!this.isIndicationOnView() || !this.isIndicatorVisible()) { + return; + } + + const groupCount = this._getGroupCount() || 1; + const $container = this._dateTableScrollable.$content(); + const height = this.getIndicationHeight(); + const rtlOffset = this._getRtlOffset(this.getCellWidth()); + + this._renderIndicator(height, rtlOffset, $container, groupCount); + + // TODO Old render: delete this code with the old render. + if (!this.isRenovatedRender()) { + this._setCurrentTimeCells(); + } + } + + // Temporary new render methods. + // TODO Old render: replace base call methods by these after the deleting of the old render. + protected _setCurrentTimeCells(): void { + const timePanelCells = this._getTimePanelCells(); + const currentTimeCellIndices = this._getCurrentTimePanelCellIndices(); + currentTimeCellIndices.forEach((timePanelCellIndex) => { + timePanelCells.eq(timePanelCellIndex) + .addClass(TIME_PANEL_CURRENT_TIME_CELL_CLASS); + }); + } + + protected _cleanCurrentTimeCells(): void { + (this.$element() as any) + .find(`.${TIME_PANEL_CURRENT_TIME_CELL_CLASS}`) + .removeClass(TIME_PANEL_CURRENT_TIME_CELL_CLASS); + } } registerComponent('dxSchedulerWorkSpace', SchedulerWorkSpaceIndicator as any); diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_date_header_data_generator.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_date_header_data_generator.ts index 936085e52904..a1f6f8e4ec3f 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_date_header_data_generator.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_date_header_data_generator.ts @@ -118,7 +118,8 @@ export class DateHeaderDataGenerator { : completeViewDataMap[index]; // NOTE: Should leave dates as is when creating time row in timelines. - const shouldShiftDates = !isTimelineView(viewType) || viewType === VIEWS.TIMELINE_MONTH; + const shouldShiftDatesForHeaderText = !isTimelineView(viewType) + || viewType === VIEWS.TIMELINE_MONTH; return slicedByColumnsData.map(({ startDate, @@ -126,14 +127,15 @@ export class DateHeaderDataGenerator { isFirstGroupCell, isLastGroupCell, ...restProps - }, index) => { - const shiftedStartDate = shouldShiftDates - ? timeZoneUtils.addOffsetsWithoutDST(startDate, -viewOffset) + }, idx: number) => { + const shiftedStartDate = timeZoneUtils.addOffsetsWithoutDST(startDate, -viewOffset); + const shiftedStartDateForHeaderText = shouldShiftDatesForHeaderText + ? shiftedStartDate : startDate; const text = getHeaderCellText( - index % cellCountInGroupRow, - shiftedStartDate, + idx % cellCountInGroupRow, + shiftedStartDateForHeaderText, headerCellTextFormat, getDateForHeaderText, { @@ -149,7 +151,7 @@ export class DateHeaderDataGenerator { ...restProps, startDate, text, - today: dateUtils.sameDate(startDate, today), + today: dateUtils.sameDate(shiftedStartDate, today), colSpan, isFirstGroupCell: isGroupedByDate || (isFirstGroupCell && !isVerticalGrouping), isLastGroupCell: isGroupedByDate || (isLastGroupCell && !isVerticalGrouping), diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_time_panel_data_generator.ts b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_time_panel_data_generator.ts index 426d0d3df767..c731982e5844 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_time_panel_data_generator.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/view_model/m_time_panel_data_generator.ts @@ -1,6 +1,23 @@ +import dateUtils from '@js/core/utils/date'; import { getDisplayedRowCount } from '@js/renovation/ui/scheduler/view_model/to_test/views/utils/base'; import { getTimePanelCellText } from '@js/renovation/ui/scheduler/view_model/to_test/views/utils/week'; import { getIsGroupedAllDayPanel, getKeyByGroup } from '@js/renovation/ui/scheduler/workspaces/utils'; +import { dateUtilsTs } from '@ts/core/utils/date'; +import { shiftIntegerByModule } from '@ts/core/utils/math'; + +const toMs = dateUtils.dateToMilliseconds; + +interface TimePanelGeneratorCellData { + date: Date; + index: number; + duration: number; +} + +interface TimePanelVisibleInterval { + startViewDate: Date; + realEndViewDate: Date; + showCurrentTimeIndicator: boolean; +} export class TimePanelDataGenerator { constructor(public _viewDataGenerator) { @@ -18,7 +35,11 @@ export class TimePanelDataGenerator { hoursInterval, endDayHour, viewOffset, + today, + showCurrentTimeIndicator, } = options; + const rowsCount = completeViewDataMap.length - 1; + const realEndViewDate = completeViewDataMap[rowsCount][completeViewDataMap[rowsCount].length - 1].endDate; const rowCountInGroup = this._viewDataGenerator.getRowCount({ intervalCount, @@ -36,8 +57,8 @@ export class TimePanelDataGenerator { startDayHour, endDayHour, }); - let allDayRowsCount = 0; + let allDayRowsCount = 0; return completeViewDataMap.map((row, index) => { const { allDay, @@ -74,6 +95,23 @@ export class TimePanelDataGenerator { isFirstGroupCell: isVerticalGrouping && isFirstGroupCell, isLastGroupCell: isVerticalGrouping && isLastGroupCell, index: Math.floor(cellIndex / cellCountInGroupRow), + highlighted: this.isTimeCellShouldBeHighlighted( + today, + viewOffset, + { + startViewDate, + realEndViewDate, + showCurrentTimeIndicator, + }, + { + date: startDate, + index: index - allDayRowsCount + 1, + // NOTE: The 'cellDuration' (in ms) here created from the float 'hoursInterval' value. + // It may be not equal integer value but very close to it. + // Therefore, we round this value here. + duration: Math.round(cellDuration), + }, + ), }; }); } @@ -134,4 +172,61 @@ export class TimePanelDataGenerator { }; }, { previousGroupIndex: -1, previousGroupedData: [] }); } + + private isTimeCellShouldBeHighlighted( + today: Date, + viewOffset: number, + { + startViewDate, + realEndViewDate, + showCurrentTimeIndicator, + }: TimePanelVisibleInterval, + cellData: TimePanelGeneratorCellData, + ): boolean { + // NOTE: today date value shifted by -viewOffset for the render purposes. + // Therefore, we roll-backing here this shift. + const realToday = dateUtilsTs.addOffsets(today, [viewOffset]); + // NOTE: start view date value calculated from the render options and hasn't viewOffset. + // So, we must shift it by viewOffset to get the real start view date here. + const realStartViewDate = dateUtilsTs.addOffsets(startViewDate, [viewOffset]); + + if ( + !showCurrentTimeIndicator + || realToday < realStartViewDate + || realToday > realEndViewDate + ) { + return false; + } + + const realTodayTimeMs = shiftIntegerByModule(realToday.getTime(), toMs('day')); + const [startMs, endMs] = this.getHighlightedInterval(cellData); + + return realTodayTimeMs >= startMs && realTodayTimeMs < endMs; + } + + private getHighlightedInterval({ + date, + index, + duration, + }: TimePanelGeneratorCellData): [startMs: number, endMs: number] { + const timeMs = shiftIntegerByModule(date.getTime(), toMs('day')); + + switch (true) { + case index === 0: + return [ + timeMs, + shiftIntegerByModule(timeMs + duration, toMs('day')), + ]; + case index % 2 === 0: + return [ + timeMs, + shiftIntegerByModule(timeMs + 2 * duration, toMs('day')), + ]; + default: + return [ + shiftIntegerByModule(timeMs - duration, toMs('day')), + shiftIntegerByModule(timeMs + duration, toMs('day')), + ]; + } + } } diff --git a/packages/devextreme/js/renovation/ui/scheduler/workspaces/base/__tests__/work_space.test.tsx b/packages/devextreme/js/renovation/ui/scheduler/workspaces/base/__tests__/work_space.test.tsx index cfcca1b2063b..845ce6357acb 100644 --- a/packages/devextreme/js/renovation/ui/scheduler/workspaces/base/__tests__/work_space.test.tsx +++ b/packages/devextreme/js/renovation/ui/scheduler/workspaces/base/__tests__/work_space.test.tsx @@ -2772,6 +2772,7 @@ describe('WorkSpace', () => { key: 0, text: '12:00 AM', groups: undefined, + highlighted: false, }, { startDate: new Date(2021, 9, 25, 0, 30), groupIndex: undefined, @@ -2782,6 +2783,7 @@ describe('WorkSpace', () => { key: 1, text: '', groups: undefined, + highlighted: false, }]); }); @@ -2825,6 +2827,7 @@ describe('WorkSpace', () => { key: 0, text: '12:00 AM', groups: undefined, + highlighted: false, }, { startDate: new Date(2021, 9, 25, 0, 30), groupIndex: undefined, @@ -2835,9 +2838,11 @@ describe('WorkSpace', () => { key: 1, text: '', groups: undefined, + highlighted: false, }], groupIndex: undefined, isGroupedAllDayPanel: false, + topVirtualRowHeight: undefined, key: '0', }], isGroupedAllDayPanel: false, @@ -2905,6 +2910,7 @@ describe('WorkSpace', () => { isGenerateWeekDaysHeaderData: false, isProvideVirtualCellsWidth: false, groupByDate: false, + today: expect.any(Date), }); expect(mockCreateGroupedDataMapProvider) .toHaveBeenCalledTimes(1); diff --git a/packages/devextreme/js/renovation/ui/scheduler/workspaces/base/time_panel/cell.tsx b/packages/devextreme/js/renovation/ui/scheduler/workspaces/base/time_panel/cell.tsx index ec78e94f43b4..ce85c560ff85 100644 --- a/packages/devextreme/js/renovation/ui/scheduler/workspaces/base/time_panel/cell.tsx +++ b/packages/devextreme/js/renovation/ui/scheduler/workspaces/base/time_panel/cell.tsx @@ -3,6 +3,7 @@ import { ComponentBindings, JSXComponent, JSXTemplate, + OneWay, Template, } from '@devextreme-generator/declarations'; import { CellBase as Cell, CellBaseProps } from '../cell'; @@ -10,18 +11,19 @@ import { DateTimeCellTemplateProps } from '../../types'; export const viewFunction = ({ props: { + className, text, isFirstGroupCell, isLastGroupCell, - className, timeCellTemplate: TimeCellTemplate, + highlighted, }, timeCellTemplateProps, }: TimePanelCell): JSX.Element => ( {!TimeCellTemplate && ( @@ -40,6 +42,8 @@ export const viewFunction = ({ @ComponentBindings() export class TimePanelCellProps extends CellBaseProps { + @OneWay() highlighted?: boolean; + @Template() timeCellTemplate?: JSXTemplate; } diff --git a/packages/devextreme/js/renovation/ui/scheduler/workspaces/base/time_panel/layout.tsx b/packages/devextreme/js/renovation/ui/scheduler/workspaces/base/time_panel/layout.tsx index 5f3d674d84b6..9f21d64ad996 100644 --- a/packages/devextreme/js/renovation/ui/scheduler/workspaces/base/time_panel/layout.tsx +++ b/packages/devextreme/js/renovation/ui/scheduler/workspaces/base/time_panel/layout.tsx @@ -10,7 +10,7 @@ import { RefObject, } from '@devextreme-generator/declarations'; import { Row } from '../row'; -import { TimePanelCell as Cell } from './cell'; +import { TimePanelCell } from './cell'; import { CellBase } from '../cell'; import { Table } from '../table'; import { AllDayPanelTitle } from '../date_table/all_day_panel/title'; @@ -61,6 +61,7 @@ export const viewFunction = ({ isFirstGroupCell, isLastGroupCell, key, + highlighted, } = cell; return ( @@ -68,7 +69,7 @@ export const viewFunction = ({ className="dx-scheduler-time-panel-row" key={key} > - ); diff --git a/packages/devextreme/js/renovation/ui/scheduler/workspaces/base/work_space.tsx b/packages/devextreme/js/renovation/ui/scheduler/workspaces/base/work_space.tsx index 41f217c52da0..6a0cc799769b 100644 --- a/packages/devextreme/js/renovation/ui/scheduler/workspaces/base/work_space.tsx +++ b/packages/devextreme/js/renovation/ui/scheduler/workspaces/base/work_space.tsx @@ -159,6 +159,7 @@ export const prepareGenerationOptions = ( hoursInterval, type, cellDuration, + today, } = workSpaceProps; const { headerCellTextFormat, @@ -190,6 +191,7 @@ export const prepareGenerationOptions = ( currentDate, startDate, firstDayOfWeek, + today, isGenerateTimePanelData: isRenderTimePanel, isGenerateWeekDaysHeaderData, @@ -592,6 +594,7 @@ export class WorkSpace extends JSXComponent) border, so -1px bottom is ok here. */ + bottom: -1px; + height: 2px; + background-color: $scheduler-time-indicator-color; } } diff --git a/packages/devextreme/testing/testcafe/helpers/mockDate.ts b/packages/devextreme/testing/testcafe/helpers/mockDate.ts new file mode 100644 index 000000000000..59dd11d1b344 --- /dev/null +++ b/packages/devextreme/testing/testcafe/helpers/mockDate.ts @@ -0,0 +1,53 @@ +/* eslint-disable no-global-assign */ +import { ClientFunction } from 'testcafe'; + +type WindowDateMockExtended = + Window + & typeof globalThis + & { + dateMock?: DateMockManager; + }; + +class DateMockManager { + private readonly originDateCtor = Date; + + mock(dateNowValueISO: string): void { + const OriginDate = this.originDateCtor; + + const MockDate = function mockDateCtor(...args: [any]): Date { + return args.length > 0 + ? new OriginDate(...args) + : new OriginDate(dateNowValueISO); + }; + MockDate.now = function mockDateNow() { return new OriginDate(dateNowValueISO).getTime(); }; + MockDate.parse = OriginDate.parse; + MockDate.UTC = OriginDate.UTC; + Object.setPrototypeOf(MockDate, OriginDate.prototype); + + // TODO: Improve this hard type cast. + Date = MockDate as unknown as DateConstructor; + } + + clear(): void { + Date = this.originDateCtor; + Object.setPrototypeOf(Date, this.originDateCtor.prototype); + } +} + +export const dateNowMock = async (dateNowValueISO: string): Promise => ClientFunction(() => { + const extenderWindow = window as WindowDateMockExtended; + + if (!(extenderWindow.dateMock instanceof DateMockManager)) { + extenderWindow.dateMock = new DateMockManager(); + } + + extenderWindow.dateMock.mock(dateNowValueISO); +}, { dependencies: { dateNowValueISO, DateMockManager } })(); + +export const clearDateNowMock = async (): Promise => ClientFunction( + () => { + const extenderWindow = window as WindowDateMockExtended; + extenderWindow.dateMock?.clear(); + extenderWindow.dateMock = undefined; + }, +)(); diff --git a/packages/devextreme/testing/testcafe/tests/scheduler/viewOffset/currentTimeIndicator.ts b/packages/devextreme/testing/testcafe/tests/scheduler/viewOffset/currentTimeIndicator.ts new file mode 100644 index 000000000000..2827b16cd84f --- /dev/null +++ b/packages/devextreme/testing/testcafe/tests/scheduler/viewOffset/currentTimeIndicator.ts @@ -0,0 +1,120 @@ +import { createScreenshotsComparer } from 'devextreme-screenshot-comparer'; +import createWidget from '../../../helpers/createWidget'; +import url from '../../../helpers/getPageUrl'; +import { clearDateNowMock, dateNowMock } from '../../../helpers/mockDate'; +import Scheduler from '../../../model/scheduler'; + +fixture.disablePageReloads`Offset: Current time indicator` + .page(url(__dirname, '../../container.html')); + +const SCHEDULER_SELECTOR = '#container'; + +const getTestOptionsByViewName = (viewName: string): [ + cellDuration: number, + mockDate: string, +] => { + switch (viewName) { + case 'day': + return [ + 120, + '2023-12-01T07:15:00', + ]; + case 'week': + return [ + 120, + '2023-11-29T07:15:00', + ]; + case 'timelineDay': + return [ + 360, + '2023-12-07:15:00', + ]; + case 'timelineWeek': + return [ + 720, + '2023-11-27T07:15:00', + ]; + case 'timelineMonth': + return [ + 1440, + '2023-12-03T07:15:00', + ]; + default: + throw Error(`Unexpected view name: ${viewName}`); + } +}; + +const getScreenshotName = ( + view: string, + mockDate: string, + currentDate: string, + offset: number, +): string => `offset_time-indicator_view-${view}_now-${mockDate.replace(/:/g, '-')}_cur-${currentDate.replace(/:/g, '-')}_offset-${offset}.png`; + +const TEST_CASES: [string, string, number][] = [ + ['day', '2023-12-01', 0], + ['day', '2023-11-30', 720], + ['day', '2023-11-30', 1440], + ['day', '2023-12-01', -720], + ['day', '2023-12-02', -1440], + ['week', '2023-11-29', 0], + ['week', '2023-11-29', 720], + ['week', '2023-11-29', 1440], + ['week', '2023-11-29', -720], + ['week', '2023-11-29', -1440], + ['timelineDay', '2023-12-01', 0], + ['timelineDay', '2023-11-30', 720], + ['timelineDay', '2023-11-30', 1440], + ['timelineDay', '2023-12-01', -720], + ['timelineDay', '2023-12-02', -1440], + ['timelineWeek', '2023-12-01', 0], + ['timelineWeek', '2023-12-01', 720], + ['timelineWeek', '2023-12-01', 1440], + ['timelineWeek', '2023-12-01', -720], + ['timelineWeek', '2023-12-01', -1440], + ['timelineMonth', '2023-12-01', 0], + ['timelineMonth', '2023-12-01', 720], + ['timelineMonth', '2023-12-01', 1440], + ['timelineMonth', '2023-12-01', -720], + ['timelineMonth', '2023-12-01', -1440], +]; + +TEST_CASES.forEach(([view, currentDate, offset]) => { + const [ + cellDuration, + mockDate, + ] = getTestOptionsByViewName(view); + + test(` +Should correctly render current time indicator ( +${view}, +now: ${mockDate}, +current: ${currentDate}, +offset: ${offset} +)`, async (t) => { + const { takeScreenshot, compareResults } = createScreenshotsComparer(t); + const scheduler = new Scheduler(SCHEDULER_SELECTOR); + + await takeScreenshot(getScreenshotName( + view, + mockDate, + currentDate, + offset, + ), scheduler.workSpace); + + await t.expect(compareResults.isValid()) + .ok(compareResults.errorMessages()); + }).before(async () => { + await dateNowMock(mockDate); + await createWidget('dxScheduler', { + dataSource: [], + currentView: view, + shadeUntilCurrentTime: true, + cellDuration, + currentDate, + offset, + }); + }).after(async () => { + await clearDateNowMock(); + }); +}); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.currentTimeIndication.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.currentTimeIndication.tests.js index 16658975390e..b88625f72d08 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.currentTimeIndication.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.currentTimeIndication.tests.js @@ -48,14 +48,17 @@ module('Current Time Cell Indication Updating', { view: 'timelineDay', cellIndices: [4], timeDifference: HOUR, + highlightedCellsCount: 8, }, { view: 'timelineWeek', cellIndices: [28], timeDifference: HOUR, + highlightedCellsCount: 8, }, { view: 'timelineMonth', cellIndices: [8], timeDifference: HOUR * 48, + highlightedCellsCount: 1, }]; const testBasicView = (assert, scheduler, clock, currentTimeCellsNumber, cellIndices) => { @@ -144,7 +147,8 @@ module('Current Time Cell Indication Updating', { ...getBaseConfig({ type: view }), }); - testTimelineView(assert, scheduler, this.clock, timeDifference, 1, cellIndices); + const highlightedCellsCount = view === 'timelineMonth' ? 1 : 8; + testTimelineView(assert, scheduler, this.clock, timeDifference, highlightedCellsCount, cellIndices); }); }); @@ -167,15 +171,18 @@ module('Current Time Cell Indication Updating', { view: 'timelineDay', cellIndices: [4, 12], timeDifference: HOUR, + highlightedCellsCount: 16, }, { view: 'timelineWeek', cellIndices: [28, 84], timeDifference: HOUR, + highlightedCellsCount: 16, }, { view: 'timelineMonth', cellIndices: [8, 39], timeDifference: HOUR * 48, - }].forEach(({ view, cellIndices, timeDifference }) => { + highlightedCellsCount: 2, + }].forEach(({ view, cellIndices, timeDifference, highlightedCellsCount }) => { test(`Current Header Panel Cell indication should work correctly when horizontal grouping is used in ${view} view`, function(assert) { const scheduler = createWrapper({ ...getBaseConfig({ @@ -186,11 +193,11 @@ module('Current Time Cell Indication Updating', { groups: ['priorityId'], }); - testTimelineView(assert, scheduler, this.clock, timeDifference, 2, cellIndices); + testTimelineView(assert, scheduler, this.clock, timeDifference, highlightedCellsCount, cellIndices); }); }); - timelineViewsConfig.forEach(({ view, cellIndices, timeDifference }) => { + timelineViewsConfig.forEach(({ view, cellIndices, timeDifference, highlightedCellsCount }) => { test(`Current Header Panel Cell indication should work correctly when grouping by date is used in ${view} view`, function(assert) { const scheduler = createWrapper({ ...getBaseConfig({ @@ -201,7 +208,7 @@ module('Current Time Cell Indication Updating', { groups: ['priorityId'], }); - testTimelineView(assert, scheduler, this.clock, timeDifference, 1, cellIndices); + testTimelineView(assert, scheduler, this.clock, timeDifference, highlightedCellsCount, cellIndices); }); }); @@ -225,7 +232,7 @@ module('Current Time Cell Indication Updating', { }); }); - timelineViewsConfig.forEach(({ view, cellIndices, timeDifference }) => { + timelineViewsConfig.forEach(({ view, cellIndices, timeDifference, highlightedCellsCount }) => { test(`Current Header Panel Cell indication should work correctly when vertical is used in ${view} view`, function(assert) { const scheduler = createWrapper({ ...getBaseConfig({ @@ -235,7 +242,7 @@ module('Current Time Cell Indication Updating', { groups: ['priorityId'], }); - testTimelineView(assert, scheduler, this.clock, timeDifference, 1, cellIndices); + testTimelineView(assert, scheduler, this.clock, timeDifference, highlightedCellsCount, cellIndices); }); }); @@ -279,7 +286,7 @@ module('Current Time Cell Indication Updating', { let currentTimeCells = scheduler.workSpace.getHeaderPanelCurrentTimeCells(); - assert.equal(currentTimeCells.length, 0, 'Correct number of current time cells'); + assert.equal(currentTimeCells.length, 2, 'Correct number of current time cells'); this.clock.tick(timeDifference); @@ -287,7 +294,7 @@ module('Current Time Cell Indication Updating', { const currentCell = headerPanelCells.eq(cellIndex); currentTimeCells = scheduler.workSpace.getHeaderPanelCurrentTimeCells(); - assert.equal(currentTimeCells.length, 1, 'Correct number of current time cells'); + assert.equal(currentTimeCells.length, 2, 'Correct number of current time cells'); assert.ok(currentCell.hasClass(HEADER_PANEL_CURRENT_TIME_CELL_CLASS), 'The current time cell has current-time class'); }); }); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/view_data_provider.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/view_data_provider.tests.js index 56785c8c297b..0b1ed0cab5d4 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/view_data_provider.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/view_data_provider.tests.js @@ -1624,6 +1624,7 @@ module('View Data Provider', { startDate: new Date(2021, 0, 10, 5), groupIndex: undefined, groups: undefined, + highlighted: false, index: 0, isFirstGroupCell: false, isLastGroupCell: false, @@ -1634,6 +1635,7 @@ module('View Data Provider', { startDate: new Date(2021, 0, 10, 6), groupIndex: undefined, groups: undefined, + highlighted: false, index: 1, isFirstGroupCell: false, isLastGroupCell: false, @@ -1661,6 +1663,7 @@ module('View Data Provider', { startDate: new Date(2021, 0, 10), groupIndex: undefined, groups: undefined, + highlighted: false, index: 0, isFirstGroupCell: false, isLastGroupCell: false, @@ -1670,6 +1673,7 @@ module('View Data Provider', { startDate: new Date(2021, 0, 10, 5), groupIndex: undefined, groups: undefined, + highlighted: false, index: 0, isFirstGroupCell: false, isLastGroupCell: false, @@ -1680,6 +1684,7 @@ module('View Data Provider', { startDate: new Date(2021, 0, 10, 6), groupIndex: undefined, groups: undefined, + highlighted: false, index: 1, isFirstGroupCell: false, isLastGroupCell: false, @@ -1712,6 +1717,7 @@ module('View Data Provider', { startDate: new Date(2021, 0, 10), groupIndex: 0, groups: { groupId: 1 }, + highlighted: false, index: 0, isFirstGroupCell: true, isLastGroupCell: false, @@ -1721,6 +1727,7 @@ module('View Data Provider', { startDate: new Date(2021, 0, 10, 5), groupIndex: 0, groups: { groupId: 1 }, + highlighted: false, index: 0, isFirstGroupCell: true, isLastGroupCell: false, @@ -1731,6 +1738,7 @@ module('View Data Provider', { startDate: new Date(2021, 0, 10, 6), groupIndex: 0, groups: { groupId: 1 }, + highlighted: false, index: 1, isFirstGroupCell: false, isLastGroupCell: true, @@ -1742,6 +1750,7 @@ module('View Data Provider', { startDate: new Date(2021, 0, 10), groupIndex: 1, groups: { groupId: 2 }, + highlighted: false, index: 0, isFirstGroupCell: true, isLastGroupCell: false, @@ -1751,6 +1760,7 @@ module('View Data Provider', { startDate: new Date(2021, 0, 10, 5), groupIndex: 1, groups: { groupId: 2 }, + highlighted: false, index: 0, isFirstGroupCell: true, isLastGroupCell: false, @@ -1761,6 +1771,7 @@ module('View Data Provider', { startDate: new Date(2021, 0, 10, 6), groupIndex: 1, groups: { groupId: 2 }, + highlighted: false, index: 1, isFirstGroupCell: false, isLastGroupCell: true, @@ -1792,6 +1803,7 @@ module('View Data Provider', { startDate: new Date(2021, 0, 10, 5), groupIndex: 0, groups: { groupId: 1 }, + highlighted: false, index: 0, isFirstGroupCell: true, isLastGroupCell: false, @@ -1802,6 +1814,7 @@ module('View Data Provider', { startDate: new Date(2021, 0, 10, 6), groupIndex: 0, groups: { groupId: 1 }, + highlighted: false, index: 1, isFirstGroupCell: false, isLastGroupCell: true, @@ -1812,6 +1825,7 @@ module('View Data Provider', { startDate: new Date(2021, 0, 10, 5), groupIndex: 1, groups: { groupId: 2 }, + highlighted: false, index: 0, isFirstGroupCell: true, isLastGroupCell: false, @@ -1822,6 +1836,7 @@ module('View Data Provider', { startDate: new Date(2021, 0, 10, 6), groupIndex: 1, groups: { groupId: 2 }, + highlighted: false, index: 1, isFirstGroupCell: false, isLastGroupCell: true,